如何在 sed 中 post- 处理捕获组同时保留其他捕获组?

How can I post-process a capture group while preserving others in sed?

我有一个包含 3 个捕获组的字符串,我想保留第一个和第三个,但对第二个进行替换。我如何在 sed 中表达它?

具体来说,我有一个输入字符串:

top-level.subpath.one.subpath.two.subpath.forty-five

而且我想保留第一个.之前的部分,将中间部分缩短到每个单词的第一个字母,并保留最后一个.之后的部分。结果应如下所示:

top-level.s.o.s.t.s.forty-five

为了保留捕获组,我有:

sed -r 's/([^.]*)(.*)(\..*)/.../'

这让我:

top-level....forty-five

为了将 .subpath.one.subpath.two.subpath 之类的东西转换为仅首字母,我有:

sed -r 's/(\.[^.])[^\.]*//g'

这让我:

.s.o.s.t.s

我想基本上应用第二个 sed 表达式来捕获组 2。有什么方法可以链接 sed 替换以仅对第二个捕获组执行第二个替换,同时保留第一个和第三个?

您可以使用

sed -E ':a; s/^(.*\.[^.])[^.]+(\.)//; ta' file > newfile         # GNU sed
sed -E -e :a -e 's/^(.*\.[^.])[^.]+(\.)//' -e ta file > newfile  # FreeBSD sed

online demo详情:

  • -E - 启用 POSIX ERE 语法(+ 现在是一个 一个或多个 量词, (...) 被解析作为分组结构)
  • :a - 设置一个 a 标签
  • s/^(.*\.[^.])[^.]+(\.)// - 找到零个或多个字符,一个 . 然后是 . 以外的任何单个字符(将其捕获到第 1 组),然后是一个或多个其他字符比 .,然后匹配并捕获到第 2 组一个点字符,匹配被替换为串联的第 1 组 + 第 2 组值
  • ta - 成功替换后转到 a 标签。

一个简单的 awk 解决方案,适用于任何版本的 awk,包括 MacOS:

s='top-level.subpath.one.subpath.two.subpath.forty-five'
awk 'BEGIN{FS=OFS="."} {for(i=2;i<NF;++i) $i=substr($i,1,1)}1' <<< "$s"

top-level.s.o.s.t.s.forty-five

awk 命令使用. 作为输入和输出字段分隔符。我们遍历字段位置 2last-1 并将每个字段的值替换为该字段的第一个字符。最后打印完整记录。


一个 BSD sed 解决方案做同样的事情:

sed -E -e ':x' -e 's/(.+\..)[^.]+\././; tx' <<< "$s"

top-level.s.o.s.t.s.forty-five

这可能适合您 (GNU sed):

sed -E ':a;s/(\..*)\B.(.*\.)//;ta' file

捕获第一个和最后一个句点并挖空中间部分,删除任何并排的单词字符。


改善@anubhava 的 sed 回答:

sed -E 's/(\..)[^.]+\././g;s//./g' file

使用全局标志并重复相同的替换提供了 2 个命令的解决方案。

为了回答您的问题,根本不可能对单个子表达式捕获的 string/group 执行 运行 命令。

我会使用这个 sed 来完成你的任务:

sed -E 's/(\.[^.])[^.]+\././g; s/(\.[^.])[^.]+\././g'
  • 两个 s 命令相同,但必须 运行 两次以修改相邻字段,因为模式以(文字)点开头和结尾以避免匹配第一个和最后一个字段.
  • FWIW,这在基准测试中一直比 sed -E -e :goto -e 's/(\.[^.])[^.]+\././; t goto' 快 - 使用 GNU sed 快 15%,使用 busybox sed 快 25%。

还有这种使用 GNU sed 的方法:

sed -E 's/([^.])[^.]*\././2g'
  • Ng 在第 N 个匹配项开始全局替换。
  • 这是 GNU sed 中记录的功能,但是 POSIX 表示将数字与 g 组合会产生未定义的行为。
  • 如果您需要保留的不仅仅是第一个字段,它可能会很有用。

使用 GNU awk gensub():

$ awk 'BEGIN{FS=OFS="."} {beg=; end=$NF; [=10=]=gensub(/([^.])[^.]+/,"\1","g"); =beg; $NF=end} 1' file
top-level.s.o.s.t.s.forty-five