我有一个带有标记线的文本,例如:
aaa
---
bbb
---
ccc
我需要获取从最后一个标记(不包括)到 EOF 的文本。在这种情况下,它将是
ccc
POSIX.2 中有一种优雅的方式吗?现在我使用两次运行:第一次运行nl
和grep
最后一次运行分别使用相应的行号。然后我提取行号并用于sed
提取有问题的块。
文本段可能非常大,所以我担心使用一些文本添加方法,就像我们将文本添加到缓冲区中一样,如果遇到标记,我们会清空缓冲区,这样在 EOF 处,我们就有了最后一个块缓冲。
答案1
除非您的段真的很大(例如:您真的无法腾出那么多 RAM,大概是因为这是一个控制大型文件系统的小型嵌入式系统),否则单遍确实是更好的方法。不仅因为它会更快,而且最重要的是因为它允许源成为流,从中读取但未保存的任何数据都会丢失。这确实是 awk 的工作,尽管 sed 也可以做到。
sed -n -e 's/^---$//' -e 't a' \
-e 'H' -e '$g' -e '$s/^\n//' -e '$p' -e 'b' \
-e ':a' -e 'h' # you are not expected to understand this
awk '{if (/^---$/) {chunk=""} # separator ==> start new chunk
else {chunk=chunk $0 RS}} # append line to chunk
END {printf "%s", chunk}' # print last chunk (without adding a newline)
如果必须使用两遍方法,请确定最后一个分隔符的行偏移并从中打印。或者确定字节偏移并从中打印。
</input/file tail -n +$((1 + $(</input/file # print the last N lines, where N=…
grep -n -e '---' | # list separator line numbers
tail -n 1 | # take the last one
cut -d ':' -f 1) )) # retain only line number
</input/file tail -n +$(</input/file awk '/^---$/ {n=NR+1} END {print n}')
</input/file tail -c +$(</input/file LC_CTYPE=C awk '
{pos+=length($0 RS)} # pos contains the current byte offset in the file
/^---$/ {last=pos} # last contains the byte offset after the last separator
END {print last+1} # print characters from last (+1 because tail counts from 1)
')
附录:如果您拥有的不仅仅是 POSIX,这里有一个简单的一次性版本,它依赖于 awk 的通用扩展,允许记录分隔符RS
是正则表达式(POSIX 只允许单个字符)。它并不完全正确:如果文件以记录分隔符结尾,它将打印最后一个记录分隔符之前的块而不是空记录。使用的第二个版本RT
避免了该缺陷,但RT
特定于 GNU awk。
awk -vRS='(^|\n)---+($|\n)' 'END{printf $0}'
gawk -vRS='(^|\n)---+($|\n)' 'END{if (RT == "") printf $0}'
答案2
lnum=$(($(sed -n '/^---$/=' file | sed '$!d') +1)); sed -n "${lnum},$ p" file
第一个sed
输出“---”行的行号...
第二个sed
从第一个 sed 的输出中提取最后一个数字...
将该数字加 1 以获取“ccc”块的开头...
第三个'sed' 从“ccc”块的开头输出到 EOF
更新 (修改了吉尔斯方法的信息)
好吧,我想知道如何格伦·杰克曼的 tac
会执行,所以我对三个答案进行了时间测试(在撰写本文时)...每个测试文件包含 100 万行(各自的行号)。
所有答案都符合预期......
这里有时间..
吉尔斯 sed
(单程)
# real 0m0.470s
# user 0m0.448s
# sys 0m0.020s
吉尔斯 awk
(单程)
# very slow, but my data had a very large data block which awk needed to cache.
吉尔斯“两次通过”(第一种方法)
# real 0m0.048s
# user 0m0.052s
# sys 0m0.008s
吉尔斯“双遍”(第二种方法)...非常快
# real 0m0.204s
# user 0m0.196s
# sys 0m0.008s
吉尔斯“两次通过”(第三种方法)
# real 0m0.774s
# user 0m0.688s
# sys 0m0.012s
吉尔斯'gawk'(RT 方法)...非常快,但不是 POSIX。
# real 0m0.221s
# user 0m0.200s
# sys 0m0.020s
格伦·杰克曼...非常快,但不是 POSIX。
# real 0m0.022s
# user 0m0.000s
# sys 0m0.036s
弗雷德熊
# real 0m0.464s
# user 0m0.432s
# sys 0m0.052s
麦基·梅塞尔
# real 0m0.856s
# user 0m0.832s
# sys 0m0.028s
答案3
两次通过策略似乎是正确的事情。我会使用 sed 而不是awk(1)
.两个通道可能如下所示:
$ LINE=`awk '/^---$/{n=NR}END{print n}' file`
获取行号。然后回显从该行号开始的所有文本:
$ awk "NR>$LINE" file
这不应该需要过多的缓冲。
答案4
你可以只使用ed
ed -s infile <<\IN
.t.
1,?===?d
$d
,p
q
IN
工作原理:t
复制当前 ( .
) 行 - 启动时始终是最后一行ed
(以防分隔符出现在最后一行),1,?===?d
删除直到并包括上一个匹配项的所有行(ed
仍在最后一行) )然后$d
删除(重复的)最后一行,,p
打印文本缓冲区(替换为w
以就地编辑文件)最后q
退出ed
。
如果您知道输入中至少有一个分隔符(并且不关心它是否也被打印),那么
sed 'H;/===/h;$!d;x' infile
会更短。
它是如何工作的:它将所有行附加到旧缓冲区,遇到匹配时H
覆盖旧缓冲区,当更改缓冲区(和自动打印) 时删除除 la t之外的所有行。h
d
$
x