Jes*_*ick 18 bash quotes printf posix sh
假设我有一个#!/bin/sh
可以采用各种位置参数的脚本,其中一些可能包含空格,两种引号或两种引号等.我想迭代"$@"
并为每个参数立即以某种方式处理它,或者保存以供以后使用.在脚本结束时,我想启动(可能exec
)另一个进程,传递一些这些参数,所有特殊字符都保持不变.
如果我没有对参数进行处理,othercmd "$@"
那么工作正常,但我需要提取一些参数并稍微处理它们.
如果我可以假设Bash,那么我可以printf %q
用来计算我eval
以后可以引用的args的引用版本,但这不适用于例如Ubuntu的Dash(/bin/sh
).
是否printf %q
可以使用内置函数和POSIX定义的实用程序在简单的Bourne shell脚本中编写任何等效文件,比如说我可以复制到脚本中的函数?
例如,脚本以ls
相反的顺序尝试其参数:
#!/bin/sh
args=
for arg in "$@"
do
args="'$arg' $args"
done
eval "ls $args"
Run Code Online (Sandbox Code Playgroud)
适用于许多情况:
$ ./handle goodbye "cruel world"
ls: cannot access cruel world: No such file or directory
ls: cannot access goodbye: No such file or directory
Run Code Online (Sandbox Code Playgroud)
但不是在'
使用时:
$ ./handle goodbye "cruel'st world"
./handle: 1: eval: Syntax error: Unterminated quoted string
Run Code Online (Sandbox Code Playgroud)
以下工作正常,但依赖于Bash:
#!/bin/bash
args=
for arg in "$@"
do
printf -v argq '%q' "$arg"
args="$argq $args"
done
eval "ls $args"
Run Code Online (Sandbox Code Playgroud)
这是绝对可行的。
您看到的 Jesse Glick 的答案大致就在那里,但它有几个错误,我还有一些替代方案供您考虑,因为这是我不止一次遇到的问题。
首先,您可能已经知道这一点,echo 是一个坏主意,如果目标是可移植性,则应该使用 printf echo 的实现将 -n 视为一个特殊选项,而其他实现则将其视为打印的普通参数。所以就变成了这样:
esceval()
{
printf %s "$1" | sed "s/'/'\"'\"'/g"
}
Run Code Online (Sandbox Code Playgroud)
或者,不是通过将嵌入的单引号转义为:
'"'"'
Run Code Online (Sandbox Code Playgroud)
..相反,您可以将它们变成:
'\''
Run Code Online (Sandbox Code Playgroud)
..风格差异我猜(我想性能差异无论哪种方式都可以忽略不计,尽管我从未测试过)。生成的 sed 字符串如下所示:
esceval()
{
printf %s "$1" | sed "s/'/'\\\\''/g"
}
Run Code Online (Sandbox Code Playgroud)
(这是四个反斜杠,因为双引号吞下其中两个,留下两个,然后 sed 吞下一个,只留下一个。就我个人而言,我发现这种方式更具可读性,因此这就是我将在涉及的其余示例中使用的内容它,但两者应该是等效的。)
但是,我们仍然有一个错误:命令替换将从命令输出中删除至少一个(但在许多 shell 中为全部)尾随换行符(并非所有空格,只是专门的换行符)。因此,除非您在参数的最后有换行符,否则上述解决方案有效。然后你会失去那个/那些换行符。修复显然很简单:在从引用/esceval 函数输出之前,在实际命令值之后添加另一个字符。顺便说一句,无论如何我们已经需要这样做,因为我们需要用单引号开始和停止转义参数。老实说,我不明白为什么一开始就没有这样做。您有两种选择:
esceval()
{
printf '%s\n' "$1" | sed "s/'/'\\\\''/g; 1 s/^/'/; $ s/$/'/"
}
Run Code Online (Sandbox Code Playgroud)
这将确保参数已经完全转义,在构建最终字符串时无需添加更多单引号。这可能是您将获得的最接近单个、可内联版本的东西。如果您对 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 \'
}
Run Code Online (Sandbox Code Playgroud)
您可能会注意到这里看似多余的引用:
printf %s "${UNESCAPED%%\'*}""'\''"
Run Code Online (Sandbox Code Playgroud)
..这可以替换为:
printf %s "${UNESCAPED%%\'*}'\''"
Run Code Online (Sandbox Code Playgroud)
我做前者的唯一原因是因为从前有 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 \'
)
}
Run Code Online (Sandbox Code Playgroud)
“但是等等”,你说:“我想在一个命令中对多个参数执行什么操作?并且我希望输出对于我作为用户来说仍然看起来不错且清晰易读,如果我出于任何原因从命令行运行它.”
不要害怕,我已经为您提供了:
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"
}
Run Code Online (Sandbox Code Playgroud)
.. 或同样的事情,但只有 shell 版本:
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"
)
}
Run Code Online (Sandbox Code Playgroud)
在最后四个中,您可以折叠一些外部 printf 语句并将它们的单引号滚动到另一个 printf 中 - 我将它们分开,因为我觉得当您可以看到单独的开始和结束单引号时,逻辑更加清晰打印报表。
PS 还有我制作的这个怪物,它是一个 polyfill,它会在前两个版本之间进行选择,这取决于你的 shell 是否能够支持必要的变量替换语法(虽然它看起来很糟糕,因为只有 shell 版本必须放在一个 eval-ed 字符串中,以防止不兼容的 shell 在看到它时呕吐):https : //github.com/mentalisttraceur/esceval/blob/master/sh/esceval.sh