打印所有匹配给定模式的最后一行之前的所有行的简洁方法

7

我正在尝试找到一个简洁的Shell单行命令,可以给我所有文件中的行,直到某个模式为止。

使用情况是将日志文件中的所有行转储,直到我发现一些标记,表示服务器已重新启动。

这里有一个愚蠢的仅使用Shell的方法:

tail_file_to_pattern() {
    pattern=$1
    file=$2

    tail -n$((1 + $(wc -l $file | cut -d' ' -f1) - $(grep -E -n "$pattern" $file | tail -n 1 | cut -d ':' -f1))) $file
}

以下是一个稍微可靠一些的Perl方法,可以从stdin读取文件:

perl -we '
    push @lines => $_ while <STDIN>;
    my $pattern = $ARGV[0];
    END {
        my $last_match = 0;
        for (my $i = @lines; $i--;) {
            $last_match = $i and last if $lines[$i] =~ /$pattern/;
        }
        print @lines[$last_match..$#lines];
    }
'

当然,您可以更高效地通过打开文件,寻找到末尾并向后查找直到找到匹配行来完成这项操作。

打印出自第一个出现的所有内容很容易,例如:

sed -n '/PATTERN/,$p'

但是我还没有想到一种方法,可以按照最后一次出现的方式打印所有内容。


1
你的标题说“直到最后一个模式的所有行”,但是你的两个示例脚本打印了从最后一个模式到结尾的所有行。我猜标题有误导作用? - John Zwinck
如果模式通常会出现在文件末尾附近,您可能需要考虑使用File::ReadBackwards(将其推入缓冲区直到达到模式或文件开头)。 - ikegami
7个回答

6
这里提供一种仅使用sed的解决方案。要在$file中打印每一行,必须从与$pattern匹配的最后一行开始
sed -e "H;/${pattern}/h" -e '$g;$!d' $file

请注意,与您的示例一样,只有文件包含该模式时才能正常工作。否则,它会输出整个文件。
以下是其执行过程的分解,其中包含sed命令:
[H]将每行追加到sed的“保持空间”中,但不要将其回显到标准输出[d]。
当我们遇到这个模式时,[h]丢弃保持空间并从匹配行重新开始。
当我们到达文件末尾时,将保持空间复制到模式空间[g],以便它将回显到标准输出。
也请注意,对于非常大的文件,它很可能变得很慢,因为任何单次通过的解决方案都需要在内存中保存许多行。

+1:那个sed处理真是太棒了。只有一行代码就能实现OP想要的功能。 - David W.

4

另一种方法是:tac“$file”| sed -n'/ PATTERN /,$ p'| tac

编辑:如果您没有tac,请通过定义来模拟

tac() {
    cat -n | sort -nr | cut -f2
}

虽然丑陋但符合POSIX标准。


我没有tac二进制文件。鉴于原帖没有指定操作系统,最好提供适用于所有操作系统的解决方案。 - ghoti
你可以使用 tail -r 代替 tac。虽然这个解决方案并不完全符合问题的要求。为了达到这个目的,你需要使用 sed -n "1,/${pattern}/p" - Rob Davis
1
@ghoti:嗯,看起来你没有使用GNU/coreutils。显然tac不是POSIX标准。如果你坚持使用POSIX,请使用cat -n | sort -nr | cut -f2代替tac(哦,我们又变丑了!) - Jo So
@RobDavis:tail -r也不是POSIX标准,而且在我的Debian系统上也不可用。至于第二部分:确实,标题与正文问题不符。但请给出整行命令,应该是tac | sed -n '1,/PATTERN/p' | tac(或tac替代品)。 - Jo So

4

逐行将数据加载到数组中,当您找到匹配的模式时,请丢弃该数组。在结束时打印剩余内容。

 while (<>) {
     @x=() if /$pattern/;
     push @x, $_;
 }
 print @x;

作为一条命令:
 perl -ne '@x=() if /$pattern/;push @x,$_;END{print @x}' input-file

3

Sed 的 q 命令可以解决这个问题:

sed "/$pattern/q" $file

这将打印出直到有匹配模式的那一行。之后,sed将会打印最后一行并退出。


这段代码执行了问题标题和第一行的要求,但不符合提问者的实际需求。我认为他想要匹配给定模式的最后一行及其之后的所有行。 - Rob Davis
@RobDavis - 你说得对。我读了第一段,觉得“嘿,这很简单”。我可能得使用Awk去想出点什么。 - David W.

3
我建议简化你的Shell脚本:
```shell 我不熟悉Shell脚本,但我希望能够提供帮助!
```
tail -n +$(grep -En "$pattern" "$file" | tail -1 | cut -d: -f1) "$file"

这段代码更加简洁,因为它:

  • 使用tail的+选项,从指定行打印到结尾,而无需计算从该行到结尾的距离。
  • 使用更加简洁的方式表达命令行选项。

并且通过引用$file修复了一个错误(这样可以处理文件名中包含空格的文件)。


1

这个问题的标题和描述不匹配。

对于问题的标题,@David W.的答案加1。此外:

sed -ne '1,/PATTERN/p'

关于问题,您已经拥有一些解决方案。

请注意,tac 可能是特定于 Linux 的。在 BSD 或 OSX 中似乎不存在。如果您想要一个跨平台的解决方案,请勿依赖 tac。

当然,几乎任何解决方案都需要将数据缓存在内存中或者提交两次以进行分析和处理。例如:

#!/usr/local/bin/bash

tmpfile="/tmp/`basename $0`,$$"
trap "rm $tmpfile" 0 1 2 5
cat > $tmpfile

n=`awk '/PATTERN/{n=NR}END{print NR-n+1}' $tmpfile`

tail -$n $tmpfile

请注意,我使用的tail是针对FreeBSD的。如果您使用Linux,则可能需要改为tail -n $n $tmpfile

你可以在OSX上使用tail -r来获得tac的功能。 - Mark Setchell
没错,但 -r 选项在 Linux 中不存在,因此它也不是跨平台的。如果我反对其中一个,那么不反对另一个就是虚伪的。 :) - ghoti
我完全理解并同意 - 我只是想指出,主要是为了以后的读者,如果他们想在OS X上使用tac,可以使用tail -r代替...而不是让你的声明说它似乎不存在。 - Mark Setchell

1

Rob Davis向我指出你说想要的不是你真正的问题:

你说:

我试图找到一个简洁的Shell单行命令,它将给我文件中所有在某个模式之前的所有行。

但最后你的帖子中,你却说:

但我还没有想到一种打印从最后出现位置开始的所有内容的方法。

我已经回答了你的第一个问题。这里是你第二个问题的一个解决方案:从正则表达式打印到文件末尾的单行命令:

awk '{ if ($0 ~ /'"$pattern"'/) { flag = 1 } if (flag == 1) { print $0 } }' $file

一个类似的 Perl 一行代码:
export pattern="<regex>"
export file="<file>"
perl -ne '$flag=1 if /$ENV{pattern}/;print if $flag;' $file

除非他想要模式最后一次出现之后的行,我相信。 - Rob Davis
@RobDavis - 你说得对。你的解决方案是最好的。它只有一行代码且与平台无关。 - David W.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接