编辑文件中的最后一个实例。

8
我有一个巨大的文本文件(~1.5GB),其中有许多行以“.Ends”结尾。
我需要一个Linux oneliner(perl\ awk\ sed)来查找文件中最后一个出现的“.Ends”位置,并添加几行在其之前
我尝试使用tac两次,并卡在我的perl上:
当我使用:
tac ../../test | perl -pi -e 'BEGIN {$flag = 1} if ($flag==1 && /.Ends/) {$flag = 0 ; print "someline\n"}' | tac
它首先打印“someline\n”,然后才打印.Ends
结果是:

.Ends
someline 当我使用:
tac ../../test | perl -e 'BEGIN {$flag = 1} print ; if ($flag==1 && /.Ends/) {$flag = 0 ; print "someline\n"}' | tac
它什么也不打印。
当我使用:
tac ../../test | perl -p -e 'BEGIN {$flag = 1} print $_ ; if ($flag==1 && /.Ends/) {$flag = 0 ; print "someline\n"}' | tac
它会将所有内容打印两次:

.Ends
someline
.Ends
是否有一种平稳的方法来执行此编辑?
不必遵循我的解决方向,我不挑剔...
奖励-如果这些行可以来自另一个文件,那就太好了(但确实不是必须的)。
编辑
测试输入文件:
gla2 
fla3 
dla4 
rfa5 
.Ends
shu
sha
she
.Ends
res
pes
ges
.Ends  
--->
...
pes
ges
someline
.Ends
# * some irrelevant junk * #

你是正确的。完成了。 - user2141046
不,最后一个.Ends之后还有其他各种行,但我不关心这些。 - user2141046
2
虽然您可能不关心它们(在最后一个“ .Ends”之后的行),但在提出解决方案时,这将很重要,即始终更换最后一行会更容易。 - markp-fuso
1
为什么需要一个自动化的函数来在一个地方编辑“文件”?听起来你只需要使用带有搜索功能的文本编辑器即可。 - TLP
1
关于“不相关”的问题 - 是的,它是相关的。如果您在问题中没有说明最后一个.Ends之后可能还有其他行,并且在示例中也没有包含最后一个.Ends之后的行,则试图帮助您的人可能会合理地创建和测试依赖于.Ends是最后一行的解决方案,从而浪费他们的时间,对您的影响则相对较小。 - Ed Morton
显示剩余5条评论
7个回答

5

假设该短语的最后一个实例在文件末尾,从末尾处理文件可以极大地提高性能。例如,可以使用File::ReadBackwards

由于您需要在最后标记之前向文件添加其他文本,因此我们必须复制其余部分以便在添加后将其放回。

use warnings;
use strict;
use feature 'say';
use Path::Tiny;
use File::ReadBackwards;
    
my $file = shift // die "Usage: $0 file\n"; 

my $bw = File::ReadBackwards->new($file);

my @rest_after_marker; 

while ( my $line = $bw->readline ) { 
    unshift @rest_after_marker, $line;
    last if $line =~ /\.Ends/;
}
# Position after which to add text and copy back the rest
my $pos = $bw->tell;    
$bw->close;

open my $fh, '+<', $file or die $!;    
seek $fh, $pos, 0;
truncate $fh, $pos;    
print $fh $_ for path("add.txt")->slurp, @rest_after_marker;

要添加的新文本位于最后一个.Ends之前,可能存在于add.txt文件中。

问题在于最后一个.Ends标记之后有多少文件?我们将所有内容都复制到内存中,以便能够写回。如果太多了,就将其复制到临时文件中而不是内存中,然后从那里使用并删除该文件。


注意,这会直接在输入文件中进行编辑。 - zdim
这不是一个一行代码的问题。代码似乎是有效的(而且我真的更喜欢原地编辑),但这不是我要求的... - user2141046
1
@user2141046 嗯,是的...我刚刚删除了一条注释,因为我认为它在一般情况下有点不相关。(而且,人们经常提到它只是为了发现它并不重要——这里还有一些其他要求不清楚。)这段代码完全符合要求,并且尽可能高效,在处理1.5GB文件时可能很重要。但如果它只是一个“一”行程序,那么请随意丢弃(当然可以缩短并转换为命令行程序,但我认为这样做是不合适的)。我希望它对其他人仍然有用。 - zdim
我同意,而且无论如何我都投了你一票。让它成为更大的利益。 :) - user2141046
@user2141046,关于“这不是一行代码”的问题,当然可以。没有什么能阻止你将它放在一行中。 - ikegami

4
使用GNU的sed-i.bak选项会在原地保存原始文件的同时创建一个带有.bak扩展名的备份文件。
$ sed -Ezi.bak 's/(.*)(\.Ends)/\1newline\nnewline\n\2/' input_file
$ cat input_file
gla2
fla3
dla4
rfa5
.Ends
shu
sha
she
.Ends
res
pes
ges
.Ends
--->
...
pes
ges
someline
newline
newline
.Ends

我必须试一试 - 这个解决方案可能适用于小文件,但对于我正在处理的大文件,我怀疑会出现问题... - user2141046
是的,正如我所料 - 它无法处理较大的文件。 - user2141046

3

输入:

$ cat test.dat
dla4
.Ends
she
.Ends
res
.Ends
abc

$ cat new.dat
newline 111
newline 222

有一个关于 OP 的 tac | <process> | tac 方法的奇妙想法:

$ tac test.dat | awk -v new_dat="new.dat" '1;/\.Ends/ && !(seen++) {system("tac " new_dat)}' | tac
dla4
.Ends
she
.Ends
res
newline 111
newline 222
.Ends
abc

另一个与awk有关的想法,它用输入文件的双重遍历替换了双重调用tac:

$ awk -v new_dat="new.dat" 'FNR==NR { if ($0 ~ /\.Ends/) lastline=FNR; next} FNR==lastline { system("cat "new_dat) }; 1' test.dat test.dat
dla4
.Ends
she
.Ends
res
newline 111
newline 222
.Ends
abc

注意事项:

  • 这两种解决方案都将修改后的数据写入标准输出(与原始代码相同)
  • 这两种解决方案都不会修改原始输入文件(test.dat

太棒了!我非常喜欢中间所定义的seen,而且从oneliner调用系统是新鲜事对我来说。我会继续保持帖子的开放,以便看看是否有人能建议一个在原地编辑的技巧,但你的答案是有效的并且完全合法!谢谢。 - user2141046
哇,编辑很有趣。我也会尝试一下。 - user2141046
/.Ends/ 会匹配包含 FooEndsBar 的行,你不能依赖于 system("tac " new_dat) 命令的输出出现在调用它的 awk 命令的输出中的任何位置(不确定为什么,可能是缓存问题,但我曾经看到被调用的命令输出在所有 awk 输出之后而不是其中间),你需要调用该命令并使用 while getline 循环然后从 awk 中打印出来,以确保输出顺序的稳健性。 - Ed Morton
1
通常不保证可行的事情在它们不可行之前通常是可行的。你不能测试可能不起作用的东西,发现它在你的测试中起作用,并从中推断它将永远起作用。例如,像 for ( i in arr ) print i 这样的 awk 循环通常会按特定顺序打印 i,但有时则不会。同样,/^.Ends/ 将匹配你想要的内容,但也会匹配你不想要的字符串,例如 BEnds,因此它可能会对你正在测试的数据做出你想要的事情,但随后在不同的数据上失败。 - Ed Morton
1
在我的test中,你的第一种解决方案比zdim的慢50倍,而你的第二种解决方案比第一种慢2倍。TLP的速度非常慢。 - ikegami
显示剩余4条评论

1

输入:

$ cat test.dat
dla4
.Ends
she
.Ends
res
.Ends
abc

$ cat new.dat
newline 111
newline 222

一种 ed 方法:

$ ed test.dat >/dev/null 2>&1 <<EOF
1
?.Ends
-1r new.dat
wq
EOF

或者作为一行代码:

$ ed test.dat < <(printf '%s\n' 1 ?.Ends '-1r new.dat' wq) >/dev/null 2>&1

其中:

  • >/dev/null 2>&1 - 强制禁止显示诊断和信息消息
  • 1 - 跳转到第一行
  • ?.Ends - 在文件中向后查找字符串.Ends(即查找文件中的最后一个.Ends
  • -1r new.dat - 在文件中向上移动/返回1行(-1)并读取new.dat的内容
  • wq - w写入并q退出(也称保存并退出)

这将生成:

$ cat test.dat
dla4
.Ends
she
.Ends
res
newline 111
newline 222
.Ends
abc

注意:与OP当前代码将修改后的数据写入标准输出不同,此解决方案会修改原始输入文件(test.dat)。


我相信你的答案是可行的(hack,你之前的两个答案都可以运行,我还在尝试理解第二个),但这不是一个单行代码。 - user2141046
@user2141046 关于 not a one-liner ... 一个“简单”的解决方案是将代码放在函数包装器中,或者将其放在文件中,然后源化该文件 ... 这两种方法都可以允许在命令提示符下使用“一行代码”解决方案。 - markp-fuso
说实话...我不是ed用户,所以这个答案花了我大约15分钟的时间来研究和测试,但在那个过程中,我想起了一些例子,其中多行答案(如上所示)被折叠成单行...类似于(但不要引用我):ed '1;?.Ends;-1r new.dat;wq' test.dat - markp-fuso
最终结果是,在许多情况下,多行代码 可以 简化为一行代码。 - markp-fuso
1
@user2141046 顺便说一句,在和谷歌先生聊了几分钟之后,我也能够想出如何把这个问题写成一行代码了。答案已更新。 - markp-fuso
谢谢,但我会坚持你的另一个答案,使用 awk。规则就是,如果它能工作 - 就不要修复它 :) - user2141046

1

由于您想从文件中读取新行:

$ cat new
foo
bar
etc

$ tac file | awk 'NR==FNR{str=$0 ORS str; next} {print} $0==".Ends"{printf "%s", str; str=""}' new - | tac
gla2
fla3
dla4
rfa5
.Ends
shu
sha
she
.Ends
res
pes
ges
.Ends
--->
...
pes
ges
someline
foo
bar
etc
.Ends
# * some irrelevant junk * #

上述假设您发布的示例输入中某些行中.Ends后面的空格是一个错误。如果它们确实存在,则将$0==".Ends"更改为/^\.Ends[[:space:]]*$/,或者甚至更改为/^[[:space:]]*\.Ends[[:space:]]*$/,如果这些行还可能有前导空格,或者只需使用/\.Ends/,如果在.Ends之前/之后可能有任何字符。请注意保留HTML标签。

请问在这个awk命令中,“new”后面的破折号是做什么用的?我不熟悉单破折号(并且在我的环境中将其别名为“less”,因此想要防止冲突)。 - user2141046
1
在每个 shell 脚本中,输入上下文中的 - 表示 stdin。不要将其别名为 less(我不知道您可以将符号设置为别名!),否则您将遇到问题。 - Ed Morton

0

首先让grep进行搜索,然后使用awk注入行。

$ cat insert
new content
new content

$ line=$(cat insert)

$ awk -v var="${line}" '
      NR==1{last=$1; next} 
      FNR==last{print var}1' <(grep -n "^\.Ends$" file | cut -f 1 -d : | tail -1) file
rfa5 
.Ends
she
.Ends
ges
.Ends  
ges
new content
new content
.Ends
ges
ges

数据

$ cat file
rfa5 
.Ends
she
.Ends
ges
.Ends  
ges
.Ends
ges
ges

你的答案依赖于某些操作系统的花招,而我的操作系统(csh)不支持,例如圆括号和在执行“set line = `cat insert`”时保存空格,因此我无法检查它。 - user2141046
1
@user2141046 请阅读 https://www.google.com/search?q=csh+why+not 找到的一些或全部文章。 - Ed Morton
1
@EdMorton 这不是我能控制的 - 这就是我所拥有的和我的工具所需的。当我尝试使用逗号别名时,我阅读了这些文章,结果每个逗号符号都有5个字符... - user2141046
2
@user2141046 如果你的老板强制让你使用csh编写脚本,你应该反对,因为这会影响你的生产力和编写简洁、健壮、高效、可移植解决方案的能力。我希望你的老板能够接受这个反馈。我不知道是否有任何工具必须调用或被调用csh而不是其他shell,但如果存在这样的工具,它们的设计很糟糕,应该用其他可移植的工具替换(或者如果是shell脚本,你应该在顶部添加一个csh shebang)。 - Ed Morton

0
两个总体要点事先说明:
  1. 当你将perl的输出导入到tac时,运行perl -i进行原地编辑是没有意义的。

  2. $flag默认为假。你可以交换其含义,使代码更方便:

    - BEGIN {$flag = 1} if ($flag==1 && /.Ends/) {$flag = 0 ; print "..."}
    + if (!$f && /.Ends/) {$f=1; print "..."}
    
现在来回答这些问题:

When I use:

tac ../../test | perl -pi -e 'BEGIN {$flag = 1} if ($flag==1 && /.Ends/) {$flag = 0 ; print "someline\n"}' | tac

It first prints the someline\n and only than prints the .Ends. The result is:.Ends\nsomeline.

是的,因为你在倒退,输出被放在.Ends之后。你可以反转当前行和新行的输出:
perl -pe 'if (!$f && /.Ends/) {$f=1 ; print $_ . "someline\n" ; $_=""}'

When I use:

tac ../../test | perl  -e 'BEGIN {$flag = 1} print ; if ($flag==1 && /.Ends/) {$flag = 0 ; print "someline\n"}' | tac

It doesn’t print anything.

你只是缺少了-n。它有效。
perl -ne ...

[...] 它会打印两次所有内容:
对此不需要解释 :)
一般来说,使用三个命令并不是一个坏主意:通过将perl的输出导入到临时文件中,可以避免高内存使用。否则,第二个tac命令需要将整个输入保留在内存中。
awk看起来非常相似:
tac test | awk '!f && $0==".Ends" {print $0 ORS "newline2" ORS "newline1"; f=1; next}1' | tac

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