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

问题描述 投票:183回答:20

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

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

如果我使用

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

它取代了所有#includes。

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

command-line sed text-processing
20个回答
119
投票
 # 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,/RE/s//to_that/' file 

Source


3
投票

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

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

基本上使用grep找到第一次出现并停在那里。还打印行号,即5行。管道进入sed并删除:以及之后的所有内容,只需要留下行号。管道进入sed,添加s /.*/替换到结尾,给出一个1行脚本,管道进入最后一个sed作为文件脚本运行。

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


2
投票

我会用awk脚本执行此操作:

awk -f awkscript headerfile.h > headerfilenew.h

然后用awk运行它:

ed

可能很草率,我是新手。


2
投票

作为替代建议,您可能需要查看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 命令。

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

2
投票

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

${nowms}

它仅更改第一次出现。

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

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

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

在我的情况下,我将命令放在双引号中,因为我在带有变量的Bash脚本中使用它。


2
投票

如果要处理的文件中没有ed语句,请使用FreeBSD 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 的“不匹配”错误:

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

2
投票

这可能适合你(GNU sed):

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

或者如果内存不是问题:

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

2
投票

如果有人来这里替换所有行中第一次出现的字符(比如我自己),请使用:

-z

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


2
投票

使用GNU sed的s/…/…/选项,您可以处理整个文件,就像它只有一行一样。这样一来,s/…/…/只能替换整个文件中的第一场比赛。请记住:-z只替换每行中的第一个匹配,但是使用sed选项,sed -z 's/#include/#include "newfile.h"\n#include' 将整个文件视为一行。

s/text.*//

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

  • s/text[^\n]*//可以改写为[^\n][^\n]*匹配除换行符之外的所有内容。 text将匹配s/^text//之后的所有符号,直到达到换行符。
  • s/(^|\n)text//可以改写为s/text$//
  • s/text(\n|$)//可以改写为sed -e 0,/'<isTag>false<\/isTag>'/{s/'<isTag>false<\/isTag>'//} -e 's/ *$//' -e '/^$/d' source.txt > output.txt

0
投票

以下命令删除文件中第一次出现的字符串。它也删除了空行。它出现在xml文件中,但它适用于任何文件。

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

命令:

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

源文件(source.txt)

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

结果文件(output.txt)

sed -rn '0,/foo(bar).*/ s%%\1%p'

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


0
投票

没什么新的,但也许更具体的答案: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'

使用0x2200003 提取窗口ID会产生:

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

260
投票

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

输入示例:输出:

     Apple       Banana
     Orange      Orange
     Apple       Apple

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

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

0
投票

POSIXly(也在sed中有效),只使用一个正则表达式,只需要一行内存(像往常一样):

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.

解释:

qazxswpoi

55
投票
sed '0,/pattern/s/pattern/replacement/' filename

这对我有用。

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

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


38
投票

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

这里的示例使用简化的用例:仅在第一个匹配行中将'foo'替换为'bar'。 由于使用ANSI C-quoted strings ($'...')提供样本输入线,bashkshzsh被假定为壳。


仅限GNU sed

Ben Hoffstein's anwswer向我们展示了GNU提供了对POSIX specification for sed的扩展,它允许以下2地址形式:0,/re/re代表这里的任意正则表达式)。

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

  • 与符合POSIX标准的形式1,/re/形成对比,后者创建了一个范围,从第一行到最后一行匹配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-features-only sed,如BSD(macOS)sed(也将与GNU sed一起使用):

由于0,/re/不能使用,如果1,/re/碰巧发生在第一行(见上文),re将不会检测到MikhailVS's answer,因此需要对第1行进行特殊处理。

$ 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 提到了这项技术,在这里举了一个具体的例子:

//

注意:

  • 这里使用空的正则表达式s快捷方式两次:一次用于范围的端点,一次用于foo调用;在这两种情况下,regex sed都被隐式重用,允许我们不必复制它,这使得更短和更易维护的代码。
  • POSIX t在某些功能之后需要实际换行符,例如在标签名称之后甚至是其遗漏,就像-e在这里一样;策略性地将脚本拆分为多个-e选项是使用实际换行符的替代方法:结束每个1 s/foo/bar/脚本块,其中通常需要换行。

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

当发生这种情况时,范围地址2(通常从第2行开始发现第一次出现)将不匹配,并且不会处理该范围,因为当当前行已经是1,//时会计算地址。

相反,如果第一行没有匹配,将输入sed,并找到真正的第一场比赛。

净效应与GNU 0,/re/potong's answer相同:只有第一次出现被替换,无论是发生在第一行还是其他任何一行。


非范围方法

sed演示了绕过对范围的需求的循环技术;因为他使用GNU $ 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 语法,这里是符合POSIX的等价物:

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

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

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

1.61803

[1] 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失败:使用sed: -e expression #1, char 0: no previous regular expression(BSD / macOS)和1(GNU),因为在处理第1行时(由于行号//开始范围),还没有应用正则表达式,所以sed没有什么都指。 除了GNU 0,/re/的特殊//语法之外,任何以行号开头的范围都有效地排除了使用awk '/#include/ && !done { print "#include \"newfile.h\""; done=1;}; 1;' file.c


23
投票

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

/#include/ && !done

说明:

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

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

1;

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

linuxtopia sed FAQ

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


17
投票

相当全面的sed '0,/RE/s//to_that/' file 答案集合。它还强调了人们提供的一些答案不适用于非GNU版本的sed,例如

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

在非GNU版本中必须是

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

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

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

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

例如:

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

12
投票
#include

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

但是,如果第一个#include在第1行,那么第1行和下一个后续的sed都会有前面的行。如果您正在使用GNU 0,/^#include/,它有一个扩展,其中1,(而不是sed s/#include/#include "newfile.h"\n#include/1 )将做正确的事情。


11
投票

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

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

8
投票

可能的解决方案:

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

说明:

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