继自: shell 命令替换中的意外行为
我有一个命令可以接受大量参数,其中一些可以合法地包含空格(可能还有其他东西)
我编写了一个脚本,可以为我生成这些参数,并带有引号,但我必须复制并粘贴输出,例如
./somecommand
<output on stdout with quoting>
./othercommand some_args <output from above>
我试图通过简单地简化这个
./othercommand $(./somecommand)
并遇到了上面提到的意外行为。问题是——othercommand
鉴于某些参数需要引用并且无法更改,命令替换是否可以可靠地用于生成参数?
答案1
我编写了一个脚本,可以为我生成这些参数,并带有引号
如果 shell 的输出被正确引用,并且您信任输出,然后你就可以运行eval
它了。
假设您有一个支持数组的 shell,那么最好使用一个 shell 来存储您获得的参数。
如果./gen_args.sh
产生类似的输出'foo bar' '*' asdf
,那么我们可以运行来填充一个用结果调用eval "args=( $(./gen_args.sh) )"
的数组。这args
就是三个元素foo bar
,,,*
。asdf
我们可以"${args[@]}"
像平常一样单独扩展数组元素:
$ eval "args=( $(./gen_args.sh) )"
$ for var in "${args[@]}"; do printf ":%s:\n" "$var"; done
:foo bar:
:*:
:asdf:
(注意引号。"${array[@]}"
扩展为所有元素作为未修改的不同参数。没有引号,数组元素将受到分词的影响。请参见例如BashGuide 上的数组页面.)
然而,eval
将愉快地运行任何 shell 替换,因此$HOME
输出将扩展到您的主目录,并且命令替换实际上会在运行的 shell 中运行命令eval
。的输出"$(date >&2)"
将创建一个空数组元素并在标准输出上打印当前日期。如果gen_args.sh
从某些不受信任的来源(例如网络上的另一台主机、其他用户创建的文件名)获取数据,则这是一个问题。输出可以包含任意命令。(如果get_args.sh
本身是恶意的,则不需要输出任何内容,直接运行恶意命令即可。)
shell 引用的替代方法是在脚本的输出中使用其他字符作为分隔符,如果没有 eval 就很难解析 shell 引用。您需要选择实际参数中不需要的一个。
让我们选择#
,并让脚本输出foo bar#*#asdf
。现在我们可以使用未引用的命令扩展将命令的输出拆分为参数。
$ IFS='#' # split on '#' signs
$ set -f # disable globbing
$ args=( $( ./gen_args3.sh ) ) # assign the values to the array
$ for var in "${args[@]}"; do printf ":%s:\n" "$var"; done
:foo bar:
:*:
:asdf:
IFS
如果您在脚本中的其他地方依赖于分词(unset IFS
应该可以使其成为默认值),则需要稍后进行设置,并且set +f
如果您想稍后使用通配符,也可以使用。
如果您不使用 Bash 或其他具有数组的 shell,则可以使用位置参数。替换args=( $(...) )
为set -- $(./gen_args.sh)
并使用then"$@"
代替"${args[@]}"
。 (在这里,您也需要在 周围加上引号"$@"
,否则位置参数会受到分词的影响。)
答案2
问题是,一旦您的somecommand
脚本输出 的选项othercommand
,这些选项实际上只是文本,并且受 shell 标准解析的支配(受到$IFS
发生的情况以及有效的 shell 选项的影响,在一般情况下您会不是能够控制)。
而不是somecommand
使用输出选项,使用它会更容易、更安全、更强大称呼 othercommand
。该somecommand
脚本将是包装脚本aroundothercommand
而不是某种帮助程序脚本,您必须记住以某种特殊方式作为otherscript
.包装器脚本是一种非常常见的提供工具的方法,该工具仅使用另一组选项调用其他一些类似的工具(只需检查file
其中的命令/usr/bin
实际上是 shell 脚本包装器)。
在bash
,ksh
或中zsh
,您可以轻松地创建一个包装器脚本,该脚本使用数组来保存各个选项,如下othercommand
所示:
options=( "hi there" "nice weather" "here's a star" "*" )
options+=( "bonus bumblebee!" ) # add additional option
然后调用othercommand
(仍在包装器脚本中):
othercommand "${options[@]}"
的扩展"${options[@]}"
将确保数组的每个元素都被单独引用并作为单独的参数options
呈现。othercommand
包装器的用户会忘记它实际上正在调用的事实othercommand
,这会不是如果脚本只是生成命令行选项作为othercommand
输出,则为 true。
在 中/bin/sh
,用于$@
保存选项:
set -- "hi there" "nice weather" "here's a star" "*"
set -- "$@" "bonus bumblebee!" # add additional option
othercommand "$@"
(set
是用于设置位置参数的命令$1
,等$2
。这些是在标准 POSIX shell 中$3
组成数组的内容。首字母表示没有给出选项,只有参数。是$@
--
set
--
真的仅当第一个值恰好是以-
) 开头时才需要。
请注意,它是双引号$@
,${options[@]}
确保元素不会单独进行单词分割(以及文件名通配)。
答案3
如果somecommand
输出采用可靠且良好的 shell 语法,您可以使用eval
:
$ eval sh test.sh $(echo '"hello " "hi and bye"')
hello
hi and bye
但您必须确保输出具有有效的引用等,否则您最终可能还会在脚本外部运行命令:
$ cat test.sh
for var in "$@"
do
echo "|$var|"
done
$ ls
bar baz test.sh
$ eval sh test.sh $(echo '"hello " "hi and bye"; echo rm *')
|hello |
|hi and bye|
rm bar baz test.sh
请注意,它echo rm bar baz test.sh
没有传递给脚本(因为;
)并且作为单独的命令运行。我添加了|
周围$var
来突出这一点。
通常,除非您可以完全信任 的输出somecommand
,否则不可能可靠地使用其输出来构建命令字符串。