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\.2\.840\.
作为起始模式。
- 将 'character to replace' 表达式修改为 'not
x
or .
'。
- 仅使用
\.
作为尾部图案。
如果您确定只希望匹配数字,您可以将 [^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 也行
虽然 perl
、sed
和 awk
解决方案可能是更好的选择,但 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
我希望能够替换 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 thesed
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\.2\.840\.
作为起始模式。 - 将 'character to replace' 表达式修改为 'not
x
or.
'。 - 仅使用
\.
作为尾部图案。
如果您确定只希望匹配数字,您可以将 [^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 也行
虽然 perl
、sed
和 awk
解决方案可能是更好的选择,但 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