【问题标题】:sed replace last line matching patternsed 替换最后一行匹配模式
【发布时间】:2025-04-23 09:50:01
【问题描述】:

给定这样的文件:

a
b
a
b

我希望能够使用sed 来替换文件中包含“a”实例的最后一行。所以如果我想用“c”替换它,那么输出应该是这样的:

a
b
c
b

请注意,无论它可能遇到多少匹配,或者所需模式或文件内容可能是什么的详细信息,我都需要它来工作。提前致谢。

【问题讨论】:

  • 我还是无法理解你的问题。您说要替换最后一行,但您替换了示例中的第二行。
  • @texasbruce,不是文件的最后一行,而是匹配指定模式的最后一行。
  • 要删除而不是替换最后一个匹配项,请将脚本中的任何s 更改为d。 (有一个重复的问题问这个问题。)

标签: regex bash sed


【解决方案1】:

不完全是 sed:

tac file | sed '/a/ {s//c/; :loop; n; b loop}' | tac

测试

% printf "%s\n" a b a b a b | tac | sed '/a/ {s//c/; :loop; n; b loop}' | tac
a
b
a
b
c
b

反转文件,然后为 first 匹配,进行替换,然后无条件地吞下文件的其余部分。然后重新反转文件。

注意,一个空的正则表达式(这里为s//c/)意味着重新使用之前的正则表达式(/a/

除了非常简单的程序之外,我不是 sed 的忠实粉丝。我会使用 awk:

tac file | awk '/a/ && !seen {sub(/a/, "c"); seen=1} 1' | tac

【讨论】:

  • +1 @glenn jackman,但我不明白:loop , n , b loop 在 sed 命令中的用法,你能解释一下吗?
  • tac file|sed '1,/a/s/a/c/'|tac
  • @NSD,伪代码,match line with "a", substitute "c" for it, then while true; print the current line and fetch the next line——记录在案的here
  • 原来 sed 不执行在同一行开始和结束的范围,所以如果文件中的最后一行是匹配项,我的想法将失败。 GNU sed 已针对该特定问题进行了修复,0,/a/s/a/c/ 会做我想让1,/a/ 做的事情。
  • @jthill:关于需要 GNU sed0,... 范围功能的要点;请注意,这个答案也是 GNU 特定的,因为它使用了非标准实用程序 tac 和特定的 sed 语法。在 macOS 上,您必须使用 tail -r | sed -e '/a/ {s//c/; :loop' -e 'n; bloop' -e '}' | tail -r。似乎 POSIX 并没有完全强制要求使用换行实用程序。
【解决方案2】:

这里有很多好的答案;这是一个概念上简单的两遍sed 解决方案,由tail 辅助,它POSIX 兼容,不会将整个文件读入内存,类似于@987654321 @:

sed "$(sed -n '/a/ =' file | tail -n 1)"' s/a/c/' file
  • sed -n '/a/=' file 输出匹配正则表达式a 的行的编号(函数=),tail -n 1 提取输出的最后行,即文件file 中包含正则表达式最后次出现的行号。

  • 将命令替换 $(sed -n '/a/=' file | tail -n 1) 直接放在 ' s/a/c' 之前会产生一个外部 sed 脚本,例如 3 s/a/c/(带有示例输入),该脚本仅在正则表达式所在的最后一个执行所需的替换发生了。

如果在输入文件中没有找到该模式,则整个命令是有效的空操作。

【讨论】:

  • 很好的解释。 Eran 的解决方案不符合 POSIX 标准吗?或者还有其他理由使用它吗?
  • @TTT:他大部分符合 POSIX(tail -1 必须是 tail -n 1),但主要是他的嵌入式命令(grep + cut + tail) 是不必要的复杂。此外,他的命令使用旧的命令替换语法 (`...`),而不是现代的 $(...) 语法。
【解决方案3】:

这可能对你有用(GNU sed):

sed -r '/^PATTERN/!b;:a;$!{N;/^(.*)\n(PATTERN.*)/{h;s//\1/p;g;s//\2/};ba};s/^PATTERN/REPLACEMENT/' file

或其他方式:

sed '/^PATTERN/{x;/./p;x;h;$ba;d};x;/./{x;H;$ba;d};x;b;:a;x;/./{s/^PATTERN/REPLACEMENT/p;d};x' file

或者如果你喜欢:

sed -r ':a;$!{N;ba};s/^(.*\n?)PATTERN/\1REPLACEMENT/' file

经过反思,这个解决方案可能会取代前两个:

sed  '/a/,$!b;/a/{x;/./p;x;h};/a/!H;$!d;x;s/^a$/c/M' file

如果在文件中找不到正则表达式,则文件将原封不动地通过。一旦正则表达式匹配,所有行都将存储在保留空间中,并在满足一个或两个条件时打印。如果遇到后续的正则表达式,则打印保留空间的内容并用最新的正则表达式替换它。在文件的末尾,第一行保留空间将保留最后一个匹配的正则表达式,并且可以替换它。

【讨论】:

  • 最后一个 (sed -r ':a;$!{N;ba};s/^(.*\n?)PATTERN/\1REPLACEMENT/' file) 是我最喜欢的。它使用 .* 的贪心来匹配最后一个实例,而不是第一个或任何其他实例。
【解决方案4】:

另一个:

tr '\n' ' ' | sed 's/\(.*\)a/\1c/' | tr ' ' '\n'

在行动:

$ printf "%s\n" a b a b a b | tr '\n' ' ' | sed 's/\(.*\)a/\1c/' | tr ' ' '\n'
a
b
a
b
c
b

【讨论】:

  • 如果行中有空格,这将用空格分割行。对于给定的数据,它可以工作,但不一定能很好地概括。
  • 但是,这种方法仍然是创新的,通过删除行克服了sed的逐行处理限制,它可以帮助解决许多未来的问题
【解决方案5】:

另一种方法:

sed "`grep -n '^a$' a | cut -d \: -f 1 | tail -1`s/a/c/" a

这种方法的优点是您可以在文件上按顺序运行两次,而不是将其读入内存。这在大文件中可能很有意义。

【讨论】:

  • 一个小提示:我认为在tail 之后执行cut 会更有计算效率。
【解决方案6】:

这一切都在一个单一的awk完成

awk 'FNR==NR {if ($0~/a/) f=NR;next} FNR==f {$0="c"} 1' file file
a
b
c
b

这会读取文件两次。第一次运行找到最后一个a,第二次运行改变它。

【讨论】:

    【解决方案7】:

    不能容忍缓冲整个输入时的两遍解决方案:

    sed "$(sed -n /a/= <em>file</em> | sed -n '$s/$/ s,a,c,/p' )" <em>file</em>

    (此版本的早期版本在安装 redhat bash-4.1 时遇到了历史扩展的错误,这样可以避免 $!d 被错误地扩展。)

    缓冲尽可能少的一次性解决方案:

    sed '/a/!{1h;1!H};/a/{x;1!p};$!d;g;s/a/c/'
    

    最简单的:

    tac | sed '0,/a/ s/a/c/' | tac
    

    【讨论】:

    • 我无法让您的第一个示例工作。错误(使用 6 个垂直条显示行分隔): sed "$(sed -n /a/=file| sed '$df -h;s/$/ s,a,c,/' )"file ||| ||| sed:-e 表达式#1,字符 3:命令后的额外字符 |||||| sed:-e 表达式#1,字符 5:命令后的额外字符 |||||| sed:-e 表达式#1,字符 1:未知命令:`f'
    • @TTT 看起来你有历史替换(我有$!d,你有$df -h),你使用单引号吗?还有其他东西(我有/a/= file,你有/a/=file,没有空格分隔args)。
    • 很好,我没有注意到替换;是的,它是单引号的,我直接复制粘贴了您的代码,我认为这是因为该语句在 $() 内,因为输入 '$!d' 本身不会导致一个替代品。有趣的是,上面代码中肉眼可见的 file 之前的空格在复制粘贴时不会出现,所以我认为它不应该在那里。看起来你的标记语法有一些我不熟悉的时髦的下划线用法,这可能就是这样做的。添加空格删除了第二个和第三个错误。
    • 嘎嘎。出于某种原因,我没想到有人会这样做,对不起。我已经修好了,所以你现在可以这样做了。
    • 没问题,解决了 3 个错误中的 2 个。仍然得到历史替换。我尝试了一些不同的变体来防止历史替换,但没有运气。根据this post,唯一的解决方法是暂时关闭历史替换。我可以确认这行得通。你能用这个更新你的答案吗?
    【解决方案8】:

    tac infile.txt | sed "s/a/c/; ta ; b ; :a ; N ; ba" | tac

    第一个tac 反转infile.txt 的行,sed 表达式(参见https://*.com/a/9149155/2467140)将'a' 的第一个匹配项替换为'c' 并打印剩余的行,最后一个@987654326 @ 将行反转回原来的顺序。

    【讨论】:

    • 感谢指向语法解释的链接。
    【解决方案9】:

    这是一种只使用awk的方法:

    awk '{a[NR]=$1}END{x=NR;cnt=1;while(x>0){a[x]=((a[x]=="a"&&--cnt==0)?"c <===":a[x]);x--};for(i=1;i<=NR;i++)print a[i]}' file
    
    $ cat f
    a
    b
    a
    b
    f
    s
    f
    e
    a
    v
    $ awk '{a[NR]=$1}END{x=NR;cnt=1;while(x>0){a[x]=((a[x]=="a"&&--cnt==0)?"c <===":a[x]);x--};for(i=1;i<=NR;i++)print a[i]}' f
    a
    b
    a
    b
    f
    s
    f
    e
    c <===
    v
    

    【讨论】:

      【解决方案10】:

      也可以在perl中完成:

      perl -e '@a=reverse<>;END{for(@a){if(/a/){s/a/c/;last}}print reverse @a}' temp > your_new_file
      

      测试:

      > cat temp
      a
      b
      c
      a
      b
      > perl -e '@a=reverse<>;END{for(@a){if(/a/){s/a/c/;last}}print reverse @a}' temp
      a
      b
      c
      c
      b
      > 
      

      【讨论】:

        【解决方案11】:

        这是另一个选择:

        sed -e '$ a a' -e '$ d' file 
        

        第一个命令附加一个a,第二个命令删除最后一行。来自sed(1) man page

        $ 匹配最后一行。

        d 删除模式空间。开始下一个周期。

        a text 附加文本,每个嵌入的换行符前面都有一个反斜杠。

        【讨论】:

        • 不是我的意思。我已经更新了原始问题以澄清。
        【解决方案12】:

        命令如下:

        sed '$s/.*/a/' filename.txt
        

        它正在发挥作用:

        > echo "a
        > b
        > a
        > b" > /tmp/file.txt
        
        > sed '$s/.*/a/' /tmp/file.txt
        a
        b
        a
        a
        

        【讨论】:

        • 不是我的意思。我已经更新了原始问题以澄清。
        【解决方案13】:

        awk-only 解决方案:

        awk '/a/{printf "%s", all; all=$0"\n"; next}{all=all $0"\n"} END {sub(/^[^\n]*/,"c",all); printf "%s", all}' file
        

        解释:

        • 当一行匹配a 时,将打印前一个a 到(不包括)当前a 之间的所有行(即存储在变量all 中的内容)
        • 当一行与a 不匹配时,它会附加到变量all
        • a 匹配的最后一行将无法打印其all 内容,因此您在END 块中手动将其打印出来。不过,在此之前,您可以将匹配 a 的行替换为您想要的任何内容。

        【讨论】:

          【解决方案14】:

          给定:

          $ cat file
          a
          b
          a
          b
          

          您可以使用 POSIX grep 来计算匹配项:

          $ grep -c '^a' file
          2
          

          然后将该号码输入awk 以打印替换:

          $ awk -v last=$(grep -c '^a' file) '/^a/ && ++cnt==last{ print "c"; next } 1' file
          a
          b
          c
          b
          

          【讨论】:

            最近更新 更多