提问人:Magi 提问时间:11/14/2023 最后编辑:John KugelmanMagi 更新时间:11/16/2023 访问量:104
查找并返回包含字符串的行块
Find and return blocks of lines containing a string
问:
我有一个以下类型的大文件:
key = asbh
some
lines
of
**text**
key = kafeia
some
more
**text**
and
additionally
more
**text**
and
more
key = lklfh
this
is
another
block
注意(如果重要):“key”行从不包含感兴趣的字符串(“text”)。
我将一个块称为一个以“key”开头的一行和下一行之间的所有行(所以在这个例子中,3个块)。我想返回所有包含字符串“text”的块。 即期望的输出:
key = asbh
some
lines
of
**text**
key = kafeia
some
more
**text**
and
additionally
more
**text**
and
more
我尝试了多种方法,我希望我的方向是正确的,但似乎无法让它工作。 这些是我的尝试:
less myfile.txt | sed -n '/key/,/text/p' | less
我相信这可能从它第一次看到“key”开始并继续前进(所以返回很多不相关的块),直到它看到“text”某处并停止。 这是受到这里类似问题的启发,但它没有拉动多个块的条件,也没有在块内匹配模式的条件。
less myfile.txt | grep -Pzl '(?s)^key([^key]|\n)*text' | less
我认为这可能会更好,如果我能让它工作,我可能会扩展它,因为它目前只尝试获取键和文本之间的文本(而不是直到下一个键)。
我试图理解 if 语句是如何工作的,特别是考虑到这个线程,但我是 unix 的新手,所以如果有人可以解释,我将不胜感激。
答:
为了代码的整洁度和可读性,我建议使用 Python 或 bash 脚本文件。
with open('file.txt', 'r') as file:
lines = file.readlines()
blocks = []
current_block = []
for line in lines:
if line.startswith('key'):
# Check if the current block contains 'text'
if any('text' in block_line for block_line in current_block):
blocks.extend(current_block)
current_block = [line]
else:
current_block.append(line)
# Check the last block in case it ends with 'text'
if any('text' in block_line for block_line in current_block):
blocks.extend(current_block)
# Print or use the blocks as needed
for block in blocks:
print(block.strip())
如果坚持在一行中执行此操作,也可以使用以下代码:
awk '/^key/ { if (block ~ /text/) print block; block=""; } { block = block $0 RS } END { if (block ~ /text/) print block }' file.txt
我在几个文件上检查了这种方法,它是正确的。我希望它对你有用。
评论
Bash 可能不是最好的工具,但它肯定可以完成这项工作。以下代码段没有任何错误处理;这只是一个恰好适用于问题输入的示例。
read_blocks() {
local -r needle="$1"
local -n _blocks="$2" # declare -A
local -n _keys="$3" # declare -a
local -i needle_seen=0
local line key='UNSET'
_blocks=()
_keys=()
while IFS= read -r line; do
if [[ "$line" = 'key = '* ]]; then
((needle_seen)) && _keys+=("$key") || unset '_blocks["$key"]'
((needle_seen = 0)) || :
key="${line#key = }"
else
[[ "$line" = *"$needle"* ]] && ((++needle_seen)) || :
_blocks["$key"]+="$line"$'\n'
fi
done
((needle_seen)) && _keys+=("$key") || unset '_blocks["$key"]'
}
declare -A blocks
declare -a keys
read_blocks 'text' 'blocks' 'keys' < /path/to/input
for key in "${keys[@]}"; do
printf 'key = %s\n' "$key"
printf '%s' "${blocks["$key"]}"
done
如果不需要保留块的顺序,那么你可以完全放弃数组,在写入输出时简单地迭代。keys
${!blocks[@]}
这可能对你有用 (GNU sed):
sed -n '/^key/!{H;$!d};x;/text/p' file
关闭隐式打印。-n
如果一行没有开始,请将其附加到保留空间并删除,除非它是最后一行。key
否则,交换到保留空间,如果集合匹配,则打印它。text
注意文件结束条件自然会下降到匹配条件。保持/模式空间在匹配时触发。key
评论
--debug
使用“段落”模式微不足道,因为您有一个空行分隔块。awk
$: awk -v RS= -v ORS='\n\n' /text/ file
key = asbh
some
lines
of
**text**
key = kafeia
some
more
**text**
and
additionally
more
**text**
and
more
我重新添加了显式换行符;请注意,这将在文件末尾附加一个,即使它不存在。
解释:
-v
在运行中设置 ariables。
将 ecord 分隔符设置为空,启用“段落模式”,直到看到它看到一个空行,并将它读取的所有内容(是的,整个块)称为一个“记录”。
将 Utput ecord 分隔符设置为两个换行符,在段落模式读取删除空行后将其添加回去。
扫描触发器字符串的几行的“记录”。如果它返回 true,由于我没有提供任何其他要执行的代码,它会打印该块。
(请注意,由于这是一个如此简单的检查,我不需要在它周围加上引号;我选择将它们关闭以减少噪音。YMMV.)v
RS=
R
S
ORS=$'\n\n'
O
R
S
/text/
补遗
刚刚注意到 tshiono 的评论比我早几个小时。
相应地修改了我自己的 - 到期的信用。
评论
key
awk
awk -v RS= -v ORS="\n\n" '/^key/ && /text/' myfile.txt
更少
cat foo | grep bar
grep bar foo
grep bar <foo
cat
less
cat
less