了解 `find` 的 -exec 选项

Zso*_*agy 116 shell find

我发现自己不断地查找语法

find . -name "FILENAME"  -exec rm {} \;
Run Code Online (Sandbox Code Playgroud)

主要是因为我不明白这-exec部分是如何工作的。大括号、反斜杠和分号的含义是什么?该语法还有其他用例吗?

Kus*_*nda 176

这个答案来自以下部分:

  • 基本用法 -exec
  • 使用-exec结合sh -c
  • 使用 -exec ... {} +
  • 使用 -execdir

基本用法 -exec

-exec选项采用带有可选参数的外部实用程序作为其参数并执行它。

如果字符串{}出现在给定命令的任何位置,它的每个实例都将被当前正在处理的路径名替换(例如./some/path/FILENAME)。在大多数 shell 中,这两个字符{}不需要被引用。

该命令需要以;forfind结束才能知道它在哪里结束(因为之后可能有更多选项)。为了保护;免受shell 的影响,它需要被引用为\;or ';',否则 shell 会将其视为find命令的结尾。

示例(\前两行的末尾仅用于续行):

find . -type f -name '*.txt'      \
   -exec grep -q 'hello' {} ';'   \
   -exec cat {} ';'
Run Code Online (Sandbox Code Playgroud)

这将找到-type f名称与*.txt当前目录中或当前目录下的模式匹配的所有常规文件 ( ) 。然后它将测试该字符串是否hello出现在任何找到的文件中grep -q(它不产生任何输出,只是一个退出状态)。对于那些包含字符串的文件,cat将执行以将文件内容输出到终端。

每个-exec也都像对由 找到的路径名进行“测试” find,就像-type-name一样。如果命令返回零退出状态(表示“成功”),find则考虑命令的下一部分,否则find命令继续使用下一个路径名。这在上面的示例中用于查找包含 string 的文件hello,但忽略所有其他文件。

上面的例子说明了两个最常见的用例-exec

  1. 作为进一步限制搜索的测试。
  2. 对找到的路径名执行某种操作(通常,但不一定,在find命令的末尾)。

使用-exec结合sh -c

-exec可以执行的命令仅限于带有可选参数的外部实用程序。直接使用 shell 内置函数、函数、条件、管道、重定向等-exec是不可能的,除非包装在像sh -c子 shell 之类的东西中。

如果bash需要功能,则使用bash -c代替sh -c.

sh -c/bin/sh使用命令行上给出的脚本运行,后跟该脚本的可选命令行参数。

一个单独使用sh -c的简单例子,没有find

sh -c 'echo  "You gave me $1, thanks!"' sh "apples"
Run Code Online (Sandbox Code Playgroud)

这将两个参数传递给子 shell 脚本。这些将被放置在脚本中$0$1供脚本使用。

  1. 字符串sh。这将$0在脚本内部可用,如果内部 shell 输出错误消息,它将使用此字符串作为前缀。

  2. 该参数在脚本中apples可用$1,如果有更多参数,则这些参数将作为 可用$2$3等等。它们也将在列表中可用"$@"(除了$0不属于 的一部分"$@")。

这与 结合起来很有用,-exec因为它允许我们制作任意复杂的脚本,这些脚本作用于find.

示例:查找所有具有特定文件名后缀的常规文件,并将该文件名后缀更改为其他一些后缀,其中后缀保存在变量中:

from=text  #  Find files that have names like something.text
to=txt     #  Change the .text suffix to .txt

find . -type f -name "*.$from" -exec sh -c 'mv "$3" "${3%.$1}.$2"' sh "$from" "$to" {} ';'
Run Code Online (Sandbox Code Playgroud)

在内部脚本中,$1将是 string text$2将是字符串txt$3并将是find为我们找到的任何路径名。参数扩展${3%.$1}将采用路径名并.text从中删除后缀。

或者,使用dirname/ basename

find . -type f -name "*.$from" -exec sh -c '
    mv "$3" "$(dirname "$3")/$(basename "$3" ".$1").$2"' sh "$from" "$to" {} ';'
Run Code Online (Sandbox Code Playgroud)

或者,在内部脚本中添加变量:

find . -type f -name "*.$from" -exec sh -c '
    from=$1; to=$2; pathname=$3
    mv "$pathname" "$(dirname "$pathname")/$(basename "$pathname" ".$from").$to"' sh "$from" "$to" {} ';'
Run Code Online (Sandbox Code Playgroud)

请注意,在这最后的变动,变量from以及to在子shell是从外部脚本中的相同名称的变量不同。

以上是从-execwith调用任意复杂脚本的正确方法findfind在循环中使用

for pathname in $( find ... ); do
Run Code Online (Sandbox Code Playgroud)

容易出错且不优雅(个人意见)。它在空格上拆分文件名,调用文件名通配符,并且还强制 shellfind在运行循环的第一次迭代之前扩展完整结果。

也可以看看:


使用 -exec ... {} +

所述;在端部可以通过替换+。这会导致find使用尽可能多的参数(找到的路径名)执行给定的命令,而不是为每个找到的路径名执行一次。 该字符串{} 必须出现在 之前+,它才能工作

find . -type f -name '*.txt' \
   -exec grep -q 'hello' {} ';' \
   -exec cat {} +
Run Code Online (Sandbox Code Playgroud)

在这里,find将收集生成的路径名并一次执行cat尽可能多的路径名。

find . -type f -name "*.txt" \
   -exec grep -q "hello" {} ';' \
   -exec mv -t /tmp/files_with_hello/ {} +
Run Code Online (Sandbox Code Playgroud)

同样在这里,mv将尽可能少地执行。最后一个示例需要mv来自 coreutils 的GNU (支持该-t选项)。

使用-exec sh -c ... {} +也是使用任意复杂脚本循环一组路径名的有效方法。

基本原理与使用时相同-exec sh -c ... {} ';',但脚本现在需要更长的参数列表。这些可以通过"$@"在脚本内部循环来循环。

我们上一节中更改文件名后缀的示例:

from=text  #  Find files that have names like something.text
to=txt     #  Change the .text suffix to .txt

find . -type f -name "*.$from" -exec sh -c '
    from=$1; to=$2
    shift 2  # remove the first two arguments from the list
             # because in this case these are *not* pathnames
             # given to us by find
    for pathname do  # or:  for pathname in "$@"; do
        mv "$pathname" "${pathname%.$from}.$to"
    done' sh "$from" "$to" {} +
Run Code Online (Sandbox Code Playgroud)

使用 -execdir

还有-execdir(由大多数find变体实现,但不是标准选项)。

-exec与不同之处在于,给定的 shell 命令使用找到的路径名的目录作为其当前工作目录执行,并且{}将包含找到的路径名的基本名称而不包含其路径(但 GNUfind仍将基本名称前缀为./,而 BSDfindsfind不会)。

例子:

find . -type f -name '*.txt' \
    -execdir mv -- {} 'done-texts/{}.done' \;
Run Code Online (Sandbox Code Playgroud)

这会将每个找到的*.txt-file移动到与找到文件的位置相同的目录中的预先存在的done-texts子目录。该文件也将通过向其添加后缀来重命名。, 在这些实现中需要标记选项的结尾,而这些实现不以. 围绕包含参数两边的引号不是作为一个整体需要,如果你的shell是。另请注意,并非所有实现都会在那里扩展(不会)。.done--find./{}(t)cshfind{}sfind

这会有点棘手,-exec因为我们必须从找到的文件的基本名称中取出来{}形成文件的新名称。我们还需要目录名称 from{}done-texts正确定位目录。

有了-execdir,像这样的事情变得更容易了。

使用-exec代替的相应操作-execdir必须使用子外壳:

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "$( dirname "$name" )/done-texts/$( basename "$name" ).done"
    done' sh {} +
Run Code Online (Sandbox Code Playgroud)

或者,

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "${name%/*}/done-texts/${name##*/}.done"
    done' sh {} +
Run Code Online (Sandbox Code Playgroud)

  • `-exec` 接受一个 _program 和 arguments_ 并运行它;_some_ shell 命令只包含一个程序和参数,但很多不包含。shell 命令可以包括重定向和管道;`-exec` 不能(尽管整个 `find` 可以重定向)。shell 命令可以使用`; && if` 等;`-exec` 不能,尽管 `-a -o` 可以做一些。shell 命令可以是别名或 shell 函数,也可以是内置命令;`-exec` 不能。shell 命令可以扩展变量;`-exec` 不能(尽管运行 `find` 的外壳可以)。一个 shell 命令每次都可以用不同的方式替换 `$(command)`;`-exec` 不能。... (8认同)
  • 在这里说它是一个 shell 命令是错误的,`find -exec cmd arg \;` 不会调用一个 shell 来解释一个 shell 命令行,它直接运行 `execlp("cmd", "arg")`,而不是 `execlp ("sh", "-c", "cmd arg")`(如果 `cmd` 不是内置的,shell 最终会执行与 `execlp("cmd", "arg")` 等效的操作)。 (2认同)
  • 您可以澄清在 `-exec` 之后以及直到 `;` 或 `+` 的所有 `find` 参数组成了要与其参数一起执行的命令,并且将 `{}` 参数的每个实例替换为当前的文件(带有`;`),并且`{}` 作为`+` 之前的最后一个参数,替换为作为单独参数的文件列表(在`{} +` 的情况下)。IOW `-exec` 接受 _several_ 参数,以 `;` 或 `{}` `+` 结尾。 (2认同)