如何从字符串变量运行包含字符串变量的命令?

如何从字符串变量运行包含字符串变量的命令?

我正在尝试从字符串变量运行包含字符串变量的命令:

$ X="bash -c 'echo OK'" ; $X
OK': -c: line 1: unexpected EOF while looking for matching `''

为什么不起作用?如何修复/正确执行?

请注意,我需要以 身份运行命令$X

请注意,我需要echo OK通过运行bash -c

答案1

因为它尝试运行名为bash -c 'echo OK'

eval $X会工作

答案2

帖子 我们如何运行存储在变量中的命令? 包含这个很长 ilkkachu 的回答 对这个问题进行了非常透彻的讨论。

我将在这里引用这个优秀的答案。

unix.SE 上的许多问题已经讨论了这个问题,我会尝试在这里收集我能想到的所有问题。下面是

  • 描述各种尝试失败的原因和方式,
  • 使用函数正确执行此操作的方法(对于固定命令)或
  • 使用 shell 数组 (Bash/ksh/zsh) 或$@伪数组 (POSIX sh),它们都允许构建命令行片段,例如,如果你只需要改变一些选项
  • 以及有关如何使用它来执行此操作的说明eval

最后有一些参考资料。

就此处的目的而言,是否只将命令参数或命令名称存储在变量中并不重要。在启动命令之前,它们的处理方式类似,此时 shell 只需将第一个单词作为要运行的命令的名称。


失败原因

你面临这些问题的原因是单词拆分非常简单,不适合复杂的情况,并且从变量扩展的引号不起引号的作用,而只是普通字符。

(请注意,关于引号的部分与其他每种编程语言类似:例如,char *s = "foo()"; printf("%s\n", s)不调用foo()C 中的函数,而只是打印字符串 foo()。这在宏处理器中有所不同,例如 m4、C 预处理器或 Make(在某种程度上)。shell 是一种编程语言,而不是宏处理器。)

在类 Unix 系统中,shell 负责处理命令行上的引号和变量扩展,将其从单个字符串转换为底层系统调用传递给启动命令的字符串列表。程序本身看不到 shell 处理的引号。例如,如果给定命令ls -l "foo bar",shell 会将其转换为三个字符串ls-lfoo bar (删除引号),并将它们传递给ls(即使命令名称也会被传递,尽管并非所有程序都使用它。)

问题中提出的案例:

此处的赋值将单个字符串分配ls -l "/tmp/test/my dir"abc

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

下面,$abc按空格拆分,并ls获取三个参数-l"/tmp/test/mydir"。这里的引号只是数据,因此第二个参数前面有一个引号,第三个参数后面有一个引号。该选项有效,但路径被错误处理,因为将ls引号视为文件名的一部分:

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

这里,扩展名被引号括起来,因此它被保留为一个单词。shell 尝试查找一个字面上称为 的程序ls -l "/tmp/test/my dir",包括空格和引号。

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

这里,$abc被拆分,并且只有第一个结果单词被用作 的参数-c,因此 Bash 只ls在当前目录中运行。其他单词是 bash 的参数,并且用于填充 $0$1等。

$ bash -c $abc
'my dir'

对于bash -c "$abc"、 和eval "$abc",有一个额外的 shell 处理步骤,这确实使引号起作用,但是还会导致所有 shell 扩展被重新处理因此存在意外运行(例如,根据用户提供的数据)命令替换的风险,除非您非常小心地引用。


更好的方法

存储命令的两种更好的方法是 a) 使用函数,b) 使用数组变量(或位置参数)。

使用函数:

只需声明一个包含命令的函数,然后像运行命令一样运行该函数即可。函数中命令的扩展仅在命令运行时处理,而不是在命令定义时处理,并且您无需引用单个命令。不过,这实际上只在您需要存储固定命令(或多个固定命令)时才有用。

# define it
myls() {
    ls -l "/tmp/test/my dir"
}

# run it
myls

也可以定义多个函数并使用变量来存储最终要运行的函数的名称。

使用数组:

数组允许创建多字变量,其中各个字包含空格。在这里,各个字存储为不同的数组元素,扩展"${array[@]}"将每个元素扩展为单独的 shell 字:

# define the array
mycmd=(ls -l "/tmp/test/my dir")

# expand the array, run the command
"${mycmd[@]}"

命令写在括号内,与运行命令时写的方式完全相同。在这两种情况下,shell 的处理方式相同,只是在一种情况下,它只保存结果字符串列表,而不是用它来运行程序。

不过,稍后扩展数组的语法有点糟糕,并且它周围的引号很重要。

数组还允许您逐段构建命令行。例如:

mycmd=(ls)               # initial command
if [ "$want_detail" = 1 ]; then
    mycmd+=(-l)          # optional flag, append to array
fi
mycmd+=("$targetdir")    # the filename

"${mycmd[@]}"

或者保持命令行的各部分不变并使用数组仅填充其中的一部分,例如选项或文件名:

options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir

somecommand "${options[@]}" "${files[@]}" "$target"

somecommand这里是通用的占位符名称,而不是任何真正的命令。)

数组的缺点是它们不是标准功能,因此普通的 POSIX shell(例如dash,Debian/Ubuntu 中的默认 shell /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

somecommand "$@"

当然,"$@"通常会填充脚本本身的参数,因此您必须在重新利用之前将它们保存在某处 "$@"

要有条件地传递单个参数,您还可以使用替代值扩展${var:+word}并进行一些仔细的引用。在这里,我们-f 仅当文件名非空时才包含文件名:

file="foo bar"
somecommand ${file:+-f "$file"}

使用eval(此处要小心!)

eval接受一个字符串并将其作为命令运行,就像在 shell 命令行中输入一样。这包括所有引用和扩展处理,这既有用又危险。

在简单的情况下,它允许做我们想要做的事情:

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

使用eval,引号会被处理,因此ls最终只会看到两个参数-l/tmp/test/my dir,就像我们想要的那样。eval也足够智能,可以连接它获得的任何参数,因此eval $cmd 在某些情况下也可以工作,但例如,所有空格都会更改为单个空格。 最好在那里引用变量,因为这样可以确保它不会被修改为eval

然而,在命令字符串中包含用户输入是危险的eval。例如,这似乎有效:

read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";

但是如果用户输入包含单引号,他们就可以摆脱引号并运行任意命令!例如,输入'$(whatever)'.txt,你的脚本会愉快地运行命令替换。但结果可能并非如此rm -rf(甚至更糟)。

问题在于 的值$filename嵌入在eval运行的命令行中。它在 之前被扩展eval,例如命令ls -l ''$(whatever)'.txt'。您需要预处理输入以确保安全。

如果我们以另一种方式执行,将文件名保存在变量中,并让eval命令扩展它,那么它会更安全:

read -r filename
cmd='ls -ld "$filename"'
eval "$cmd";

注意,外引号现在是单引号,因此不会发生内部扩展。因此,eval看到命令ls -l "$filename"并自行安全地扩展文件名。

但这和将命令存储在函数或数组中没有太大区别。使用函数或数组,不存在这样的问题,因为单词始终保持分开,并且没有对 的内容进行引用或其他处理filename

read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"

使用它的唯一原因几乎eval就是变化部分涉及无法通过变量引入的 shell 语法元素(管道、重定向等)。但是,你需要引用/转义所有内容别的在命令行上需要保护以避免额外的解析步骤(请参阅下面的链接)。无论如何,最好避免在命令中嵌入来自用户的输入 eval


参考

相关内容