8

假设我们在文件中有一些任意文字,我们需要用其他文字替换。

通常,我们只需使用sed (1) 或awk (1) 并编写如下代码:

sed "s/$target/$replacement/g" file.txt

但是,如果 $target 和/或 $replacement 可能包含对sed (1) 敏感的字符,例如正则表达式,该怎么办。你可以逃避它们,但假设你不知道它们是什么——它们是任意的,好吗?您需要编写一些代码来转义所有可能的敏感字符 - 包括“/”分隔符。例如

t=$( echo "$target" | sed 's/\./\\./g; s/\*/\\*/g; s/\[/\\[/g; ...' ) # arghhh!

对于这么简单的问题,这很尴尬。

perl (1) 有 \Q ... \E 引号,但即使这样也无法处理$target.

perl -pe "s/\Q$target\E/$replacement/g" file.txt

我刚刚发布了答案!所以我真正的问题是,“有没有更好的方法在 sed/awk/perl 中进行文字替换?”

如果没有,我会把它留在这里,以防它有用。

4

5 回答 5

8

实现的quotemeta\Q绝对可以满足您的要求

所有不匹配的 ASCII 字符/[A-Za-z_0-9]/前面都会有一个反斜杠

由于这可能是在 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

或者,使用-sswitch,它为程序启用命令行开关

# 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"

这打印

用。。。来代替 &
是的

我使用了评论中提到的字符。

另一种方法

#!/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"

为了便于阅读,代码在这里被分行(因此需要\)。相同的打印输出。

于 2019-01-06T08:11:17.640 回答
3

又是我!

这是使用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) 对原始文本进行十六进制编码,并使用十六进制编码的搜索字符串进行搜索替换。最后我们对结果进行十六进制解码。

编辑:我忘记\n从 xxd 输出 ( | tr -d '\n') 中删除,以便模式可以跨越 xxd 的 60 列输出。当然,这依赖于 GNUsed在很长的行上运行的能力(仅受内存限制)。

编辑:这也适用于多线目标,例如

目标=$'foo\nbar' 替换=$'bar\nfoo'

于 2019-01-06T07:55:57.727 回答
1

使用 awk 你可以这样做:

awk -v t="$target" -v r="$replacement" '{gsub(t,r)}' file

上面期望t是一个正则表达式,使用它一个你可以使用的字符串

awk -v t="$target" -v r="$replacement" '{while(i=index($0,t)){$0 = substr($0,1,i-1) r substr($0,i+length(t))} print}' file

灵感来自这篇文章

请注意,如果替换字符串包含目标,这将无法正常工作。上面的链接也有解决方案。

于 2019-01-06T08:03:13.260 回答
1

这是对wef 答案的增强。

我们可以通过删除特殊字符来消除各种特殊字符和字符串(^, ., [, *, $, \(, \), \{, \}, \+, \?, &, \1, ..., 不管什么,以及/分隔符)的特殊含义问题  具体来说,我们可以将所有内容都转换为十六进制;那么我们只有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) 结合形成,该命令将其从内部撕开。它变为,即(  + <code>71)。我们可以通过用空格分隔搜索文本、替换文本和文件中的数据字节来防止这种情况。这是一个程式化的解决方案:2451s/45/67/g24512671&q26

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函数,因为我使用了该函数 3 次,然后我定义decode了对称性。如果您不想定义 decode函数,只需将最后一行更改为

encode file.txt | sed "s/$left/$right/g" | xxd -p –r

请注意,该encode函数将文件中数据(文本)的大小增加了三倍,然后将其sed作为单行发送——甚至在末尾没有换行符。GNU sed 似乎能够处理这个问题;其他版本可能做不到。

作为一个额外的好处,这个解决方案可以处理多行搜索和替换(换句话说,搜索和替换包含换行符的字符串)。

于 2020-04-01T04:31:29.963 回答
1

我可以解释为什么这不起作用:

perl(1) 有 \Q ... \E 引号,但即使这样也无法处理 $target 中的“/”分隔符。

原因是因为\Qand \E(quotemeta) 转义是在解析正则表达式之后处理的,并且除非存在定义正则表达式的有效模式分隔符,否则不会解析正则表达式。

例如,这里尝试使用传递给 perl的字符串/etc/中的变量来替换字符串:/etc/hosts

$target="/etc/";
perl -pe "s/\Q$target\E/XXX/" <<<"/etc/hosts";

在 shell 扩展字符串中的变量后,perl 收到s/\Q/etc/\E/XXX/不是有效正则表达式的命令,因为它不包含三个模式分隔符(perl 看到五个分隔符,即 s/…/…/…/…/)。因此,\Qand\E甚至都不会被执行

正如@zdim 建议的那样,解决方案是将变量传递给perl,使其在解析正则表达式后包含在正则表达式中,例如:

perl -s -pe 's/\Q$target\E/XXX/ig' -- -target="/etc/" <<<"/etc/123"
于 2021-10-05T21:41:14.663 回答