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 为预期的字符串。