是否有可能使用sed可靠地转义正则expression式元字符
我想知道是否有可能编写一个100%可靠的sed
命令来转义inputstring中的任何正则expression式元字符,以便它可以在随后的sed命令中使用。 喜欢这个:
#!/bin/bash # Trying to replace one regex by another in an input file with sed search="/abc\n\t[az]\+\([^ ]\)\{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
我知道有更好的工具可以使用固定string而不是模式,例如awk
, perl
或python
。 我只想certificate是否有可能与sed
。 我会说让我们专注于基本的POSIX正则expression式,以获得更多的乐趣! 🙂
我已经尝试了很多东西,但是随时可以find一个打破我的尝试的input。 我认为把它抽象为script to escape
不会导致任何人走错方向。
顺便说一句, 这里的讨论来了。 我认为这可能是一个收集解决scheme的好地方,可能会打破和/或阐述它们。
注意:
- 如果您正在寻找基于此答案中讨论的技术的预先打包的function :
-
bash
函数可以在多行replace中实现健壮的转义 ,可以在这篇文章的底部find(另外还有一个使用perl
内置的perl
解决scheme来支持这种转义)。 - @ EdMorton的答案包含一个工具 (
bash
脚本),强大的执行单行replace 。
-
- 所有的片段都假设
bash
是shell(POSIX兼容的重新configuration是可能的):
单线解决scheme
转义string文字作为正则expression式在sed
:
在信用到期时给予信用:我在下面的答案中find了正则expression式。
假设searchstring是一个单行string:
search='abc\n\t[az]\+\([^ ]\)\{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'
- 除了
^
以外的每个字符都放置在它自己的字符集expression式中,以将其视为文字。- 请注意,
^
是一个字符。 你不能表示为[^]
,因为它在那个位置有特殊的含义(否定)。
- 请注意,
- 然后,
^
字符。 逃脱为\^
。
该方法是强大的,但效率不高。
稳健性来自于不试图预测所有特殊的正则expression式字符 – 这些字符在正则expression式方面会有所不同 – 但只关注所有正则expression式方言共享的 2个特征 :
- 在字符集内指定文字字符的能力。
- 能够将文字
^
转义为\^
转义string文字作为sed
s///
命令中的replacestring :
sed
s///
命令中的replacestring不是一个正则expression式,但它可以识别引用由正则expression式( &
)匹配的整个string或索引( \1
, \2
,…)的特定捕获组结果的占位符 。 ..),所以这些必须和(习惯的)正则expression式分隔符一起被转义。
假设replacestring是一个单行string:
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
多线解决scheme
转义多行string文字作为正则expression式在sed
:
注意 :如果在尝试匹配之前已经读取了多个input行 (可能是ALL),则这是唯一有意义的。
由于像sed
和awk
这样的工具默认一次只能在一行上操作,所以需要额外的步骤来使它们一次读取多行。
# Define sample multi-line literal. search='/abc\n\t[az]\+\([^ ]\)\{2,3\}\3 /def\n\t[AZ]\+\([^ ]\)\{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"
- 多行inputstring中的换行符必须转换为
'\n'
string ,这就是换行符中的换行符。 -
$!a\'$'\n''\\n'
将string'\n'
$!a\'$'\n''\\n'
附加到每个输出行,但是最后一个换行符被忽略,因为它是由<<<
添加的。 -
tr -d '\n
然后从string中删除所有实际的换行符(sed
在打印模式空间时会添加一个换行符),用'\n'
string有效地replaceinput中的所有换行符。
-
-e ':a' -e '$!{N;ba' -e '}'
是符合POSIX标准的sed
语言forms,可以读取所有input行循环,因此在随后的命令一旦。
转义多行string文字作为sed
s///
命令中的replacestring :
# 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"
- inputstring中的换行符必须保留为实际换行符,但是转义。
-
-e ':a' -e '$!{N;ba' -e '}'
是一个sed
习惯用法的符合POSIX的forms,它读取所有input行循环。 -
's/[&/\]/\\&/g
像在单行解决scheme中那样转义所有&
,\
和/
instances。 -
s/\n/\\&/g'
g'then\
-prefixes all actual newlines。 -
IFS= read -d '' -r
用于按原样读取sed
命令的输出(以避免自动删除命令replace($(...)
)将执行的尾随换行符)。 -
${REPLY%$'\n'}
然后删除一个尾部的换行符,<<<
已隐式地附加到input。
基于上述(对于sed
) bash
函数 :
-
quoteRe()
引号(转义)在正则expression式中使用 -
quoteSubst()
引号用于s///
调用的replacestring 。 - 都能正确处理多行input
- 请注意,因为
sed
在默认情况下会一次读取一行,所以在多行string中使用quoteRe()
只在同时显式读取多个(或全部)行的sed
命令中才有意义。 - 此外,使用命令replace(
$(...)
)来调用函数将不适用于具有尾随换行符的string; 在这种情况下,使用类似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'}" }
例:
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 '}'
来一次读取所有input,以便多行replace工作。
perl
解决scheme:
Perl内置了对在正则expression式中使用的任意string的转义支持 : quotemeta()
函数或其等价的\Q...\E
引用 。
单行和多行string的方法是一样的; 例如:
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
来一次读取所有input,以便多线replace工作。 -
-s
选项允许在脚本之后,在任何文件名操作数之前放置-<var>=<val>
样式的Perlvariables定义。
build立在@ mklement0在这个线程中的答案 ,下面的工具将使用sed
和bash
replace任何其他单行string的任何单行string(而不是正则expression式):
$ 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"
为了说明这个工具的必要性,可以考虑直接调用sed
用d&e\1f
代替a.*/b{2,}\nc
:
$ 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
这很有用的原因是,如果需要,可以使用单词分隔符来replace单词,例如在GNU sed
语法中,它可以很容易地被扩充:
sed "s/\<$escOld\>/$escNew/g" "$file"
而实际上对string进行操作的工具(例如awk
的index()
)不能使用单词分隔符。