Rsync过滤器:仅复制一个模式

我正在尝试创建一个目录,用于存放仅包含我的LaTeX编译的PDF文件。我喜欢将每个项目保存在单独的文件夹中,所有文件夹都位于一个名为LaTeX的大文件夹中。所以我尝试运行:

rsync -avn *.pdf ~/LaTeX/ ~/Output/

这个命令应该在~/LaTeX/目录中找到所有的pdf文件并将它们传输到输出文件夹。但是它并不起作用,告诉我找不到匹配项为"*.pdf"的文件。如果我省略这个过滤器,命令会列出在LaTeX下项目文件夹中的所有文件。所以问题出在*.pdf过滤器上。我试过用我的家目录的完整路径替换~/,但没有效果。

我使用的是zsh。我尝试在bash中做同样的事情,甚至带有列出每个子目录中每个文件的过滤器...这是怎么回事?

为什么rsync不能理解我的pdf文件过滤器呢?


好的。所以更新:不,我正在尝试中。
rsync -avn --include="*/" --include="*.pdf" LaTeX/ Output/

这给我了整个文件列表。我猜是因为一切都符合第一个模式...

嗯,你似乎是对的...我认为我的答案(使用zsh的**模式)应该可以解决问题。 - Marcel Stimberg
可能是Can anyone tell me why rsync is no copying files from the source's subfolders?的重复问题。 - Gilles 'SO- stop being evil'
11个回答

总结一下:

rsync -am --include='*.pdf' --include='*/' --exclude='*' ~/LaTeX/ ~/Output/

Rsync将源文件复制到目标文件夹。如果你将*.pdf作为源文件传递给它,shell会自动将当前目录中所有具有.pdf扩展名的文件列出来。由于没有传递任何目录作为源文件,所以不会进行递归遍历。
因此,你需要运行rsync -a ~/LaTeX/ ~/Output/,但是同时带上一个过滤器告诉rsync只复制.pdf文件。当你阅读手册时,rsync的过滤器规则可能会让人感到困惑,但实际上只需要几条简单的规则就可以构建许多示例。
包含和排除:
通过名称或位置排除文件很容易:--exclude=*~,--exclude=/some/relative/location(相对于源参数,例如这将排除~/LaTeX/some/relative/location)。
如果您只想匹配一些文件或位置,请包含它们,并且包括每个目录(例如使用--include=*/),然后用--exclude='*'排除其余部分。原因是:
如果您排除一个目录,那么它下面的所有内容都会被排除。排除的文件将不会被考虑。
如果您包含一个目录,这并不自动包含其内容。在最新版本中,--include='directory/***'可以实现这一点。
对于每个文件,第一个匹配规则适用(未匹配的内容将被包含)。
模式:
如果模式不包含/,它适用于文件名而不包括目录。
如果模式以/结尾,它仅适用于目录。
如果模式以/开头,它适用于从传递给rsync的目录开始的整个路径。
*表示单个目录组件的任意子字符串(即不匹配/);**匹配任何路径子字符串。
如果源参数以/结尾,则其内容将被复制(rsync -r a/ b为每个a/foo创建b/foo)。否则,将复制目录本身(rsync -r a b创建b/a)。
在这里,我们需要包含*.pdf,包括其中包含的目录,并排除其他所有内容。
rsync -a --include='*.pdf' --include='*/' --exclude='*' ~/LaTeX/ ~/Output/

请注意,这将复制所有目录,即使其中没有匹配的文件或子目录。可以通过使用--prune-empty-dirs选项来避免这种情况(这不是一个通用解决方案,因为您无法通过显式匹配来复制目录,但这是一个罕见的需求)。
rsync -am --include='*.pdf' --include='*/' --exclude='*' ~/LaTeX/ ~/Output/

与我的解决方案(使用zsh的**模式)相比,这个方法会在目标目录中重新创建目录结构。我不确定这是否是OP想要的... - Marcel Stimberg
@Michael,我无法复现这个问题。我刚刚尝试了使用rsync 3.1.1和rsync 3.1.3在本地目录之间进行同步,它只会列出在目标位置上缺失或不同的文件(以及它们所在的目录)。 - Gilles 'SO- stop being evil'
为什么要使用-m选项? - a06e
@becko 因为rsync需要遍历和复制所有目录,以便找到并复制所有的.pdf文件。-m选项告诉它不要复制那些最终没有任何.pdf文件可复制的目录子树。 - Gilles 'SO- stop being evil'

rsync -av --include="*/" --include="*.pdf" --exclude="*" ~/Latex/ ~/Output/ --dry-run

默认情况下是包括所有内容的,所以你必须明确地排除所有在包括要传输的文件之后的一切。 删除--dry-run以实际传输文件。
如果你从以下开始:
--exclude '*' --include '*.pdf'

然后贪婪匹配会把一切都排除在外。
如果你尝试:
--include '*.pdf' --exclude '*' 

只有顶层文件夹中的PDF文件将被传输。它不会遵循任何目录,因为这些目录被“*”排除在外。

7截至2014年3月17日,这是最好的答案,因为它完全解决了原始帖子的问题。请投票支持!如果您加入--prune-empty-dirs(或快捷键-m),您甚至可以在目标位置省去许多空目录,除非您希望它们作为提醒或结构蓝图。 - porg
3最佳答案,--include="*/" 是关键。 - Martin Konicek
对我来说没用。 - Felipe

如果您使用像*.pdf这样的模式,shell会“展开”该模式,即用当前目录中的所有匹配项替换该模式。您运行的命令(在本例中为rsync)不知道您尝试使用模式的事实。

当您使用zsh时,有一个简单的解决方案:可以使用**模式递归匹配文件夹。请尝试以下方法:

rsync -avn ~/LaTeX/**/*.pdf ~/Output/

这样做会不会将当前目录中的所有pdf文件以及/LaTeX/目录下的所有内容都复制到/Output/目录中? - SamB
我猜你是指 rsync -avn ~/LaTeX/**/*.pdf ~/Output,但使用 --include 的解决方案更具可扩展性。 - Adam Byrtek
抱歉,我匆忙中更正了我输入错误的命令...我同意在SamB的版本中使用include命令更好,尽管它稍微复杂一些,并且只适用于rsync,而**在其他情况下可能也会派上用场。 - Marcel Stimberg
我曾担心如果你的foo/文件夹中有一些pdf文件,而另一些pdf文件在foo/bar/文件夹中,你想要将它们全部移动,这个解决方案可能行不通,但实际上它是有效的。我喜欢这个答案,但我确实也想保留文件夹结构,所以这个方法不适用。不过,我确实喜欢zsh的**技巧,将来肯定会用到! - Seamus
1Bash 4 采用了相同的功能。噢,这里不需要使用 rsync,cp 就可以了。在某些系统中,如果有很多文件,执行 cd ~/Latex && cp -p **/*.pdf ~/Output 可以避免“命令行太长”的错误。 - Gilles 'SO- stop being evil'
1请注意,rsync中用于包含和排除过滤器的模式也具有相同功能的**。您可以通过将它们放在引号中来转义其他shell中的星号字符。 - Dan Pritts

你可以使用find和一个中间文件列表(files_to_copy)来解决你的问题。确保你在你的主目录下,然后执行以下操作: find LaTeX/ -type f -a -iname "*.pdf" > files_to_copy && rsync -avn --files-from=files_to_copy ~/ ~/Output/ && rm files_to_copy 已在Bash中测试通过。

我认为find是最强大的解决方案,但我会选择使用find的-exec选项或者使用xargs。类似这样:find LaTeX/ -type f -iname "*.pdf" -print0 | xargs -0 -i rsync -avn {} Output/ - Steven D
是的...我也建议找一下...虽然我想rsync肯定能做到这个。 - gabe.
这也是一个巧妙的解决方案,适用于更困难的问题:我可以使用它来排除那些文档类为“standalone”的文件,或者那些没有与其同名的“.tex”文件的文件,因为这些文件很可能是被包含在某个文档中的图片... - Seamus
2rsync选项--files-from可以从标准输入读取。这将起作用 find LaTeX/ -type f -a -iname "*.pdf" | rsync -avn --files-from=- ~/ ~/Output/ - Juan Calero

根据manpage中的"INCLUDE/EXCLUDE PATTERN RULES"部分,实现这个的方法是:
rsync -avn --include="*/" --include="*.pdf" ~/Latex/ ~/Output/

这与kbrd的答案之间的关键区别是--include="*/"标志,它告诉rsync继续复制找到的任何目录,无论它们的名称是什么。这是必需的,因为rsync不会递归进入子目录,除非已经指示要复制该子目录。
此外,请注意引号将阻止shell尝试将模式扩展为相对于当前目录的文件名,并且可能执行以下操作之一:
  1. 成功并损坏您的过滤器(虽然在那样的标志中不太可能,但你真的永远不知道有人会创建一个名为--include=foo.pdf的文件...)

  2. 失败,并且可能生成错误而不是运行命令(正如你发现的zsh默认情况下所做的那样)。


这样的话,它只会复制PDF文件和目录结构,而kbrd会复制文件,但忽略结构? - Seamus
1嗯,实际上这似乎仍然试图复制一切,我猜这是因为没有过滤器的缘故,所以包含已经存在的额外内容并不会改变任何东西。如果你明白我的意思的话... - Seamus
8--include="*.pdf"之后,你需要加上--exclude="*",否则会传输所有文件。 - jmanning2k
@jmanning2k:啊,好知道! - SamB

这是我首选的解决方案:
find source_dir -iname '*.jpg' -print0 |  rsync -0 -v --files-from=- . destination_dir/

find命令比rsync的包含/排除规则更容易理解 :-)

如果你只想复制pdf文件,只需将.jpg改为.pdf


怎么样?
rsync -avn --include="*.pdf" ~/Latex/ ~/Output/

1不,man rsync 将过滤器放在选项之后、源/目的地之前。我尝试过这样做,但没有成功。 - Seamus
1你的方法可以在当前文件夹中找到.pdf文件,但不是递归地查找,而我想要的是递归查找。(a选项用于存档,其中包括使复制变为递归。) - Seamus
1抱歉,我的错误。我更新了我的答案。 - kbyrd
+1有一点点接近,同时也给了我一个线索,告诉我如何在手册页面中找到相关材料。(希望我理解得没错。:-)) - SamB

这里有一个不使用find命令也能正常工作的方法。与已经发布的答案不同之处在于过滤规则的顺序。在rsync命令中,过滤规则的工作方式类似于iptables规则,文件匹配到的第一个规则将被应用。根据手册页面的说明:
当构建要传输的文件/目录列表时,rsync会逐个检查每个名称是否与包含/排除模式列表匹配,并且按顺序执行第一个匹配的模式:如果是排除模式,则跳过该文件;如果是包含模式,则不跳过该文件;如果没有找到匹配的模式,则不跳过该文件。
因此,您需要使用以下命令:
rsync -avn --include="**.pdf" --exclude="*" ~/LaTeX/ ~/Output/

注意“**.pdf”模式。根据man页面的说明:
如果模式包含/(不包括尾部/)或“**”,则它将与完整路径名匹配,包括任何前导目录。如果模式不包含/或“**”,则仅与文件名的最后一个组件匹配。(请记住,该算法是递归应用的,因此“完整文件名”实际上可以是从起始目录向下的任何路径部分)
在我的小测试中,这确实递归地遍历目录树,并且只选择pdf文件。

1你是如何进行测试的?根据我对文档的理解和我的实验验证,你的命令应该只复制顶级目录中的 *.pdf 文件(而不包括 ~/LaTeX/foo/bar.pdf)。 - Gilles 'SO- stop being evil'
1@Gilles Crud。你是对的。我发誓我测试过这个并且它有效,但是我似乎无法重现它。现在我实际阅读了我引用的手册,所以它不起作用是有道理的。抱怨。 - Steven D
1嗯,我找出了我的测试错在哪里。我的“小测试”是在一个包含我自己的.tex和.pdf文件的目录上进行的。然后我创建了一个名为“测试”的子目录,并在该子目录中创建了test.pdf和test.tex文件。但是,我没有注意到我的顶级目录中有一个test.pdf,可能是因为我进行的某个快速LaTeX实验。 - Steven D
我还是不明白**是什么意思。如果有例子的话会更好。;) - buhtz

在对@Giles的回答进行更新时,请考虑到在当前版本(>=3.x.x)中必须更改包含和排除命令的顺序,以便在构建正确的文件列表时将包含选项放在排除选项之前。这也是我个人的最佳实践,通常先放置“包括所有子目录”的指令,然后再放置文件模式:
rsync -avh --include='*/' --include='file-pattern' --exclude='*' /sourcedir/ /targetdir/

在你的情况下,即例如:
rsync -avh --include='*/' -include='*.pdf' --exclude='*' ~/LaTeX/ ~/Output/

可以从https://www.samba.org/ftp/rsync/rsync.html的“过滤规则”章节中进一步解释:

请注意,当使用--recursive (-r)选项时(这是-a选项隐含的),每个路径的每个子目录组件都会从左到右地被访问,每个目录都有机会在其内容之前被排除。通过这种方式,包含/排除模式会递归地应用于文件系统树中每个节点的路径名(传输内部的节点)。排除模式在rsync找到要发送的文件时会终止目录遍历阶段。

例如,要包含“/foo/bar/baz”,必须不排除“/foo”和“/foo/bar”目录。如果其中一个父目录被排除,则会阻止其内容的检查,切断rsync对这些路径的递归,并使得对“/foo/bar/baz”的包含变得无效(因为rsync无法在被截断的目录层次结构的部分中匹配到它从未看到的东西)。

在使用以“*”结尾的规则时,路径排除的概念尤为重要。例如,以下方法行不通:

+ /some/path/this-file-will-not-be-found
+ /file-is-included
- *

这个失败是因为父目录"some"被"*"规则排除了,所以rsync从不访问"some"或"some/path"目录中的任何文件。一个解决办法是通过使用单个规则"+ */"(将其放在"- *"规则之前的某个位置)来要求包括层次结构中的所有目录,并可能使用--prune-empty-dirs选项。另一种解决办法是为需要访问的所有父目录添加特定的包含规则。例如,以下一组规则可以正常工作:
+ /some/
+ /some/path/
+ /some/path/this-file-is-found
+ /file-also-included
- *

以下是一些排除/包含匹配的示例:
"- *.o" would exclude all names matching *.o
"- /foo" would exclude a file (or directory) named foo in the transfer-root directory
"- foo/" would exclude any directory named foo
"- /foo/*/bar" would exclude any file named bar which is at two levels below a directory named foo in the transfer-root directory
"- /foo/**/bar" would exclude any file named bar two or more levels below a directory named foo in the transfer-root directory
The combination of "+ */", "+ *.c", and "- *" would include all directories and C source files but nothing else (see also the --prune-empty-dirs option)
The combination of "+ foo/", "+ foo/bar.c", and "- *" would include only the foo directory and foo/bar.c (the foo directory must be explicitly included or it would be excluded by the "*")

在“+”或“-”后面可以接受以下修饰符:
A / specifies that the include/exclude rule should be matched against the absolute pathname of the current item. For example, "-/ /etc/passwd" would exclude the passwd file any time the transfer was sending files from the "/etc" directory, and "-⁠/ subdir/foo" would always exclude "foo" when it is in a dir named "subdir", even if "foo" is at the root of the current transfer.
A ! specifies that the include/exclude should take effect if the pattern fails to match. For instance, "-! */" would exclude all non-directories.
A C is used to indicate that all the global CVS-exclude rules should be inserted as excludes in place of the "-⁠C". No arg should follow.
An s is used to indicate that the rule applies to the sending side. When a rule affects the sending side, it prevents files from being transferred. The default is for a rule to affect both sides unless --delete-excluded was specified, in which case default rules become sender-side only. See also the hide (H) and show (S) rules, which are an alternate way to specify sending-side includes/excludes.
An r is used to indicate that the rule applies to the receiving side. When a rule affects the receiving side, it prevents files from being deleted. See the s modifier for more info. See also the protect (P) and risk (R) rules, which are an alternate way to specify receiver-side includes/excludes.
A p indicates that a rule is perishable, meaning that it is ignored in directories that are being deleted. For instance, the -C option's default rules that exclude things like "CVS" and "*.o" are marked as perishable, and will not prevent a directory that was removed on the source from being deleted on the destination.
An x indicates that a rule affects xattr names in xattr copy/delete operations (and is thus ignored when matching file/dir names). If no xattr-matching rules are specified, a default xattr filtering rule is used (see the --xattrs option).

从源目录内部生成一个只包含头文件 (../include) 的目录:
rsync -avh --prune-empty-dirs --exclude="build" --include="*/" --include="*.h" --exclude="*" ./* ../include/

这将排除所有空目录和目录build