这是绝对可行的。
您看到的 Jesse Glick 的答案大致就在那里,但它有几个错误,我还有其他一些选择供您考虑,因为这是我不止一次遇到的问题。
首先,您可能已经知道,echo 是一个坏主意,如果目标是可移植性,则应该使用 printf:如果它接收的参数是“-n”,则“echo”在 POSIX 中具有未定义的行为,并且在实践中, echo 的某些实现将 -n 视为特殊选项,而其他实现仅将其视为打印的普通参数。所以就变成了这样:
esceval()
{
printf %s "$1" | sed "s/'/'\"'\"'/g"
}
或者,不是通过将嵌入的单引号转义为:
'"'"'
..相反,您可以将它们变成:
'\''
..我猜的风格差异(我想无论哪种方式性能差异都可以忽略不计,尽管我从未测试过)。生成的 sed 字符串如下所示:
esceval()
{
printf %s "$1" | sed "s/'/'\\\\''/g"
}
(这是四个反斜杠,因为双引号会吞下其中两个,留下两个,然后 sed 会吞下一个,只留下一个。就个人而言,我发现这种方式更具可读性,因此我将在其余部分使用涉及它的例子,但两者应该是等价的。)
但是,我们仍然有一个错误:命令替换将从命令输出中删除至少一个(但在许多 shell 中是全部)尾随换行符(不是所有空格,只是换行符)。因此,除非您在参数的最后有换行符,否则上述解决方案有效。然后你会失去那个/那些换行符。修复显然很简单:在实际命令值之后添加另一个字符,然后再从引用/esceval 函数输出。顺便说一句,我们已经需要这样做了,因为我们需要用单引号开始和停止转义参数。你有两种选择:
esceval()
{
printf '%s\n' "$1" | sed "s/'/'\\\\''/g; 1 s/^/'/; $ s/$/'/"
}
这将确保参数已经完全转义,在构建最终字符串时无需添加更多单引号。这可能是您将获得的最接近单个可内联版本的东西。如果你对 sed 依赖没问题,你可以在这里停下来。
如果您对 sed 依赖项不满意,但可以假设您的 shell 实际上是 POSIX 兼容的(仍然有一些,特别是 Solaris 10 及更低版本上的 /bin/sh,它将无法执行下一个变体 - 但您需要关心的几乎所有 shell 都可以执行此操作):
esceval()
{
printf \'
unescaped=$1
while :
do
case $unescaped in
*\'*)
printf %s "${unescaped%%\'*}""'\''"
unescaped=${unescaped#*\'}
;;
*)
printf %s "$unescaped"
break
esac
done
printf \'
}
您可能会注意到这里看似多余的引用:
printf %s "${unescaped%%\'*}""'\''"
..这可以替换为:
printf %s "${unescaped%%\'*}'\''"
我这样做的唯一原因是,曾几何时,Bourne shell 在将变量替换为带引号的字符串时存在错误,其中变量周围的引号并没有完全在变量替换的位置开始和结束。因此,这是我偏执的便携习惯。在实践中,你可以做后者,这不会是一个问题。
如果您不想在 shell 环境的其余部分中破坏变量 unescaped,则可以将该函数的全部内容包装在子 shell 中,如下所示:
esceval()
{
(
printf \'
unescaped=$1
while :
do
case $unescaped in
*\'*)
printf %s "${unescaped%%\'*}""'\''"
unescaped=${unescaped#*\'}
;;
*)
printf %s "$unescaped"
break
esac
done
printf \'
)
}
“但是等等”,你说:“我想在一个命令中对 MULTIPLE 参数执行什么操作?如果我从命令行运行它,我希望作为用户的输出仍然看起来不错且清晰易读不管什么原因。”
别害怕,我有你:
esceval()
{
case $# in 0) return 0; esac
while :
do
printf "'"
printf %s "$1" | sed "s/'/'\\\\''/g"
shift
case $# in 0) break; esac
printf "' "
done
printf "'\n"
}
..或相同的东西,但只有外壳版本:
esceval()
{
case $# in 0) return 0; esac
(
while :
do
printf "'"
unescaped=$1
while :
do
case $unescaped in
*\'*)
printf %s "${unescaped%%\'*}""'\''"
unescaped=${unescaped#*\'}
;;
*)
printf %s "$unescaped"
break
esac
done
shift
case $# in 0) break; esac
printf "' "
done
printf "'\n"
)
}
在最后四个中,您可以折叠一些外部 printf 语句并将它们的单引号向上滚动到另一个 printf - 我将它们分开,因为我觉得当您可以看到开始和结束单时它使逻辑更加清晰 -单独打印语句上的引号。
附:我还做了这个怪物,它是一个 polyfill,它将在前两个版本之间进行选择,具体取决于你的 shell 是否能够支持必要的变量替换语法(虽然看起来很糟糕,因为只有 shell 的版本必须是在一个 eval-ed 字符串中,以防止不兼容的 shell 在看到它时吐出):https://github.com/mentalisttraceur/esceval/blob/master/sh/esceval.sh