$ ls -l /tmp/test/my\ dir/
total 0
我想知道为什么以下运行上述命令的方法失败或成功?
$ abc='ls -l "/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
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
$ bash -c $abc
'my dir'
$ bash -c "$abc"
total 0
$ eval $abc
total 0
$ eval "$abc"
total 0
这已在 unix.SE 上的许多问题中讨论过,我将尝试收集我能在这里提出的所有问题。下面是对各种尝试失败的原因和方式的描述,以及使用函数(对于固定命令)或 shell 数组(Bash/ksh/zsh)或
$@
伪数组(POSIX sh)正确执行此操作的方法,以及关于使用eval
来执行此操作的说明。一些参考在最后。出于此处的目的,它是否只是命令参数或要存储在变量中的命令名称并不重要。在启动命令之前,它们的处理方式类似,此时 shell 仅将第一个单词作为要运行的命令的名称。
为什么会失败
您面临这些问题的原因是分词非常简单并且不适合复杂的情况,并且从变量扩展的引号不充当引号,而只是普通字符。
(请注意,关于引号的部分与其他所有编程语言相似:例如
char *s = "foo()"; printf("%s\n", s)
,不调用foo()
C 中的函数,而只是打印字符串foo()
。shell 是一种编程语言,而不是宏处理器。)请记住,在命令行上处理引号和变量扩展的是 shell,将其从单个字符串转换为最终传递给启动命令的字符串列表。程序本身看不到任何引号。例如,如果给定 command
ls -l "foo bar"
,shell 会将其转换为三个字符串ls
,-l
andfoo bar
(删除引号),并将它们传递给ls
. (即使是命令名也被传递了,虽然不是所有的程序都使用它。)问题中提出的案例:
这里的赋值将单个字符串分配
ls -l "/tmp/test/my dir"
给abc
:下面,
$abc
在空格处拆分,并ls
获取三个参数-l
,"/tmp/test/my
anddir"
(在第二个前面有一个引号,在第三个后面有另一个引号)。该选项有效,但路径处理不正确:在这里,扩展被引用,所以它被保留为一个单词。shell 试图找到一个字面上称为 的程序
ls -l "/tmp/test/my dir"
,包括空格和引号。在这里,
$abc
被分割了,只有第一个结果词被作为参数-c
,所以 Bash 只是ls
在当前目录中运行。其他词是 bash 的参数,用于填充$0
,$1
等。使用
bash -c "$abc"
, 和eval "$abc"
,还有一个额外的 shell 处理步骤,它确实使引号起作用,但也会导致所有 shell 扩展被再次处理,因此存在意外运行的风险,例如从用户提供的数据进行命令替换,除非你是引用非常小心。更好的方法来做到这一点
存储命令的两种更好的方法是 a) 使用函数,b) 使用数组变量(或位置参数)。
使用函数:
只需在内部声明一个带有命令的函数,然后像运行命令一样运行该函数。函数内命令的扩展仅在命令运行时处理,而不是在定义时处理,您不需要引用各个命令。
使用数组:
数组允许创建多个单词变量,其中单个单词包含空格。在这里,各个单词存储为不同的数组元素,
"${array[@]}"
扩展将每个元素扩展为单独的 shell 单词:语法有点可怕,但数组还允许您逐段构建命令行。例如:
或保持部分命令行不变并使用数组填充其中的一部分,如选项或文件名:
数组的缺点是它们不是标准功能,因此普通的 POSIX shell(如Debian/Ubuntu
dash
中的默认值/bin/sh
)不支持它们(但见下文)。但是,Bash、ksh 和 zsh 可以,因此您的系统很可能有一些支持数组的 shell。使用
"$@"
在不支持命名数组的 shell 中,仍然可以使用位置参数(伪数组
"$@"
)来保存命令的参数。以下应该是与上一节中的代码位等效的可移植脚本位。该数组被替换
"$@"
为位置参数列表。设置"$@"
是用 完成的set
,并且双引号"$@"
很重要(这些导致列表的元素被单独引用)。首先,简单地存储一个带有参数的命令
"$@"
并运行它:有条件地为命令设置部分命令行选项:
仅
"$@"
用于选项和操作数:(当然,
"$@"
通常会填充脚本本身的参数,因此您必须在重新调整用途之前将它们保存在某个地方"$@"
。)使用
eval
(这里要小心!)eval
接受一个字符串并将其作为命令运行,就像在 shell 命令行中输入它一样。这包括所有的报价和扩展处理,这既有用又危险。在简单的情况下,它允许做我们想做的事:
随着
eval
,引号被处理,所以ls
最终只看到两个参数-l
和/tmp/test/my dir
,就像我们想要的那样。eval
也很聪明,可以连接它得到的任何参数,所以eval $cmd
在某些情况下也可以工作,但是例如所有的空白运行将被更改为单个空格。最好在此处引用变量,因为这将确保它未被修改为eval
.但是,在命令字符串中包含用户输入是很危险的
eval
。例如,这似乎有效:但是如果用户给出的输入包含单引号,他们可以跳出引号并运行任意命令!例如,使用 input
'$(whatever)'.txt
,您的脚本会愉快地运行命令替换。相反,它可能是rm -rf
(或更糟)。问题在于 的值
$filename
嵌入在eval
运行的命令行中。它之前被扩展过eval
,它看到了例如命令ls -l ''$(whatever)'.txt'
。您需要对输入进行预处理以确保安全。如果我们以另一种方式来做,将文件名保留在变量中,并让
eval
命令扩展它,它又会更安全:请注意,外部引号现在是单引号,因此不会发生内部扩展。因此,
eval
看到命令ls -l "$filename"
并安全地扩展文件名本身。但这与仅将命令存储在函数或数组中没有太大区别。对于函数或数组,就没有这样的问题,因为单词一直是分开的,并且没有对
filename
.几乎唯一使用的原因
eval
是可变部分涉及无法通过变量(管道、重定向等)引入的 shell 语法元素。但是,您随后需要在命令行上引用/转义需要保护免受额外解析步骤影响的所有其他内容(请参见下面的链接)。无论如何,最好避免在命令中嵌入来自用户的输入eval
!参考
运行(重要)命令的最安全方法是
eval
. 然后,您可以像在命令行上一样编写命令,它的执行就像您刚刚输入它一样。但是你必须引用所有内容。简单案例:
不是那么简单的情况:
如果它需要一个数组来执行,那就把它变成一个数组!
第一行将字符串转换为数组。第二行执行命令。
这似乎不适用于链式命令,例如使用
&&
or;
。第二个引号中断命令。
当我运行时:
它给了我一个错误。
但是当我跑步时
完全没有错误
当时没有办法解决这个问题(对我来说),但您可以通过在目录名称中没有空格来避免错误。
这个答案 说 eval 命令可以用来解决这个问题,但它对我不起作用:(
abc
运行存储在变量中的任何(平凡/非平凡)命令的另一个技巧是:并按
UpArrow
或Ctrl-p
将其带入命令行。与任何其他方法不同,如果需要,您可以在执行之前对其进行编辑。此命令会将变量的内容作为新条目附加到 Bash 历史记录中,您可以通过
UpArrow
.