如何使用 sed 替换多行字符串?

Bel*_*dez 353 sed utilities regular-expression

我注意到,如果我添加\n到一个模式来替换 using sed,它不匹配。例子:

$ cat > alpha.txt
This is
a test
Please do not
be alarmed

$ sed -i'.original' 's/a test\nPlease do not/not a test\nBe/' alpha.txt

$ diff alpha.txt{,.original}

$ # No differences printed out
Run Code Online (Sandbox Code Playgroud)

我怎样才能让它发挥作用?

Pet*_*r.O 306

在最简单的调用sed的,它有一个模式空间,即文本行。\n输入中的1 行分隔文本。模式空间中的单行没有\n......这就是你的正则表达式没有找到任何东西的原因。

您可以将多行读入模式空间并以惊人的方式操纵事物,但需要付出比平常更多的努力.. Sed 有一组命令允许这种类型的事情......这是一个指向sed 命令摘要的链接. 这是我找到的最好的一个,让我滚动。

但是,一旦您开始使用 sed 的微命令,请忘记“单行”的想法。将它像结构化程序一样进行布局直到您感觉到它是很有用的……它非常简单,而且同样不寻常。您可以将其视为文本编辑的“汇编语言”。

总结:将 sed 用于简单的事情,也许更多一些,但总的来说,当它超出单行工作范围时,大多数人更喜欢其他东西......
我会让其他人提出其他建议......我真的不确定最好的选择是什么(我会使用 sed,但那是因为我不太了解 perl。)


sed '/^a test$/{
       $!{ N        # append the next line when not on the last line
         s/^a test\nPlease do not$/not a test\nBe/
                    # now test for a successful substitution, otherwise
                    #+  unpaired "a test" lines would be mis-handled
         t sub-yes  # branch_on_substitute (goto label :sub-yes)
         :sub-not   # a label (not essential; here to self document)
                    # if no substituion, print only the first line
         P          # pattern_first_line_print
         D          # pattern_ltrunc(line+nl)_top/cycle
         :sub-yes   # a label (the goto target of the 't' branch)
                    # fall through to final auto-pattern_print (2 lines)
       }    
     }' alpha.txt  
Run Code Online (Sandbox Code Playgroud)

这是相同的脚本,浓缩成明显更难阅读和使用的内容,但有些人会怀疑地称之为单行

sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;ty;P;D;:y}}' alpha.txt
Run Code Online (Sandbox Code Playgroud)

这是我的命令“备忘单”

:  # label
=  # line_number
a  # append_text_to_stdout_after_flush
b  # branch_unconditional             
c  # range_change                     
d  # pattern_delete_top/cycle          
D  # pattern_ltrunc(line+nl)_top/cycle 
g  # pattern=hold                      
G  # pattern+=nl+hold                  
h  # hold=pattern                      
H  # hold+=nl+pattern                  
i  # insert_text_to_stdout_now         
l  # pattern_list                       
n  # pattern_flush=nextline_continue   
N  # pattern+=nl+nextline              
p  # pattern_print                     
P  # pattern_first_line_print          
q  # flush_quit                        
r  # append_file_to_stdout_after_flush 
s  # substitute                                          
t  # branch_on_substitute              
w  # append_pattern_to_file_now         
x  # swap_pattern_and_hold             
y  # transform_chars                   
Run Code Online (Sandbox Code Playgroud)

  • 现在开枪给我。有史以来最糟糕的语法! (256认同)
  • 这是一个绝妙的解释,但我倾向于同意@Gili。 (75认同)
  • 你的备忘单包含了一切。 (17认同)
  • 请注意,该语法是特定于 GNU 的(`#` 命令未与前一个命令分开,`s` 的 RHS 中的 `\n`)。使用 GNU `sed`,您还可以使用 `-z` 来使用 NUL 分隔的记录(如果是文本(根据定义不包含 NUL),则在整个输入中插入)。 (16认同)
  • 在这里使用 `t` 命令不需要标签——如果没有给定标签,它默认分支到脚本的末尾。所以`sed '/^a test$/{$!{N;s/^a test\n请不要$/not a test\nBe/;t;P;D}}' alpha.txt` 完全一样在任何情况下都作为您的命令。当然,对于这个*特定*文件,`sed '/test/{N;s/.*/not a test\nBe/}' alpha.txt` 也做同样的事情,但我的第一个例子在逻辑上等同于 *所有*可能的文件。另请注意,替换字符串中的 `\n` 不会产生换行符;你需要一个反斜杠 ` \ ` 后跟一个实际的换行符来做到这一点。 (4认同)
  • @StéphaneChazelas 您对 GNU `sed` 的 `-z` 评论实际上应该是一个答案,而不是答案的脚注。 (3认同)

cod*_*ead 243

使用perl代替sed

$ perl -0777 -i.original -pe 's/a test\nPlease do not/not a test\nBe/igs' alpha.txt
$ diff alpha.txt{,.original}
2,3c2,3
< not a test
< Be
---
> a test
> Please do not
Run Code Online (Sandbox Code Playgroud)

-pi -e是标准的“就地替换”命令行序列,-0777 会导致 perl 将文件全部吞掉。请参阅perldoc perlrun以了解有关它的更多信息。

  • +1 &amp; 不同意罗伯托。通常问题的措辞是为了不知道更好的方法。当没有实质性的上下文差异时(如这里),最佳解决方案应该至少与特定问题的解决方案一样多。 (89认同)
  • 我认为上面的 `sed` 答案证明 Perl 答案是关于主题的。 (84认同)
  • 稍微简单一点:使用“-p0e”不需要“-0777”。http://unix.stackexchange.com/a/181215/197502 (12认同)
  • 谢谢!对于多行工作,perl 胜出!我最终使用` $ perl -pi -e 's/bar/baz/' fileA` 就地更改文件。 (7认同)
  • 原发帖者要求使用 `sed` 并使用 awk 或 perl 进行回复是很常见的。我认为这不是主题,因此,抱歉,但我打了一个负号。 (4认同)
  • 这种方法如何用于不是来自文件而是作为管道一部分的多行输入?就我而言,`echo -e 'first \n secondary' | sed 's/$/kangaroo/'` 不起作用,因为我得到的是两只袋鼠,而不是一只袋鼠。我可以应用类似的方法来解决我的问题吗? (2认同)

小智 139

我认为,最好\n用其他符号替换符号,然后照常工作:

例如未工作的源代码:

cat alpha.txt | sed -e 's/a test\nPlease do not/not a test\nBe/'
Run Code Online (Sandbox Code Playgroud)

可以改为:

cat alpha.txt | tr '\n' '\r' | sed -e 's/a test\rPlease do not/not a test\rBe/'  | tr '\r' '\n'
Run Code Online (Sandbox Code Playgroud)

如果有人不知道,\n是 UNIX 行结束,\r\n-windows,\r-经典的 Mac OS。普通的 UNIX 文本不使用\r符号,因此在这种情况下使用它是安全的。

您也可以使用一些异国情调的符号来临时替换 \n。例如 - \f(换页符号)。您可以在此处找到更多符号。

cat alpha.txt | tr '\n' '\f' | sed -e 's/a test\fPlease do not/not a test\fBe/'  | tr '\f' '\n'
Run Code Online (Sandbox Code Playgroud)

  • +1 这个聪明的黑客!特别有用的是关于使用特殊符号临时替换换行符的建议,除非您绝对确定您正在编辑的文件的内容。 (16认同)

ant*_*tak 66

考虑到所有因素,吞噬整个文件可能是最快的方法。

基本语法如下:

sed -e '1h;2,$H;$!d;g' -e 's/__YOUR_REGEX_GOES_HERE__...'
Run Code Online (Sandbox Code Playgroud)

请注意,如果文件非常大,吞食整个文件可能不是一种选择。对于这种情况,此处提供的其他答案提供了定制的解决方案,保证在较小的内存占用上工作。

对于所有其他 hack 和 slash 情况,仅在前面-e '1h;2,$H;$!d;g'加上原始sed正则表达式参数就可以完成工作。

例如

$ echo -e "Dog\nFox\nCat\nSnake\n" | sed -e '1h;2,$H;$!d;g' -re 's/([^\n]*)\n([^\n]*)\n/Quick \2\nLazy \1\n/g'
Quick Fox
Lazy Dog
Quick Snake
Lazy Cat
Run Code Online (Sandbox Code Playgroud)

有什么作用-e '1h;2,$H;$!d;g'

12,$$!部分是线说明符限制哪些行上的直接下面的命令运行。

  • 1: 仅第一行
  • 2,$: 从第二行开始的所有行
  • $!: 除了最后一行

如此扩展,这就是在 N 行输入的每一行上发生的情况。

  1: h, d
  2: H, d
  3: H, d
  .
  .
N-2: H, d
N-1: H, d
  N: H, g
Run Code Online (Sandbox Code Playgroud)

g命令没有给出行说明符,但前面的d命令有一个特殊的子句“ Start next cycle. ”,这可以防止g在除最后一行之外的所有行上运行。

至于每个命令的含义:

  • 每行的第一个h后跟Hs 将所述输入行复制到sed保留空间中。(想想任意文本缓冲区。)
  • 之后,d丢弃每一行以防止这些行被写入输出。在保持空间却得以保留。
  • 最后,在最后一行,g保持空间恢复每一行的累积,以便sed能够在整个输入上运行其正则表达式(而不是一次一行的方式),因此能够匹配\ns。


and*_*coz 48

sed有三个命令来管理多行操作:N,DP(将它们与普通的 n,d和进行比较p)。

在这种情况下,您可以匹配模式的第一行,用于N将第二行附加到模式空间,然后用于s进行替换。

就像是:

/a test$/{
  N
  s/a test\nPlease do not/not a test\nBe/
}
Run Code Online (Sandbox Code Playgroud)

  • 这太棒了!比公认的答案更简单,但仍然有效。 (3认同)

Pet*_*ino 42

GNUsed有一个-z选项,允许使用 OP 尝试应用的语法。(手册页

例子:

$ cat alpha.txt
This is
a test
Please do not
be alarmed
Run Code Online (Sandbox Code Playgroud)
$ sed -z 's/a test\nPlease do not\nbe/not a test\nBe/' -i alpha.txt
Run Code Online (Sandbox Code Playgroud)
$ cat alpha.txt
This is
not a test
Be alarmed
Run Code Online (Sandbox Code Playgroud)

请注意:如果您使用^and$它们现在匹配以 NUL 字符(不是\n)分隔的行的开头和结尾。并且,为了确保\n替换所有(-separated)行上的匹配项,不要忘记使用g全局替换标志(例如s/.../.../g)。


致谢: @stéphane-chazelas在上面的评论中首先提到了 -z。

  • 我不确定为什么这不是公认的答案。它是相当干净的 sed,带有一个简单的命令行标志。 (11认同)
  • 作为旁注,sed 不接受连接选项,例如“sed -iz ...”将不起作用,您需要单独指定它们“sed -i -z ...”,如答案中所示。 (4认同)
  • 比公认的答案简单得多,并且有效。 (3认同)
  • *“我不确定为什么这不是公认的答案。”* → 可能是因为该解决方案特定于 sed 的一种实现。它无法在没有 GNU sed 的系统上运行(例如 [Mac OS X](https://apple.stackexchange.com/q/195590)、[Busybox](https://www.busybox.net/downloads/BusyBox) .html#sed)、BSD…)。 (3认同)

Gil*_*il' 17

你可以,但很难。我建议切换到不同的工具。如果有一个正则表达式永远不会匹配您要替换的文本的任何部分,您可以将其用作 GNU awk 中的 awk 记录分隔符。

awk -v RS='a' '{gsub(/hello/, "world"); print}'
Run Code Online (Sandbox Code Playgroud)

如果您的搜索字符串中从来没有两个连续的换行符,您可以使用 awk 的“段落模式”(一个或多个空白行分隔记录)。

awk -v RS='' '{gsub(/hello/, "world"); print}'
Run Code Online (Sandbox Code Playgroud)

一个简单的解决方案是使用 Perl 并将文件完全加载到内存中。

perl -0777 -pe 's/hello/world/g'
Run Code Online (Sandbox Code Playgroud)

  • 另请参阅 GNU `sed` 的 `-z` 选项(在发布该答案后于 2012 年添加):`seq 10 | sed -z 's/4\n5/a\nb/'`。 (4认同)
  • @sebix `perl -0777 -pe '...' &lt;input-file &gt;output-file`。要就地修改文件,`perl -0777 -i -pe '...' filename` (2认同)

mug*_*896 11

我认为这是 2 行匹配的 sed 解决方案。

sed -n '$!N;s@a test\nPlease do not@not a test\nBe@;P;D' alpha.txt
Run Code Online (Sandbox Code Playgroud)

如果你想要 3 行匹配,那么......

sed -n '1{$!N};$!N;s@aaa\nbbb\nccc@xxx\nyyy\nzzz@;P;D'
Run Code Online (Sandbox Code Playgroud)

如果你想要 4 行匹配,那么......

sed -n '1{$!N;$!N};$!N;s@ ... @ ... @;P;D'
Run Code Online (Sandbox Code Playgroud)

如果“s”命令中的替换部分缩小了行,那么像这样更复杂一点

# aaa\nbbb\nccc shrink to one line "xxx"

sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@xxx@;$!N;$!N};P;D'
Run Code Online (Sandbox Code Playgroud)

如果更换部分长出线,那么像这样更复杂一点

# aaa\nbbb\nccc grow to five lines vvv\nwww\nxxx\nyyy\nzzz

sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@vvv\nwww\nxxx\nyyy\nzzz@;P;s/.*\n//M;P;s/.*\n//M};P;D'
Run Code Online (Sandbox Code Playgroud)

第二种方法是对通常的小型文本文件(需要一个 shell 脚本文件)进行简单的逐字复制和粘贴替换

#!/bin/bash

# copy & paste content that you want to substitute

AA=$( cat <<\EOF | sed -z -e 's#\([][^$*\.#]\)#\\\1#g' -e 's#\n#\\n#g'
a test
Please do not
EOF
)

BB=$( cat <<\EOF | sed -z -e 's#\([&\#]\)#\\\1#g' -e 's#\n#\\n#g'
not a test
Be
EOF
)

sed -z -i 's#'"${AA}"'#'"${BB}"'#g' *.txt   # apply to all *.txt files
Run Code Online (Sandbox Code Playgroud)


gib*_*ies 5

sed -i'.original' '/a test/,/Please do not/c not a test \nBe' alpha.txt
Run Code Online (Sandbox Code Playgroud)

这里/a test/,/Please do not/被视为(多行)文本块,c更改命令后跟新文本not a test \nBe

如果要替换的文本很长,我建议使用ex语法。


小智 5

除了 Perl,用于流(和文件)的多行编辑的通用且方便的方法是:

例如,首先根据需要创建一些新的 UNIQUE 行分隔符

$ S=__ABC__                     # simple
$ S=__$RANDOM$RANDOM$RANDOM__   # better
$ S=$(openssl rand -hex 16)     # ultimate
Run Code Online (Sandbox Code Playgroud)

然后在您的 sed 命令(或任何其他工具)中,您将 \n 替换为 ${S},例如

$ cat file.txt | awk 1 ORS=$S |  sed -e "s/a test${S}Please do not/not a test\nBe/" | awk 1 RS=$S > file_new.txt
Run Code Online (Sandbox Code Playgroud)

( awk 用你的替换 ASCII 行分隔符,反之亦然。)

  • 不要对设计为确定性的过程使用随机值。在与随机值匹配的边缘情况下,它们会使您的解决方案随机失败,并且您将很难重现问题(因为它是随机引起的)。请改用专为该问题设计的命令。`sed -z` 就是这样一个解决方案,因为文本流通常不包含 NUL 字符。 (2认同)