我们如何运行存储在变量中的命令?


34
$ 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


Answers:


54

在unix.SE上的许多问题中对此进行了讨论,我将尝试在此处收集所有我可以提出的问题。参考文献最后。


为什么失败

遇到这些问题的原因是单词拆分,以及从变量扩展的引号不充当引号,而只是普通字符。

问题中提出的案例:

$ abc='ls -l "/tmp/test/my dir"'

在这里,$abc被分割,并ls得到两个参数"/tmp/test/mydir"((在第一个的前面和第二个的后面加上引号):

$ $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[@]}"

参考文献


2
您可以通过这样做绕过评估报价cmd="ls -l $(printf "%q" "$filename")"。不是很漂亮,但是如果用户对使用eval完全不满意,它将有所帮助。对于通过类似的东西发送命令,例如ssh foohost "ls -l $(printf "%q" "$filename")",或在此问题的精神上,它也非常有用ssh foohost "$cmd"
帕特里克

没有直接关系,但是您对目录进行了硬编码吗?在这种情况下,您可能需要查看别名。像这样的东西: $ alias abc='ls -l "/tmp/test/my dir"'
跳兔

4

运行(非平凡的)命令的最安全方法是eval。然后,您可以像在命令行上一样编写命令,并且该命令的执行方式与刚输入命令时完全相同。但是您必须引用所有内容。

简单的情况:

abc='ls -l "/tmp/test/my dir"'
eval "$abc"

不是那么简单的情况:

# command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
eval "$abc"

3

第二个引号使命令中断。

当我跑步时:

abc="ls -l '/home/wattana/Desktop'"
$abc

它给了我一个错误。

但是当我跑步时

abc="ls -l /home/wattana/Desktop"
$abc

完全没有错误

暂时没有办法解决(对我来说),但是您可以通过在目录名中没有空格来避免该错误。

这个答案 说eval命令可以用来解决这个问题,但是对我不起作用:(


1
是的,只要不需要带有嵌入式空格(或包含glob字符的文件名)的文件名就可以。
ilkkachu

0

如果此方法不起作用'',则应使用``

abc=`ls -l /tmp/test/my\ dir/`

更新更好的方法:

abc=$(ls -l /tmp/test/my\ dir/)

这会将命令的结果存储在变量中。OP(奇怪地)想要将命令本身保存在变量中。哦,您应该真正开始使用$( command... )而不是反引号。
roaima

非常感谢您的澄清和提示!
巴伦

0

python3单行代码怎么样?

bash-4.4# pwd
/home
bash-4.4# CUSTOM="ls -altr"
bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", shell=True)"
total 8
drwxr-xr-x    2 root     root          4096 Mar  4  2019 .
drwxr-xr-x    1 root     root          4096 Nov 27 16:42 ..
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.