是否可以使用sed可靠地转义正则表达式元字符

时间:2015-04-13 19:17:00

标签: regex sed

我想知道是否可以编写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

我知道有更好的工具可以使用固定字符串而不是模式,例如awkperlpython。我想用sed证明是否可能。我会说让我们专注于基本的POSIX正则表达式,以获得更多乐趣! :)

我已经尝试了很多东西但是任何时候我都能找到一个打破了我的尝试的输入。我认为保持抽象是因为script to escape不会导致任何人走错方向。

是的,讨论出现了here。我认为这可能是一个收集解决方案的好地方,可能会破坏和/或精心制作它们。

3 个答案:

答案 0 :(得分:63)

注意:

  • 如果您正在寻找基于此答案中讨论的技术的预先包装的功能
    • bash函数即使在 多行替换中也可以找到强大的转义 在此帖子的底部(以及使用perl内置支持进行此类转义的perl解决方案。)
    • @EdMorton's answer包含工具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字符串:

/


MULTI-line Solutions


转义MULTI-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)时才有意义。
由于默认情况下sedsed等工具一次在行上运行,因此需要额外的步骤来使它们一次读取多行。< / 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'成语形式,它将所有输入行读取为循环,因此后续命令可在所有输入行上运行马上。

    • 如果您正在使用 GNU -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上构建,以下工具将使用sedbash将任何单行字符串(而不是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/6646161https://stackoverflow.com/a/29613573/6646161):

's/[^^\\]/[&]/g; s/\^/\\^/g; s/\\/\\\\/g'

似乎是错误的:

  • 先执行 s/\^/\\^/g 然后再执行 s/\\/\\\\/g 是错误的,因为任何 ^ 首先转义到 \^ 之后都会再次转义其 \

更好的方法似乎是:'s/[^\^]/[&]/g; s/[\^]/\\&/g;'

  • [^^\\] 和 sed (BRE/ERE) 应该只是 [^\^](或 [^^\])。 \ 在括号表达式中没有特殊含义,不需要引用。