我想在任何现有的#include之前,用一个额外的include指令更新大量的c++源文件。对于这类任务,我通常使用带有sed的小型bash脚本来重写文件。
我如何得到sed替换一个文件中字符串的第一次出现,而不是替换每一次出现?
如果我用
sed s/#include/#include "newfile.h"\n#include/
它替换所有#includes。
我们也欢迎为达到同样目的而提出的其他建议。
我想在任何现有的#include之前,用一个额外的include指令更新大量的c++源文件。对于这类任务,我通常使用带有sed的小型bash脚本来重写文件。
我如何得到sed替换一个文件中字符串的第一次出现,而不是替换每一次出现?
如果我用
sed s/#include/#include "newfile.h"\n#include/
它替换所有#includes。
我们也欢迎为达到同样目的而提出的其他建议。
当前回答
以下是许多有用的现有答案的概述,并附有解释:
这里的例子使用了一个简化的用例:只在第一个匹配的行中将单词'foo'替换为'bar'。 由于使用ANSI c引号字符串($'…')来提供示例输入行,bash、ksh或zsh被假定为shell。
GNU仅适用:
Ben Hoffstein的回答告诉我们,GNU为sed提供了POSIX规范的扩展,它允许以下2-address形式:0,/re/ (re在这里表示任意正则表达式)。
0,/re/也允许正则表达式匹配第一行。换句话说:这样的地址将创建一个范围,从第一行到匹配re的行,无论re出现在第一行还是任何后续行。
与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特性的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
注意:
The empty regex // shortcut is employed twice here: once for the endpoint of the range, and once in the s call; in both cases, regex foo is implicitly reused, allowing us not to have to duplicate it, which makes both for shorter and more maintainable code. POSIX sed needs actual newlines after certain functions, such as after the name of a label or even its omission, as is the case with t here; strategically splitting the script into multiple -e options is an alternative to using an actual newlines: end each -e script chunk where a newline would normally need to go.
1s /foo/bar/只在第一行替换foo,如果在第一行找到的话。 如果是,t将分支到脚本的末尾(跳过该行中剩余的命令)。(t函数只有在最近的s调用执行了实际的替换时才会分支到一个标签;在没有标签的情况下,就像这里的情况一样,脚本的结尾被分支到)。
当这种情况发生时,范围地址1,//(通常从第2行开始查找第一个出现的地址)将不匹配,并且该范围将不会被处理,因为当当前行已经是2时,该地址将被计算。
相反,如果第一行没有匹配项,则输入1,//,并找到真正的第一个匹配项。
最终效果与GNU sed的0,/re/相同:只替换第一个出现的内容,无论它出现在第一行还是其他任何一行。
NON-range方法
Potong的回答展示了绕过范围需求的循环技术;因为他使用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' yields $'1bar\n2bar'; i.e., both lines were updated, because line number 1 matches the 1st line, and regex /foo/ - the end of the range - is then only looked for starting on the next line. Therefore, both lines are selected in this case, and the s/foo/bar/ substitution is performed on both of them. sed '1,/foo/ s//bar/' <<<$'1foo\n2foo\n3foo' fails: with sed: first RE may not be empty (BSD/macOS) and sed: -e expression #1, char 0: no previous regular expression (GNU), because, at the time the 1st line is being processed (due to line number 1 starting the range), no regex has been applied yet, so // doesn't refer to anything. With the exception of GNU sed's special 0,/re/ syntax, any range that starts with a line number effectively precludes use of //.
其他回答
使用FreeBSD ed,避免ed在文件中没有include语句时出现“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
以下是许多有用的现有答案的概述,并附有解释:
这里的例子使用了一个简化的用例:只在第一个匹配的行中将单词'foo'替换为'bar'。 由于使用ANSI c引号字符串($'…')来提供示例输入行,bash、ksh或zsh被假定为shell。
GNU仅适用:
Ben Hoffstein的回答告诉我们,GNU为sed提供了POSIX规范的扩展,它允许以下2-address形式:0,/re/ (re在这里表示任意正则表达式)。
0,/re/也允许正则表达式匹配第一行。换句话说:这样的地址将创建一个范围,从第一行到匹配re的行,无论re出现在第一行还是任何后续行。
与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特性的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
注意:
The empty regex // shortcut is employed twice here: once for the endpoint of the range, and once in the s call; in both cases, regex foo is implicitly reused, allowing us not to have to duplicate it, which makes both for shorter and more maintainable code. POSIX sed needs actual newlines after certain functions, such as after the name of a label or even its omission, as is the case with t here; strategically splitting the script into multiple -e options is an alternative to using an actual newlines: end each -e script chunk where a newline would normally need to go.
1s /foo/bar/只在第一行替换foo,如果在第一行找到的话。 如果是,t将分支到脚本的末尾(跳过该行中剩余的命令)。(t函数只有在最近的s调用执行了实际的替换时才会分支到一个标签;在没有标签的情况下,就像这里的情况一样,脚本的结尾被分支到)。
当这种情况发生时,范围地址1,//(通常从第2行开始查找第一个出现的地址)将不匹配,并且该范围将不会被处理,因为当当前行已经是2时,该地址将被计算。
相反,如果第一行没有匹配项,则输入1,//,并找到真正的第一个匹配项。
最终效果与GNU sed的0,/re/相同:只替换第一个出现的内容,无论它出现在第一行还是其他任何一行。
NON-range方法
Potong的回答展示了绕过范围需求的循环技术;因为他使用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' yields $'1bar\n2bar'; i.e., both lines were updated, because line number 1 matches the 1st line, and regex /foo/ - the end of the range - is then only looked for starting on the next line. Therefore, both lines are selected in this case, and the s/foo/bar/ substitution is performed on both of them. sed '1,/foo/ s//bar/' <<<$'1foo\n2foo\n3foo' fails: with sed: first RE may not be empty (BSD/macOS) and sed: -e expression #1, char 0: no previous regular expression (GNU), because, at the time the 1st line is being processed (due to line number 1 starting the range), no regex has been applied yet, so // doesn't refer to anything. With the exception of GNU sed's special 0,/re/ syntax, any range that starts with a line number effectively precludes use of //.
这里可能的解决方案是告诉编译器包含头文件,而不在源文件中提到它。在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
使用GNU sed的-z选项,您可以像处理一行文件一样处理整个文件。这样,s/…/…/将只替换整个文件中的第一个匹配项。记住:s/…/…/只替换每行中的第一个匹配项,但是使用-z选项sed将整个文件视为一行。
sed -z 's/#include/#include "newfile.h"\n#include'
在一般情况下,您必须重写sed表达式,因为模式空间现在包含整个文件,而不仅仅是一行。一些例子:
s /文本。*//可以重写为s/text[^\n]*//。[^\n]匹配除换行符以外的所有内容。[^\n]*将匹配文本后的所有符号,直到出现换行符。 S /^text//可以重写为S /(^|\n)text//。 S /text$//可以重写为S /text(\n|$)//。