如何使用grep查找之前匹配的行上面的一行

3

我有一些日志文件,日期只会间歇性地附加。我的日志文件大致如下:

Monday 2017
foo foo foo foo foo foo foo foo foo foo foo foo 
foo foo foo ALARM foo foo foo foo foo foo foo foo 
foo foo foo foo foo foo foo foo foo foo foo foo 
foo foo foo foo foo foo ALARM foo foo foo foo foo
foo foo foo foo foo foo foo foo foo foo foo foo

我正在制作一个脚本,大致如下:
grep 'ALARM' myfile.log | tail -1

我需要搜索最后一个警报上面的以前日期条目,并将其包含在我的结果中。 我不知道匹配的警报行上面会出现多少行。

期望输出:

Monday 2017
foo foo foo foo foo foo ALARM foo foo foo foo foo

3
这可能需要比“grep”更强大的工具。如果日期总是在指定行数之上,你可以尝试使用(例如)grep -B10 'ALARM' myfile.log | grep 'Monday',但即使这种解决方案也存在一些缺陷。 - 0x5453
你如何判断一行是日期输入?它是否是唯一缩进的行? - Benjamin W.
请在您的问题中添加所需的输出,以针对该示例输入。 - Cyrus
周一2017似乎不是一个日期,您能提供日期格式吗? - Nahuel Fouilleul
6个回答

2
假设日期格式为周一 2017
grep -E 'Monday 2017|ALARM' | grep -B1 'ALARM'

第二个grep是为了删除ALARM匹配之间的多个日期模式,
编辑:重新阅读问题,似乎只想要最后一行匹配ALARM,我会使用以下perl一行代码:
perl -ne 'if(/Monday 2017/){$last_date=$_}if(/ALARM/){$date=$last_date;$line=$_}END{print $date,$line}' <<END
Monday 2017
foo foo foo foo foo foo foo foo foo foo foo foo 
foo foo foo ALARM foo foo foo foo foo foo foo foo 
foo foo foo foo foo foo foo foo foo foo foo foo 
foo foo foo foo foo foo ALARM foo foo foo foo foo
foo foo foo foo foo foo foo foo foo foo foo foo
END

1
你可以使用tac逐行反转流(参见seq 10 | tac以查看其作用)。这不是很便宜,请注意,但如果你的内容足够小,这可以提供一个简单的解决方案:
grep -B 9999999 lastSearchTerm my.log | tac | grep -B 9999999 firstSearchTerm | tac

这将打印从第一个搜索词到最后一个搜索词的块。
grep -B 9999999 lastSearchTerm my.log | tac | tail -n +2 | grep -m 1 lastBeforeTerm

这将仅打印最后一个包含 lastBeforeTerm 和 lastSearchTerm 之前的行。
对于您的特定情况,应该这样做:
grep -B 9999999 ALARM my.log | tac | {
  IFS= read -e line
  grep -m 1 '2017'
  echo "$line"
}

(将2017部分调整为与时间戳类似的任何行。)

当然,这不是最快的解决方案,但它简单易行,适用于小型输入。


这对我来说非常有效!我不熟悉tac,也不知道IFS=在做什么,但还是谢谢。我得检查一些其他的回复,但这个对我的需求很有效。我将 -B 改为 300,因为它可能不会超过那个范围。通常在归档之前,整个日志每次都不超过3000行。 - ditch
IFS是内部字段分隔符,它是一个影响read(以及其他操作)工作方式的变量。将其设置为空值使read可以读取完整行而不特别处理空格等内容。另外,选项-e使read在处理反斜杠时不具有任何特殊含义。模式IFS= read -e line的意思是:逐行原样读取,不去除空格或解释反斜杠等内容。 - Alfe
我在我的测试机上让它工作了,但是当我把它放到服务器上时,它就无法工作了。原来服务器有一个不支持-m选项的旧版本grep。所以我对它进行了修改,现在它正在工作(除非我可以通过某种方式简化它)。 fail_recov () { clear echo echo "Locations last Failure/Recovery:" grep -E '2017|2018|AlarmString' myfile.log >test.tmp grep -B 100 'AlarmString' test.tmp| tac | { IFS= read -e line grep '2018\|2017' | head -1 echo "$line" } rm -f test.tmp - ditch

1

Awk + tac解决方案:

样例myfile.log内容:

some text text text
Sunday 2017
foo foo foo foo foo foo foo foo foo foo foo foo 
foo foo foo ALARM foo foo foo foo foo foo foo foo 
foo foo foo foo foo foo foo foo foo foo foo foo 
bar foo foo foo foo foo ALARM foo foo foo foo foo
bar foo foo foo foo foo foo foo foo foo foo foo
Monday 2017
foo foo foo foo foo foo foo foo foo foo foo foo 
foo foo foo ALARM foo foo foo foo foo foo foo foo 
foo foo foo foo foo foo foo foo foo foo foo foo 
foo foo foo foo foo foo ALARM foo foo foo foo foo
text foo foo foo foo foo foo foo foo foo foo foo

工作内容:

awk '/ALARM/{ f=1 }f && /^[A-Z][a-z]+ 2[0-9]{3}/{ print; exit }' <(tac myfile.log)
  • tac myfile.log - 反向打印文件行
  • /ALARM/{ f=1 } - 遇到ALARM行时,使用标志f设置处理的起始阶段
  • /^[A-Z][a-z]+ 2[0-9]{3}/ - 表示"日期"行的模式
  • print; exit - 打印当前行(作为结果行)并立即终止脚本执行

输出:

Monday 2017

0

使用Grep无法高效地完成这个任务。这里有一个简单的Sed结构可以记住:

sed -n '/before/ {h;n;}; /after/ {x;p;x;p;}' < input.txt

这个程序会存储最近匹配模式before的行,然后在遇到后续匹配模式after的行时将其打印出来。然后,它也会打印出匹配after的行。具体来说:

  • -n标志抑制了每一行的输出——我们将告诉Sed手动输出我们想要的内容。
  • /before/ - 当我们找到一个被模式before匹配的行时...
    • h - 将其保存到保持空间缓冲区以备后用。
    • n - 继续到下一行。
  • /after/ - 当我们找到一个被模式after匹配的行时...
    • x;p - 用保持缓冲区(before)中的内容交换该行并将其打印出来。
    • x;p - 将after再次从保持缓冲区中交换出来并将其打印出来。

这个程序运行非常快,因为我们可以在一次过滤输入的情况下完成操作,而不需要先将输出进行管道传输或反转文件。


现在,让我们将其应用到问题的示例中:
sed -n '/^date pattern$/ {h;n;}; /ALARM/ {x;p;x;p;}' < input.txt

这只是将特定的模式插入到我上面描述的Sed程序中 - 每次看到ALARM时,它输出最近看到的日期和匹配的行。因为问题只想在每个日期后显示包含ALARM最后一行,所以我们需要稍微修改程序:

sed -n '
    /^date pattern$/ {
        :alarm
        x
        /ALARM/ {s/^\(date pattern\)\n.*\n\(.*ALARM.*\)$/\1\n\2/;p;n;}
    }
    /ALARM/ H
    $ b alarm
' < input.txt

这里的缓存不仅包含日期,还有包含ALARM的每一行,直到Sed遇到下一个日期。然后它将打印出在缓存中保留的最后一条ALARM和该日期。我们检查ALARM是否存在,以便在没有警报发生时不打印日期。:alarm声明了一个分支标签,我们可以使用b alarm返回到该标签,就像我们对文件的最后一行(用$表示)处理剩余的任何内容一样。

在这些示例中,我使用了[A-Z][a-z]\+day [0-9]\{4\}作为date pattern,但根据需要进行调整。

编辑:我认为我误解了问题。看起来我们只想要整个文件中的最后一个日期和最后一行警报。如果是这样,首先使用Tac反转文件更快,但会消耗更多的内存:

tac input.txt | sed -n '/ALARM/ {h;:a;n;/^date pattern$/ {p;x;p;q;}; ba;}'

采用这种方法,我们将最后一个警报存储在文件中,并在找到并打印文件中的最后一个日期后进行打印。我们使用q尽快退出,以避免处理其余部分。如果我们的系统上没有Tac,我们也可以使用Sed来反转文件:

sed '1!G;h;$!d' < input.txt | sed ...

0
假设“日期”由包含day和四位数字的行表示:
tac myfile.log \
    | sed -En '/ALARM/,/day [[:digit:]]{4}/{/day [[:digit:]]{4}/{p;q}}'

与其他解决方案一样,这个使用tac来倒序打印行;然后sed命令执行以下操作:

-n默认情况下抑制输出。

/ALARM/,/day [[:digit:]]{4}/ { # In the range from ALARM to the date
    /day [[:digit:]]{4}/{      # On the line of the date
        p                      # Print just that line
        q                      # Exit
    }
}

q 的作用是在找到我们想要的内容后避免读取文件的其余部分。

请注意,有些 sed 命令可能需要额外的分号,例如 {p;q;}


0

awk 解决方案,

awk 'NF==2 {d=$0}; /ALARM/ { printf("%s\n%s\n", d, $0)}' sample.txt

输出:

Monday 2017
foo foo foo ALARM foo foo foo foo foo foo foo foo 
Monday 2017
foo foo foo foo foo foo ALARM foo foo foo foo foo

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