bash 如何将 shell 命令应用于嵌套(并且转义不佳)子目录中的许多文件?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/753823/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-17 20:49:05  来源:igfitidea点击:

How do I apply a shell command to many files in nested (and poorly escaped) subdirectories?

bashshellfindfor-loopescaping

提问by Varkhan

I'm trying to do something like the following:

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

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

But the command isn't working because $file is very odd. Because my directory tree has crappy file names (including spaces), I need to escape the findcommand. But none of the obvious escapes seem to work: -lsgives me the space-delimited filename fragments -fprintdoesn't do any better.

但是该命令不起作用,因为 $file 很奇怪。因为我的目录树有蹩脚的文件名(包括空格),我需要转义find命令。但是明显的转义似乎都不起作用: -ls给我以空格分隔的文件名片段 -fprint并没有做得更好。

I also tried: for file in "find . *.foo -ls"; do echo $file; done - but that gives all of the responses from find in one long line.

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

Any hints? I'm happy for any workaround, but am frustrated that I can't figure this out.

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

Thanks, Alex

谢谢,亚历克斯

(Hi Matt!)

(嗨,马特!)

回答by lhunath

You have plenty of answers that explain well how to do it; but for the sake of completion I'll repeat and add to it:

你有很多答案可以很好地解释如何去做;但为了完成起见,我将重复并添加:

xargsis only ever useful for interactive use (when you know all your filenames are plain - no spaces or quotes) or when used with the -0option. Otherwise, it'll break everything.

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

findis a very useful tool; put using it to pipe filenames into xargs(even with -0) is rather convoluted as findcan do it all itself with either -exec command {} \;or -exec command {} +depending on what you want:

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

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

The former runs somecommandwith one argumentfor each file recursively in /paththat matches pattern.

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

The latter runs somecommandwith as many arguments as fit on the command lineat once for files recursively in /paththat match pattern.

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

Which one to use depends on somecommand. If it can take multiple filename arguments (like rm, grep, etc.) then the latter option is faster (since you run somecommandfar less often). If somecommandtakes only one argument then you need the former solution. So look at somecommand's man page.

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

More on find: http://mywiki.wooledge.org/UsingFind

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

In bash, foris a statement that iterates over arguments. If you do something like this:

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

for foo in "$bar"

you're giving foroneargument to iterate over (note the quotes!). If you do something like this:

你给了for一个参数来迭代(注意引号!)。如果你做这样的事情:

for foo in $bar

you're asking bashto take the contents of barand tear it apart wherever there are spaces, tabs or newlines (technically, whatever characters are in IFS) and use the pieces of that operation as arguments to for. That is NOT filenames. Assuming that the result of a tearing long string that contains filenames apart wherever there is whitespace yields in a pile of filenames is just wrong. As you have just noticed.

你问bash取的内容bar,并把它拆开哪里有空格,制表符或换行符(在技术上,无论字符是IFS),并使用该操作的片作为参数传递给了。 那不是文件名。假设在一堆文件名中产生包含空格的长字符串的结果是错误的。正如你刚刚注意到的那样。

The answer is: Don't use for, it's obviously the wrong tool. The above findcommands all assume that somecommandis an executable in PATH. If it's a bashstatement, you'll need this construct instead (iterates over find's output, like you tried, but safely):

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

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

This uses a while-readloop that reads parts of the string findoutputs until it reaches a NULLbyte (which is what -print0uses to separate the filenames). Since NULLbytes can't be part of filenames (unlike spaces, tabs and newlines) this is a safe operation.

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

If you don't need somebashstatementto be part of your script (eg. it doesn't change the script environment by keeping a counter or setting a variable or some such) then you can still use find's -execto run your bashstatement:

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

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

Here, the -execexecutes a bashcommand with three or more arguments.

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

  1. The bash statement to execute.
  2. A --. bashwill put this in $0, you can put anything you like here, really.
  3. Your filename or filenames (depending on whether you used {} \;or {} +respectively). The filename(s) end(s) up in $1(and $2, $3, ... if there's more than one, of course).
  1. 要执行的 bash 语句。
  2. 一个--bash把这个放进去$0,你可以放任何你喜欢的东西,真的。
  3. 您的文件名或文件名(取决于您是否使用{} \;{} +分别使用)。文件名以$1(and $2, $3, ... 如果有多个,当然)结尾。

The bashstatement in the first findcommand here runs somebashstatementwith the filename as argument.

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

The bashstatement in the second findcommand here runs a for(!) loop that iterates over each positional parameter (that's what the reduced forsyntax - for foo; do- does) and runs a somebashstatementwith the filename as argument. The difference here between the very first findstatement I showed with -exec {} +is that we run only one bashprocess for lots of filenames but still one somebashstatementfor eachof those filenames.

此处bash第二个find命令中的语句运行for( !) 循环,该循环遍历每个位置参数(这就是简化for语法 - for foo; do- 所做的)并somebashstatement以文件名作为参数运行 a 。find我展示的第一个语句之间的区别-exec {} +在于,我们只bash为许多文件名运行一个进程,但仍然somebashstatement每个文件名运行一个进程。

All this is also well explained in the UsingFindpage linked above.

所有这些也在UsingFind上面链接的页面中得到了很好的解释。

回答by Varkhan

Instead of relying on the shell to do that work, rely on find to do it:

与其依赖 shell 来完成这项工作,不如依赖 find 来完成:

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

Then the file name will be properly escaped, and never interpreted by the shell.

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

回答by Tanktalus

find . -name '*.foo' -print0 | xargs -0 -n 1 somecommand

It does get messy if you need to run a number of shell commands on each item, though.

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

回答by andrewdotn

find . -name '*.foo' -print0 | xargs -0 sh -c 'for F in "${@}"; do ...; done' "
#!/bin/bash
IFS=$'\n'
function RecurseDirs
{
for f in "$@"
do
  newf=echo "${f}" | sed -e 's/[\/:\*\?#"\|<>]/_/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 .
"

回答by Alister Bulman

xargsis your friend. You will also want to investigate the -0 (zero) option with it. find(with -print0) will help to produce the list. The Wikipedia page has some good examples.

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

Another useful reason to use xargs, is that if you have many files (dozens or more), xargs will split them up into individual calls to whatever xargs is then called upon to run (in the first wikipedia example, rm)

使用的另一个有用的原因xargs是,如果您有许多文件(数十个或更多),xargs 会将它们拆分为单独的调用,然后调用任何 xargs 来运行(在第一个维基百科示例中,rm

回答by dreynold

I had to do something similar some time ago, renaming files to allow them to live in Win32 environments:

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

##代码##

This is probably a little simplistic, doesn't avoid name collisions, and I'm sure it could be done better -- but this does remove the need to use basename on the find results (in my case) before performing my sed replacement.

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

I might ask, what are you doing to the found files, exactly?

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