【问题标题】:Is it possible to escape regex metacharacters reliably with sed是否可以使用 sed 可靠地转义正则表达式元字符
【发布时间】:2015-06-19 05:37:38
【问题描述】:

我想知道是否可以编写一个 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。我认为这可能是收集解决方案并可能破坏和/或详细说明它们的好地方。

【问题讨论】:

  • @Barmar 是的。这只是理论上的。
  • @hek2mgl 你想让输入搜索字符串中的\n 匹配什么?为了让它匹配文件中的文字\n,它需要在模式中是\\n(这是printf所做的)。
  • @EdMorton it should be treated a literal backslash followed by a literal n - 是的,我是这个意思。
  • 不要忘记,根据 sed 版本和选项,使用了几个标准正则表达式。在这种情况下,一些转义字符成为其他版本的正则表达式特殊含义,如 {( 之间posix 和 gnu 本机。
  • 对于LHS如果你不知道什么数据会显示最好注意分隔符,我看到的答案仍然使用斜线但是斜线有点常见,你可以使用非- 可打印字符,如 SOH \001 - 请参阅我对此处使用此问题的另一个问题的回答:stackoverflow.com/a/29238669/4401178 在 RHS 上,我知道没有完美的解决方案,但您可以快速扫描字符串并在运行时找到唯一的替换值,你可以在这里看到这种技术:github.com/AdamDanischewski/r-n-f-bash-rename-script

标签: regex sed


【解决方案1】:

注意:

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

    • bash 函数可在 这篇文章的底部(加上一个perl 解决方案,它使用perl 对这种转义的内置支持)。
    • @EdMorton's answer 包含一个工具bash 脚​​本),可以可靠地执行单行替换
      • Ed 的答案现在有 改进的 版本的 sed 下面使用的命令,如果您希望 转义字符串文字以用于可能使用其他正则表达式处理工具,例如 awkperl 简而言之:对于跨工具使用,\ 必须转义为 \\ 而不是而不是[\],这意味着:你必须使用
        sed 's/[^^\\]/[&amp;]/g; s/\^/\\^/g; s/\\/\\\\/g'而不是下面使用的
        sed 's/[^^]/[&amp;]/g; s/\^/\\^/g'命令
  • 所有的 sn-ps 都假定 bash 作为 shell(符合 POSIX 的重新表述是可能的):


单线解决方案


转义字符串文字以用作sed中的正则表达式

在应得的情况下给予信用:我在this answer中找到了下面使用的正则表达式。

假设搜索字符串是一个单行行的字符串:

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'
  • ^ 之外的每个字符都放在其自己的字符集[...] 表达式中,以将其视为文字。
    • 请注意,^ 是一个字符。你不能代表[^],因为它在那个位置有特殊的意义(否定)。
  • 然后,^ 字符。转义为\^
    • 请注意,您不能通过在每个字符前面放置 \ 来逃避每个字符,因为这会将文字字符转换为元字符,例如\&lt;\b 在某些工具中是单词边界,\n 是换行符,\{ 是 RE 间隔的开始,例如 \{1,3\},等等。

该方法稳健,但效率不高。

稳健性来自于试图预测所有特殊的正则表达式字符——这会因正则表达式方言而异——而是只关注两个特性由所有正则表达式方言共享

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

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

seds/// 命令中的替换字符串不是正则表达式,但它可以识别 占位符 指的是正则表达式匹配的整个字符串 (&amp;) 或特定捕获- 按索引(\1\2、...)对结果进行分组,因此必须对这些结果以及(习惯)正则表达式分隔符 / 进行转义。

假设替换字符串是一个单行行的字符串:

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


多线解决方案


转义多行字符串文字以用作sed 中的正则表达式

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

# 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'strings,这是正则表达式中换行符的编码方式。
  • $!a\'$'\n''\\n'string '\n' 附加到除最后一行之外的每个输出行(最后一个换行符被忽略,因为它是由 &lt;&lt;&lt; 添加的)
  • tr -d '\n 然后从字符串中删除所有实际 换行符(sed 在打印其模式空间时添加一个),有效地将输入中的所有换行符替换为'\n' 字符串。
  • -e ':a' -e '$!{N;ba' -e '}'sed 习惯用法的 POSIX 兼容形式,它循环读取 所有 输入行,因此让后续命令一次对所有输入行进行操作。

    • 如果您使用 GNU sed(仅限),您可以使用其-z 选项来简化一次读取所有输入行:
      sed -z "s/$searchEscaped/foo/" &lt;&lt;&lt;"$search"

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

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


bash 函数 基于上述(对于sed):

  • quoteRe() 引号(转义)用于 regex
  • quoteSubst() 引号用于s/// 调用的替换字符串
  • 两者都能正确处理多行输入
    • 请注意,因为sed 默认读取 行,所以将quoteRe() 与多行字符串一起使用仅在sed 显式读取多个(或all) 行。
    • 此外,使用命令替换 ($(...)) 调用函数对于具有 尾随 换行符的字符串不起作用;在这种情况下,使用类似IFS= read -d '' -r escapedValue &lt;(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 '}' 一次读取所有输入,以便多行替换起作用。



perl解决方案:

Perl 内置支持转义任意字符串以在正则表达式中使用文字:quotemeta() function 或其等效的\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 一次读取所有输入,以便多行替换起作用。

  • -s 选项允许将 -&lt;var&gt;=&lt;val&gt; 样式的 Perl 变量定义放在脚本之后的 -- 之后,在任何文件名操作数之前。

【讨论】:

  • FWIW,较新的 sed 允许 sed -z 匹配 NUL 分隔行,因此匹配项可以包括 \n。使用示例:find -print0 | sed -z ... | xargs --null script 等。带有\n 的多行正则表达式非常方便,因为 Linux(或 Windows 的 Ubuntu)允许文件名中的换行符(例如:echo help me world &gt; $'\n\nminime\nwas here\n'
  • 谢谢,@ChristianBongiorno。但是,我不太了解您描述的用例;您是在谈论用bind 定义的键盘宏吗?呼应值如何发挥作用?能举个例子吗?
  • @Tino:谢谢,我在答案中添加了一个基于-z 的变体,但请注意,这与旧或新本身无关,而是关于 GNU sed,将 -z 定义为非标准选项,而其他 sed 实现,例如 macOS 上的 BSD sed,则没有。
  • @ChristianBongiorno:谢谢你的解释,但我还是不明白,很遗憾;不过,我很好奇:问一个关注这方面的新问题怎么样?
  • 这是一个很好的答案。在我的情况下,我需要转义字符串以输入到 sed,并需要它在 POSIX sh(而不是 bash)中工作,所以我最终得到:fixed=`printf '%s\n' "${val}" | sed 's#\/#\\\/#g' | sed 's/\&amp;/\\\&amp;/g' `
【解决方案2】:

基于此线程中的@mklement0's answer,以下工具将使用sedbash 将任何单行字符串(而不是正则表达式)替换为任何其他单行字符串:

$ cat sedstr
#!/bin/bash
old="$1"
new="$2"
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&amp;e\1f

$ 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"

而实际操作字符串的工具(例如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 等实现,因此使用这种形式的转义。

【讨论】:

    【解决方案3】:

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

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

    好像错了:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-06-07
      • 2011-09-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多