【问题标题】:perform an operation for *each* item listed by grep对 grep 列出的 *each* 项目执行操作
【发布时间】:2012-03-29 18:38:05
【问题描述】:

如何对 grep 单独列出的每个项目执行操作?

背景:

我使用 grep 列出所有包含某种模式的文件:

grep -l '<pattern>' directory/*.extension1

我想删除所有列出的文件但也所有文件名相同但扩展名不同的文件:.extension2

我尝试使用管道,但它似乎将 grep 的输出作为一个整体。

在 find 中有 -exec 选项,但 grep 没有这样的选项。

【问题讨论】:

    标签: bash list shell grep exec


    【解决方案1】:

    如果我理解您的规范,您希望:

    grep --null -l '<pattern>' directory/*.extension1 | \
        xargs -n 1 -0 -I{} bash -c 'rm "$1" "${1%.*}.extension2"' -- {}
    

    这与@triplee 的评论所描述的基本相同,只是它是换行安全的。

    这是怎么回事?

    grep--null 将返回以空值而不是换行符分隔的输出。由于文件名中可以​​有换行符,用换行符分隔使得无法安全地解析 grep 的输出,但 null 不是文件名中的有效字符,因此是一个很好的分隔符。

    xargs 将采用一系列以换行符分隔的项目并执行给定命令,将尽可能多的这些项目(每个参数一个)传递给给定命令(如果没有给出命令,则传递给echo)。因此,如果你说:

    printf 'one\ntwo three \nfour\n' | xargs echo
    

    xargs 将执行 echo one 'two three' four。这对于文件名是不安全的,因为文件名可能包含嵌入的换行符。

    -0 切换到 xargs 将其从寻找换行符分隔符更改为空分隔符。这使它与我们从grep --null 得到的输出相匹配,并且可以安全地处理文件名列表。

    通常xargs 只是将输入附加到命令的末尾。 -I 切换到xargs 将其更改为用输入替换指定的替换字符串。要了解这个想法,请尝试以下实验:

    printf 'one\ntwo three \nfour\n' | xargs -I{} echo foo {} bar
    

    并注意与之前的printf | xargs 命令的区别。

    在我的解决方案中,我执行的命令是bash,我将-c 传递给它。 -c 开关使 bash 执行以下参数中的命令(然后终止),而不是启动交互式 shell。下一个块'rm "$1" "${1%.*}.extension2"'-c 的第一个参数,是bash 将执行的脚本。 -c 的脚本参数后面的任何参数都被指定为脚本的参数。这个,如果我要说:

    bash -c 'echo $0' "Hello, world"
    

    然后Hello, world 将被分配给$0(脚本的第一个参数),并且在脚本中我可以将echo 返回。

    因为$0 通常是为脚本名称保留的,所以我传递了一个虚拟值(在本例中为--)作为第一个参数,然后,我写了{} 来代替第二个参数,这是我为xargs 指定的替换字符串。这将被 xargs 替换,每个文件名在执行 bash 之前从 grep 的输出中解析。

    迷你 shell 脚本可能看起来很复杂,但其实很简单。首先,整个脚本是单引号的,以防止调用 shell 解释它。在脚本中,我调用rm 并将两个文件名传递给它以删除:$1 参数,这是在上面替换替换字符串时传递的文件名,以及${1%.*}.extension2。后者是$1 变量的参数替换。重要的部分是%.*,上面写着

    • % "从变量末尾开始匹配,去掉匹配模式的最短字符串。
    • .* 模式是一个句点,后面跟着任何东西。

    这有效地从文件名中删除扩展名(如果有)。你可以自己观察效果:

    foo='my file.txt'
    bar='this.is.a.file.txt'
    baz='no extension'
    printf '%s\n'"${foo%.*}" "${bar%.*}" "${baz%.*}"
    

    由于扩展名已被剥离,我将所需的备用扩展名.extension2 连接到剥离的文件名以获得备用文件名。

    【讨论】:

    • 这看起来不错,但是你能解释一下这些命令吗?我也想学点东西:)
    【解决方案2】:

    如果这符合您的要求,请通过 /bin/sh 管道输出。

    grep -l 'RE' folder/*.ext1 | sed 's/\(.*\).ext1/rm "&" "\1.ext2"/'
    

    或者如果 sed 让你发痒:

    grep -l 'RE' folder/*.ext1 | while read file; do
      echo rm "$file" "${file%.ext1}.ext2"
    done
    

    如果输出看起来像您要运行的命令,请删除 echo

    但您也可以使用 find 来做到这一点:

    find /path/to/start -name \*.ext1 -exec grep -q 'RE' {} \; -print | ...
    

    其中... 是 sed 脚本或从 whiledone 的三行代码。

    这里的想法是find 将...嗯,根据你给它的限定符“找到”东西——即,匹配文件 glob“*.ext”的东西,并且结果“执行”成功。 -q 告诉 grep 在 {}find 提供的文件)中查找 RE,并以 TRUE 或 FALSE 退出而不生成任何自己的输出。

    在 find 中执行此操作与使用 grep 执行此操作之间的唯一真正区别在于,如果需要,您可以使用 find 的出色条件集合来进一步缩小搜索范围。 man find 了解详情。默认情况下,find 会递归到子目录中。

    【讨论】:

      【解决方案3】:

      您可以将列表通过管道传输到 xargs:

      grep -l '<pattern>' directory/*.extension1 | xargs rm
      

      至于第二组具有不同扩展名的文件,我会这样做(在测试时像往常一样使用xargs echo rm 进行试运行;我还没有测试过,它可能无法与以下文件名一起正常工作其中的空格):

      filelist=$(grep -l '<pattern>' directory/*.extension1)
      echo $filelist | xargs rm
      echo ${filelist//.extension1/.extension2} | xargs rm
      

      【讨论】:

      • 如果您没有任何带有换行符的文件名,您可以执行类似 'grep -l 'pattern' directory/*.extension1 | sed 'p;s/\.extension1$/.extension2/' | xargs rm`
      • @tripleee:我同意,这可能更好!没想到。
      • 我会非常小心地将未引用的扩展和管道呼应到xargs rm - 您可能会意外炸毁某些东西。
      【解决方案4】:

      将结果传送到xargs,它将允许您为每个匹配运行一个命令。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-02-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-12-13
        • 2016-09-27
        相关资源
        最近更新 更多