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


217

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

如何sed仅替换文件中字符串的第一个匹配项而不是替换每个匹配项?

如果我用

sed s/#include/#include "newfile.h"\n#include/

它替换了所有#includes。

也欢迎实现相同目标的替代建议。

Answers:


135
 # 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---

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

sed '0,/foo/s//bar/' file 

资源


86
我认为我更喜欢“或如果您愿意”的解决方案。解释答案也应该是很好的-使答案直接解决问题,然后进行概括,而不是仅仅概括。但是很好的答案。
乔纳森·勒夫勒

7
仅供参考,对于Mac用户,您必须将0替换为1,因此:sed'1,/ RE / s // to_that /'文件
mhost 2014年

1
@mhost否,如果在#1行中找到了pattern,它将代替,而如果pattern在另一行中则不会,但是仍然是第一个找到的pattern。PS应该指出,这0,仅适用于gnu sed
Jotne,2014年

11
有人可以解释一下“或者您是否喜欢”解决方案吗?我不知道将“ from”模式放在哪里。
Jean-Luc Nacif Coelho

3
@ Jean-LucNacifCoelho:使用s//-即空的正则表达式-意味着隐式重用了最近应用的正则表达式;在这种情况下,RE。此便捷的快捷方式意味着您不必在s通话中重复范围结尾的正则表达式。
mklement0

289

一个sed脚本,将只能通过“香蕉”替换“苹果”的第一次出现

     Input:      Output:

     Apple       Banana
     Apple       Apple
     Orange      Orange
     Apple       Apple

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

sed '0,/Apple/{s/Apple/Banana/}' input_filename

前两个参数0/Apple/是范围说明符。该s/Apple/Banana/是什么是该范围内执行。因此,在这种情况下,“在开始(0)的范围内,直到的第一个实例Apple,替换AppleBanana。只有第一个Apple会被替换。

背景:传统上sed,范围说明符也是 “从此处开始”和“从此处结束”(含)。但是,最低的“ begin”是第一行(第1行),如果“ end here”是一个正则表达式,则仅尝试在“ begin”之后的下一行进行匹配,因此最早的结尾是line 2.因此,由于范围是包括在内的,因此最小的可能范围是“ 2行”,最小的起始范围是第1行和第2行(即,如果第1行出现了情况,那么第2行出现的情况也将被更改,在这种情况下不希望如此) )。GNUsed添加了自己的扩展名,允许将start指定为“ pseudo”,line 0以便范围的结尾可以为line 1,从而允许其范围为“仅第一行”

或简化版本(像这样的空RE //意味着可以重复使用在其之前指定的RE ,因此等效):

sed '0,/Apple/{s//Banana/}' input_filename

花括号对于命令来说是可选s,因此这也是等效的:

sed '0,/Apple/s//Banana/' input_filename

所有这些sed仅在GNU上起作用。

您也可以使用homebrew在OS X上安装GNU sed brew install gnu-sed


166
翻译成人类语言:从第0行开始,继续直到匹配“ Apple”,然后在大括号中执行替换。CFR:grymoire.com/Unix/Sed.html#uh-29
mariotomo

8
在OS X上,我得到sed: 1: "…": bad flag in substitute command: '}'
ELLIOTTCABLE

5
在OS X上使用@ELLIOTTCABLE,请使用sed -e '1s/Apple/Banana/;t' -e '1,/Apple/s//Banana/'。从@MikhailVS的答案(当前)到下面。
djb 2015年

8
也无需括号即可工作:sed '0,/foo/s/foo/bar/'
Innokenty 2015年

5
我知道了sed: -e expression #1, char 3: unexpected ,'
乔纳森·

56
sed '0,/pattern/s/pattern/replacement/' filename

这对我有用。

sed '0,/<Menu>/s/<Menu>/<Menu><Menu>Sub menu<\/Menu>/' try.txt > abc.txt

编者注:两者都仅适用于GNU sed


2
@Landys这仍然会替换其他行中的实例;不仅是初审
2015年

1
@sarat是的,您是对的。sed '1,/pattern/s/pattern/replacement/' filename仅在Mac上仅当“模式不会出现在第一行”时才有效。由于不正确,我将删除之前的评论。可以在这里找到详细信息(linuxtopia.org/online_books/linux_tool_guides/the_sed_faq/…)。Andy的答案仅适用于GNU sed,而不适用于Mac。
Landys 2015年

45

现有许多有用答案概述,并附有解释

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


sedGNU

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

0,/re/也允许正则表达式在第一行进行匹配。换句话说:这样的地址将创建从第一线到一个范围,并包括相匹配的线re-是否re在第一行上或任何后续的线路中发生。

  • 与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

仅POSIX功能,sed例如BSD(macOS)sed(也可以与GNU一起使用 sed):

由于0,/re/无法使用,并且表单1,/re/将无法检测re它是否恰好发生在第一行(请参见上文),因此需要对第一行进行特殊处理

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

注意:

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

  • POSIX sed在某些功能之后需要实际的换行符,例如在标签名之后,甚至在其省略之后,例如t此处;从策略上将脚本分成多个-e选项是使用实际换行符的一种替代方法:在每个-e脚本块的正常换行处结束。

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

发生这种情况时,1,//通常会从第2行开始找到第一个匹配项的范围地址将匹配,并且不会处理该范围,因为该地址是在当前行已经存在时才进行评估的2

相反,如果第一行没有匹配项,1,// 则将输入该值,并找到真正的第一个匹配项。

净效果是一样的与GNU sed0,/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

循环技术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

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/语法外,任何行号开头的范围都将有效地禁止使用//


25

您可以使用awk做类似的事情。

awk '/#include/ && !done { print "#include \"newfile.h\""; done=1;}; 1;' file.c

说明:

/#include/ && !done

当该行与“ #include”匹配并且尚未处理时,在{}之间运行该动作语句。

{print "#include \"newfile.h\""; done=1;}

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

1;

这意味着“打印出该行”-一个空操作默认会打印$ 0,这会打印出整行。一个衬里,比sed IMO更容易理解:-)


4
这个答案比依赖于gnu sed等的sed解决方案更可移植。(例如OS-X中的sed很烂!)
Jay Taylor

1
这确实更容易理解,但是对我来说,它添加了一行而不是替换它;使用的命令: awk '/version/ && !done {print " \"version\": \"'${NEWVERSION}'\""; done=1;}; 1;' package.json
谷物杀手

同样,这是最容易理解的命令,但是它在找到的字符串上方添加了一行,而不是替换了它
Flion

1
答案很可读。这是我的版本,它代替字符串而不是添加新行。awk '/#include/ && !done { gsub(/#include/, "include \"newfile.h\""); done=1}; 1' file.c
奥西里斯·德·琼

18

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

sed '0,/RE/s//to_that/' file

在非GNU版本中必须

sed -e '1s/RE/to_that/;t' -e '1,/RE/s//to_that/'

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

这是可以同时使用的版本:

-e '/RE/{s//to_that/;:a' -e '$!N;$!ba' -e '}'

例如:

sed -e '/Apple/{s//Banana/;:a' -e '$!N;$!ba' -e '}' filename

实际上,已经在Ubuntu Linux v16和FreeBSD v10.2上进行了测试。谢谢。
Sopalajo de Arrierez,

12
#!/bin/sed -f
1,/^#include/ {
    /^#include/i\
#include "newfile.h"
}

该脚本的工作方式:对于1到第一行之间的#include行(在第1行之后),如果该行以开头#include,则在指定的行之前添加。

但是,如果第一个#include在第1行中,则第1行和下一个随后#include的行都将在该行之前。如果您使用的是GNU sed,则它具有一个扩展名0,/^#include/(而不是1,),它将做正确的事情。


11

只需在末尾添加出现次数:

sed s/#include/#include "newfile.h"\n#include/1

7
不幸的是,这不起作用。它替换文件的每一行上的第一个匹配项,而不是文件中的第一个匹配项。
David Dibben

1
此外,它是GNU sed扩展,不是标准sed功能。
乔纳森·莱夫勒

10
嗯...时间流逝。POSIX 2008/2013用于sed指定替换命令,[2addr]s/BRE/replacement/flags并带有:并指出“标志的值应为零或更大的值: n 仅替换在模式空间中找到的BRE的第n次出现。” 因此,至少在POSIX 2008中,结尾1不是GNU sed扩展。的确,即使在SUS / POSIX 1997标准中,该功能也得到了支持,因此我在2008
。– Jonathan Leffler 2016年

7

可能的解决方案:

    /#include/!{p;d;}
    i\
    #include "newfile.h"
    :a
    n
    ba

说明:

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

sed: file me4.sed line 4: ":" lacks a label
rogerdpack

显然,在最近的sed版本中有所更改,并且不再允许使用空标签。更新了答案
mitchnull

4

我知道这是一篇旧文章,但是我曾经使用过一个解决方案:

grep -E -m 1 -n 'old' file | sed 's/:.*$//' - | sed 's/$/s\/old\/new\//' - | sed -f - file

基本上使用grep打印第一个匹配项并在此停止。另外打印行号,即5:line。用管道将其插入sed并删除:和之后的所有内容,因此只剩下一个行号。通过管道将其添加到sed中,在末尾添加s /.*/ replace,从而生成1行脚本,该脚本将通过管道传输到最后一个sed中,以作为文件上的脚本运行。

因此,如果regex = #include和replace = blah并且grep发现的第一个匹配项位于第5行,则通过管道传输到最后一个sed的数据将为5s/.*/blah/

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


我非常讨厌多行sed脚本或sed命令,除了s和行号以外,什么都没有,所以我对这种方法很满意。这是我用于用例(与bash一起使用)的内容:filepath = / etc / hosts; patt ='^ \(127 \ .0 \ .0 \ .1。* \)'; repl ='\ 1 newhostalias'; sed $(IFS =:linearray =($(grep -E -m 1 -n“ $ patt”“ $ filepath”))&& echo $ {linearray [0]})s /“ $ patt” /“ $ repl” /“ $ filepath”
parity3

有用。尽管只有sed足够聪明才能接受sed -f -其中的一些,但您可以解决它:)
rogerdpack

3

如果有人来这里替换所有行中第一个出现的字符(例如我自己),请使用以下命令:

sed '/old/s/old/new/1' file

-bash-4.2$ cat file
123a456a789a
12a34a56
a12
-bash-4.2$ sed '/a/s/a/b/1' file
123b456a789a
12b34a56
b12

例如,通过将1更改为2,可以替换所有第二个a。


2
你并不需要做的这一切,'s/a/b/'手段match a,和do just first match for every matching line
Samveen

我和萨姆芬在一起。此外,这并不回答此问的问题。我建议删除此答案。
Socowi

问题是“文件中的首次出现”而不是“一行中的首次出现”
Manuel Romeiro

3

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

sed -z 's/#include/#include "newfile.h"\n#include'

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

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

2

我会用awk脚本来做到这一点:

BEGIN {i=0}
(i==0) && /#include/ {print "#include \"newfile.h\""; i=1}
{print $0}    
END {}

然后用awk运行它:

awk -f awkscript headerfile.h > headerfilenew.h

可能草率,我是新来的。


2

作为替代建议,您可能需要查看ed命令。

man 1 ed

teststr='
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
'

# for in-place file editing use "ed -s file" and replace ",p" with "w"
# cf. http://wiki.bash-hackers.org/howto/edit-ed
cat <<-'EOF' | sed -e 's/^ *//' -e 's/ *$//' | ed -s <(echo "$teststr")
   H
   /# *include/i
   #include "newfile.h"
   .
   ,p
   q
EOF

2

我终于在一个用于在RSS feed中的每个项目中插入唯一时间戳的Bash脚本中使它工作:

        sed "1,/====RSSpermalink====/s/====RSSpermalink====/${nowms}/" \
            production-feed2.xml.tmp2 > production-feed2.xml.tmp.$counter

它仅更改第一次出现。

${nowms}是Perl脚本设置的时间(以毫秒为单位),$counter是用于脚本内循环控制的计数器,\允许命令在下一行继续。

读入文件,并将stdout重定向到工作文件。

我的理解方式是,1,/====RSSpermalink====/通过设置范围限制来告诉sed何时停止,然后s/====RSSpermalink====/${nowms}/是熟悉的sed命令将第二个字符串替换为第一个字符串。

就我而言,我将命令放在双引号中,因为我在带有变量的Bash脚本中使用了该命令。


2

如果要处理的文件中没有语句,请使用FreeBSD ed并避免出现ed“ no match”错误include

teststr='
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
'

# using FreeBSD ed
# to avoid ed's "no match" error, see
# *emphasized text*http://codesnippets.joyent.com/posts/show/11917 
cat <<-'EOF' | sed -e 's/^ *//' -e 's/ *$//' | ed -s <(echo "$teststr")
   H
   ,g/# *include/u\
   u\
   i\
   #include "newfile.h"\
   .
   ,p
   q
EOF

这与timo答案非常相似,但一年后才添加。
乔纳森·勒夫勒

2

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

sed -si '/#include/{s//& "newfile.h\n&/;:a;$!{n;ba}}' file1 file2 file....

或如果内存不是问题:

sed -si ':a;$!{N;ba};s/#include/& "newfile.h\n&/' file1 file2 file...

0

以下命令删除文件中字符串的首次出现。它也删除了空行。它显示在xml文件中,但可以与任何文件一起使用。

如果您使用xml文件并且要删除标签,则很有用。在此示例中,它删除了“ isTag”标签的第一次出现。

命令:

sed -e 0,/'<isTag>false<\/isTag>'/{s/'<isTag>false<\/isTag>'//}  -e 's/ *$//' -e  '/^$/d'  source.txt > output.txt

源文件(source.txt)

<xml>
    <testdata>
        <canUseUpdate>true</canUseUpdate>
        <isTag>false</isTag>
        <moduleLocations>
            <module>esa_jee6</module>
            <isTag>false</isTag>
        </moduleLocations>
        <node>
            <isTag>false</isTag>
        </node>
    </testdata>
</xml>

结果文件(output.txt)

<xml>
    <testdata>
        <canUseUpdate>true</canUseUpdate>
        <moduleLocations>
            <module>esa_jee6</module>
            <isTag>false</isTag>
        </moduleLocations>
        <node>
            <isTag>false</isTag>
        </node>
    </testdata>
</xml>

ps:它对我在Solaris SunOS 5.10(旧版本)上不起作用,但是在Linux 2.6(sed版本4.1.5)上起作用


这看起来与许多先前的答案非常相似,但有一点相同,即仅适用于GNU sed(因此不适用于Solaris)。您应该删除它-确实,它对于回答已经有4½年历史的问题并没有提供独特的新信息。当然,它确实有一个可行的示例,但是当问题的答案与此答案相同时,这具有可争议的价值。
乔纳森·莱夫勒

0

没什么新鲜的,但也许更具体的答案: sed -rn '0,/foo(bar).*/ s%%\1%p'

示例:xwininfo -name unity-launcher产生如下输出:

xwininfo: Window id: 0x2200003 "unity-launcher"

  Absolute upper-left X:  -2980
  Absolute upper-left Y:  -198
  Relative upper-left X:  0
  Relative upper-left Y:  0
  Width: 2880
  Height: 98
  Depth: 24
  Visual: 0x21
  Visual Class: TrueColor
  Border width: 0
  Class: InputOutput
  Colormap: 0x20 (installed)
  Bit Gravity State: ForgetGravity
  Window Gravity State: NorthWestGravity
  Backing Store State: NotUseful
  Save Under State: no
  Map State: IsViewable
  Override Redirect State: no
  Corners:  +-2980+-198  -2980+-198  -2980-1900  +-2980-1900
  -geometry 2880x98+-2980+-198

提取带有xwininfo -name unity-launcher|sed -rn '0,/^xwininfo: Window id: (0x[0-9a-fA-F]+).*/ s%%\1%p'产生的窗口ID :

0x2200003

0

POSIXly(在sed中也有效),仅使用一个正则表达式,仅需要为一行存储(通常):

sed '/\(#include\).*/!b;//{h;s//\1 "newfile.h"/;G};:1;n;b1'

解释:

sed '
/\(#include\).*/!b          # Only one regex used. On lines not matching
                            # the text  `#include` **yet**,
                            # branch to end, cause the default print. Re-start.
//{                         # On first line matching previous regex.
    h                       # hold the line.
    s//\1 "newfile.h"/      # append ` "newfile.h"` to the `#include` matched.
    G                       # append a newline.
  }                         # end of replacement.
:1                          # Once **one** replacement got done (the first match)
n                           # Loop continually reading a line each time
b1                          # and printing it by default.
'                           # end of sed script.

0

用例可能是您的事件分布在整个文件中,但是您知道唯一需要关注的是前10、20或100行。

然后简单地解决这些问题就可以解决此问题 -即使OP的措辞仅是最先考虑的。

sed '1,10s/#include/#include "newfile.h"\n#include/'

0

此处可能的解决方案可能是告诉编译器包括标头,而不在源文件中提及标头。在GCC中,有以下选项:

   -include file
       Process file as if "#include "file"" appeared as the first line of
       the primary source file.  However, the first directory searched for
       file is the preprocessor's working directory instead of the
       directory containing the main source file.  If not found there, it
       is searched for in the remainder of the "#include "..."" search
       chain as normal.

       If multiple -include options are given, the files are included in
       the order they appear on the command line.

   -imacros file
       Exactly like -include, except that any output produced by scanning
       file is thrown away.  Macros it defines remain defined.  This
       allows you to acquire all the macros from a header without also
       processing its declarations.

       All files specified by -imacros are processed before all files
       specified by -include.

Microsoft的编译器具有/ FI(强制包含)选项。

对于某些常见的标头(例如平台配置),此功能可能很方便。Linux内核的Makefile -include为此使用。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.