270

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

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

如果我使用

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

它取代了所有#includes。

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

4

24 回答 24

342

sed仅将第一次出现的“Apple”替换为“Banana”的脚本

例子

     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中,范围说明符也是“从这里开始”和“从这里结束”(包括)。然而,最低的“开始”是第一行(第 1 行),如果“这里的结束”是一个正则表达式,那么它只会尝试匹配“开始”之后的下一行,所以最早可能的结束是行2.因此,由于范围包括在内,最小可能范围是“2 行”,最小起始范围是第 1 行和第 2 行(即,如果第 1 行出现,第 2 行的出现也将改变,在这种情况下不需要)。GNUsed 添加了自己的扩展,允许将 start 指定为“伪” line 0,以便范围的结尾可以是line 1,允许它的范围为“仅第一行”

或简化版本(类似空的 RE//表示重复使用之前指定的版本,因此这是等效的):

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

并且花括号对于命令是可选s的,所以这也是等价的:

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

所有这些都只在 GNU 上工作sed

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

于 2012-02-26T13:27:07.140 回答
168
 # 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 

来源

于 2008-09-29T12:29:27.737 回答
62
sed '0,/pattern/s/pattern/replacement/' filename

这对我有用。

例子

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

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

于 2010-08-17T12:30:50.230 回答
59

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

此处的示例使用简化的用例:仅在第一个匹配行中将单词 'foo' 替换为 'bar'。
由于使用ANSI C 引用的字符串 ( $'...')来提供示例输入行,因此bash, ksh, 或被zsh假定为 shell。


仅限GNU sed

Ben Hoffstein 的回答向我们展示了 GNU 为 POSIX 规范提供了一个扩展,允许以下2-address 形式:(在这里表示任意正则表达式)。sed0,/re/re

0,/re/也允许正则表达式在第一行匹配。换句话说:这样的地址将创建一个范围,从第一行到匹配的行(包括匹配的行re)——无论是re出现在第一行还是任何后续行。

  • 将此与符合 POSIX 的表单进行对比,后者创建了一个从第一行到包括后续1,/re/行匹配re的行的范围;换句话说:如果匹配发生在第一行,这将不会检测到匹配的第一次出现,并且还会阻止使用速记来重用最近使用的正则表达式(见下一点)。1re//

如果您将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

笔记:

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

  • POSIXsed在某些函数之后需要实际的换行符,例如在标签的名称之后,甚至在它的省略之后,就像t这里的情况一样;策略性地将脚本拆分为多个-e选项是使用实际换行符的替代方法:-e在通常需要换行符的地方结束每个脚本块。

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

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

反之,如果第一行没有匹配,1,// 就会被输入,并且会找到真正的第一个匹配。

最终效果与 GNU 相同:仅替换第一次出现,无论它出现在第一行还是其他任何地方sed0,/re/


非范围方法

potong 的回答演示了绕过 range 需求的循环技术;因为他使用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.618031,/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开始范围),还没有应用正则表达式,所以//没有引用任何东西。
    除了 GNUsed的特殊0,/re/语法外,任何以行号开头的范围都有效地排除了使用//.
于 2015-10-29T14:07:39.637 回答
31

您可以使用 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",我们需要转义引号。然后我们将 done 变量设置为 1,因此我们不会添加更多包含。

1;

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

于 2008-09-29T12:29:39.887 回答
23

关于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
于 2012-07-12T19:03:55.120 回答
12
#!/bin/sed -f
1,/^#include/ {
    /^#include/i\
#include "newfile.h"
}

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

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

于 2008-09-29T12:41:52.137 回答
10

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

sed s/#include/#include "newfile.h"\n#include/1
于 2008-09-29T12:30:36.453 回答
7

一个可能的解决方案:

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

解释:

  • 读取行直到我们找到#include,打印这些行然后开始新的循环
  • 插入新的包含行
  • 进入一个只读取行的循环(默认情况下 sed 也会打印这些行),我们不会从这里回到脚本的第一部分
于 2008-09-29T12:38:12.893 回答
7

使用 GNU sed 的-z选项,您可以处理整个文件,就好像它只有一行一样。这样 as/…/…/只会替换整个文件中的第一个匹配项。请记住: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|$)//
于 2019-02-12T13:16:06.193 回答
6

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

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/.

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

于 2015-05-24T04:32:20.833 回答
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

可能很草率,我是新手。

于 2008-11-18T02:24:04.540 回答
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
于 2011-04-28T12:52:54.713 回答
2

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

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

它仅更改第一次出现。

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

读入文件并将标准输出重定向到工作文件。

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

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

于 2011-11-17T19:41:53.680 回答
2

使用FreeBSD ed并避免在要处理的文件中ed没有语句的情况下出现“不匹配”错误: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
于 2012-05-16T10:45:24.420 回答
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...
于 2013-07-31T07:31:24.987 回答
2

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

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。

于 2017-07-20T03:43:05.760 回答
1

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

然后简单地解决这些问题就可以解决问题——即使 OP 的措辞只考虑到第一。

sed '1,10s/#include/#include "newfile.h"\n#include/'
于 2019-10-01T08:00:29.907 回答
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

于 2013-02-04T09:00:08.507 回答
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

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

0x2200003
于 2017-01-17T00:37:46.373 回答
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.
于 2018-10-11T05:37:15.870 回答
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用于此目的。

于 2019-12-03T01:43:04.907 回答
-1
sed -e 's/pattern/REPLACEMENT/1' <INPUTFILE
于 2020-02-26T09:23:12.037 回答
-1

我将提出一个不完全是原始问题所要求的建议,但对于那些也想专门替换可能第二次出现的匹配或任何其他具体枚举的正则表达式匹配的人。使用 python 脚本和 for 循环,如果需要,从 bash 脚本中调用它。这对我来说是这样的,我正在替换包含字符串 --project 的特定行:

def replace_models(file_path, pixel_model, obj_model):
    # find your file --project matches
    pattern = re.compile(r'--project.*')
    new_file = ""
    with open(file_path, 'r') as f:
        match = 1
        for line in f:
            # Remove line ending before we do replacement
            line = line.strip()
            # replace first --project line match with pixel
            if match == 1:
                result = re.sub(pattern, "--project='" + pixel_model + "'", line)
            # replace second --project line match with object
            elif match == 2:
                result = re.sub(pattern, "--project='" + obj_model + "'", line)
            else:
                result = line
            # Check that a substitution was actually made
            if result is not line:
                # Add a backslash to the replaced line
                result += " \\"
                print("\nReplaced ", line, " with ", result)
                # Increment number of matches found
                match += 1
            # Add the potentially modified line to our new file
            new_file = new_file + result + "\n"
        # close file / save output
        f.close()
    fout = open(file_path, "w")
    fout.write(new_file)
    fout.close()
于 2021-09-20T18:52:08.533 回答