转义sed bash脚本变量中的问号字符

5
我有一组包含链接的html文件,链接的格式为http://mywebsite.com/showfile.cgi?key=somenumber,但我想去掉问号(其实是因为Firefox不喜欢问号,会随机将其转换成%3F,当然这个问题另说)。
然而,我认为我的代码导致问号字符在将选项存储为变量时无法正确读取、保存或处理。
# Doesn't work (no pattern matched)
SED_OPTIONS='-i s/\.cgi\?key/\.cgikey/g'

# Works e.g. http://mywebsite.com/showfileblah?key=somenumber
SED_OPTIONS='-i s/\.cgi/blah/g'

# Leaves question mark in e.g. http://mywebsite.com/showfile.blah?key=somenumber
SED_OPTIONS='-i s/cgi\?/blah/g'

# Actual sed command run when using SED_OPTIONS (I define FILES earlier in
# the code)
sed $SED_OPTIONS $FILES

# Not using the SED_OPTIONS variable works
# e.g. http://mywebsite.com/showfile.cgikey=somenumber
sed -i s/\.cgi\?key/\.cgikey/g $FILES

如何使用SED_OPTIONS变量来获取完整的命令?

"-i s/cgi?/blah/g" 实际上应该给你 http://mywebsite.com/showfile.blahkey=somenumber - Amal Murali
我知道!如果我使用SED_OPTIONS='-i s/cgi?/blah/g'来定义它,它会保留问号,但是如果我在调用sed命令的同一行内内联写入选项"sed -i s/cgi?/blah/g $FILES",它会删除问号。 - user3553107
2个回答

7

将选项和参数列表存储在变量中的最安全方法是使用数组

此外:

  • 您正在使用基本正则表达式(没有-r-E选项),因此?不是特殊字符,不需要转义。
  • 替换字符串中,这不是一个正则表达式,不要转义.
  • 不需要g选项,因为您每行只替换1次。
# Create array with individual options/arguments.
SED_ARGS=( '-i' 's/\.cgi?key/.cgikey/' )

# Invoke `sed` with array - note the double-quoting.
sed "${SED_ARGS[@]}" $FILES

同样地,在输入文件列表中使用数组会更加安全。如果单个文件名包含嵌入的空格或其他可被 shell 扩展的元素,则 $FILES 将不能正常工作。
一般而言: - 对于单引号字符串字面值(例如此处的 sed 脚本),应使用单引号将其括起来,以防止 shell 对其进行解释。 - 对于双引号变量引用,应使用双引号将其括起来,以防止 shell 对其执行额外的操作,如路径名扩展(globbing)和单词拆分(按空格分成多个标记)。

3

我建议将sed的参数存储在一个数组中:

SED_OPTIONS=( '-i' '-e' 's/\.cgi?key/\.cgikey/g' )

sed "${SED_OPTIONS[@]}" $FILES

然而,这只是问题的一部分。
首先要注意的是,当您输入时:
sed -i s/\.cgi\?key/\.cgikey/g $FILES

sed所看到的脚本参数实际上是:

s/.cgi?key/.cgikey/g

因为您没有使用任何引号来保留反斜杠。(为了演示,使用printf "%s\n" s/\.cgi\?key/\.cgikey/g,从而避免任何关于echo是否解释反斜杠的问题。)这样做的一个副作用是,URL可能会变成:

http://example.com/nodotcgi?key=value

将被映射到:

http://example.com/nodo.cgikey=value

使用单引号设置SED_OPTIONS确保必要的反斜杠得到保留,而不在问号前放置反斜杠就能够工作。我在我的Mac上同时拥有GNU sed和BSD sed; 我把它们别名为gnu-sed和bsd-sed以便于明确区分。请注意,BSD sed需要为-i指定后缀,并且不会接受标准输入与-i一起使用。因此,我从命令中删除了-i。
$ URLS=(http://example.com/script.cgi?key=value http://example.com/nodotcgi?key=value)
$ SED_OPTIONS=( '-e' 's/\.cgi?key/\.cgikey/g' )
$ printf "%s\n" "${URLS[@]}" | bsd-sed "${SED_OPTIONS[@]}"
http://example.com/script.cgikey=value
http://example.com/nodotcgi?key=value
$ printf "%s\n" "${URLS[@]}" | gnu-sed "${SED_OPTIONS[@]}"
http://example.com/script.cgikey=value
http://example.com/nodotcgi?key=value
$ SED_OPTIONS=( '-e' 's/\.cgi\?key/\.cgikey/g' )
$ printf "%s\n" "${URLS[@]}" | bsd-sed "${SED_OPTIONS[@]}"
http://example.com/script.cgikey=value
http://example.com/nodotcgi?key=value
$ printf "%s\n" "${URLS[@]}" | gnu-sed "${SED_OPTIONS[@]}"
http://example.com/script.cgi?key=value
http://example.com/nodotcgi?key=value
$

请注意,当问号前有反斜线时(示例的第二部分),两个版本的sed之间的行为差异。

1
正如您所看到的,“No”是答案(?不被解释为一个或多个 i)。 在标准(经典)的 sed正则表达式中,?不是元字符。 如果您使用扩展的正则表达式或类似语法,则会遇到不同的一组问题,但基本的 sed应该/确实使用基本的正则表达式,其中不是元字符。 - Jonathan Leffler
1
我认为这意味着下一个字符被解释为它本身而不是元字符,但替换字符串中的元字符数量严格受限(有反斜杠本身,以及 sed 中的 \n 表示换行符,然后是 \1 等用于替换捕获的内容,就差不多了)。因此,\.. 都会替换为 . - Jonathan Leffler
也许这就是你的意思,但为了明确起见:在 GNU sed 中,替换字符串中的 \n(和其他控制字符转义)确实有效,但这是一种非 POSIX 扩展,在 FreeBSD sed 中不起作用。虽然 \. 偶尔会起作用,但通常最好不要引用不需要引用的字符,因为这种行为没有受到 POSIX 的规范,不同的 sed 版本可能会有不同的行为:“紧接着任何字符(而不是’&’、’\’、数字或用于此命令的分隔符字符)的 ’\’ 的含义未指定。”- http://man.cx/sed - mklement0
谢谢您的补充,您所指的一致性是指什么?支持使用字符串 '\n' 表示换行吗? - mklement0
1
是的,GNU的sed比标准的更加一致。在s///命令的左侧和右侧\n具有不同的含义有点让人烦恼。 - Jonathan Leffler
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接