了解`find`的-exec选项


53

我发现自己一直在寻找

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

主要是因为我看不到该-exec零件的工作原理。花括号,反斜杠和分号的含义是什么?该语法还有其他用例吗?


11
@Philippos:我明白你的意思。请记住,手册页是参考,即对那些了解此内容的人有用,以查找语法。对于刚接触该主题的人来说,他们通常是神秘而正式的才有用。您会发现,接受的答案是手册页条目长度的大约10倍,这是有原因的。
Zsolt Szilagy

6
甚至旧的POSIX man页面都读取了一个Utility_name或仅包含两个字符“ {}”的参数,也应替换为当前的路径名,这对我来说似乎足够了。另外,它还有一个示例-exec rm {} \;,就像您的问题一样。在我的时代,除了“大灰墙”以外,几乎没有其他资源可以打印书籍man(纸张比存储还便宜)。因此,我知道这对于刚接触该主题的人就足够了。您的最后一个问题是公平的。不幸的是,@ Kusalananda和我自己都没有答案。
Philippos

1
Comeon @Philippos。您是真的告诉Kusalananda他的手册页没有改善吗?:-)
Zsolt Szilagy

1
@allo尽管xargs有时很方便,find但如果没有它,可以将多个路径参数传递给命令。-exec command... {} +(使用+而不是\;)通过后将经过尽可能多的路径command...(每个操作系统对命令行的持续时间都有自己的限制)。就像一样xargs,以+终止的形式find-exec动作也将command...多次运行(在极少数情况下,由于路径太多而无法满足限制)。
卡根

2
@ZsoltSzilagy我既没有告诉过我,也不是那个意思。他确实很好地喂饱了你,我只是觉得你已经够大了可以自己吃饭。(-;
Philippos

Answers:


90

该答案分为以下几部分:

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

的基本用法 -exec

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

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

该命令需要与被终止;find知道其在此结束(因为可能有进一步的选项之后)。为了保护;外壳程序,请用\;或引起引用';',否则外壳程序会将其视为find命令的结尾。

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

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

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

每个对象-execfind-type和一样,-name对通过找到的路径名进行“测试” 。如果命令返回退出状态为零(表示“成功”),find则考虑命令的下一部分,否则find命令以下一个路径名继续。在上面的示例中,这用于查找包含字符串的文件hello,但忽略所有其他文件。

上面的示例说明了以下两个最常见的用例-exec

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

使用-exec结合sh -c

-exec可以执行的命令仅限于带有可选参数的外部实用程序。-exec不能直接使用shell的内置函数,条件,条件,管道,重定向等,除非将其包装在诸如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脚本:

  1. 字符串sh。这将$0在脚本内部提供,并且如果内部外壳输出错误消息,它将在此字符串之前添加前缀。

  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将是字符串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" {} ';'

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

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

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

以上是从调用任意复杂的脚本的正确方法-execfindfind像这样循环使用

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

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

也可以看看:


使用 -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(支持该-t选项)中的GNU 。

使用-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命令以找到的路径名的目录作为当前工作目录执行,并且{}将包含找到的路径名的基名(不带路径)(但是BNU find仍会以开头为基名./,而BSD find不会这样做)。

例:

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

这会将每个找到的*.txt文件移动到与找到文件所在目录相同的目录中的done-texts子目录。该文件也将通过添加后缀.done来重命名。

这将有些棘手,-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 {} +

要么,

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

7
-exec接受程序和参数并运行它;一些 shell命令仅由程序和参数组成,而许多则不包括。Shell命令可以包括重定向和管道。-exec不能(尽管整体find可以重定向)。一个shell命令可以使用; && ifetc; -exec尽管-a -o可以做一些,却不能。Shell命令可以是别名或Shell函数,也可以是内置命令。-exec不能。shell命令可以扩展vars。-exec不能(尽管运行find罐头的外壳)。Shell命令$(command)每次可以替换为不同的命令。-exec不能。...
dave_thompson_085'9

...一个shell命令可以遍历,-exec不能-尽管find可以像大多数遍历一样遍历文件,所以很少使用。
dave_thompson_085'9

@ dave_thompson_085当然,shell命令sh本身也可以,可以完全完成所有这些操作
Tavian Barnes

2
说这是一个shell命令是错误的,find -exec cmd arg \;它没有调用shell来解释shell命令行,而是execlp("cmd", "arg")直接运行,而不是execlp("sh", "-c", "cmd arg")(对于shell,execlp("cmd", "arg")如果cmd没有内置,它最终会执行与之等效的操作)。
斯特凡Chazelas

2
您可以弄清楚该命令find之后-exec,之后;+组成该命令要执行的所有参数,参数的每个实例都{}替换为当前文件(带有;),并且{}是最后一个参数+替换为文件列表之前的最后一个参数。作为单独的参数(在这种{} +情况下)。IOW -exec接受多个参数,以;或终止{} +
斯特凡Chazelas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.