是否有可能使用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而不是模式,例如awkperlpython 。 我只想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),则这是唯一有意义的。
由于像sedawk这样的工具默认一次只能在一行上操作,所以需要额外的步骤来使它们一次读取多行。

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


基于上述(对于sedbash函数

  • 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在这个线程中的答案 ,下面的工具将使用sedbashreplace任何其他单行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" 

为了说明这个工具的必要性,可以考虑直接调用sedd&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进行操作的工具(例如awkindex() )不能使用单词分隔符。