如何使用sed只替换文件中的第一个匹配项?

Dav*_*ben 195 command-line text-processing sed

我想在任何现有的#includes之前用额外的include指令更新大量的C++源文件.对于这种任务,我通常使用带有sed的小bash脚本来重写文件.

如何sed更换文件中第一次出现的字符串而不是替换每次出现?

如果我使用

sed s/#include/#include "newfile.h"\n#include/
Run Code Online (Sandbox Code Playgroud)

它取代了所有#includes.

也欢迎提供相同建议的替代建议.

小智 268

写一个sed脚本,只会用"Banana"替换第一次出现的"Apple"

输入示例:输出:

     Apple       Banana
     Orange      Orange
     Apple       Apple
Run Code Online (Sandbox Code Playgroud)

这是一个简单的脚本:编者注:仅适用于GNU sed.

sed '0,/Apple/{s/Apple/Banana/}' filename
Run Code Online (Sandbox Code Playgroud)

  • 翻译成人类语言:从第0行开始,继续直到你匹配'Apple',用大括号执行替换.cfr:http://www.grymoire.com/Unix/Sed.html#uh-29 (155认同)
  • 在OS X上,我得到`sed:1:"...":替换命令中的坏标志:'}'` (8认同)
  • 也没有括号的工作:`sed'0,/ foo/s/foo/bar /'` (8认同)
  • OS X上的@ELLIOTTCABLE,使用`sed -e'1s/Apple/Banana /; t'-e'1,/ Apple/s // Banana /'`.从@ MikhailVS的回答(目前)下面的方式. (5认同)
  • 我得到了`sed:-e表达式#1,字符3:意外的`,'`这个 (3认同)
  • @Jonathan 你能用普遍接受的 Apple-Banana 说法改写你的答案吗?我想我遇到了同样的问题,因为我的“香蕉”恰好包含一个 bash 变量:`sed -i -E "0,|(Apple).*|{s|(Apple:).*|Banana: " $BananaVariable"/data.txt|}" 文件名` (2认同)

Ben*_*ein 123

 # sed script to change "foo" to "bar" only on the first occurrence
 1{x;s/^/first/;x;}
 1,/foo/{x;/first/s///;x;s/foo/bar/;}
 #---end of script---
Run Code Online (Sandbox Code Playgroud)

或者,如果您愿意:编者注:仅适用于GNU sed.

sed '0,/RE/s//to_that/' file 
Run Code Online (Sandbox Code Playgroud)

资源

  • 我想我更喜欢'或者你喜欢'的解决方案.解释答案也是好的 - 并且使答案直接解决问题,然后概括,而不是仅概括.但是答案很好. (79认同)
  • 有人可以解释'或者你是否先做'解决方案?我不知道在哪里放"从"模式. (8认同)
  • 对于Mac用户的FYI,你必须用1代替0,所以:sed'1,/ RE/s // to_that /'文件 (7认同)
  • @Jean-LucNacifCoelho:使用`s//`——即一个_empty_正则表达式——意味着最近应用的正则表达式被隐式重用;在这种情况下,`RE`。这个方便的快捷方式意味着您不必在 `s` 调用中复制范围结束正则表达式。 (4认同)
  • 完整示例:`echo "abc\nabc" | sed '0,/^abc/s//ab/'` (3认同)

小智 56

sed '0,/pattern/s/pattern/replacement/' filename
Run Code Online (Sandbox Code Playgroud)

这对我有用.

例

sed '0,/<Menu>/s/<Menu>/<Menu><Menu>Sub menu<\/Menu>/' try.txt > abc.txt
Run Code Online (Sandbox Code Playgroud)

编者注:两者都只适用于GNU sed.

  • @Landys这仍然替换其他行中的实例; 不只是第一个例子 (2认同)

mkl*_*nt0 39

一个概述了许多有益的现有的答案,与补充说明:

这里的示例使用简化的用例:仅在第一个匹配行中将'foo'替换为'bar'.
由于使用的ANSI C引号字符串($'...'),以提供所述样品输入线,bash,ksh,或zsh假定为壳.


sed仅限GNU:

本Hoffstein的anwswer告诉我们,GNU提供了一个扩展的POSIX规范sed,允许下列2地址形式:0,/re/(re代表一个任意的正则表达式在这里).

0,/re/允许正则表达式在第一行匹配.换句话说:这样的地址将创建从第1行到包括匹配行的范围re- 无论re是在第1行还是在任何后续行上.

  • 与POSIX兼容的形式对比这1,/re/,它创建了一个从第1行直到并包括相匹配的匹配线范围re上后续线; 换句话说:如果它碰巧发生在第一行,这将不会检测到re匹配的第一次出现,并且还阻止使用速记//来重用最近使用的正则表达式(参见下一点).[1]

如果将0,/re/地址与s/.../.../使用相同正则表达式的(替换)调用组合在一起,则命令将仅在匹配的第一行上执行替换re.为重用最近应用的正则表达式
sed提供了一个方便的快捷方式:空分隔符对,//.

$ sed '0,/foo/ s//bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo' 
1st bar         # only 1st match of 'foo' replaced
Unrelated
2nd foo
3rd foo
Run Code Online (Sandbox Code Playgroud)

仅限POSIX功能sed,如BSD(macOS)sed(也适用于GNU sed):

由于0,/re/不能使用并且表格1,/re/不会检测re是否恰好发生在第一行(见上文),因此需要对第1行进行特殊处理.

MikhailVS的回答提到了这种技术,在这里举了一个具体的例子:

$ sed -e '1 s/foo/bar/; t' -e '1,// s//bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar         # only 1st match of 'foo' replaced
Unrelated
2nd foo
3rd foo
Run Code Online (Sandbox Code Playgroud)

注意:

  • 空的正则表达式//快捷方式在这里使用两次:一次用于范围的端点,一次用于s调用; 在这两种情况下,regex都foo被隐式重用,允许我们不必复制它,这使得更短和更易维护的代码.

  • POSIX sed在某些功能之后需要实际的换行符,例如在标签名称之后甚至是其遗漏之后,就像t这里的情况一样; 策略性地将脚本拆分为多个-e选项是使用实际换行符的替代方法:结束每个-e脚本块通常需要换行的位置.

1 s/foo/bar/foo如果在那里找到,只替换在第一行.如果是这样,则t分支到脚本的末尾(跳过该行上的剩余命令).(t仅当最近的s调用执行实际替换时,该函数才会分支到标签;如果没有标签,则此处的情况就是脚本的末尾分支到).

发生这种情况时,范围地址1,//(通常从第2行开始查找第一个匹配项)将不匹配,并且不会处理范围,因为在当前行已经计算时会计算地址2.

相反,如果第一行没有匹配,1,// 则输入,并找到真正的第一场比赛.

净效果是一样的与GNU sed的0,/re/:只有第一发生替换,不管它发生在第一线或任何其他.


非范围方法

potong的回答演示了绕过范围需求的循环技术 ; 因为他使用GNU语法,所以这里是符合POSIX的等价物: sed

循环技术1:在第一次匹配时,执行替换,然后输入一个循环,只是按原样打印剩余的行:

$ sed -e '/foo/ {s//bar/; ' -e ':a' -e '$!{n;ba' -e '};}' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar
Unrelated
2nd foo
3rd foo
Run Code Online (Sandbox Code Playgroud)

循环技术2,仅适用于小文件:将整个输入读入内存,然后对其执行单个替换.

$ sed -e ':a' -e '$!{N;ba' -e '}; s/foo/bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar
Unrelated
2nd foo
3rd foo
Run Code Online (Sandbox Code Playgroud)

[1] 1.61803提供1,/re/了随后出现的情况的例子s//:
- sed '1,/foo/ s/foo/bar/' <<<$'1foo\n2foo'产量$'1bar\n2bar'; 即两条线都被更新,因为线号1与第一条线匹配,而正则表达式/foo/- 范围的结束 - 仅在下一条线上开始查找.因此,在这种情况下选择两条线,并且s/foo/bar/对它们两者执行替换.
- sed '1,/foo/ s//bar/' <<<$'1foo\n2foo\n3foo' 失败:使用sed: first RE may not be empty(BSD/macOS)和sed: -e expression #1, char 0: no previous regular expression(GNU),因为在处理第一行时(由于行号1开始于该范围),尚未应用正则表达式,因此//不引用任何内容.
除了GNU sed的特殊0,/re/语法之外,任何以行号开头的范围都会有效地排除使用//.


ric*_*chq 24

你可以使用awk做类似的事情..

awk '/#include/ && !done { print "#include \"newfile.h\""; done=1;}; 1;' file.c
Run Code Online (Sandbox Code Playgroud)

说明:

/#include/ && !done
Run Code Online (Sandbox Code Playgroud)

当行匹配"#include"并且我们尚未处理它时,在{}之间运行操作语句.

{print "#include \"newfile.h\""; done=1;}
Run Code Online (Sandbox Code Playgroud)

这打印#include"newfile.h",我们需要转义引号.然后我们将done变量设置为1,因此我们不添加更多包含.

1;
Run Code Online (Sandbox Code Playgroud)

这意味着"打印出行" - 空行动默认打印$ 0,打印出整行.一个班轮,比sed IMO更容易理解:-)

  • 这个答案比sed解决方案更便携,它依赖于gnu sed等.(例如,OS-X中的sed糟透了!) (4认同)

Mik*_*lVS 17

关于linuxtopia sed FAQ的全面答案.它还强调了人们提供的一些答案不适用于非GNU版本的sed,例如

sed '0,/RE/s//to_that/' file
Run Code Online (Sandbox Code Playgroud)

在非GNU版本中必须是

sed -e '1s/RE/to_that/;t' -e '1,/RE/s//to_that/'
Run Code Online (Sandbox Code Playgroud)

但是,此版本不适用于gnu sed.

这是一个适用于以下两者的版本:

-e '/RE/{s//to_that/;:a' -e '$!N;$!ba' -e '}'
Run Code Online (Sandbox Code Playgroud)

例如:

sed -e '/Apple/{s//Banana/;:a' -e '$!N;$!ba' -e '}' filename
Run Code Online (Sandbox Code Playgroud)


Soc*_*owi 14

使用 GNU sed 选项,-z您可以处理整个文件,就好像它只是一行一样。这样 as/\xe2\x80\xa6/\xe2\x80\xa6/只会替换整个文件中的第一个匹配项。请记住:s/\xe2\x80\xa6/\xe2\x80\xa6/仅替换每行中的第一个匹配项,但使用该-z选项sed会将整个文件视为单行。

\n\n
sed -z \'s/#include/#include "newfile.h"\\n#include\'\n
Run Code Online (Sandbox Code Playgroud)\n\n

在一般情况下,您必须重写 sed 表达式,因为模式空间现在保存整个文件而不是仅一行。一些例子:

\n\n
    \n
  • s/text.*//可以重写为s/text[^\\n]*//. [^\\n]匹配除换行符之外的所有内容。[^\\n]*将匹配之后的所有符号,text直到到达换行符。
  • \n
  • s/^text//可以重写为s/(^|\\n)text//.
  • \n
  • s/text$//可以重写为s/text(\\n|$)//.
  • \n
\n


Chr*_*ung 12

#!/bin/sed -f
1,/^#include/ {
    /^#include/i\
#include "newfile.h"
}
Run Code Online (Sandbox Code Playgroud)

此脚本的工作原理:对于1和第1行之间的#include行(第1行之后),如果行以该行开头#include,则在前面添加指定的行.

但是,如果第一#include行在第1行,那么第1行和下一#include行将在前面添加该行.如果你正在使用GNU sed,它有一个扩展,其中0,/^#include/(而不是1,)将做正确的事情.

  • 提及0的+1是GNU扩展. (3认同)

une*_*ist 11

只需在最后添加出现次数:

sed s/#include/#include "newfile.h"\n#include/1
Run Code Online (Sandbox Code Playgroud)

  • 嗯...时间过去了.[`sed`]的POSIX 2008/2013(http://pubs.opengroup.org/onlinepubs/9699919799/utilities/sed.html)使用以下命令指定替换命令:_` [2addr] s/BRE/replacement/flags` _并注意到"标志的值应为零或更多:_n_替换仅在模式空间内找到的BRE的第n次出现." 因此,至少在POSIX 2008中,尾随的"1"不是GNU"sed"扩展.实际上,即使在[SUS/POSIX 1997](http://pubs.opengroup.org/onlinepubs/7990989775/)标准中,这也得到了支持,所以我在2008年严重失控. (9认同)
  • 不幸的是,这不起作用.它取代了文件每一行的第一次出现,而不是文件中第一次出现. (7认同)

mit*_*ull 8

可能的解决方案:

    /#include/!{p;d;}
    i\
    #include "newfile.h"
    :
    n
    b
Run Code Online (Sandbox Code Playgroud)

说明:

  • 读取行直到我们找到#include,打印这些行然后开始新的循环
  • 插入新的包含行
  • 进入一个只读取行的循环(默认sed也会打印这些行),我们不会从这里回到脚本的第一部分


小智 6

我知道这是一篇旧帖子,但我有一个我曾经使用过的解决方案:

grep -E -m 1 -n 'old' file | sed 's/:.*$//' - | sed 's/$/s\/old\/new\//' - | sed -f - file
Run Code Online (Sandbox Code Playgroud)

基本上使用 grep 打印第一次出现并停止。另外打印行号,即5:line. 将其导入 sed 并删除 : 以及之后的任何内容,因此您只剩下一个行号。将其通过管道传输到 sed 中,将 s/.*/replace 添加到结束编号​​,这将生成一个 1 行脚本,该脚本通过管道传输到最后一个 sed 以作为文件上的脚本运行。

因此,如果 regex =#include和 replace =blah并且 grep 发现的第一次出现在第 5 行,那么通过管道传输到最后一个 sed 的数据将是5s/.*/blah/.

即使第一次出现在第一行也有效。