如何在 sed 和 awk(和 perl)中搜索和替换任意文字字符串
How to search & replace arbitrary literal strings in sed and awk (and perl)
假设我们在文件中有一些任意文字,我们需要用其他文字替换。
通常,我们会使用 sed(1) 或 awk(1) 并编写如下代码:
sed "s/$target/$replacement/g" file.txt
但是,如果 $target and/or $replacement 可能包含对 sed(1) 敏感的字符,例如正则表达式,该怎么办?你可以逃避它们,但假设你不知道它们是什么——它们是任意的,好吗?您需要编写一些代码来转义所有可能的敏感字符——包括“/”分隔符。例如
t=$( echo "$target" | sed 's/\./\./g; s/\*/\*/g; s/\[/\[/g; ...' ) # arghhh!
对于这么简单的问题,这就很尴尬了。
perl(1) 有 \Q ... \E 引号,但即使这样也无法处理 $target
.[= 中的 '/' 分隔符14=]
perl -pe "s/\Q$target\E/$replacement/g" file.txt
我刚刚发布了一个答案!!所以我真正的问题是,"is there a better way to do literal replacements in sed/awk/perl?"
如果没有,我会把它留在这里以备不时之需。
又是我!
这是使用 xxd(1) 的更简单方法:
t=$( echo -n "$target" | xxd -p | tr -d '\n')
r=$( echo -n "$replacement" | xxd -p | tr -d '\n')
xxd -p file.txt | sed "s/$t/$r/g" | xxd -p -r
... 所以我们用 xxd(1) 对原始文本进行十六进制编码,并使用十六进制编码的搜索字符串进行搜索替换。最后我们对结果进行十六进制解码。
编辑:我忘记从 xxd 输出 (| tr -d '\n'
) 中删除 \n
,以便模式可以跨越 xxd 的 60 列输出。当然,这依赖于 GNU sed
对超长行的操作能力(仅受内存限制)。
编辑:这也适用于多行目标,例如
目标=$'foo\nbar'
替换=$'bar\nfoo'
使用 awk 你可以这样做:
awk -v t="$target" -v r="$replacement" '{gsub(t,r)}' file
以上期望 t
是一个正则表达式,要使用它一个字符串你可以使用
awk -v t="$target" -v r="$replacement" '{while(i=index([=11=],t)){[=11=] = substr([=11=],1,i-1) r substr([=11=],i+length(t))} print}' file
灵感来自 this post
请注意,如果替换字符串包含目标,这将无法正常工作。上面的 link 也有解决方案。
实现 \Q
的 quotemeta 绝对满足您的要求
all ASCII characters not matching /[A-Za-z_0-9]/
will be preceded by a backslash
因为这大概是在 shell 脚本中,问题实际上是 shell 变量如何以及何时被插值以及 Perl 程序最终看到的内容。
最好的方法是避免计算出插值混乱,而是将那些 shell 变量正确传递给 Perl 单行代码。这可以通过多种方式完成;有关详细信息,请参阅 。
要么将 shell 变量简单地作为参数传递
#!/bin/bash
# define $target
perl -pe"BEGIN { $patt = shift }; s{\Q$patt}{$replacement}g" "$target" file.txt
其中所需的参数从 @ARGV
中删除并在 BEGIN
块中使用,因此在运行时之前;然后 file.txt
得到处理。这里的正则表达式中不需要 \E
。
或者,使用 -s
switch,为程序启用命令行开关
# define $target, etc
perl -s -pe"s{\Q$patt}{$replacement}g" -- -patt="$target" file.txt
需要--
来标记参数的开始,开关必须在文件名之前。
最后,您还可以导出shell变量,然后可以通过%ENV
在Perl脚本中使用这些变量;但总的来说,我宁愿推荐以上两种方法中的任何一种。
一个完整的例子
#!/bin/bash
# Last modified: 2019 Jan 06 (22:15)
target="/{"
replacement="&"
echo "Replace $target with $replacement"
perl -wE'
BEGIN { $p = shift; $r = shift };
$_=q(ah/{yes); s/\Q$p/$r/; say
' "$target" "$replacement"
这会打印
Replace /{ with &
ah&yes
我在评论中使用过的字符。
反之
#!/bin/bash
# Last modified: 2019 Jan 06 (22:05)
target="/{"
replacement="&"
echo "Replace $target with $replacement"
perl -s -wE'$_ = q(ah/{yes); s/\Q$patt/$repl/; say' \
-- -patt="$target" -repl="$replacement"
这里为了便于阅读,代码被分行了(因此需要 \
)。相同的打印输出。
这是一个增强功能
.
我们可以去掉各种特殊字符的特殊含义问题
和字符串(^
、.
、[
、*
、$
、\(
、\)
、\{
, \}
, \+
, \?
,
&
、</code>、……等等,还有 <code>/
分隔符)
删除特殊字符。
具体来说,我们可以将所有内容都转换为十六进制;
那么我们只有 0
-9
和 a
-f
需要处理。
本例演示原理:
$ echo -n '3.14' | xxd
0000000: 332e 3134 3.14
$ echo -n 'pi' | xxd
0000000: 7069 pi
$ echo '3.14 is a transcendental number. 3614 is an integer.' | xxd
0000000: 332e 3134 2069 7320 6120 7472 616e 7363 3.14 is a transc
0000010: 656e 6465 6e74 616c 206e 756d 6265 722e endental number.
0000020: 2020 3336 3134 2069 7320 616e 2069 6e74 3614 is an int
0000030: 6567 6572 2e0a eger..
$ echo "3.14 is a transcendental number. 3614 is an integer." | xxd -p \
| sed 's/332e3134/7069/g' | xxd -p -r
pi is a transcendental number. 3614 is an integer.
当然,sed 's/3.14/pi/g'
也会改变 3614
。
以上略有简化;它不考虑边界。
考虑这个(有点做作的)例子:
$ echo -n 'E' | xxd
0000000: 45 E
$ echo -n 'g' | xxd
0000000: 67 g
$ echo '$Q Eak!' | xxd
0000000: 2451 2045 616b 210a $Q Eak!.
$ echo '$Q Eak!' | xxd -p | sed 's/45/67/g' | xxd -p -r
&q gak!
因为$
(24
) 和Q
(51
)
合并形成 2<b><i>45</i></b>1
,
s/45/67/g
命令将其从内部撕开。
它将 2451
更改为 2671
,即 &q
(26
+ 71
)。
我们可以通过在搜索文本中分隔数据字节来防止这种情况,
替换文本和带空格的文件。
这是一个程式化的解决方案:
encode() {
xxd -p -- "$@" | sed 's/../& /g' | tr -d '\n'
}
decode() {
xxd -p -r -- "$@"
}
left=$( printf '%s' "$search" | encode)
right=$(printf '%s' "$replacement" | encode)
encode file.txt | sed "s/$left/$right/g" | decode
我定义了一个 encode
函数,因为我使用了该函数三次,
然后我为对称定义了 decode
。
如果您不想定义 decode
函数,只需将最后一行更改为
encode file.txt | sed "s/$left/$right/g" | xxd -p –r
请注意,encode
函数将数据(文本)的大小增加了三倍
在文件中,然后通过 sed
作为单行发送
——最后甚至没有换行符。
GNU sed 似乎能够处理这个问题;
其他版本可能无法。
作为一个额外的好处,这个解决方案处理多行搜索和替换
(换句话说,搜索和替换包含换行符的字符串)。
我可以解释为什么这不起作用:
perl(1) has \Q ... \E quotes but even that can't cope with the '/' separator in $target.
原因是因为 \Q
和 \E
(quotemeta) 转义是在解析正则表达式之后处理的,除非有定义正则表达式的有效模式定界符,否则不会解析正则表达式。
例如,这里尝试使用传递给 perl 的字符串中的变量替换 /etc/hosts
中的字符串 /etc/
:
$target="/etc/";
perl -pe "s/\Q$target\E/XXX/" <<<"/etc/hosts";
在 shell 扩展字符串中的变量后,perl 收到命令 s/\Q/etc/\E/XXX/
,这不是有效的正则表达式,因为它不包含三个模式定界符(perl 看到五个定界符,即, s/…/…/…/…/
). 因此,\Q
和 \E
甚至从未执行过。
按照@zdim 的建议,解决方案是将变量传递给 perl,在解析正则表达式后将它们包含在正则表达式中,例如:
perl -s -pe 's/\Q$target\E/XXX/ig' -- -target="/etc/" <<<"/etc/123"
假设我们在文件中有一些任意文字,我们需要用其他文字替换。
通常,我们会使用 sed(1) 或 awk(1) 并编写如下代码:
sed "s/$target/$replacement/g" file.txt
但是,如果 $target and/or $replacement 可能包含对 sed(1) 敏感的字符,例如正则表达式,该怎么办?你可以逃避它们,但假设你不知道它们是什么——它们是任意的,好吗?您需要编写一些代码来转义所有可能的敏感字符——包括“/”分隔符。例如
t=$( echo "$target" | sed 's/\./\./g; s/\*/\*/g; s/\[/\[/g; ...' ) # arghhh!
对于这么简单的问题,这就很尴尬了。
perl(1) 有 \Q ... \E 引号,但即使这样也无法处理 $target
.[= 中的 '/' 分隔符14=]
perl -pe "s/\Q$target\E/$replacement/g" file.txt
我刚刚发布了一个答案!!所以我真正的问题是,"is there a better way to do literal replacements in sed/awk/perl?"
如果没有,我会把它留在这里以备不时之需。
又是我!
这是使用 xxd(1) 的更简单方法:
t=$( echo -n "$target" | xxd -p | tr -d '\n')
r=$( echo -n "$replacement" | xxd -p | tr -d '\n')
xxd -p file.txt | sed "s/$t/$r/g" | xxd -p -r
... 所以我们用 xxd(1) 对原始文本进行十六进制编码,并使用十六进制编码的搜索字符串进行搜索替换。最后我们对结果进行十六进制解码。
编辑:我忘记从 xxd 输出 (| tr -d '\n'
) 中删除 \n
,以便模式可以跨越 xxd 的 60 列输出。当然,这依赖于 GNU sed
对超长行的操作能力(仅受内存限制)。
编辑:这也适用于多行目标,例如
目标=$'foo\nbar' 替换=$'bar\nfoo'
使用 awk 你可以这样做:
awk -v t="$target" -v r="$replacement" '{gsub(t,r)}' file
以上期望 t
是一个正则表达式,要使用它一个字符串你可以使用
awk -v t="$target" -v r="$replacement" '{while(i=index([=11=],t)){[=11=] = substr([=11=],1,i-1) r substr([=11=],i+length(t))} print}' file
灵感来自 this post
请注意,如果替换字符串包含目标,这将无法正常工作。上面的 link 也有解决方案。
实现 \Q
的 quotemeta 绝对满足您的要求
all ASCII characters not matching
/[A-Za-z_0-9]/
will be preceded by a backslash
因为这大概是在 shell 脚本中,问题实际上是 shell 变量如何以及何时被插值以及 Perl 程序最终看到的内容。
最好的方法是避免计算出插值混乱,而是将那些 shell 变量正确传递给 Perl 单行代码。这可以通过多种方式完成;有关详细信息,请参阅
要么将 shell 变量简单地作为参数传递
#!/bin/bash
# define $target
perl -pe"BEGIN { $patt = shift }; s{\Q$patt}{$replacement}g" "$target" file.txt
其中所需的参数从 @ARGV
中删除并在 BEGIN
块中使用,因此在运行时之前;然后 file.txt
得到处理。这里的正则表达式中不需要 \E
。
或者,使用 -s
switch,为程序启用命令行开关
# define $target, etc
perl -s -pe"s{\Q$patt}{$replacement}g" -- -patt="$target" file.txt
需要--
来标记参数的开始,开关必须在文件名之前。
最后,您还可以导出shell变量,然后可以通过%ENV
在Perl脚本中使用这些变量;但总的来说,我宁愿推荐以上两种方法中的任何一种。
一个完整的例子
#!/bin/bash
# Last modified: 2019 Jan 06 (22:15)
target="/{"
replacement="&"
echo "Replace $target with $replacement"
perl -wE'
BEGIN { $p = shift; $r = shift };
$_=q(ah/{yes); s/\Q$p/$r/; say
' "$target" "$replacement"
这会打印
Replace /{ with & ah&yes
我在评论中使用过的字符。
反之
#!/bin/bash
# Last modified: 2019 Jan 06 (22:05)
target="/{"
replacement="&"
echo "Replace $target with $replacement"
perl -s -wE'$_ = q(ah/{yes); s/\Q$patt/$repl/; say' \
-- -patt="$target" -repl="$replacement"
这里为了便于阅读,代码被分行了(因此需要 \
)。相同的打印输出。
这是一个增强功能
我们可以去掉各种特殊字符的特殊含义问题
和字符串(^
、.
、[
、*
、$
、\(
、\)
、\{
, \}
, \+
, \?
,
&
、</code>、……等等,还有 <code>/
分隔符)
删除特殊字符。
具体来说,我们可以将所有内容都转换为十六进制;
那么我们只有 0
-9
和 a
-f
需要处理。
本例演示原理:
$ echo -n '3.14' | xxd
0000000: 332e 3134 3.14
$ echo -n 'pi' | xxd
0000000: 7069 pi
$ echo '3.14 is a transcendental number. 3614 is an integer.' | xxd
0000000: 332e 3134 2069 7320 6120 7472 616e 7363 3.14 is a transc
0000010: 656e 6465 6e74 616c 206e 756d 6265 722e endental number.
0000020: 2020 3336 3134 2069 7320 616e 2069 6e74 3614 is an int
0000030: 6567 6572 2e0a eger..
$ echo "3.14 is a transcendental number. 3614 is an integer." | xxd -p \
| sed 's/332e3134/7069/g' | xxd -p -r
pi is a transcendental number. 3614 is an integer.
当然,sed 's/3.14/pi/g'
也会改变 3614
。
以上略有简化;它不考虑边界。 考虑这个(有点做作的)例子:
$ echo -n 'E' | xxd
0000000: 45 E
$ echo -n 'g' | xxd
0000000: 67 g
$ echo '$Q Eak!' | xxd
0000000: 2451 2045 616b 210a $Q Eak!.
$ echo '$Q Eak!' | xxd -p | sed 's/45/67/g' | xxd -p -r
&q gak!
因为$
(24
) 和Q
(51
)
合并形成 2<b><i>45</i></b>1
,
s/45/67/g
命令将其从内部撕开。
它将 2451
更改为 2671
,即 &q
(26
+ 71
)。
我们可以通过在搜索文本中分隔数据字节来防止这种情况,
替换文本和带空格的文件。
这是一个程式化的解决方案:
encode() {
xxd -p -- "$@" | sed 's/../& /g' | tr -d '\n'
}
decode() {
xxd -p -r -- "$@"
}
left=$( printf '%s' "$search" | encode)
right=$(printf '%s' "$replacement" | encode)
encode file.txt | sed "s/$left/$right/g" | decode
我定义了一个 encode
函数,因为我使用了该函数三次,
然后我为对称定义了 decode
。
如果您不想定义 decode
函数,只需将最后一行更改为
encode file.txt | sed "s/$left/$right/g" | xxd -p –r
请注意,encode
函数将数据(文本)的大小增加了三倍
在文件中,然后通过 sed
作为单行发送
——最后甚至没有换行符。
GNU sed 似乎能够处理这个问题;
其他版本可能无法。
作为一个额外的好处,这个解决方案处理多行搜索和替换 (换句话说,搜索和替换包含换行符的字符串)。
我可以解释为什么这不起作用:
perl(1) has \Q ... \E quotes but even that can't cope with the '/' separator in $target.
原因是因为 \Q
和 \E
(quotemeta) 转义是在解析正则表达式之后处理的,除非有定义正则表达式的有效模式定界符,否则不会解析正则表达式。
例如,这里尝试使用传递给 perl 的字符串中的变量替换 /etc/hosts
中的字符串 /etc/
:
$target="/etc/";
perl -pe "s/\Q$target\E/XXX/" <<<"/etc/hosts";
在 shell 扩展字符串中的变量后,perl 收到命令 s/\Q/etc/\E/XXX/
,这不是有效的正则表达式,因为它不包含三个模式定界符(perl 看到五个定界符,即, s/…/…/…/…/
). 因此,\Q
和 \E
甚至从未执行过。
按照@zdim 的建议,解决方案是将变量传递给 perl,在解析正则表达式后将它们包含在正则表达式中,例如:
perl -s -pe 's/\Q$target\E/XXX/ig' -- -target="/etc/" <<<"/etc/123"