如何在 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 也有解决方案。

实现 \Qquotemeta 绝对满足您的要求

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-9a-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"