BSD sed (Mac) 如何替换从第n次出现到行尾?
BSD sed (Mac) How to replace from nth occurrence till the end of the line?
在GNU sed
中会是这样的
's/foo/bar/3g' <<< "foofoofoofoofoo"
Output: "foofoobarbarbar"
BSD sed
中的相同命令出现以下错误
sed: 1: "s/foo/bar/3g": more than one number or 'g' in substitute flags
我如何在 BSD sed
上实现它?
我搜索了 SO 并找到了 this 但所有答案都是针对 GNU 的。
我读了这个人,但我很难弄明白。
一个选项是使用标签和 t
命令实现循环:
$ sed -e ':l' -e 's/foo/bar/3' -e 'tl' <<< 'foofoofoofoofoo'
foofoobarbarbar
请小心,因为如果您的替换文本与原始 RE 匹配(例如 s/f.x/fox/
),那么您将陷入无限循环,如果它在替换后生成原始文本,那么您将得到意想不到的结果,例如:
$ sed 's/foo/oo/3g' <<< 'foofoofffoo'
foofooffoo
$ sed -e ':l' -e 's/foo/oo/3' -e 'tl' <<< 'foofoofffoo'
foofoooo
请注意,第一个版本之所以有效,是因为它在文本的一次传递中进行所有替换,因此先前的替换不被视为当前替换传递的字符串的一部分。
你不能没有困难地做到这一点。
如 GNU sed 手册所述:
g
Apply the replacement to all matches to the regexp, not just the first.
number
Only replace the numberth match of the regexp.
interaction in s
command Note: the POSIX standard does not specify what should happen when you mix the g
and number modifiers, and currently there is no widely agreed upon meaning across sed implementations. For GNU sed, the interaction is defined to be: ignore matches before the numberth, and then match and replace all matches from the numberth on.)
在 Mac OS X 上,但是,这有效:
▶ sed 's/foo/bar/3' <<< 'foofoofoofoofoo'
foofoobarfoofoo
这样做:
▶ sed 's/foo/bar/g' <<< 'foofoofoofoofoo'
barbarbarbarbar
但是如果它们一起使用,就会出现问题中提到的错误。
提供了一个聪明而简单的解决方案,我添加了这个额外的解释,因为我认为它会有所帮助。1 他的答案的早期版本展示了这个,令人困惑的是,在测试时什么也没做:
▶ sed ':a; s/foo/bar/3; ta' <<< 'foofoofoofoofoo'
foofoofoofoofoo
同时,BSD 手册也没有提供任何解释。但是,POSIX 手册指出:
The b, t, and : commands are documented to ignore leading white space, but no mention is made of trailing white space.
因此,这有效:
▶ sed -e :a -e s/foo/bar/3 -e ta <<< 'foofoofoofoofoo'
foofoobarbarbar
这也有效:
▶ sed '
:a
s/foo/bar/3
ta
' <<< 'foofoofoofoofoo'
foofoobarbarbar
在任何情况下,脚本所做的是在循环中用 bar
替换第 3 个 foo
,直到替换失败,此时脚本结束。注意 t
(测试)的使用,仅当先前的 s///
命令替换了某些内容时才会分支。
要了解脚本在其每个循环迭代中的作用,这很有帮助:
▶ sed -n -e :a -e s/foo/bar/3p -e ta <<< 'foofoofoofoofoo'
foofoobarfoofoo
foofoobarbarfoo
foofoobarbarbar
1 该答案的原始版本没有解释,尽管现在已扩展很多。 Oguz 表示他希望我在单独的答案中添加此信息。
如果不是简单的 s/old/new,则只需使用 awk 而不是 sed。在任何 UNIX 机器上的任何 shell 中使用任何 awk:
$ cat tst.awk
{
head = ""
tail = [=10=]
cnt = 0
while ( match(tail,old) ) {
tgt = substr(tail,RSTART,RLENGTH)
if ( ++cnt >= beg ) {
tgt = new
}
head = head tgt
tail = substr(tail,RSTART+RLENGTH)
}
print head tail
}
$ awk -v old='foo' -v new='bar' -v beg=3 -f tst.awk <<< "foofoofoofoofoo"
foofoobarbarbar
当然是几行代码,但它是解决许多问题的极其常用的代码,所以了解它是件好事,很容易看出它在做什么,而且很容易修改它来做任何其他事情。
如果您更喜欢简洁而不是清晰和高效,您可以将其简化为:
$ cat tst.awk
{
head = ""
cnt = 0
while ( match([=11=],old) ) {
head = head (++cnt < beg ? substr([=11=],RSTART,RLENGTH) : new)
[=11=] = substr([=11=],RSTART+RLENGTH)
}
print head [=11=]
}
甚至可怕的 "one-liner":
awk -v o='foo' -v n='bar' -v b=3 '{h="";c=0;while(s=match([=12=],o)){h=h (++c<b?substr([=12=],s,RLENGTH):n);[=12=]=substr([=12=],s+RLENGTH)}[=12=]=h[=12=]}1' <<< "foofoofoofoofoo"
foofoobarbarbar
awk中的另一个 * 用于单行处理:
$ echo foofoofoofoofoo |
awk -v n=3 'BEGIN{RS="foo"}{ORS=NR<n?RS:"bar"}1'
foofoobarbarbar
* 在 gawk、mawk 和 Busybox awk 上测试成功。在 awk-20121220 上失败。
如果perl
没问题:
$ echo 'foofoofoofoofoo' | perl -pe '$c=0; s/foo/++$c<3 ? $& : "bar"/ge'
foofoobarbarbar
$c=0
对每行输入,初始化计数器
e
修饰符用于允许Perl代码而不是替换部分中的字符串
++$c<3 ? $& : "bar"
根据计数器,保留或替换匹配的文本
这可能对你有用:
sed -e ':a' -e 's/foo/\'$'\n/2' -e 'ta' -e 's/\'$'\n/bar/g' file
为第 n 次出现(在本例中为 2)设置一个循环,并将其替换为唯一的 character/string(在本例中为换行符)。当循环失败时,全局替换唯一的 character/string 为预期的字符串。
在GNU sed
中会是这样的
's/foo/bar/3g' <<< "foofoofoofoofoo"
Output: "foofoobarbarbar"
BSD sed
中的相同命令出现以下错误
sed: 1: "s/foo/bar/3g": more than one number or 'g' in substitute flags
我如何在 BSD sed
上实现它?
我搜索了 SO 并找到了 this 但所有答案都是针对 GNU 的。 我读了这个人,但我很难弄明白。
一个选项是使用标签和 t
命令实现循环:
$ sed -e ':l' -e 's/foo/bar/3' -e 'tl' <<< 'foofoofoofoofoo'
foofoobarbarbar
请小心,因为如果您的替换文本与原始 RE 匹配(例如 s/f.x/fox/
),那么您将陷入无限循环,如果它在替换后生成原始文本,那么您将得到意想不到的结果,例如:
$ sed 's/foo/oo/3g' <<< 'foofoofffoo'
foofooffoo
$ sed -e ':l' -e 's/foo/oo/3' -e 'tl' <<< 'foofoofffoo'
foofoooo
请注意,第一个版本之所以有效,是因为它在文本的一次传递中进行所有替换,因此先前的替换不被视为当前替换传递的字符串的一部分。
你不能没有困难地做到这一点。
如 GNU sed 手册所述:
g
Apply the replacement to all matches to the regexp, not just the first.
number
Only replace the numberth match of the regexp.
interaction in
s
command Note: the POSIX standard does not specify what should happen when you mix theg
and number modifiers, and currently there is no widely agreed upon meaning across sed implementations. For GNU sed, the interaction is defined to be: ignore matches before the numberth, and then match and replace all matches from the numberth on.)
在 Mac OS X 上,但是,这有效:
▶ sed 's/foo/bar/3' <<< 'foofoofoofoofoo'
foofoobarfoofoo
这样做:
▶ sed 's/foo/bar/g' <<< 'foofoofoofoofoo'
barbarbarbarbar
但是如果它们一起使用,就会出现问题中提到的错误。
▶ sed ':a; s/foo/bar/3; ta' <<< 'foofoofoofoofoo'
foofoofoofoofoo
同时,BSD 手册也没有提供任何解释。但是,POSIX 手册指出:
The b, t, and : commands are documented to ignore leading white space, but no mention is made of trailing white space.
因此,这有效:
▶ sed -e :a -e s/foo/bar/3 -e ta <<< 'foofoofoofoofoo'
foofoobarbarbar
这也有效:
▶ sed '
:a
s/foo/bar/3
ta
' <<< 'foofoofoofoofoo'
foofoobarbarbar
在任何情况下,脚本所做的是在循环中用 bar
替换第 3 个 foo
,直到替换失败,此时脚本结束。注意 t
(测试)的使用,仅当先前的 s///
命令替换了某些内容时才会分支。
要了解脚本在其每个循环迭代中的作用,这很有帮助:
▶ sed -n -e :a -e s/foo/bar/3p -e ta <<< 'foofoofoofoofoo'
foofoobarfoofoo
foofoobarbarfoo
foofoobarbarbar
1 该答案的原始版本没有解释,尽管现在已扩展很多。 Oguz 表示他希望我在单独的答案中添加此信息。
如果不是简单的 s/old/new,则只需使用 awk 而不是 sed。在任何 UNIX 机器上的任何 shell 中使用任何 awk:
$ cat tst.awk
{
head = ""
tail = [=10=]
cnt = 0
while ( match(tail,old) ) {
tgt = substr(tail,RSTART,RLENGTH)
if ( ++cnt >= beg ) {
tgt = new
}
head = head tgt
tail = substr(tail,RSTART+RLENGTH)
}
print head tail
}
$ awk -v old='foo' -v new='bar' -v beg=3 -f tst.awk <<< "foofoofoofoofoo"
foofoobarbarbar
当然是几行代码,但它是解决许多问题的极其常用的代码,所以了解它是件好事,很容易看出它在做什么,而且很容易修改它来做任何其他事情。
如果您更喜欢简洁而不是清晰和高效,您可以将其简化为:
$ cat tst.awk
{
head = ""
cnt = 0
while ( match([=11=],old) ) {
head = head (++cnt < beg ? substr([=11=],RSTART,RLENGTH) : new)
[=11=] = substr([=11=],RSTART+RLENGTH)
}
print head [=11=]
}
甚至可怕的 "one-liner":
awk -v o='foo' -v n='bar' -v b=3 '{h="";c=0;while(s=match([=12=],o)){h=h (++c<b?substr([=12=],s,RLENGTH):n);[=12=]=substr([=12=],s+RLENGTH)}[=12=]=h[=12=]}1' <<< "foofoofoofoofoo"
foofoobarbarbar
awk中的另一个 * 用于单行处理:
$ echo foofoofoofoofoo |
awk -v n=3 'BEGIN{RS="foo"}{ORS=NR<n?RS:"bar"}1'
foofoobarbarbar
* 在 gawk、mawk 和 Busybox awk 上测试成功。在 awk-20121220 上失败。
如果perl
没问题:
$ echo 'foofoofoofoofoo' | perl -pe '$c=0; s/foo/++$c<3 ? $& : "bar"/ge'
foofoobarbarbar
$c=0
对每行输入,初始化计数器e
修饰符用于允许Perl代码而不是替换部分中的字符串++$c<3 ? $& : "bar"
根据计数器,保留或替换匹配的文本
这可能对你有用:
sed -e ':a' -e 's/foo/\'$'\n/2' -e 'ta' -e 's/\'$'\n/bar/g' file
为第 n 次出现(在本例中为 2)设置一个循环,并将其替换为唯一的 character/string(在本例中为换行符)。当循环失败时,全局替换唯一的 character/string 为预期的字符串。