【问题标题】:Count how many files contain a string in the last line计算有多少文件在最后一行包含一个字符串
【发布时间】:2017-05-28 11:02:05
【问题描述】:

我想统计当前目录有多少文件在最后一行有字符串"A"

第一个解决方案:tail -n 1 * | grep \"A\"| wc -l

这很好用,但是当有更多文件时它会bash: /usr/bin/tail: Argument list too long。 有没有办法绕过它?

如果我还可以选择获取 哪些 文件包含它,则可以加分。

编辑:我的文件夹包含 343729 个文件

EDIT2:@tso 在他的评论中有用地指出了文章I'm getting "Argument list too long". How can I process a large list in chunks?

结果:

@tso 解决方案for f in $(find . -type f); do tail -1 $f|grep \"A\"; done|wc -l 大约需要 20 分钟

@lars 解决方案grep -P "\"A\"*\Z" -r . | wc -l 大约需要 20 分钟

@mklement0 解决方案printf '%s\0' * | xargs -0 sh -c 'tail -q -n 1 "$@" | grep \"A\"' - | wc -l 大约需要 10 分钟

@james 解决方案(在 cmets 中)for i in * ; do awk 'END{if(/a/)print FILENAME}' "$i" ; done 大约需要 25 分钟

@codeforester find . -type f -exec tail -n 1 -- {} + | grep -EB 1 '^[^=]+A' | grep -c '^==>' 需要超过 20 分钟。

@mklement0 和@codeforester 解决方案还有一个优点,如果我想更改 grep 模式,我第二次运行它需要零时间,我猜这是由于某种缓存。

我已经接受了@mklement0 的答案似乎是最快的,但我仍然想提及@tso 和@lars 的贡献,并且根据我的个人知识,这是一个更简单且适应性强的解决方案。

【问题讨论】:

    标签: bash shell tail


    【解决方案1】:
    • xargs 能够克服最大值。通过有效地将调用批处理到尽可能少的调用中来限制命令行长度。

    • shell 的内置,例如printf受制于最大值。命令行长度。

    知道了这一点,您可以使用以下方法(假设您的 xargs 实现支持 NUL 终止输入的 -0 选项,并且您的 tail 实现支持多个文件操作数和 -q 选项用于抑制文件名标题。
    这两个假设都适用于这些实用程序的 GNU (Linux) 和 BSD/macOS 实现):

    printf '%s\0' * | xargs -0 sh -c 'tail -q -n 1 "$@" | grep \"A\"' - | wc -l
    

    【讨论】:

    • 不应该是xargs -0 sh -c 'tail -q -n 1 "$@" | grep \"A\" - '-在单引号内吗?
    • @codeforester: 否:对于sh,需要未引用的-,因为没有这个虚拟参数,第一个xargs提供的操作数将绑定到@987654333 @ 而不是$1,然后"$@" 将无法正常工作。
    【解决方案2】:

    这样使用findtailgrep怎么样?这将比必须遍历每个文件更有效。此外,tail -1 只会读取文件的最后一行,因此 I/O 效率很高。

    find . -maxdepth 1 -type f -exec tail -n 1 -- {} + | grep -EB 1 '^[^=]+A' | grep -c '^==>'
    
    • find 将批量调用tail -1,一次传递ARG_MAX 文件名
    • tail 将打印每个文件的最后一行,并以模式 "==> file_name 作为前缀
    • grep -EB 1 '^[^=]+A' 将查找模式 A 并获取前一行(它会在查找匹配项时排除 file_name 行)
    • grep -c '^==>' 将统计匹配模式的文件数

    如果您不需要知道匹配文件的名称,而只需获取文件数,您可以这样做:

    find . -maxdepth 1 -type f -exec tail -q -n 1 -- {} + | grep -c 'A'
    

    【讨论】:

    • 谢谢! IT 工作,即使它输出很多 find: ‘tail’ terminated by signal 13
    • 不确定你是否知道,但是如果我更改 grep 模式,第二次需要 0 次。我想这是由于某种缓存。太棒了!
    • 可能是因为缓存。你能说哪个更有效吗?我的find -exec 解决方案与@mklement0 的xargs 解决方案?
    • @coderofester xargs 解决方案更快,在我的情况下大约需要一半的时间,但我不知道我是否可以概括这个答案。
    【解决方案3】:

    使用 GNU awk:

    $ cat foo
    b
    a
    $ cat bar
    b
    b
    $ awk 'ENDFILE{if(/a/){c++; print FILENAME}}END{print c}' * 
    foo
    1
    

    【讨论】:

    • 我喜欢这种方法,特别是因为它打印文件名,但是使用* 仍然会得到bash: /usr/bin/awk: Argument list too long
    • 好吧,如果文件数量是你的问题:for i in * ; do awk 'END{if(/a/)print FILENAME}' "$i" ; done 这不会给你计数,只是文件列表,所以你可以通过管道将它传递给wc -l。碰巧我有一个包含一百万个文件的目录,而且似乎可以工作。
    【解决方案4】:

    尝试查找:

    for f in $(find . -type f); do tail -1 $f|grep PATERN; done|wc -l
    

    【讨论】:

    • 这种方法有效!另外,我可能错了,因为这不是我的领域,但它看起来像是在整个文件中搜索或模式,而不是仅在最后一行。
    • 循环ls的结果(或find的输出相同)是dangerous
    • 以这种方式更标准地使用find 是通过xargs 或使用-exec 选项。
    • 替代find . -type f -exec sed '${/A/p};d' {} \; | wc -l
    • 对于超过 300k 文件的 OP 案例效率极低。
    【解决方案5】:

    如果 grep 支持 -P 选项,这可能会起作用:

    grep -P "A\Z" -r . | wc -l
    

    man pcrepattern。简而言之:

    • \Z 匹配主题末尾的也匹配主题末尾的换行符之前
    • \z 仅匹配主题的末尾

    试试\Z\z

    要查看哪些文件匹配,您将只使用grep 部分而不使用wc 的管道。

    【讨论】:

    • 我将* 更改为-r .,这意味着递归地查看当前目录。如果当前目录仅包含您感兴趣的文件并且没有其他文件并且没有其他目录,这可能会起作用。
    • 不应该是grep -P "A" \z -r .吗?
    • @LorenzoBelli 不,PCRE 是A\Z:根据您的要求,锚定到\Z\z 的“A”。 bash 中的"A" \z 可能具有类似的效果,但我不确定从 bash 传递到 grep 的空间和参数。我会把它放在引号中,作为答案中的一个论点。
    • grep \Z 是读取文件的最后一行还是读取整个文件?
    • @codeforester \Z 是像 ^$ 这样的锚。因此我相信它会读取整个文件。但是必须检查来源才能确定:)。
    【解决方案6】:

    这将返回文件的数量:

    grep -rlP "A\z" | wc -l
    

    如果你想得到名字那么简单:

    grep -rlP "A\Z"
    

    【讨论】:

    • 谢谢@Samy。但是,我正在尝试仅在文件的最后一行搜索,而不是在完整文件中搜索。
    • 没问题@LorenzoBelli。抱歉没有注意到,我已经用 Lars 的解决方案更新了答案。祝你好运。
    猜你喜欢
    • 2023-03-22
    • 1970-01-01
    • 1970-01-01
    • 2014-12-16
    • 2019-11-03
    • 2013-11-01
    • 1970-01-01
    • 2012-12-06
    • 1970-01-01
    相关资源
    最近更新 更多