从文件中读取索引“n”低于与给定正则表达式匹配的行的所有行
Posted
技术标签:
【中文标题】从文件中读取索引“n”低于与给定正则表达式匹配的行的所有行【英文标题】:Read from file all lines that have an index `n` lower than the lines that match a given regex 【发布时间】:2017-11-13 05:26:10 【问题描述】:我想从文件file.txt
中读取索引n
低于匹配给定正则表达式regex
的行的所有行。例如文件
hello my friend
foo
_bar_
I love this bar
poof
kouki
splash in the water
bar
如果regex=bar
和n=2
,那么我们想读
hello my friend
foo
kouki
我找到了解决这个问题的方法,用的是一个笨重的班轮
sed -n `grep -n bar file.txt | awk -F ":" 'print ($1 - 2)' | tr '\n' 'X'
| sed 's+X+p;+g' | sed 's/.$//'` < file.txt
是否有更好(更快、更易阅读)的解决方案?
(我提出这个问题的目的纯粹是为了教育)
【问题讨论】:
【参考方案1】:与awk
:
$ awk '/bar/ && FNR>2 print li[-2]
li[-2]=li[-1]; li[-1]=$0' file
hello my friend
foo
kouki
在比赛前打印第 nth 行可以更通用(无需将整个文件保存在内存中):
$ awk -v n=3 '/bar/ && FNR>n print li[n]
for (i=n;i>1;i--)
li[i]=li[i-1]
li[1]=$0' file
hello my friend
poof
【讨论】:
++ 表示性能良好、可读、可移植和泛化良好的解决方案。 对于我来说,缓慢、脆弱、复杂、不可移植、不可扩展的 sed 和 bash “解决方案”正在获得支持,这绝对让我感到震惊。当然这是正确的做法。【参考方案2】:短sed方法:
sed -n '1N;2N;/bar[^\n]*$/P;N;D' file.txt
输出:
hello my friend
foo
kouki
详情:
1N;2N;
- 将前 3 行读入模式空间
/bar[^\n]*$/
- 检查最后一行是否匹配 bar
。 ([^\n]*$
- 确保它是捕获的 3 行部分的最后一行)
P;
- 如果找到上述匹配,则打印模式空间的第一行
N
- 将换行符添加到模式空间,然后将下一行输入附加到模式空间
D
- 删除模式空间中的文本直到第一个换行符,并使用生成的模式空间重新启动循环(即关于前 3 行 - 第一行 hello my friend
将被打印并从模式空间和新循环将在下一行开始foo
)
【讨论】:
我正在寻找这个,但你更快更强大!P;N;D
的使用很好的示范!
@RomanPerekhrest 哇,看起来非常优雅+1。我不熟悉P
、N
和D
在sed
中的使用(我现在在man
中读到了它们)。您介意解释一下它是如何工作的吗?
虽然我不知道为什么,但这个解决方案不适用于 BSD/macOS sed
。然而,我不愿意投入工作去寻找原因,这让我们明白了这一点,@Remi.b:你不熟悉P
、N
和@987654341 并非偶然@:这些功能鲜为人知,因为它们令人费解。像 dawg 这样的awk
解决方案更容易理解和概括。
这是gnu-sed
只是应该说明。 POSIX sed
- 不
@Remi.b sed 是一个出色的工具,用于在单个行上进行简单的替换,但这就是 all 应该使用的,因为对于其他任何事情,awk 解决方案会更清晰,更简单、更健壮、更便携、更高效且更易于扩展。如果您使用 s、g 和 p(带 -n)以外的 sed 结构,那么您使用的结构在 1970 年代中期 awk 被发明时就已经过时了,所以不要浪费时间学习它们,只要学习awk 代替。【参考方案3】:
纯bash
o=0 a=()
while read -r line;do
a+=("$line")
[ "$line" ] && [ -z "$line//*bar*" ] && echo $a[o-2]
((o++))
done <file.txt
hello my friend
foo
kouki
或者,因为你说的是regex
:
while read -r line;do
a+=("$line")
[[ $line =~ bar ]] && echo $a[o-2]
((o++))
done <file.txt
但是,对于表演,我更喜欢第一种语法...
作为一个函数
grepIndex ()
local o=0 a=() line
while read -r line; do
a+=("$line")
[ "$line" ] && [ -z "$line//*$1*" ] && echo $a[o-$2]
((o++))
done
grepIndex <file.txt bar 2
hello my friend
foo
kouki
可以写成这样
grepIndex()
local o=0 a=() line
while read -r line;do
a+=("$line")
[[ $line =~ $1 ]] && echo $a[o-$2]
((o++))
done
也是。
注意:
如果 pure bash 在处理小文件时要快得多,那么对于大文件,bash 就变得矫枉过正!!看看RomanPerekhrest's answer!使用 sed 可以 成为最有效的解决方案之一(在大文件上)!
【讨论】:
我认为 underkill 可能是更好的术语:除了几行之外,纯 bash 解决方案将非常缓慢。如果您只有几行代码,那么启动awk
等外部实用程序所花费的绝对 时间将无关紧要,因为它可能仍然足够快。简而言之:除了非常特殊的场景,不要在 shell 循环中读取行。顺便说一句:在您的 read
命令前面加上 IFS=
,否则前导和尾随空格将被修剪。
阅读why-is-using-a-shell-loop-to-process-text-considered-bad-practice 了解一些但不是全部的不这样做的原因。只需使用 awk,它是发明 shell 的人发明的工具,用于从 shell 调用此类工作。以上是关于从文件中读取索引“n”低于与给定正则表达式匹配的行的所有行的主要内容,如果未能解决你的问题,请参考以下文章