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

Is it possible to escape regex metacharacters reliably with 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\}"
replace="/xyz\n\t[0-9]\+\([^ ]\)\{2,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 不会将任何人引向错误的方向。

顺便说一句,讨论开始了 。我认为这可能是收集解决方案的好地方 and/or 详细说明它们。

注:

  • 如果您正在根据此答案中讨论的技术寻找预打包功能

    • bash 函数 即使在 多行中也能实现 稳健的转义 替换 可以在这个 post 的 底部找到(加上使用 perlperl 解决方案对这种转义的内置支持)。
    • 包含一个 工具 bash 脚本),可以稳健地执行 单行 换人
      • Ed 的回答现在有 改进 版本的 sed 下面使用的命令,如果你想 转义字符串文字,以便与 其他 正则表达式处理工具一起使用,例如 awkperl. 简而言之: 对于跨工具使用,\ 必须转义为 \ 而不是 [\],这意味着:而不是
        sed 's/[^^]/[&]/g; s/\^/\^/g'下面使用的命令,必须使用
        sed 's/[^^\]/[&]/g; s/\^/\^/g; s/\/\\/g'
  • 所有片段都假定 bash 作为 shell(POSIX 兼容的重新表述是可能的):


单线解决方案


转义字符串文字以用作 sed 中的 regex:

在信用到期的地方给予信用:我在 .

中找到了下面使用的正则表达式

假设搜索字符串是行字符串:

search='abc\n\t[a-z]\+\([^ ]\)\{2,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 个特征所有正则表达式方言共有:

  • 在字符集中指定文字字符的能力。
  • 将文字 ^ 转义为 \^
  • 的能力

转义字符串文字以用作 seds/// 命令中的 替换字符串 :

sed s/// 命令中的替换字符串不是正则表达式,但它可以识别 占位符 ,它指的是匹配的整个字符串正则表达式 (&) 或索引的特定捕获组结果 (</code>, <code>, ...),因此必须将这些与(习惯的)正则表达式分隔符 /.

假设替换字符串是行字符串:

replace='Laurel & Hardy; PS' # sample input containing metachars.

replaceEscaped=$(sed 's/[&/\]/\&/g' <<<"$replace") # escape it

sed -n "s/\(.*\) \(.*\)/$replaceEscaped/p" <<<"foo bar" # if ok, outputs $replace as is


多线解决方案


sed:

中转义多行字符串文字以用作 regex

注意:只有在尝试匹配之前读取了多个输入行(可能是全部)才有意义。
由于 sedawk 等工具默认一次在 单行 上运行,因此需要额外的步骤才能使它们一次读取多行时间.

# Define sample multi-line literal.
search='/abc\n\t[a-z]\+\([^ ]\)\{2,3\}
/def\n\t[A-Z]\+\([^ ]\)\{3,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'string '\n' 附加到除最后一行之外的每个输出行(最后一个换行符被忽略,因为它是由 [=60= 添加的])
  • tr -d '\n 然后从字符串中删除所有 actual 换行符(sed 在打印其模式 space 时添加一个),有效地替换输入中的所有换行符都带有 '\n' 个字符串。
  • -e ':a' -e '$!{N;ba' -e '}'sed 习语的 POSIX 兼容形式,读作 all 输入行一个循环,因此留下后续命令一次对所有输入行进行操作。

    • 如果您正在使用 GNU sed(仅),您可以使用它的 -z 选项来简化一次读取所有输入行:
      sed -z "s/$searchEscaped/foo/" <<<"$search"

转义多行字符串文字以用作 seds/// 命令中的 替换字符串

# Define sample multi-line literal.
replace='Laurel & Hardy; PS
Masters & Johnson'

# 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" 
  • 输入字符串中的换行符必须保留为实际换行符,但 \-转义。
  • -e ':a' -e '$!{N;ba' -e '}'sed 习语的 POSIX 兼容形式,读作 all 输入行一个循环。
  • 's/[&/\]/\&/g 转义所有 &\/ 实例,就像在单行解决方案中一样。
  • s/\n/\&/g' 然后 \ - 为所有实际换行添加前缀。
  • IFS= read -d '' -r 用于读取 sed 命令的输出 原样 (以避免自动删除命令替换 ($(...)) 会执行).
  • ${REPLY%$'\n'} 然后删除 单个 尾随换行符,<<< 已将其隐式附加到输入中。


bash functions 基于以上(for sed):

  • quoteRe() 引号(转义)用于 regex
  • 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' <<<"" | tr -d '\n'; }
# SYNOPSIS
#  quoteSubst <text>
quoteSubst() {
  IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\&/g; s/\n/\&/g' <<<"")
  printf %s "${REPLY%$'\n'}"
}

示例:

from=$'Cost\(*):\n.' # sample input containing metachars. 
to='You & I'$'\n''eating A 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解法:

Perl 内置支持 转义任意字符串以供在正则表达式中使用:quotemeta() function 或其等价物 \Q...\E 引用.
单行字符串和多行字符串的方法相同;例如:

from=$'Cost\(*):\n.' # sample input containing metachars.
to='You owe me /$& for'$'\n''eating A 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 一次读取所有输入,以便多行替换有效。

  • -s 选项允许将 -<var>=<val> 风格的 Perl 变量定义放在脚本之后 -- 之后,任何文件名操作数之前。

基于此线程中的 ,以下工具将使用 sed 和 [=17= 将任何单行字符串(与正则表达式相反)替换为任何其他单行字符串]:

$ cat sedstr
#!/bin/bash
old=""
new=""
file="${3:--}"
escOld=$(sed 's/[^^\]/[&]/g; s/\^/\^/g; s/\/\\/g' <<< "$old")
escNew=$(sed 's/[&/\]/\&/g' <<< "$new")
sed "s/$escOld/$escNew/g" "$file"

为了说明此工具的必要性,考虑尝试通过直接调用 seda.*/b{2,}\nc 替换为 d&ef

$ cat file
a.*/b{2,}\nc
axx/bb\nc

$ sed 's/a.*/b{2,}\nc/d&ef/' file  
sed: -e expression #1, char 16: unknown option to `s'
$ sed 's/a.*\/b{2,}\nc/d&ef/' file
sed: -e expression #1, char 23: invalid reference  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&ef
axx/bb\nc

或使用上述工具:

$ sedstr 'a.*/b{2,}\nc' 'd&ef' file  
d&ef
axx/bb\nc

之所以有用,是因为它可以很容易地扩展为在必要时使用单词定界符替换单词,例如在 GNU sed 语法中:

sed "s/\<$escOld\>/$escNew/g" "$file"

而实际上对字符串进行操作的工具(例如 awkindex())不能使用单词分隔符。

注意:不将 \ 包裹在括号表达式中的原因是,如果您使用的工具接受 [\]] 作为括号表达式中的文字 ](例如perl 和大多数 awk 实现)来进行实际的最终替换(即代替 sed "s/$escOld/$escNew/g"),那么您不能使用以下方法:

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

通过将 \ 包含在 [] 中来转义 \,因为这样 \x 就会变成 [\][x],这意味着 \ or ] or [ or x。相反,您需要:

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

因此,虽然 [\] 可能适用于所有当前的 sed 实现,但我们知道 \ 将适用于所有 sed、awk、perl 等实现,因此使用这种形式的转义。

需要注意的是上面一些答案中使用的正则表达式在 and :

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

好像有误:

  • 首先执行 s/\^/\^/g 然后执行 s/\/\\/g 是一个错误,因为任何 ^ 首先转义到 \^ 然后其 \ 将再次转义。

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

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