我想知道是否可以编写100%可靠的sed
命令来转义输入字符串中的任何正则表达式元字符,以便可以在后续的sed命令中使用它。像这样:
#!/bin/bash
# Trying to replace one regex by another in an input file with sed
search="/abc\n\t[a-z]\+\([^ ]\)\{2,3\}\3"
replace="/xyz\n\t[0-9]\+\([^ ]\)\{2,3\}\3"
# Sanitize input
search=$(sed 'script to escape' <<< "$search")
replace=$(sed 'script to escape' <<< "$replace")
# Use it in a sed command
sed "s/$search/$replace/" input
我知道有更好的工具可以使用固定字符串而不是模式,例如awk
,perl
或python
。我想用sed
证明是否可能。我会说让我们专注于基本的POSIX正则表达式,以获得更多乐趣! :)
我已经尝试了很多东西但是任何时候我都能找到一个打破了我的尝试的输入。我认为保持抽象是因为script to escape
不会导致任何人走错方向。
答案 0 :(得分:63)
注意:
bash
函数即使在 多行替换中也可以找到强大的转义 在此帖子的底部(以及使用perl
内置支持进行此类转义的perl
解决方案。)bash
脚本),可以强有力地执行 单行替换。bash
为shell(符合POSIX标准的重新制定):sed
中的正则表达式:在信用到期时给予信用:我在this answer中找到了下面使用的正则表达式。
假设搜索字符串是单个 -line字符串:
search='abc\n\t[a-z]\+\([^ ]\)\{2,3\}\3' # sample input containing metachars.
searchEscaped=$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$search") # escape it.
sed -n "s/$searchEscaped/foo/p" <<<"$search" # if ok, echoes 'foo'
^
之外的每个字符都放在其自己的字符集[...]
表达式中,以将其视为文字。
^
是一个字符。 无法表示为[^]
,因为它在该位置具有特殊含义(否定)。^
字符。被转义为\^
。
\
来转义每个字符,因为这可以将文字字符变成元数据,例如\<
和\b
是某些工具中的字边界,\n
是换行符,\{
是RE区间的开头,如\{1,3\}
等。这种方法很稳健,但效率不高。
健壮性来自不试图预测所有特殊正则表达式字符 - 这将在正则表达式方言中有所不同 - 但仅关注2个功能由所有正则表达式方言共享 :
^
转义为\^
sed
s///
命令中转义字符串文字以用作替换字符串: sed
s///
命令中的替换字符串不是正则表达式,但它识别占位符,它引用正则表达式匹配的整个字符串({{1通过索引(&
,\1
,...)得到的特定捕获组结果,因此必须将它们与(惯用的)正则表达式分隔符\2
一起转义。
假设替换字符串是单个 -line字符串:
/
replace='Laurel & Hardy; PS\2' # sample input containing metachars.
replaceEscaped=$(sed 's/[&/\]/\\&/g' <<<"$replace") # escape it
sed -n "s/\(.*\) \(.*\)/$replaceEscaped/p" <<<"foo bar" # if ok, outputs $replace as is
中的正则表达式: 注意:只有在尝试匹配之前已读取多个输入行(可能是ALL)时才有意义。
由于默认情况下sed
和sed
等工具一次在单行上运行,因此需要额外的步骤来使它们一次读取多行。< / p>
awk
# Define sample multi-line literal.
search='/abc\n\t[a-z]\+\([^ ]\)\{2,3\}\3
/def\n\t[A-Z]\+\([^ ]\)\{3,4\}\4'
# Escape it.
searchEscaped=$(sed -e 's/[^^]/[&]/g; s/\^/\\^/g; $!a\'$'\n''\\n' <<<"$search" | tr -d '\n') #'
# Use in a Sed command that reads ALL input lines up front.
# If ok, echoes 'foo'
sed -n -e ':a' -e '$!{N;ba' -e '}' -e "s/$searchEscaped/foo/p" <<<"$search"
字符串,这就是在正则表达式中编码换行符的方式。'\n'
将字符串 $!a\'$'\n''\\n'
附加到每个输出行,但最后一个(最后一个新行被忽略,因为它是由'\n'
添加的)<<<
然后从字符串中删除所有实际换行符(tr -d '\n
每当打印其模式空间时添加一行),有效地用{{1替换输入中的所有换行符}}}。字符串。 sed
是符合POSIX标准的'\n'
成语形式,它将所有输入行读取为循环,因此后续命令可在所有输入行上运行马上。
-e ':a' -e '$!{N;ba' -e '}'
(仅限),则可以使用其sed
选项一次性简化所有输入行的读取:sed
-z
sed -z "s/$searchEscaped/foo/" <<<"$search"
命令中转义MULTI-LINE字符串文字以用作替换字符串:sed
s///
- 已转义。# Define sample multi-line literal.
replace='Laurel & Hardy; PS\2
Masters\1 & Johnson\2'
# Escape it for use as a Sed replacement string.
IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$replace")
replaceEscaped=${REPLY%$'\n'}
# If ok, outputs $replace as is.
sed -n "s/\(.*\) \(.*\)/$replaceEscaped/p" <<<"foo bar"
是符合POSIX标准的\
成语形式,它将所有输入行读取为循环。-e ':a' -e '$!{N;ba' -e '}'
转义所有sed
,'s/[&/\]/\\&/g
和&
个实例,与单行解决方案一样。\
然后/
- 为所有实际换行添加前缀。s/\n/\\&/g'
用于按读取\
命令的输出(以避免自动删除命令替换的尾随换行符{{1会执行)。IFS= read -d '' -r
然后移除单个尾随换行符,sed
已隐式附加到输入。$(...)
函数基于以上内容(适用于${REPLY%$'\n'}
):<<<
引号(转义)用于正则表达式 bash
引号,用于sed
来电的替换字符串。quoteRe()
默认情况下会在时间读取单个行,因此在quoteSubst()
命令中仅使用带有多行字符串的s///
明确地一次读取多个(或所有)行。sed
)来调用函数不会对尾随换行符的字符串起作用;在这种情况下,请使用quoteRe()
sed
$(...)
示例:强>
IFS= read -d '' -r escapedValue <(quoteSubst "$value")
请注意使用# SYNOPSIS
# quoteRe <text>
quoteRe() { sed -e 's/[^^]/[&]/g; s/\^/\\^/g; $!a\'$'\n''\\n' <<<"$1" | tr -d '\n'; }
一次读取所有输入,以便多行替换有效。
# SYNOPSIS
# quoteSubst <text>
quoteSubst() {
IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$1")
printf %s "${REPLY%$'\n'}"
}
解决方案: Perl具有内置支持,用于在正则表达式中转义任意字符串以供字面使用: quotemeta()
function或其等效from=$'Cost\(*):\n$3.' # sample input containing metachars.
to='You & I'$'\n''eating A\1 sauce.' # sample replacement string with metachars.
# Should print the unmodified value of $to
sed -e ':a' -e '$!{N;ba' -e '}' -e "s/$(quoteRe "$from")/$(quoteSubst "$to")/" <<<"$from"
引用。
单线和多线串的方法相同;例如:
-e ':a' -e '$!{N;ba' -e '}'
请注意使用perl
一次读取所有输入,以便多行替换有效。
\Q...\E
选项允许在脚本之后的from=$'Cost\(*):\n$3.' # sample input containing metachars.
to='You owe me $1/$& for'$'\n''eating A\1 sauce.' # sample replacement string w/ metachars.
# Should print the unmodified value of $to.
# Note that the replacement value needs NO escaping.
perl -s -0777 -pe 's/\Q$from\E/$to/' -- -from="$from" -to="$to" <<<"$from"
之后放置-0777
样式的Perl变量定义,在任何文件名操作数之前。
答案 1 :(得分:15)
在此线程的@mklement0's answer上构建,以下工具将使用sed
和bash
将任何单行字符串(而不是regexp)替换为任何其他单行字符串:
$ cat sedstr
#!/bin/bash
old="$1"
new="$2"
file="${3:--}"
escOld=$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<< "$old")
escNew=$(sed 's/[&/\]/\\&/g' <<< "$new")
sed "s/$escOld/$escNew/g" "$file"
为了说明是否需要此工具,请考虑直接致电a.*/b{2,}\nc
,尝试将d&e\1f
替换为sed
:
$ cat file
a.*/b{2,}\nc
axx/bb\nc
$ sed 's/a.*/b{2,}\nc/d&e\1f/' file
sed: -e expression #1, char 16: unknown option to `s'
$ sed 's/a.*\/b{2,}\nc/d&e\1f/' file
sed: -e expression #1, char 23: invalid reference \1 on `s' command's RHS
$ sed 's/a.*\/b{2,}\nc/d&e\\1f/' file
a.*/b{2,}\nc
axx/bb\nc
# .... and so on, peeling the onion ad nauseum until:
$ sed 's/a\.\*\/b{2,}\\nc/d\&e\\1f/' file
d&e\1f
axx/bb\nc
或使用上述工具:
$ sedstr 'a.*/b{2,}\nc' 'd&e\1f' file
d&e\1f
axx/bb\nc
这是有用的原因是它可以很容易地扩充,以便在必要时使用单词分隔符来替换单词,例如,在GNU sed
语法中:
sed "s/\<$escOld\>/$escNew/g" "$file"
而实际操作字符串的工具(例如awk
的{{1}})不能使用字分隔符。
答案 2 :(得分:1)
需要注意的是上面部分答案中使用的正则表达式(https://stackoverflow.com/a/29626460/6646161和https://stackoverflow.com/a/29613573/6646161):
's/[^^\\]/[&]/g; s/\^/\\^/g; s/\\/\\\\/g'
似乎是错误的:
s/\^/\\^/g
然后再执行 s/\\/\\\\/g
是错误的,因为任何 ^
首先转义到 \^
之后都会再次转义其 \
。立>
更好的方法似乎是:'s/[^\^]/[&]/g; s/[\^]/\\&/g;'
。
[^^\\]
和 sed (BRE/ERE) 应该只是 [^\^]
(或 [^^\]
)。 \
在括号表达式中没有特殊含义,不需要引用。