代码之家  ›  专栏  ›  技术社区  ›  David Dibben

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

  •  172
  • David Dibben  · 技术社区  · 17 年前

    我想在现有的任何一个包含的指令之前更新一个额外的包含指令的大量C++源文件。对于这类任务,我通常使用带有sed的小型bash脚本来重新编写文件。

    我如何得到 sed 只替换文件中字符串的第一个匹配项,而不替换每个匹配项?

    如果我使用

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

    它取代了所有的包括。

    此外,我们也欢迎其他建议来实现同样的目标。

    20 回复  |  直到 7 年前
        1
  •  108
  •   fedorqui    10 年前
     # 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

        2
  •  228
  •   rogerdpack    8 年前

    编写一个SED脚本,该脚本只会将第一个出现的“apple”替换为“banana”。

    示例输入:输出:

         Apple       Banana
         Orange      Orange
         Apple       Apple
    

    这是一个简单的脚本: 编者按:使用 GNU sed 只有。

    sed '0,/Apple/{s/Apple/Banana/}' filename
    
        3
  •  50
  •   rogerdpack    8 年前
    sed '0,/pattern/s/pattern/replacement/' filename
    

    这对我有用。

    例子

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

    编者按:两者都适用于 GNU sed 只有。

        4
  •  32
  •   mklement0    7 年前

    概述 在众多有帮助的人中 现有答案 ,补充 解释 :

    这里的示例使用了一个简化的用例:只在第一个匹配行中将单词“foo”替换为“bar”。
    由于使用 ANSI C-quoted strings ( $'...' ) 提供样本输入行, bash , ksh zsh 假设为外壳。


    GNU sed 只有:

    Ben Hoffstein's anwswer 向我们展示GNU提供了 延伸 POSIX specification for sed 允许以下两个地址的表单: 0,/re/ ( re 在此处表示任意正则表达式)。

    0,/Re/ 允许regex 比赛 也在第一行 . 换句话说:这样的地址将创建一个从第一行到匹配行的范围。 重新 -是否 重新 在第一行或任何后续行上发生。

    • 将其与符合POSIX的表单进行对比 1,/re/ ,创建从第一行到匹配的行的匹配范围 重新 后来的 行;换句话说:这个 不会检测到 重新 如果发生在 第一 线 而且 防止使用速记 // 重新使用最近使用的regex(见下一点)。 〔1〕

    如果你结合了 0,/Re/ 地址与 s/.../.../ (替换)使用 相同的 正则表达式中,您的命令将有效地只对 第一 匹配的行 重新 .
    塞德 提供方便 重用最近应用的正则表达式的快捷方式 一个 空的 定界符对, / / .

    $ 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功能 塞德 如BSD(MacOS) 塞德 (也将与 GNU 塞德 ):

    自从 0,/Re/ 无法使用和窗体 1,/Re/ 不会检测 重新 如果发生在第一行(见上文)。 1号线需要特殊处理 .

    MikhailVS's answer 提到这项技术,在这里举一个具体的例子:

    $ 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 调用;在这两种情况下,regex foo 它是隐式重用的,允许我们不必复制它,这使得代码既短又易于维护。

    • POSIX 塞德 在某些函数后需要实际换行,例如在标签名称后,甚至在标签省略后,如 t 这里;战略性地将脚本拆分为多个 -e 选项是使用实际换行的替代方法:每个换行结束 -e 在通常需要换行的地方编写块脚本。

    1 s/foo/bar/ 替换 仅在第一行,如果在那里找到的话。 如果是这样, T 分支到脚本结尾(跳过行上的其余命令)。(The T 仅当最新的 S 调用执行了实际的替换;在没有标签的情况下,如这里的情况一样,脚本的结尾被分支到)。

    当发生这种情况时,范围地址 1,// ,通常第一次出现 从2号线开始 威尔 匹配,范围将 处理,因为当当前行 2 .

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

    净效应与GNU相同 塞德 0,/Re/ :仅替换第一个匹配项,无论它出现在第一行还是任何其他行。


    非靶场进近

    potong's answer 演示 技术 那个 不需要一个范围 因为他使用 GNU 塞德 语法,这里是 符合POSIX的等价物 :

    循环技术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 与第1行匹配,并且regex /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),因为在处理第一行时(由于行号 开始这个范围),还没有应用regex,所以 / / 不涉及任何内容。
    除了GNU 塞德 特产 0,/Re/ 语法, 任何 以a开头的范围 行号 有效地阻止使用 / / .

        5
  •  22
  •   richq luc    17 年前

    你可以用锥子做类似的事情。

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

    说明:

    /#include/ && !done
    

    当行与“include”匹配时,在之间运行操作语句,但我们尚未处理它。

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

    此打印内容包括“newfile.h”,我们需要转义引号。然后我们将done变量设置为1,这样就不会添加更多的include。

    1;
    

    这意味着“打印出行”—一个空操作默认为打印$0,它打印出整行。一行,比SED IMO更容易理解:—)

        6
  •  16
  •   rogerdpack    8 年前

    相当全面的答案集 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/'
    

    但是,这个版本不适用于GNUSED。

    这是一个同时适用于以下两种情况的版本:

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

    前任:

    sed -e '/Apple/{s//Banana/;:a' -e '$!N;$!ba' -e '}' filename
    
        7
  •  12
  •   unexist    17 年前

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

    sed s/#include/#include "newfile.h"\n#include/1
    
        8
  •  12
  •   C. K. Young    17 年前
    #!/bin/sed -f
    1,/^#include/ {
        /^#include/i\
    #include "newfile.h"
    }
    

    此脚本的工作方式:对于介于1和第一行之间的行 #include (在第1行之后),如果行以 包括: ,然后预置指定的行。

    但是,如果第一个 包括: 在第1行,然后是第1行和下一个后续行 包括: 将预先安排行。如果您使用GNU sed ,它有一个扩展,其中 0,/^#include/ 而不是 1, )会做正确的事。

        9
  •  8
  •   mitchnull    17 年前

    可能的解决方案:

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

    说明:

    • 读取行,直到找到include,打印这些行,然后开始新的循环
    • 插入新的包含行
    • 输入一个只读取行的循环(默认情况下,sed也会打印这些行),我们将无法从这里返回到脚本的第一部分。
        10
  •  3
  •   Michael Edwards    11 年前

    我知道这是一篇老文章,但我有一个解决方案,我以前用过:

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

    基本上用grep找到第一次出现的地方并停在那里。同时打印行号IE 5:line。通过管道将其导入SED,然后移除:和之后的任何内容,这样您就只剩下一个行号了。将其导入到SED中,并在末尾添加s/*/replace,该结尾将提供一个单行脚本,该脚本将被导入到最后一个SED中,以作为文件中的脚本运行。

    因此,如果regex=include and replace=blah并且grep发现的第一个事件在第5行,那么最后一个sed的数据将是5s/*/blah/。

        11
  •  2
  •   wakingrufus    17 年前

    我会用一个awk脚本:

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

    然后用awk运行:

    awk -f awkscript headerfile.h > headerfilenew.h
    

    可能是马虎,我是新来的。

        12
  •  2
  •   timo    15 年前

    作为另一种建议,您可能希望查看 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
    
        13
  •  2
  •   Rob W jminkler    14 年前

    我最终在一个bash脚本中实现了这一点,该脚本用于在RSS提要的每个项目中插入一个唯一的时间戳:

            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脚本中使用它。

        14
  •  2
  •   nazq    14 年前

    使用 FreeBSD ed 避免 预计起飞时间 'S“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
    
        15
  •  2
  •   potong    12 年前

    这可能适用于您(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...
    
        16
  •  2
  •   FatihSarigol    8 年前

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

    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。

        17
  •  0
  •   Andreas Panagiotidis    13 年前

    以下命令删除文件中第一个出现的字符串。它也删除了空行。它出现在一个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:在SolarisSunos5.10(相当老的版本)上它对我不起作用,但是它在Linux2.6,SED版本4.1.5上起作用。

        18
  •  0
  •   Stephen Niedzielski    9 年前

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

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

    0x2200003
    
        19
  •  0
  •   Isaac    7 年前

    posixly(在sed中也有效),仅限 使用了regex,只需要一行内存(和往常一样):

    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.
    
        20
  •  0
  •   Socowi    7 年前

    用GNU SED -z 选项可以处理整个文件,就好像它只是一行一样。那样的话 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|$)// .
    推荐文章