在unix.SE上的许多问题中对此进行了讨论,我将尝试在此处收集所有我可以提出的问题。参考文献最后。
为什么失败
遇到这些问题的原因是单词拆分,以及从变量扩展的引号不充当引号,而只是普通字符。
问题中提出的案例:
$ abc='ls -l "/tmp/test/my dir"'
在这里,$abc
被分割,并ls
得到两个参数"/tmp/test/my
和dir"
((在第一个的前面和第二个的后面加上引号):
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
在这里,引号被引用,因此它被保留为一个单词。该外壳程序试图找到一个名为ls -l "/tmp/test/my dir"
,包含空格和引号的程序。
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
在这里,只有第一个单词or $abc
被作为的参数-c
,所以Bash只是ls
在当前目录中运行。其他的话参数庆典,并用于填充$0
,$1
等等。
$ bash -c $abc
'my dir'
使用bash -c "$abc"
和eval "$abc"
,还有一个额外的外壳处理步骤,它确实使引号生效,但也导致所有外壳扩展都被再次处理,因此存在意外地从用户提供的数据运行命令扩展的风险,除非您非常谨慎报价。
更好的方法
存储命令的两种更好的方法是:a)使用函数,b)使用数组变量(或位置参数)。
使用功能:
只需在命令内部声明一个函数,然后像执行命令一样运行该函数。函数内命令的扩展仅在命令运行时处理,而不在定义命令时处理,并且您无需引用单个命令。
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
使用数组:
数组允许在单个单词包含空格的地方创建多单词变量。在这里,各个单词存储为不同的数组元素,并且"${array[@]}"
扩展将每个元素扩展为单独的外壳单词:
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[@]}"
语法有点可怕,但是数组还允许您逐段构建命令行。例如:
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
或保持部分命令行不变,并使用数组填充其中一部分,选项或文件名:
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
数组的缺点是它们不是标准功能,因此普通的POSIX外壳程序(例如Debian / Ubuntu中dash
的默认外壳程序/bin/sh
)不支持它们(但请参见下文)。但是,Bash,ksh和zsh可以,因此您的系统可能具有一些支持数组的shell。
使用 "$@"
在不支持命名数组的shell中,仍然可以使用位置参数(伪数组"$@"
)来保存命令的参数。
以下应该是可移植的脚本位,它们等同于上一节中的代码位。数组替换为"$@"
,位置参数列表。"$@"
可以使用进行设置set
,并且双引号"$@"
很重要(这会导致列表中的元素被单独引用)。
首先,只需存储一个带有参数的命令"$@"
并运行它:
set -- ls -l "/tmp/test/my dir"
"$@"
有条件地为命令设置部分命令行选项:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "$@" -l
fi
set -- "$@" "$targetdir"
"$@"
仅"$@"
用于选项和操作数:
set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir
transmutate "$@"
(当然,"$@"
通常会使用脚本本身的参数来填充它们,因此您必须在重新使用之前将其保存在某个位置"$@"
。)
小心eval
!
由于eval
引入了更高级别的报价和扩展处理,因此您在使用用户输入时需要格外小心。例如,只要用户不输入任何单引号,此方法就起作用:
read -r filename
cmd="ls -l '$filename'"
eval "$cmd";
但是,如果他们提供输入'$(uname)'.txt
,您的脚本会愉快地运行命令替换。
带有数组的版本不受此限制,因为单词在整个时间都保持分开,因此对的内容不加引号或进行其他处理filename
。
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
参考文献