如何使用sed替换文件中的第一次出现?

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

我如何得到sed替换一个文件中字符串的第一次出现,而不是替换每一次出现?

如果我用

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

它替换所有#includes。

我们也欢迎为达到同样目的而提出的其他建议。

467513 次浏览
 # 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

Source .

你可以使用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,这样我们就不会添加更多的include。

1;

这意味着“打印出行”——一个空操作默认为打印$0,打印出整行。一个简单的程序,比sed更容易理解:-)

只需要在最后加上出现的次数:

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

一个可能的解决方案:

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

解释:

  • 读取行,直到找到#include,打印这些行,然后开始新的循环
  • 插入新的包含行
  • 输入一个只读取行(默认情况下sed也会打印这些行)的循环,我们不会从这里回到脚本的第一部分
#!/bin/sed -f
1,/^#include/ {
/^#include/i\
#include "newfile.h"
}

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

然而,如果第一个#include在第1行,那么第1行和接下来的#include都将有该行前置。如果你正在使用GNU sed,它有一个扩展,其中0,/^#include/(而不是1,)将做正确的事情。

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

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

然后用awk运行它:

awk -f awkscript headerfile.h > headerfilenew.h

可能有点草率,我是新手。

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

这对我很管用。

例子

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

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

作为一个替代建议,你可能想看看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

我终于得到了这个工作在一个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脚本中使用它。

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的第一个实例的范围内,将Apple替换为Banana。只有第一个Apple将被替换。

背景:在传统的sed中,范围说明符是“从这里开始”和“从这里结束”(包括)。然而,最低的“开始”是第一行(第一行),如果“这里的结束”是一个正则表达式,那么它只会尝试匹配“开始”之后的下一行,因此最早的结束是第2行。因此,由于range是包含的,最小的可能范围是“2行”,最小的起始范围是第1行和第2行(即,如果第1行上有一个事件,第2行上的事件也将被改变,在这种情况下不需要)。GNU sed添加了自己的扩展,允许将start指定为“伪”line 0,以便范围的结束可以是line 1,如果正则表达式匹配第一行,则允许它的范围为“仅第一行”。

或者一个简化版本(像//这样的空RE意味着重用之前指定的正则,所以这是等价的):

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

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

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

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

你也可以使用自制brew install gnu-sed在OS X上安装GNU sed。

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

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

相当全面的收集linuxtopia常见问题解答的答案。它还强调了人们提供的一些答案不能与非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

下面的命令删除文件中字符串的第一次出现。它还删除了空行。它显示在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):

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

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

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到结束数字,这将导致一个1行脚本,该脚本被管道插入到最后一个sed中,作为脚本在文件上运行。

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

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

许多有用的现有的答案概述,补充了解释:

这里的例子使用了一个简化的用例:只在第一个匹配的行中将单词'foo'替换为'bar'。
由于使用ANSI c引号字符串($'...')来提供示例输入行,bashksh,或zsh被假定为shell

GNU sed only:

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

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

  • 与posix兼容的形式1,/re/相比,后者创建了一个范围,从第一行到后续行上与re匹配的行;换句话说:这个将不会检测re匹配的第一次出现,如果它恰好出现在1st防止使用缩写//用于重用最近使用的正则表达式(见下一点).1
如果你将0,/re/地址与使用相同正则表达式的s/.../.../(替换)调用结合在一起,你的命令将有效地只在匹配re第一个行上执行替换。
sed提供了一个方便的用于重用最近应用的正则表达式的快捷方式: 分隔符对,//.

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

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

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

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

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

最终效果与GNU sed0,/re/相同:只替换第一次出现的内容,无论它出现在第一行还是其他任何一行。


NON-range方法

potong的回答演示了< em > < / em >循环技术,即绕过对范围的需求;因为他使用了GNU sed语法,下面是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匹配第一行,并且正则表达式/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/语法外,以行号开头的任何范围有效地排除了//的使用。 李< /一口> < / >

没什么新东西,但可能有一点更具体的答案: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

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

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。

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.

使用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|$)//

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

然后简单地处理这些行就可以修复问题 -即使OP的措辞只考虑第一个。

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

这里可能的解决方案是告诉编译器包含头文件,而不在源文件中提到它。在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.

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

这个特性对于一些常见的头文件来说很方便,比如平台配置。Linux内核的Makefile使用-include来实现这一点。

sed -e 's/pattern/REPLACEMENT/1' <INPUTFILE

我将提出一个建议,这并不完全是最初的问题所要求的,但对于那些还想特别替换匹配的第二次出现,或任何其他特别枚举的正则表达式匹配的人来说。使用python脚本和for循环,如果需要从bash脚本调用它。这是它看起来像我,在我替换特定行包含字符串-项目:

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()

我需要一个在GNU和BSD上都可以工作的解决方案,而且我也知道第一行永远不会是我需要更新的一行:

sed -e "1,/pattern/s/pattern/replacement/"

尝试//特性不重复pattern对我不起作用,因此需要重复它。