我了解到,使用 sed,可以通过使用空数据模式 ( -z
):来匹配模式的第 n 次出现sed -z 's/foo/bar/2'
。
使用时如何匹配行首-z
?
如果我执行:
echo $'foo\nfoo\nfoo' | sed -z 's/^foo/baz/2'
没有替换,因为^
表示整个字符串的开头:
$ echo $'foo\nfoo\nfoo' | sed -z 's/^foo/baz/g'
baz
foo
foo
在 Perl 中,有m
正则表达式修饰符(当发出声音时),但在 sed 中它没有帮助。
答案1
echo $'foo\nfoo\nfoo' | sed -Ez 's/(^|\n)foo/\1baz/2'
^
需要正确计算foo
恰好位于第一行开头的内容。
请注意,代码计算的是(^|\n)foo
,而不是 的出现次数foo
。如果你想计算foo
s,但替换仅有的当所需的出现恰好位于行的开头时,则此代码不是解决方案。例如在:
echo $'foo foo\nfoo foo\nfoo foo' | sed -Ez 's/(^|\n)foo/\1baz/3'
被foo
替换的已经不是第三次了foo
。
使用 GNU sed
4.8 进行测试。
答案2
并不像所选答案看起来那么简单。首先,2
(或 s///2 中的任何其他数字)的实际含义是什么?它只是意味着应用它的行上的第二个正则表达式匹配将被更改。
在
$ printf '%s\n' 'foo foo foo' 'foo foo foo' 'foo foo foo' | sed 's/foo/bar/2'
foo bar foo
foo bar foo
foo bar foo
sed 命令更改了foo
with bar
but for 的第二个 (2) 实例每个它应用到的行(所有行)。可以将其更改为仅在一行或一系列行上工作:
printf '%printf '%s\n' 'foo foo foo'{,,,,} | sed '3,4s/foo/bar/2'
foo foo foo
foo foo foo
foo bar foo
foo bar foo
foo foo foo
请注意,只有行3
和4
被更改,而不是全部,并且在所有这些行中,foo
被更改的实例是第二个 (2)。
这就是工作原理s/foo/bar/2
。
-z
现在,如果-z
使用 if ,则行以\0
(not \n
) 结尾。但替换的工作方式完全相同(用\0
代替\n
):
$ printf '%s\0' 'foo foo foo'{,,,,} | sed -z '3,4s/foo/bar/2' | xxd
00000000: 666f 6f20 666f 6f20 666f 6f00 666f 6f20 foo foo foo.foo
00000010: 666f 6f20 666f 6f00 666f 6f20 6261 7220 foo foo.foo bar
00000020: 666f 6f00 666f 6f20 6261 7220 666f 6f00 foo.foo bar foo.
00000030: 666f 6f20 666f 6f20 666f 6f00 foo foo foo.
混合 \0 和 \n
里面echo $'foo\nfoo\nfoo' | sed -z 's/^foo/baz/2'
还没有足够foo
的每行使更改第二个成为可能,但在下一个示例中:应该是吗?
$ printf 'foo foo foo\nfoo foo foo\n' | sed -z 's/^foo/baz/2'
foo foo foo
foo foo foo
foo
哎呀,不,行首也没有足够的。问题是:一条线从哪里开始?在换行符处或在 a 处\0
,或两者?或者没有?
^
使用“-z”时考虑“行的开头”是没有意义的。
这是 sed 的内部混乱。请记住:使用-z
是实验性的,可能会导致奇怪的问题。
模式空间
事实上,为了使替换正确工作,整个输入需要位于模式空间中。不,如果输入有 NUL ( ),则不起作用\0
,这些将被视为行分隔符(或者,用 awk 的说法,作为记录分隔符)。
$ printf 'foo\0foo\0foo\0' | sed -z 's/^foo/baz/2'
foofoofoo
我们可以使用 sed 模式空间内的整个输入文件H;1h;$!d;x;.....
,然后尝试^foo
替换:
$ printf 'foo\0foo\0foo\0\n' | sed -z 'H;1h;$!d;x;l;s/^foo/ baz /M2'
foo\000foo\000foo\000\n$foo baz foo
允许l
我们查看模式空间内的内容,并且M
需要该标志,以便 比^
第一行匹配更多。如果M
不使用 , 则^foo
仅在第一行(在模式空间的开头)匹配。
替代方案M
是:
$ printf 'foo\0foo\0foo\0' | sed -z 'H;1h;$!d;x;l;s/\(^\|\x0\)foo/ baz /2'
foo\000foo\000foo$foo baz foo
请注意,尾随\0
被删除,进入内部模式空间的是foo\000foo\000foo
,它缺少尾随\0
,而尾随是在输入中明确提供的。
我们可以\0
通过添加尾随换行符来获取所有三个:
$ printf 'foo\0foo\0foo\0\n' | sed -z 'H;1h;$!d;x;l;s/\(^\|\x0\)foo/ baz /2'
foo\000foo\000foo\000\n$foo baz foo
这清楚地表明 sed\0
有时将 a 视为分隔符,而\n
在其他情况下将 a 视为分隔符。
简而言之,带有该-z
选项的 sed 仍处于实验阶段。
答案3
在 Perl 中,有
m
正则表达式修饰符(当 slurping 时),但它没有帮助。
当然可以。
printf '%s\n' foo foo foo |\
perl -0777 -pe 's/^(foo)/++$c == 2 ? "bar" : $1/egm'
我们用 slurp -0777
,匹配很多次g
,并m
帮助^
在 slurp 中匹配,然后仅当计数器变量为 2 时e
评估in 。bar
答案4
在 slurp 模式 (-z) 中,GNU sed 将记录分隔符视为空值。但由于 ASCII 文本文件中没有空字符,因此整个文件本质上是 sed 的一条记录或一行。为了解决这个问题,我们首先将所有换行符 (\n) 更改为行分隔符 (NUL),然后在第二个匹配上应用 s/// in t 多行模式。最后进行逆变换
printf '%s\n' foo foo foo |
sed -z '
y/\n/\x00/
s/^foo/BAR/M2
y/\x00/\n/
'
输出:
foo
BAR
foo