sed头疼:在文件中插入单个匹配项的行(而不是每行)

3

经过八个多小时的搜索,我放弃了并创建了一个新的问题。这个操作很简单,但是我一直在努力让它正确地工作,似乎已经尝试了SO上的每一个其他解决方案。我需要两件事:

1.) 在整个文件中第一次出现PBS的行之前插入一行。它应该在整个文件中仅发生一次。由于sed是按行跟随,所以我怀疑每个我尝试的解决方案最终都会在文件中的每个出现位置重复插入。

因此,需要执行以下操作。原始文件:

stuff here  
stuff here  
PBS -N  
PBS -V  
stuff here 

变成:

stuff here  
stuff here  
**inserted line**  
PBS -N  
PBS -V  
stuff here  

2.) 在整个文件中最后一次出现“PBS”的行后添加一行。与之前相同:在整个文件中只能发生一次。

因此,需要执行以下操作:

stuff here  
stuff here  
PBS -N  
PBS -V  
stuff here  

变成:

stuff here  
stuff here  
PBS -N  
PBS -V  
**inserted line**  
stuff here 

我看到网上提供的所有解决方案(我已经打开了大约20个标签页)都说这应该相对容易。但我毫不掩饰地说,目前为止sed对我的自尊心造成了影响...感谢任何能帮忙的人。


我理解你的痛苦。单字符命令并不明显。我通常只使用sed进行简单的搜索和替换,或者打印/删除特定行。任何复杂的操作,我都会使用另一种更易读的语言,或者在这里为声望而奋斗 ;) - glenn jackman
6个回答

3
这里有三种方法,其中两种使用sed,一种使用awk。
使用sed单独完成:
在第一个出现之前插入一次:
$ sed ':a;$!{N;ba}; s/PBS/inserted line\nPBS/' file
stuff here
stuff here
inserted line
PBS -N
PBS -V
stuff here 

在最后一次出现后仅插入一次:

$ tac file | sed ':a;$!{N;ba}; s/PBS/inserted line\nPBS/' | tac
stuff here
stuff here
PBS -N
PBS -V
inserted line
stuff here 

它是如何工作的

  • :a;$!{N;ba};

    这将一次性读取整个文件。 (如果整个文件非常大,您需要查看其他方法。)

  • s/PBS/插入的行\nPBS/

    这执行了替换操作。

  • tac

    通常,直到我们读取整个文件之前,无法知道 PBS 的最后一个出现位置是哪个。然而,tac 反转了行的顺序。因此,原来的最后一个变成了第一个。

使用 awk

awk 的关键优势在于它允许轻松使用变量。这里我们创建了一个标志 f,当我们到达第一个 PBS 时将其设置为 true:

$ awk '/PBS/ && !f {print "inserted line"; f=1} 1'  file
stuff here
stuff here
inserted line
PBS -N
PBS -V
stuff here 

要在最后一次出现之后插入,我们可以像上面那样使用 tac 解决方案。为了多样化,此方法读取文件两次。第一次运行时,它会跟踪 PBS 的最后一行号。第二次运行时,它会打印需要打印的内容:
$ awk 'NR==FNR{if (/PBS/)n=FNR;next} 1{print} n==FNR {print "inserted line"}'  file file
stuff here
stuff here
PBS -N
PBS -V
inserted line
stuff here 

这些awk解决方案逐行处理文件。如果文件非常大,这有助于限制内存使用。

使用grep和sed

另一种方法是使用grep告诉我们需要处理的行号。这会在第一个出现之前插入:

$ sed "$(grep -n PBS file | cut -d: -f1 | head -n1)"' s/PBS/inserted line\nPBS/' file
stuff here
stuff here
inserted line
PBS -N
PBS -V
stuff here 

这会插入在最后:

$ sed  "$(grep -n PBS file | cut -d: -f1 | tail -n1)"' s/.*PBS.*/&\ninserted line/' file
stuff here
stuff here
PBS -N
PBS -V
inserted line
stuff here 

这种方法不需要一次性将整个文件读入内存。

1
感谢所有回答这个问题的人,但我最终在这里使用了 grep + sed 的解决方案。非常优雅的解决方案,谢谢John。 - CAPGuy

0

@John1924的答案是正确的。在这种情况下,您也可以以非有效的方式执行任务,例如:

  • 仅打印第一个PBS之前的行
  • 回显该行
  • 仅打印第一个PBS之后(包括)的行

例如,在./pbsfile中有以下内容时

line 1
line 2
PBS -N first
PBS -N second
line 3
PBS -V last-1
PBS -V last
line 4
line 5

以上可以举例如下:

pbsfile="./pbsfile"

(
#delete the lines after the 1st PBS
#so remains only the lines before the 1st PBS
sed  '/PBS/,$d' "$pbsfile"

#echo the needed line
echo "THIS SOULD BE INSERTED BEFORE 1st PBS"

#print only the lines after the 1st PBS
sed -n '/PBS/,$p' "$pbsfile"

)

产生:

line 1
line 2
THIS SOULD BE INSERTED BEFORE 1st PBS
PBS -N first
PBS -N second
line 3
PBS -V last-1
PBS -V last
line 4
line 5

和上面一样,你可以对最后一个PBS做同样的操作,只需要在sed之前和之后反转文件即可,例如下面的命令:

pbsfile="./pbsfile"

(
tail -r "$pbsfile" | sed -n '/PBS/,$p' | tail -r
echo "THIS SOULD BE INSERTED AFTER THE LAST PBS"
tail -r "$pbsfile" | sed  '/PBS/,$d' | tail -r
)

什么产生

line 1
line 2
PBS -N first
PBS -N second
line 3
PBS -V last-1
PBS -V last
THIS SOULD BE INSERTED AFTER THE LAST PBS
line 4
line 5

再次强调,这只是一种“替代方案”(并不有效)。


0

另一种sed方法:

sed '/PBS/ {
  # insert the new line
  i\
inserted line
  # then loop over the rest of the file, implicitly printing each line
  :a; n; ba
}' file

对于最后一次匹配,这个版本不需要使用 tac

sed '
  # read the whole file into pattern space
  :a; $!{N;ba}
  # then, use greedy matching to get to the *last* PBS
  # and non-greedy matching to get to the end of that line.
  s/.*PBS[^\n]*/&\ninserted line/   
' file

0

sed 不是这种工作的正确工具,它只适用于单个行上的简单替换。请使用 awk:

$ cat tst.awk
NR  == FNR { if (/PBS/) hits[++numHits] = NR; next }
FNR == hits[1] { print "inserted line before" }
{ print }
FNR == hits[numHits] { print "inserted line after" }

$ awk -f tst.awk file file
stuff here
stuff here
inserted line before
PBS -N
PBS -V
inserted line after
stuff here

0

这里是一个只读取文件一次的 awk

cat file
line 1
line 2
PBS -N first
PBS -N second
line 3
PBS -V last-1
PBS -V last
line 4
line 5

awk '/PBS/ {last=NR;if (!f) {first=NR;f=1}} {a[NR]=$0} END {for (i=1;i<=NR;i++) {if (i==first) a[i]="new line before\n"a[i];if (i==last) a[i]=a[i]"\nnew line after";print a[i]}}' file
line 1
line 2
new line before
PBS -N first
PBS -N second
line 3
PBS -V last-1
PBS -V last
new line after
line 4
line 5

How it works:

awk '                                       # Start
/PBS/ {                                     # Does line contains "PBS"
    last=NR                                 # Set last to current line number
    if (!f) {                               # Is flag "f" false
        first=NR                            # Yes, set first line to current line
        f=1}}                               # and set flag "f"
    {
    a[NR]=$0}                               # Store alle line in array "a"
END {
    for (i=1;i<=NR;i++) {                   # Loop trough all lines
        if (i==first)                       # Is line number equal to first hits
            a[i]="new line before\n"a[i]    # Add data before line
        if (i==last)                        # Is line number equal to last hits
            a[i]=a[i]"\nnew line after"     # Add data after line
        print a[i]}}                        # Print the line
' file


0
要让sed正常工作,您需要绕过其逐行操作,然后使用原始正则表达式重新实现它。这并不难,只是有点琐碎。
sed -E 'H;$!d;g
        s/\n[^\n]*PBS/\ninsert before first PBS-containing line&/
        s/.*PBS[^\n]*/&\ninsert after last PBS-containing line/;
        s/.//
'

H;$!d;g slurps the whole file to the hold buffer with an extra newline at the front. (H表示将当前行附加到保留缓冲区中,并在前面添加一个\n$!d表示如果这不是最后一行,则删除;g(及其以下的内容)仅在最后一行运行并检索保留缓冲区。)

因此,s/\n[^\n]*PBS将查找第一个PBS之前的换行符,因为每行之前总有一个换行符,s/.*PBS[^\n]*/将查找最后一个PBS以及所有跟随的换行符,s/.//去除了我们插入的人工换行符,以使第一次出现的搜索起作用。

请注意,您可以通过将其附加到搜索中使第一次出现的插入适用于任意n,例如对于第四个,s/\n[^\n]*PBS/\netc&/4


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