假设我有一个#!/bin/sh
脚本,它可以采用各种位置参数,其中一些可能包含空格,两种引号/两种引号等。我想迭代"$@"
并且每个参数要么以某种方式立即处理,要么保存以供日后使用。在脚本结束时,我想要启动(也许是exec
)另一个进程,传递一些这些参数,并保留所有特殊字符。
如果我没有对参数进行处理,othercmd "$@"
可以正常工作,但我需要提取一些参数并稍微处理一下。
如果我可以假设Bash,那么我可以使用printf %q
来计算我之后可以eval
引用的args版本,但是这不会对例如Ubuntu的短跑(/bin/sh
)。
是否可以使用内置函数和POSIX定义的实用程序在简单的Bourne shell脚本中编写printf %q
的等价物,比如我可以复制到脚本中的函数吗?
例如,脚本以相反的顺序尝试ls
其参数:
#!/bin/sh
args=
for arg in "$@"
do
args="'$arg' $args"
done
eval "ls $args"
适用于许多情况:
$ ./handle goodbye "cruel world"
ls: cannot access cruel world: No such file or directory
ls: cannot access goodbye: No such file or directory
但不是在使用'
时:
$ ./handle goodbye "cruel'st world"
./handle: 1: eval: Syntax error: Unterminated quoted string
以下工作正常,但依赖于Bash:
#!/bin/bash
args=
for arg in "$@"
do
printf -v argq '%q' "$arg"
args="$argq $args"
done
eval "ls $args"
答案 0 :(得分:3)
这绝对可行。
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中)尾随新行(不是所有空格,只是具体换行符)。所以上述解决方案是有效的,除非你在参数的最后有新行。然后你会丢失/那些换行符。修复显然很简单:在从quote / 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"
}
..或同样的事情,但只有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"
)
}
在最后四个中,你可以折叠一些外部printf语句并将它们的单引号转换为另一个printf - 我将它们分开,因为我觉得当你能看到起始和结束单个时它使逻辑更清晰 - 在单独的印刷声明中引用。
P.S。我也做了这个怪物,这是一个polyfill,它将在前两个版本之间进行选择,具体取决于你的shell是否能够支持必要的变量替换语法(虽然它看起来很糟糕,因为shell只有版本必须是在eval-ed字符串中,以防止不兼容的shell在看到它时发生barfing):https://github.com/mentalisttraceur/esceval/blob/master/sh/esceval.sh
答案 1 :(得分:2)
我认为这是POSIX。它通过在为for循环扩展它之后清除$@
,但只有一次,以便我们可以使用set
迭代地(反向)重新构建它。
flag=0
for i in "$@"; do
[ "$flag" -eq 0 ] && shift $#
set -- "$i" "$@"
flag=1
done
echo "$@" # To see that "$@" has indeed been reversed
ls "$@"
我意识到扭转这些论点只是一个例子,但在其他情况下你可以使用set -- "$arg" "$@"
或set -- "$@" "$arg"
这个技巧。
是的,我知道我可能刚刚重新实现(很差)ormaaj的推。
答案 2 :(得分:1)
Push。有关示例,请参阅自述文件。
答案 3 :(得分:1)
以下似乎适用于我迄今为止所做的一切,包括空格,两种引号和各种其他元字符以及嵌入的换行符:
#!/bin/sh
quote() {
echo "$1" | sed "s/'/'\"'\"'/g"
}
args=
for arg in "$@"
do
argq="'"`quote "$arg"`"'"
args="$argq $args"
done
eval "ls $args"
答案 4 :(得分:0)
如果您可以调用外部可执行文件(如其他答案中给出的 sed
解决方案),那么您也可以调用 /usr/bin/printf
。虽然 POSIX shell 内置 printf
确实不支持 %q
,但 Coreutils 的 printf
二进制文件确实支持 (since release 8.25)。
esceval() {
/usr/bin/printf '%q ' "$@"
}