Sed 替换 2 个已知模式之间的可变长度字符串

Sed to replace variable length string between 2 known patterns

我希望能够替换 2 个已知模式之间的字符串。问题是我想用仅由 'x'.

组成的相同长度的字符串替换它

假设我有一个文件包含:

Hello.StringToBeReplaced.SecondString
Hello.ShortString.SecondString

我希望输出是这样的:

Hello.xxxxxxxxxxxxxxxxxx.SecondString
Hello.xxxxxxxxxxx.SecondString

我会选择 perl:

perl -pe 's/(?<=Hello\.)(.*?)(?=\.SecondString)/ "x" x length() /e' file

这个awk应该做的:

awk -F. '{for (i=1;i<=length();i++) a=a"x";=a;a=""}1' OFS="." file
Hello.xxxxxxxxxxxxxxxxxx.SecondString
Hello.xxxxxxxxxxx.SecondString

使用 sed 循环

您可以使用 sed,尽管所需的思考并不完全明显:

sed ':a;s/^\(Hello\.x*\)[^x]\(.*\.SecondString\)/x/;t a'

这是针对 GNU sed; BSD (Mac OS X) sed 和其他版本可能比较繁琐,需要:

sed -e ':a' -e 's/^\(Hello\.x*\)[^x]\(.*\.SecondString\)/x/' -e 't a'

两者的逻辑相同:

  • 创建标签a
  • 替换前导字符串和一系列 x(捕获 1),然后是非 x,以及任意其他数据加上第二个字​​符串(捕获 2),以及将其替换为捕获 1 的内容、x 和捕获 2.
  • 的内容
  • 如果 s/// 命令进行了更改,则返回标签 a

当两个标记字符串之间没有非x时,它停止替换。

对正则表达式的两个调整允许代码在一行中识别模式的两个副本。丢失将匹配锚定到行首的 ^,并将 .* 更改为 [^.]*(以便正则表达式不那么贪婪):

$ echo Hello.StringToBeReplaced.SecondString Hello.StringToBeReplaced.SecondString |
> sed ':a;s/\(Hello\.x*\)[^x]\([^.]*\.SecondString\)/x/;t a'
Hello.xxxxxxxxxxxxxxxxxx.SecondString Hello.xxxxxxxxxxxxxxxxxx.SecondString
$

使用保留 space

hek2mgl 建议在 sed 中使用保留 space 的替代方法。这可以通过以下方式实现:

$ echo Hello.StringToBeReplaced.SecondString |
> sed 's/^\(Hello\.\)\([^.]\{1,\}\)\(\.SecondString\)/@@@/
>      h
>      s/.*@@//
>      s/./x/g
>      G
>      s/\(x*\)\n\([^@]*\)@\([^@]*\)@@.*//
>      '
Hello.xxxxxxxxxxxxxxxxxx.SecondString
$

这个脚本不如循环版本那么健壮,但当每一行都匹配前导-中-尾模式时,它可以正常工作。它首先将该行分成三个部分:第一个标记、要被破坏的位和第二个标记。它重新组织,以便两个标记由 @ 分隔,然后是 @@ 和要损坏的位。 h 将结果复制到保留 space。删除 @@ 之前的所有内容;用 x 替换要损坏的位中的每个字符,然后在模式 space 中的 x 之后复制保留 space 中的 material,用换行符分隔它们。最后,识别并捕获 x、前导标记和尾标记,忽略换行符、@@@ 加上结尾的 material,然后重新组合为前导标记,x 和尾标记。

为了使其健壮,您需要识别模式,然后将 {} 中显示的命令分组,以便仅在识别模式时执行它们:

sed '/^\(Hello\.\)\([^.]\{1,\}\)\(\.SecondString\)/{
     s/^\(Hello\.\)\([^.]\{1,\}\)\(\.SecondString\)/@@@/
     h
     s/.*@@//
     s/./x/g
     G
     s/\(x*\)\n\([^@]*\)@\([^@]*\)@@.*//
     }'

根据您的需要进行调整...

根据您的需要进行调整

[I tried one of your solutions and it worked fine.] However when I try to replace the 'hello' by my real string (which is '1.2.840.') and my second string (which is simply a dot '.'), things stop working. I guess all these dots confuse the sed command. What I try to achieve is transform this '1.2.840.10008.' to '1.2.840.xxxxx.'

And this pattern happens several times in my file with variable number of characters to be replaced between the '1.2.840.' and the next dot '.'

有时候,让您的问题足够接近真实场景很重要——这可能就是其中之一。点是一个元字符 sed 正则表达式(以及大多数其他正则表达式方言 — shell 通配符是明显的例外)。如果 'bit to be mangled' 始终是数字,那么我们可以收紧正则表达式,但实际上(当我查看前面的代码时)收紧实际上并没有对限制施加太大影响。

几乎所有使用正则表达式的解决方案都是一种平衡行为,必须在便利性和缩写与可靠性和精确性之间取得平衡。

修改代码加数据

cat <<EOF |
transform this '1.2.840.10008.' to '1.2.840.xxxxx.'
OK, and hence 1.2.840.21. and 1.2.840.20992. should lose the 21 and 20992.
EOF

sed ':a;s/\(1\.2\.840\.x*\)[^x.]\([^.]*\.\)/x/;t a'

示例输出:

transform this '1.2.840.xxxxx.' to '1.2.840.xxxxx.'
OK, and hence 1.2.840.xx. and 1.2.840.xxxxx. should lose the 21 and 20992.

脚本中的变化是:

sed ':a;s/\(1\.2\.840\.x*\)[^x.]\([^.]*\.\)/x/;t a'
  1. 添加 1\.2\.840\. 作为起始模式。
  2. 将 'character to replace' 表达式修改为 'not x or .'。
  3. 仅使用 \. 作为尾部图案。

如果您确定只希望匹配数字,您可以将 [^x.] 替换为 [0-9],在这种情况下,您不必担心所讨论的 spaces下面。

您可能决定不希望 spaces 被匹配,以便像这样的随意评论:

The net prefix is 1.2.840. And there are other prefixes too.

不会结束为:

The net prefix is 1.2.840.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.

在这种情况下,您可能需要使用:

sed ':a;s/\(1\.2\.840\.x*\)[^x. ]\([^ .]*\.\)/x/;t a'

因此,更改会继续进行,直到您获得足够精确的东西来做您想做的事,而不用对当前数据集做任何您不想做的事情。编写防弹正则表达式需要精确指定要匹配的内容,这可能非常困难。

Bash 也行

虽然 perlsedawk 解决方案可能是更好的选择,但 Bash 解决方案并不那么困难(只是更长)。 Bash 也有很好的逐字符处理能力:

#!/bin/bash

rep=0    # replace flag
skip=0   # delay reset flag

while read -r line; do                 # read each line

    for ((i=0; i<${#line}; i++)); do   # for each character in the line

        # if '.' and replace on, turn off and set skip
        [ ${line:i:1} == '.' -a $rep -eq 1 ] && { rep=0; skip=1; }

        # print char or "x" depending on replace flag
        [ $rep -eq 0 ] && printf "%c" ${line:i:1} || printf "x"

        # if '.' and replace off
        if [ ${line:i:1} == '.' -a $rep -eq 0 ]; then
            # if skip, turn skip off, else set replace on
            [ $skip -eq 1 ] && skip=0 || rep=1
        fi

    done

    printf "\n"

done

exit 0

输入

$ cat dat/replacefile.txt
Hello.StringToBeReplaced.SecondString
Hello.ShortString.SecondString

输出

$ bash replacedot.sh < dat/replacefile.txt
Hello.xxxxxxxxxxxxxxxxxx.SecondString
Hello.xxxxxxxxxxx.SecondString

为了您的理智,请使用 awk:

$ awk 'BEGIN{FS=OFS="."} {gsub(/./,"x",)} 1' file
Hello.xxxxxxxxxxxxxxxxxx.SecondString
Hello.xxxxxxxxxxx.SecondString