如何在 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
。这是因为E
是45
,g
是67
,而$Q
是2451
,所以,当您执行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
-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
函数,因为我使用了该函数 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]