如何在 sed 和 awk(和 perl)中搜索和替换任意文字字符串

Posted

技术标签:

【中文标题】如何在 sed 和 awk(和 perl)中搜索和替换任意文字字符串【英文标题】:How to search & replace arbitrary literal strings in sed and awk (and perl) 【发布时间】:2019-06-01 05:57:33 【问题描述】:

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

通常,我们只需使用 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 中进行文字替换?”

如果没有,我会把它留在这里,以备不时之需。

【问题讨论】:

对于sed,请参阅之前的讨论:Is it possible to escape regex metacharacters reliably with sed 另见Why is the escape of quotes lost in this regex substitution? 【参考方案1】:

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

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

因为这可能是在一个 shell 脚本中,所以问题在于 shell 变量如何以及何时被插值,以及 Perl 程序最终会看到什么。

最好的方法是避免解决插值混乱,而是将这些 shell 变量正确地传递给 Perl 单行器。这可以通过多种方式完成;详情请见this post。

要么将 shell 变量简单地作为参数传递

#!/bin/bash

# define $target

perl -pe"BEGIN  $patt = shift ; s\Q$patt$replacementg" "$target" file.txt

@ARGV 中删除所需的参数并在BEGIN 块中使用,因此在运行时之前;然后file.txt 得到处理。这里的正则表达式中不需要\E

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

# define $target, etc

perl -s -pe"s\Q$patt$replacementg" -- -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"

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

【讨论】:

如果$target 是一个包含分隔符的shell 变量,Perl 只能在shell 插入变量后才能看到脚本,并且最多会出现语法错误和安全问题最坏的情况。你必须告诉 Perl 哪个部分是变量,也许通过将它作为命令行参数传递。 @tripleee 确实,完全错过了他们隐含的上下文。已编辑 @melpomene 感谢您的编辑,最好在 shell 中引用它。 (但在bash 的测试中,它总是没有被引用;我想知道我是否在特定的软件版本上运气好?) @zdim 您的$target 测试字符串是否包含有趣的字符,例如*? 或空格? 找到相关链接:mywiki.wooledge.org/BashPitfalls#cp_.24file_.24target【参考方案2】:

又是我!

这是使用 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'

【讨论】:

当我第一次看到这个答案时,我觉得它很棒。几分钟后,我意识到,就像钻石一样,它是有缺陷的。例如,如果您尝试在包含$Q 的文件中将E 更改为g,它将更改为&q。这是因为E45g67,而$Q2451,所以,当您执行s/45/67/ 时,您将2451 更改为2671,即@ 987654338@ (26 + 71)。 … 我已经发布了一个解决这个问题的答案。【参考方案3】:

使用 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

灵感来自this post

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

【讨论】:

@Sundeep:你说得对,谢天谢地,格伦杰克曼找到了解决这个问题的办法 感谢您的链接 - 该解决方案非常复杂,但由于它全部在 awk 中,如果性能成为问题,它可能比 xxd 解决方案更快。我确实认为 xxd 解决方案更易于阅读和理解。【参考方案4】:

这是一个增强 wef’s answer.

我们可以去掉各种特殊字符的特殊含义问题 和字符串(^.[*$\(\)\,@987654@30@,\?,@987 &\1、……等等,还有 / 分隔符) 通过删除特殊字符。 具体来说,我们可以将所有内容都转换为十六进制; 那么我们只有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>1s/45/67/g 命令将其从内部撕开。 它将2451 更改为2671,即&amp;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 函数,因为我使用了该函数 3 次, 然后我为对称定义了decode。 如果您不想定义 decode 函数,只需将最后一行更改为

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

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

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

【讨论】:

非常正确的 AFAICS,即使它是一个极端的边缘情况——对于完全一般的情况来说,这是正确的做法。我最初的用例是不太可能出现的中等长度目标。请接受投票并感谢您的周到回答。【参考方案5】:

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

perl(1) 有 \Q ... \E 引号,但即使这样也无法处理 $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 和 awk(和 perl)中搜索和替换任意文字字符串的主要内容,如果未能解决你的问题,请参考以下文章

如何在两个模式之间打印线,包括或不包括(在 sed、AWK 或 Perl 中)?

如何在两种模式之间打印行,包括或排他(在sed,AWK或Perl中)?

如何在bash脚本中使用Bash / Sed / Awk / Perl删除分隔字符串的最后一个元素[duplicate]

Linux命令awk,sed使用

使用 sed/perl/awk 替换第一次出现的匹配文本

如何使用 sed、awk 或 gawk 仅打印匹配的内容?