【问题标题】:How do I apply a shell command to many files in nested (and poorly escaped) subdirectories?如何将 shell 命令应用于嵌套(并且转义不佳)子目录中的许多文件?
【发布时间】:2024-01-19 01:58:01
【问题描述】:

我正在尝试执行以下操作:

for file in `find . *.foo`
do
somecommand $file
done

但该命令不起作用,因为 $file 非常奇怪。因为我的目录树有糟糕的文件名(包括空格),所以我需要转义 find 命令。但所有明显的逃脱似乎都不起作用: -ls 给了我以空格分隔的文件名片段 -fprint 再好不过了。

我也试过:for file in "find。 *.foo -ls"; do echo $file; done - but that gives all of the responses from find in one long line.

有什么提示吗?我很高兴有任何解决方法,但我无法解决这个问题而感到沮丧。

谢谢, 亚历克斯

(嗨,马特!)

【问题讨论】:

  • 我假设你的意思是 'find . -名称“*.foo”'?否则'找到 . *.foo' 可能会给你带来奇怪的结果......
  • 如何使用 for 循环来做到这一点?我想以几种不同的方式提取一些文件名 - 大概这对于(非常)长的单行是可行的,但如果可能的话,我更喜欢更清晰的东西。
  • 老实说,我可能会尝试在 perl 中执行此操作 ;-)

标签: bash shell find for-loop escaping


【解决方案1】:

您有很多答案可以很好地解释如何做到这一点;但为了完整起见,我将重复并添加:

xargs 仅对交互使用有用(当您知道所有文件名都是纯格式 - 没有空格或引号时)或与 -0 选项一起使用时。否则,它会破坏一切。

find是一个非常有用的工具;使用它将文件名通过管道传输到xargs(即使使用-0)相当复杂,因为find 可以根据您的需要使用-exec command {} \;-exec command {} + 自行完成:

find /path -name 'pattern' -exec somecommand {} \;
find /path -name 'pattern' -exec somecommand {} +

前者运行somecommand 并在/path 中递归地为每个文件运行一个参数 匹配pattern

后者一次运行somecommand在命令行中使用尽可能多的参数,以递归方式在/path 中匹配pattern 的文件。

使用哪一个取决于somecommand。如果它可以采用多个文件名参数(如rmgrep 等),那么后一个选项会更快(因为您运行somecommand 的频率要低得多)。如果somecommand 只接受一个参数,那么您需要前一种解决方案。所以看看somecommand的手册页。

更多关于findhttp://mywiki.wooledge.org/UsingFind

bash 中,for 是一个迭代参数的语句。如果你这样做:

for foo in "$bar"

你正在给 for 一个 参数进行迭代(注意引号!)。如果你这样做:

for foo in $bar

您要求bash 获取bar 的内容并将其撕开,只要有空格、制表符或换行符(从技术上讲,是IFS 中的任何字符)并使用该操作的各个部分作为参数为。 这不是文件名。假设在一堆文件名中存在空格的地方将包含文件名的长字符串撕裂的结果是错误的。正如你刚刚注意到的那样。

答案是:不要使用for,这显然是错误的工具。以上find 命令都假定somecommandPATH 中的可执行文件。如果它是 bash 语句,则您将需要此构造(迭代 find 的输出,就像您尝试过的一样,但安全):

while read -r -d ''; do
    somebashstatement "$REPLY"
done < <(find /path -name 'pattern' -print0)

这使用while-read 循环读取部分字符串find 输出,直到它到达NULL 字节(这是-print0 用于分隔文件名的字节)。由于NULL 字节不能是文件名的一部分(与空格、制表符和换行符不同),这是一个安全的操作。

如果您不需要 somebashstatement 成为脚本的一部分(例如,它不会通过保留计数器或设置变量等来更改脚本环境),那么您仍然可以使用 find' s -exec 运行您的 bash 语句:

find /path -name 'pattern' -exec bash -c 'somebashstatement "$1"' -- {} \;
find /path -name 'pattern' -exec bash -c 'for file; do somebashstatement "$file"; done' -- {} +

这里,-exec 执行带有三个或更多参数的 bash 命令。

  1. 要执行的 bash 语句。
  2. --bash 会把这个放在$0 里,你可以放任何你喜欢的东西,真的。
  3. 您的文件名或文件名(取决于您分别使用的是{} \; 还是{} +)。文件名以$1 结尾(当然还有$2$3……如果有不止一个的话)。

此处第一个find 命令中的bash 语句以文件名作为参数运行somebashstatement

此处第二个find 命令中的bash 语句运行for(!) 循环,该循环遍历每个位置参数(这就是简化的for 语法-@987654387 @ - 确实)并以文件名作为参数运行somebashstatement。我用-exec {} + 展示的第一个find 语句之间的区别在于,我们只为大量文件名运行一个bash 进程,但对于这些文件名中的每个 仍然运行一个somebashstatement

上面链接的UsingFind页面也很好地解释了所有这些。

【讨论】:

    【解决方案2】:

    不要依赖 shell 来完成这项工作,而是依靠 find 来完成:

    find . -name "*.foo" -exec somecommand "{}" \;
    

    然后文件名将被正确转义,并且永远不会被 shell 解释。

    【讨论】:

      【解决方案3】:
      find . -name '*.foo' -print0 | xargs -0 -n 1 somecommand
      

      但是,如果您需要在每个项目上运行多个 shell 命令,它确实会变得很混乱。

      【讨论】:

      • 实际上,您需要 -r (因此是 GNU find 或类似的)来精确模拟 for 循环。如果输入长度为 0,xargs 将执行不带参数的命令。
      • 或者你可以使用 find 的 -exec {} + 来避免卷积: find 。 -name '*.foo' -exec somecommand {} \;
      【解决方案4】:

      xargs 是你的朋友。您还需要使用它来研究 -0(零)选项。 find(和-print0)将有助于生成列表。*页面有一些很好的例子。

      使用xargs 的另一个有用原因是,如果您有很多文件(几十个或更多),xargs 会将它们拆分为单独调用,然后调用任何 xargs 来运行(在第一个*示例中,@ 987654325@)

      【讨论】:

      • "xargs 会将它们拆分为单独的调用"。或者,如果这是不可取的,就像在极少数情况下发生的那样,那么使用“-n1 -r”来获得与 for 循环相同的行为。 -r 是 GNU 扩展,-n 是 POSIX。
      • -1 因为 xargs 已损坏,除非与 -0 选项一起使用,并且将 -0 选项与 find 一起使用是非常愚蠢的,因为 find 本身有一个 -exec {} + 谓词做同样的事情.
      • '-0 |xargs' 比转义对 '-exec {}' 的调用更容易记住,但这会变得很复杂。
      【解决方案5】:
      find . -name '*.foo' -print0 | xargs -0 sh -c 'for F in "${@}"; do ...; done' "${0}"
      

      【讨论】:

        【解决方案6】:

        前段时间我不得不做类似的事情,重命名文件以允许它们存在于 Win32 环境中:

        #!/bin/bash
        IFS=$'\n'
        function RecurseDirs
        {
        for f in "$@"
        do
          newf=echo "${f}" | sed -e 's/[\\/:\*\?#"\|&lt;&gt;]/_/g'
          if [ ${newf} != ${f} ]; then
            echo "${f}" "${newf}"
            mv "${f}" "${newf}"
            f="${newf}"
          fi
          if [[ -d "${f}" ]]; then
            cd "${f}"
            RecurseDirs $(ls -1 ".")
          fi
        done
        cd ..
        }
        RecurseDirs .
        

        这可能有点简单,并不能避免名称冲突,我相信它可以做得更好——但这确实消除了在执行我之前在查找结果(在我的情况下)上使用 basename 的需要sed 替换。

        我可能会问,你到底对找到的文件做了什么?

        【讨论】: