了解“find”的 -exec 选项

了解“find”的 -exec 选项

我发现自己不断查找语法

find . -name "FILENAME"  -exec rm {} \;

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

答案1

这个答案分为以下几个部分:

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

基本用法-exec

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

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

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

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

find . -type f -name '*.txt'      \
   -exec grep -q 'hello' {} ';'   \
   -exec cat {} ';'

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

每个-exec都类似于对 找到的路径名的“测试” find,就像-type-name一样。如果命令返回零退出状态(表示“成功”),则find考虑命令的下一部分,否则find命令继续使用下一个路径名。这在上面的示例中用于查找包含字符串 的文件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"

这会将两个参数传递给子 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" {} ';'

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

或者,使用dirname/ basename

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

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

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" {} ';'

请注意,在最后一个变体中,子 shell 中的变量fromto与外部脚本中同名的变量不同。

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

for pathname in $( find ... ); do

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

也可以看看:


使用-exec ... {} +

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

find . -type f -name '*.txt' \
   -exec grep -q 'hello' {} ';' \
   -exec cat {} +

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

find . -type f -name "*.txt" \
   -exec grep -q "hello" {} ';' \
   -exec mv -t /tmp/files_with_hello/ {} +

同样,这里mv将被执行尽可能少的次数。最后一个示例需要mvcoreutils 中的 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" {} +

使用-execdir

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

其工作方式类似于-exec给定的 shell 命令以找到的路径名的目录作为其当前工作目录执行,并且{}将包含找到的路径名的基本名称而不包含其路径(但 GNUfind仍将在基本名称前加上 前缀./,而 BSDfindsfind不会)。

例子:

find . -type f -name '*.txt' \
    -execdir mv -- {} 'done-texts/{}.done' \;

这会将每个找到的*.txt文件移动到预先存在的done-texts子目录中在与找到该文件的目录相同的目录中。该文件还将通过添加后缀.done来重命名。 ,在那些不以 . 为基本名称前缀的实现--中,需要标记选项的结尾。如果您的 shell 是 ,则需要将包含 not 的参数作为整体括起来。另请注意,并非所有实现都会在那里扩展(不会)。find./{}(t)cshfind{}sfind

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

有了-execdir,一些事情就变得更容易了。

-exec使用相反 的相应操作-execdir必须使用子 shell:

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

或者,

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "${name%/*}/done-texts/${name##*/}.done"
    done' sh {} +

相关内容