【问题标题】:How can I use xargs to copy files that have spaces and quotes in their names?如何使用 xargs 复制名称中包含空格和引号的文件?
【发布时间】:2010-09-13 16:16:20
【问题描述】:

我正在尝试复制目录下的一堆文件,并且其中一些文件的名称中有空格和单引号。当我尝试将findgrepxargs 串在一起时,出现以下错误:

find .|grep "FooBar"|xargs -I{} cp "{}" ~/foo/bar
xargs: unterminated quote

对于更健壮地使用 xargs 有什么建议吗?

这是在 Mac OS X 10.5.3 (Leopard) 上使用 BSD xargs

【问题讨论】:

  • 带有单引号的文件名的 GNU xargs 错误消息更有帮助:“xargs:不匹配的单引号;默认情况下,引号对 xargs 是特殊的,除非您使用 -0 选项”。
  • GNU xargs 还有--delimiter 选项(-d)。尝试使用\n 作为分隔符,这样可以防止xargs 将带有空格的行分隔成几个单词/参数。

标签: linux macos unix command-line xargs


【解决方案1】:

框架挑战——你在问如何使用 xargs。答案是:你不使用 xargs,因为你不需要它。

comment by user80168 描述了一种直接使用 cp 执行此操作的方法,无需为每个文件调用 cp:

find . -name '*FooBar*' -exec cp -t /tmp -- {} +

之所以有效,是因为:

  • cp -t 标志允许在cp 的开头附近给出目标目录,而不是在结尾附近。来自man cp
   -t, --target-directory=DIRECTORY
         copy all SOURCE arguments into DIRECTORY
  • -- 标志告诉cp 将后面的所有内容解释为文件名,而不是标志,因此以--- 开头的文件不会混淆cp;你仍然需要这个,因为-/-- 字符由cp 解释,而任何其他特殊字符由shell 解释。

  • find -exec command {} + 变体本质上与 xargs 相同。来自man find

   -exec command {} +                                                     
         This  variant  of the -exec action runs the specified command on
         the selected files, but the command line is built  by  appending
         each  selected file name at the end; the total number of invoca‐
         matched  files.   The command line is built in much the same way
         that xargs builds its command lines.  Only one instance of  `{}'
         is  allowed  within the command, and (when find is being invoked
         from a shell) it should be quoted (for example, '{}') to protect
         it  from  interpretation  by shells.  The command is executed in
         the starting directory.  If any invocation  returns  a  non-zero
         value  as exit status, then find returns a non-zero exit status.
         If find encounters an error, this can sometimes cause an immedi‐
         ate  exit, so some pending commands may not be run at all.  This
         variant of -exec always returns true.

通过在 find 中直接使用它,这避免了管道或 shell 调用的需要,这样您就不必担心文件名中的任何讨厌的字符。

【讨论】:

  • 惊人的发现,我不知道! " -exec utility [argument ...] {} + 与 -exec 相同,除了 ``{}'' 被替换为每次调用实用程序时尽可能多的路径名。此行为类似于 xargs(1 )。”在 BSD 实现中。
【解决方案2】:

只是不要使用xargs。这是一个简洁的程序,但在遇到不平凡的情况时,它与find 配合得并不好。

这是一种可移植 (POSIX) 解决方案,即不需要 findxargscp GNU 特定扩展的解决方案:

find . -name "*FooBar*" -exec sh -c 'cp -- "$@" ~/foo/bar' sh {} +

注意结尾+,而不是更常见的;

这个解决方案:

  • 正确处理带有嵌入空格、换行符或任何外来字符的文件和目录。

  • 适用于任何 Unix 和 Linux 系统,即使是那些不提供 GNU 工具包的系统。

  • 不使用xargs,这是一个不错且有用的程序,但需要进行太多调整和非标准功能才能正确处理find 输出。

  • 也比公认的和大多数(如果不是全部)其他答案更有效(阅读更快)。

还请注意,尽管在其他一些回复或 cmets 中引用了 {} 的说明是无用的(除非您使用的是异国情调的 fishshell)。

【讨论】:

【解决方案3】:

对我来说,我试图做一些不同的事情。我想将我的 .txt 文件复制到我的 tmp 文件夹中。 .txt 文件名包含空格和撇号字符。这适用于我的 Mac。

$ find . -type f -name '*.txt' | sed 's/'"'"'/\'"'"'/g' | sed 's/.*/"&"/'  | xargs -I{} cp -v {} ./tmp/

【讨论】:

    【解决方案4】:

    我玩了一点,开始考虑修改 xargs,并意识到对于我们在这里讨论的那种用例,在 Python 中简单地重新实现是一个更好的主意。

    一方面,整个事情有大约 80 行代码意味着很容易弄清楚发生了什么,如果需要不同的行为,您可以在比它更短的时间内将其破解成一个新脚本需要在 StackOverflow 之类的地方得到回复。

    请参阅 https://github.com/johnallsup/jda-misc-scripts/blob/master/yargshttps://github.com/johnallsup/jda-misc-scripts/blob/master/zargs.py

    使用编写好的 yargs(并安装 Python 3),您可以输入:

    find .|grep "FooBar"|yargs -l 203 cp --after ~/foo/bar
    

    一次复制 203 个文件。 (当然,这里的 203 只是一个占位符,使用像 203 这样的奇怪数字可以清楚地表明这个数字没有其他意义。)

    如果您真的想要更快且不需要 Python,请将 zargs 和 yargs 作为原型并用 C++ 或 C 重写。

    【讨论】:

      【解决方案5】:

      使用 Bash(不是 POSIX),您可以使用进程替换来获取变量中的当前行。这使您可以使用引号来转义特殊字符:

      while read line ; do cp "$line" ~/bar ; done < <(find . | grep foo)
      

      【讨论】:

        【解决方案6】:

        此方法适用于Mac OS X v10.7.5(狮子):

        find . | grep FooBar | xargs -I{} cp {} ~/foo/bar
        

        我还测试了您发布的确切语法。这在 10.7.5 上也运行良好。

        【讨论】:

        • 这行得通,但-I 暗示-L 1(手册上这么说),这意味着 cp 命令每个文件运行一次 = v 慢。
        • xargs -J % cp % 在 OSX 上可能更有效。
        • 抱歉,这是错误的。首先,它准确地产生了 TO 想要避免的错误。您必须使用 find ... -print0xargs -0 来解决 xargs 的“默认情况下引号是特殊的”。其次,通常在传递给 xargs 的命令中使用'{}' 而不是{},以防止出现空格和特殊字符。
        • 对不起 Andreas Spindler,我对 xargs 不太熟悉,经过一些实验后发现了这条线。它似乎适用于大多数评论过它并支持它的人。您介意更详细地了解它会产生什么样的错误吗?另外,您介意发布您认为更正确的确切输入吗?谢谢。
        • 这是我在 MacOS 10.15 上工作的 xargs 部分:xargs -0 -J % cp -v % /foo/bar
        【解决方案7】:

        我用Bill Star's answer在Solaris上稍作修改:

        find . -mtime +2 | perl -pe 's{^}{\"};s{$}{\"}' > ~/output.file
        

        这将在每一行加上引号。我没有使用“-l”选项,尽管它可能会有所帮助。

        我要去的文件列表可能有'-',但没有换行符。我没有将输出文件与任何其他命令一起使用,因为我想在开始通过 xargs 大量删除它们之前查看找到的内容。

        【讨论】:

          【解决方案8】:

          bill_starr's Perl version 不适用于嵌入式换行符(仅处理空格)。对于那些在例如没有 GNU 工具的 Solaris,可能是更完整的版本(使用 sed)...

          find -type f | sed 's/./\\&/g' | xargs grep string_to_find
          

          根据需要调整 find 和 grep 参数或其他命令,但 sed 将修复嵌入的换行符/空格/制表符。

          【讨论】:

            【解决方案9】:

            我发现下面的语法很适合我。

            find /usr/pcapps/ -mount -type f -size +1000000c | perl -lpe ' s{ }{\\ }g ' | xargs ls -l | sort +4nr | head -200
            

            在本例中,我在挂载在“/usr/pcapps”的文件系统中查找超过 1,000,000 字节的最大 200 个文件。

            “find”和“xargs”之间的 Perl 行转义/引用每个空格,因此“xargs”将任何带有嵌入空格的文件名作为单个参数传递给“ls”。

            【讨论】:

              【解决方案10】:

              如果你使用 Bash,你可以通过mapfilestdout 转换为行数组:

              find . | grep "FooBar" | (mapfile -t; cp "${MAPFILE[@]}" ~/foobar)
              

              好处是:

              • 它是内置的,因此速度更快。
              • 一次性使用所有文件名执行命令,速度更快。
              • 您可以将其他参数附加到文件名。对于cp,您还可以:

                find . -name '*FooBar*' -exec cp -t ~/foobar -- {} +
                

                但是,有些命令没有这样的功能。

              缺点:

              • 如果文件名太多,可能无法很好地扩展。 (限制?我不知道,但我在 Debian 下测试了 10 MB 的列表文件,其中包含 10000 多个文件名,没有问题)

              嗯...谁知道 Bash 在 OS X 上是否可用?

              【讨论】:

                【解决方案11】:

                您可以将所有这些组合成一个 find 命令:

                find . -iname "*foobar*" -exec cp -- "{}" ~/foo/bar \;
                

                这将处理带有空格的文件名和目录。您可以使用-name 获得区分大小写的结果。

                注意:传递给cp-- 标志阻止它处理以- 开头的文件作为选项。

                【讨论】:

                • 人们使用 xargs 是因为通常每次使用 200 个参数调用可执行文件 5 次要比每次使用一个参数调用 1000 次要快。
                • Chris Jester-Young 的答案应该是那里的“好答案”......顺便说一句,如果文件名以“-”开头,此解决方案将不起作用。至少,cp后面需要“--”。
                • 速度示例——超过 829 个文件,“find -exec”方法耗时 26 秒,而“find -print0 | xargs --null”方法工具耗时 0.7 秒。显着差异。
                • @tzot 一个迟到的评论,但无论如何,xargs 不需要解决您所描述的问题,find 已经通过 -exec + 标点符号支持它。
                • 没有回答如何处理空格的问题
                【解决方案12】:

                我围绕“xargs”创建了一个名为“xargsL”的小型便携式包装脚本,它解决了大部分问题。

                与 xargs 不同,xargsL 每行接受一个路径名。路径名可以包含除(显然)换行符或 NUL 字节之外的任何字符。

                文件列表中不允许或不支持引用 - 您的文件名可能包含各种空格、反斜杠、反引号、shell 通配符等 - xargsL 会将它们作为文字字符处理,不会造成任何伤害。

                作为一个额外的附加功能,如果没有输入,xargsL 将运行该命令一次!

                注意区别:

                $ true | xargs echo no data
                no data
                
                $ true | xargsL echo no data # No output
                

                任何提供给 xargsL 的参数都将传递给 xargs。

                这是“xargsL”POSIX shell 脚本:

                #! /bin/sh
                # Line-based version of "xargs" (one pathname per line which may contain any
                # amount of whitespace except for newlines) with the added bonus feature that
                # it will not execute the command if the input file is empty.
                #
                # Version 2018.76.3
                #
                # Copyright (c) 2018 Guenther Brunthaler. All rights reserved.
                #
                # This script is free software.
                # Distribution is permitted under the terms of the GPLv3.
                
                set -e
                trap 'test $? = 0 || echo "$0 failed!" >& 2' 0
                
                if IFS= read -r first
                then
                        {
                                printf '%s\n' "$first"
                                cat
                        } | sed 's/./\\&/g' | xargs ${1+"$@"}
                fi
                

                将脚本放入 $PATH 中的某个目录,不要忘记

                $ chmod +x xargsL

                那里的脚本使其可执行。

                【讨论】:

                  【解决方案13】:

                  最简单的方法就是将分隔符从任何空格更改为行尾字符,如下所示:

                  find whatever ... | xargs -d "\n" cp -t /var/tmp
                  

                  【讨论】:

                  • 这个答案简单、有效且直截了当:为 xargs 设置的默认分隔符太宽泛,需要针对 OP 想要做的事情进行缩小。我知道这是第一手资料,因为我今天在做类似的事情时遇到了同样的问题,除了在 cygwin 中。如果我阅读了 xargs 命令的帮助,我可能会避免一些头痛,但您的解决方案为我解决了这个问题。谢谢 ! (是的,OP 在 MacOS 上使用 BSD xargs,我不使用,但我希望 xargs“-d”参数存在于所有版本中)。
                  • 不错的答案,但不适用于 Mac。相反,我们可以将 find 传递到 sed -e 's_\(.*\)_"\1"_g' 以强制在文件名周围加上引号
                  • 这应该是公认的答案。问题是关于使用xargs
                  • 我收到xargs: illegal option -- d
                  • 值得指出的是,在许多 *nix 系统上,文件名可以包含换行符。你不太可能在野外遇到这种情况,但如果你在不受信任的输入上运行 shell 命令,这可能是一个问题。
                  【解决方案14】:

                  如果您的系统上的 find 和 xarg 版本不支持 -print0-0 开关(例如 AIX find 和 xargs),您可以使用这个看起来很糟糕的代码:

                   find . -name "*foo*" | sed -e "s/'/\\\'/g" -e 's/"/\\"/g' -e 's/ /\\ /g' | xargs cp /your/dest
                  

                  这里 sed 将负责转义 xargs 的空格和引号。

                  在 AIX 5.3 上测试

                  【讨论】:

                    【解决方案15】:

                    find . -print0 | grep --null 'FooBar' | xargs -0 ...

                    我不知道grep是否支持--null,也不知道xargs是否支持-0,在Leopard上,但在GNU上一切都好。

                    【讨论】:

                    • Leopard 确实支持“-Z”(它是 GNU grep),当然 find(1) 和 xargs(1) 也支持“-0”。
                    • 在 OS X 10.9 grep -{z|Z} 上表示“表现为 zgrep”(解压缩),而不是预期的“在每个文件名后打印一个零字节”。使用grep --null实现后者。
                    • find . -name 'FooBar' -print0 | xargs -0 ... 有什么问题?
                    • @QuentinPradet 显然,对于像“FooBar”这样的固定字符串,-name-path 工作得很好。 OP 已经指定使用grep,大概是因为他们想使用正则表达式过滤列表。
                    • @Hi-Angel 这正是为什么我使用xargs -0 结合 find -print0。后者打印带有 NUL 终止符的文件名,而前者以这种方式接收文件。为什么? Unix 中的文件名可以包含换行符。但它们不能包含 NUL 字符。
                    【解决方案16】:

                    对于那些依赖命令的人,而不是find,例如ls

                    find . | grep "FooBar" | tr \\n \\0 | xargs -0 -I{} cp "{}" ~/foo/bar
                    

                    【讨论】:

                    • 工作但很慢,因为-I 暗示-L 1
                    【解决方案17】:

                    您可能需要 grep Foobar 目录,例如:

                    find . -name "file.ext"| grep "FooBar" | xargs -i cp -p "{}" .
                    

                    【讨论】:

                    • 根据手册页,-i 已被弃用,而应使用-I
                    【解决方案18】:

                    我遇到了同样的问题。以下是我的解决方法:

                    find . -name '*FoooBar*' | sed 's/.*/"&"/' | xargs cp ~/foo/bar
                    

                    我使用sed 将输入的每一行替换为同一行,但用双引号括起来。在sed 手册页中,“...出现在替换中的 & 符号 (``&'') 被替换为匹配 RE...的字符串 ...”——在这种情况下, .*,整行。

                    这解决了xargs: unterminated quote 错误。

                    【讨论】:

                    • 我在 windows 上使用 gnuwin32,所以我必须使用 sed s/.*/\"&amp;\"/ 才能让它工作。
                    • 是的,但大概这不会处理带有" in 的文件名 - 除非 sed 也引用引号?
                    • 使用sed 是天才,现在是无需重写问题的正确解决方案!
                    【解决方案19】:
                    find | perl -lne 'print quotemeta' | xargs ls -d
                    

                    我相信这对于除换行符之外的任何字符都可以可靠地工作(而且我怀疑如果您的文件名中有换行符,那么您遇到的问题比这更糟糕)。它不需要 GNU findutils,只需要 Perl,所以它几乎可以在任何地方工作。

                    【讨论】:

                    • 文件名中是否可以有换行符?没听说过。
                    • 确实如此。试试,例如,mkdir test &amp;&amp; cd test &amp;&amp; perl -e 'open $fh, "&gt;", "this-file-contains-a-\n-here"' &amp;&amp; ls | od -tx1
                    • |perl -lne 'print quotemeta' 正是我一直在寻找的。这里的其他帖子对我没有帮助,因为我需要使用grep -rl 来将 PHP 文件的数量大大减少到仅受恶意软件感染的文件,而不是 find
                    • perl 和 quotemeta 远比 print0/-0 更通用 - 感谢使用空格流水线文件的通用解决方案
                    【解决方案20】:

                    请注意,其他答案中讨论的大多数选项在不使用 GNU 实用程序的平台(例如 Solaris、AIX、HP-UX)上不是标准的。请参阅 POSIX 规范以了解“标准”xargs 行为。

                    我还发现 xargs 的行为,即使没有输入,它也会至少运行一次命令,这很麻烦。

                    我编写了自己的私有版本的 xargs (xargl) 来处理名称中的空格问题(只有换行符分开 - 尽管 'find ... -print0' 和 'xargs -0' 的组合非常简洁,因为文件名不能包含 ASCII NUL '\0' 字符。我的 xargl 并不像值得发布所需的那样完整 - 特别是因为 GNU 具有至少一样好的设施。

                    【讨论】:

                    • GitHub 或者它没有发生
                    • @CoreyGoldberg:我猜当时没有发生。
                    • POSIX find 首先不需要xargs(11 年前就已经如此了)。
                    【解决方案21】:

                    这样更高效,因为它不会多次运行“cp”:

                    find -name '*FooBar*' -print0 | xargs -0 cp -t ~/foo/bar
                    

                    【讨论】:

                    • 这对我不起作用。它试图 cp ~/foo/bar 进入你找到的任何东西,但不是相反
                    • cp 的 -t 标志是一个 GNU 扩展,AFAIK,并且在 OS X 上不可用。但如果是,它将按此答案所示工作。
                    • 我使用的是 Linux。感谢您的“-t”开关。这就是我所缺少的:-)
                    【解决方案22】:

                    研究在 find 中使用带有 -print0 选项的 xargs --null 命令行选项。

                    【讨论】:

                      猜你喜欢
                      • 2012-09-30
                      • 2013-05-21
                      • 2011-02-02
                      • 2022-12-22
                      • 1970-01-01
                      • 1970-01-01
                      • 2016-01-11
                      • 2023-04-01
                      相关资源
                      最近更新 更多