如何在shell脚本中将命令存储在变量中?

194

我想把一个命令存储在一个变量中以便稍后使用(不是命令的输出,而是命令本身)。

我有一个简单的脚本如下:

command="ls";
echo "Command: $command"; #Output is: Command: ls

b=`$command`;
echo $b; #Output is: public_html REV test... (command worked successfully)

然而,当我尝试更复杂的东西时,它失败了。例如,如果我进行以下操作:

command="ls | grep -c '^'";
输出结果为:
Command: ls | grep -c '^'
ls: cannot access |: No such file or directory
ls: cannot access grep: No such file or directory
ls: cannot access '^': No such file or directory

如何将带有管道/多个命令的此类命令存储在变量中以供以后使用?


14
使用一个函数! - gniourf_gniourf
请参阅为什么shell忽略通过变量传递给它的参数中的引号字符?和类似的https://mywiki.wooledge.org/BashFAQ/050。 - tripleee
12个回答

225

使用eval函数:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"

37
现在建议使用$(...)而不是反引号。 y=$(eval $x) http://mywiki.wooledge.org/BashFAQ/082 - James Broadhead
25
只有在您信任变量内容时,eval 才是可接受的实践。如果您运行例如 x="ls $name | wc"(甚至是 x="ls '$name' | wc"),那么如果该变量可以由权限较低的用户设置,则此代码是注入或特权升级漏洞的快速通道。(例如迭代 /tmp 中的所有子目录?您最好相信系统上的每个用户都不会创建 $'/tmp/evil-$(rm -rf $HOME)\'$(rm -rf $HOME)\'/' 这样的用户)。 - Charles Duffy
21
eval 是一个巨大的漏洞磁铁,不应该在没有警告意外解析行为风险的情况下推荐使用(即使没有恶意字符串,如 @CharlesDuffy 的示例)。例如,尝试 x='echo $(( 6 * 7 ))' 然后 eval $x。你可能期望它会打印出 "42",但实际上可能不会。你能解释为什么它不起作用吗?你能解释我为什么说 "probably" 吗?如果这些问题的答案对你来说不明显,那么你就不应该使用 eval - Gordon Davisson
1
@同学,尝试在执行命令之前运行set -x以记录运行的命令,这将使查看发生的情况更容易。 - Charles Duffy
3
@学生 我还建议使用shellcheck.net来指出常见错误(以及你不应该养成的坏习惯)。 - Gordon Davisson
显示剩余4条评论

114

不要使用eval!它有一个很大的风险,会引入任意代码执行。

BashFAQ-50 - 我试图将一个命令放在变量中,但复杂情况经常失败。

将其放入数组中,并使用双引号扩展所有单词 "${arr[@]}",以免让IFS分词而拆分单词。

cmdArgs=()
cmdArgs=('date' '+%H:%M:%S')

并查看数组内的内容。使用 declare -p 可以让你查看数组内的内容,其中每个命令参数都会被单独放在索引中。如果其中一个参数包含空格,则在添加到数组时内部加上引号将防止它因单词拆分而被分割。

declare -p cmdArgs
declare -a cmdArgs='([0]="date" [1]="+%H:%M:%S")'

执行命令

"${cmdArgs[@]}"
23:15:18

或者完全使用一个bash函数来运行该命令,

cmd() {
   date '+%H:%M:%S'
}

并只需调用函数

cmd

POSIX sh没有数组,所以你能接近的是在位置参数中构建元素列表。这是一个 POSIX sh 执行邮件程序的方法。

# POSIX sh
# Usage: sendto subject address [address ...]
sendto() {
    subject=$1
    shift
    first=1
    for addr; do
        if [ "$first" = 1 ]; then set --; first=0; fi
        set -- "$@" --recipient="$addr"
    done
    if [ "$first" = 1 ]; then
        echo "usage: sendto subject address [address ...]"
        return 1
    fi
    MailTool --subject="$subject" "$@"
}

请注意,此方法只能处理没有重定向、管道、for/while循环、if语句等的简单命令。

另一个常见用例是在运行带有多个标头字段和有效负载的curl时。您可以像下面这样定义args,并在扩展的数组内容上调用curl

curlArgs=('-H' "keyheader: value" '-H' "2ndkeyheader: 2ndvalue")
curl "${curlArgs[@]}"

另一个例子,

payload='{}'
hostURL='http://google.com'
authToken='someToken'
authHeader='Authorization:Bearer "'"$authToken"'"'

现在变量已经定义好了,使用数组来存储你的命令参数。

curlCMD=(-X POST "$hostURL" --data "$payload" -H "Content-Type:application/json" -H "$authHeader")

现在进行正确的引用扩展

curl "${curlCMD[@]}"

2
@同学,如果你的原始字符串包含一个管道符号“|”,那么该字符串需要通过bash解析器的不安全部分作为代码执行。在这种情况下,请不要使用字符串;而是使用函数:Command() { echo aaa | grep a; } -- 然后你可以直接运行Command或者result=$(Command)等等。 - Charles Duffy
2
@学生:我已经在最后添加了一条注释,说明它在某些条件下不起作用。 - Inian
1
@Timo,意思是你不能将if语句传递给sendto函数;实现代码中包含if语句和其他逻辑与此限制无关。 - tripleee
2
是的,确切地说。例如,您不能 sendto if true; then echo poo; fi,因为它看起来像您正在发送 if true,这显然是语法错误,并且后续语句与 sendto 调用无关。 - tripleee
2
@tripleee: 对于提供悬赏金,我要表达我的敬意,真希望能与您和其他有用的贡献者分享它;) - Inian
显示剩余6条评论

44
var=$(echo "asdf")
echo $var
# => asdf

使用这种方法,命令会立即进行计算,并且其返回值将被存储。

stored_date=$(date)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015

与反引号相同

stored_date=`date`
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015

$(...) 中使用 eval 不会使其在以后被评估:

stored_date=$(eval "date")
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015

使用 eval,它在使用 eval 时被评估:

stored_date="date" # < storing the command itself
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:05 EST 2015
# (wait a few seconds)
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:16 EST 2015
#                     ^^ Time changed
在上面的例子中,如果你需要运行一个带参数的命令,请将它们放在你正在存储的字符串中:
stored_date="date -u"
# ...

对于Bash脚本来说,这很少相关,但是最后注意一点。小心使用eval。仅评估您控制的字符串,永远不要评估来自不受信任的用户或基于不受信任的用户输入构建的字符串。


这并没有解决原始问题,即命令包含管道“|”。 - Student
@Nate,注意当stored_date只包含date时,eval $stored_date可能足够好,但是eval "$stored_date"更加可靠。运行str=$'printf \' * %s\\n\' *'; eval "$str"并尝试在最后的"$str"周围添加或删除引号以获得示例。 :) - Charles Duffy
@CharlesDuffy 谢谢,我忘记了引用。如果我运行它,我敢打赌我的代码检查器会抱怨的。 - Nate
顺便提一下,那是一个无用的echo - tripleee

6

对于Bash,将您的命令存储如下:

command="ls | grep -c '^'"

以以下方式运行您的命令:

echo $command | bash

3
可能这种运行命令的方式与使用“eval”的风险相同。 - Derek Hazell
2
此外,如果您在 echo 变量时没有将其引用起来,那么您将破坏变量的内容。如果 command 是字符串 cd /tmp && echo *,它将会回显当前目录中的文件,而不是 /tmp 中的文件。请参见 何时在 shell 变量周围添加引号 - tripleee
要清楚,echo "$command" | bash - Julien

1

不确定为什么有这么多复杂的答案!使用 alias [command] '要执行的字符串'。例如:

alias dir='ls -l'

./dir
[pretty list of files]

别名存在很多问题。首先,它们在脚本中无法扩展,如果你正在编写脚本(因此你的问题在 Stack Overflow 上是相关的),这显然会让人失望。另外,你不能传递位置参数给别名,只能让它们使用其余的命令行而无法访问它。此外,解析规则也很棘手,所以你不能同时启用别名扩展和在同一个命令行中使用别名。底线是像所有人一样使用函数。 - tripleee

0

我遇到了以下命令的问题:

awk '{printf "%s[%s]\n", $1, $3}' "input.txt"

我需要动态构建此命令:

目标文件名input.txt是动态的,可能包含空格。

{}括号内的awk脚本printf "%s[%s]\n", $1, $3是动态的。

挑战:

  1. 如果awk脚本中有许多",请避免大量引号转义逻辑。
  2. 避免每个$字段变量的参数扩展。

下面的解决方案使用eval命令和关联数组,由于bash变量扩展和引用,无法正常工作。

解决方案:

动态构建bash变量,避免bash扩展,使用printf模板。

 # dynamic variables, values change at runtime.
 input="input file 1.txt"
 awk_script='printf "%s[%s]\n" ,$1 ,$3'

 # static command template, preventing double-quote escapes and avoid variable  expansions.
 awk_command=$(printf "awk '{%s}' \"%s\"\n" "$awk_script" "$input")
 echo "awk_command=$awk_command"

 awk_command=awk '{printf "%s[%s]\n" ,$1 ,$3}' "input file 1.txt"

执行变量命令:

bash -c "$awk_command"

另一种同样有效的选择

bash << $awk_command

-1
#!/bin/bash
#Note: this script works only when u use Bash. So, don't remove the first line.

TUNECOUNT=$(ifconfig |grep -c -o tune0) #Some command with "Grep".
echo $TUNECOUNT                         #This will return 0 
                                    #if you don't have tune0 interface.
                                    #Or count of installed tune0 interfaces.

这将命令的静态字符串输出存储在变量中,而不是存储命令本身。 - tripleee
grep -c -o 并不是完全可移植的;你可能期望它返回搜索表达式实际出现的次数,但至少 GNU grep 不会这样做(它基本上等同于没有 -ogrep -c)。 - tripleee
这个仅限于Bash的注释很奇怪;在这个简单的脚本中,没有任何不兼容任何Bourne家族的shell的内容。 - tripleee

-1

在注册订单时要小心使用:X=$(Command)

这个命令会在被调用之前就被执行。为了检查和确认这一点,您可以执行以下操作:

echo test;
X=$(for ((c=0; c<=5; c++)); do
sleep 2;
done);
echo note the 5 seconds elapsed

1
这似乎不是实际问题的答案,最好改成评论(如果适用)。 - tripleee
“注册订单”是什么意思?你能详细说明一下吗? - Peter Mortensen

-1

我尝试了各种不同的方法:

printexec() {
  printf -- "\033[1;37m$\033[0m"
  printf -- " %q" "$@"
  printf -- "\n"
  eval -- "$@"
  eval -- "$*"
  "$@"
  "$*"
}

输出:

$ printexec echo  -e "foo\n" bar
$ echo -e foo\\n bar
foon bar
foon bar
foo
 bar
bash: echo -e foo\n bar: command not found

正如您所看到的,只有第三个,"$@" 给出了正确的结果。


这是什么解释?为什么只有第三个?请通过编辑(更改)您的答案来回复,而不是在评论中回复(不要包含“编辑:”,“更新:”或类似内容 - 答案应该看起来像是今天写的)。 - Peter Mortensen
我不确定我是否足够在“eval”的复杂性上进行调查。在我看来,其中一个应该可以工作。 - mpen

-2

首先,有相应的函数可以实现这个任务。但是如果您更喜欢使用变量,那么可以像这样完成:

$ cmd=ls

$ $cmd # works
file  file2  test

$ cmd='ls | grep file'

$ $cmd # not works
ls: cannot access '|': No such file or directory
ls: cannot access 'grep': No such file or directory
 file

$ bash -c $cmd # works
file  file2  test

$ bash -c "$cmd" # also works
file
file2

$ bash <<< $cmd
file
file2

$ bash <<< "$cmd"
file
file2

或者通过一个临时文件

$ tmp=$(mktemp)
$ echo "$cmd" > "$tmp"
$ chmod +x "$tmp"
$ "$tmp"
file
file2

$ rm "$tmp"

1
我想很多人没有注意到你提到的“首先有函数”,这对于正确的方向来说是一个正确的指针,在我看来:“变量保存数据。函数保存代码”。 - Pedro
你的 bash -c $cmd 和 bash -c "$cmd" 两者有非常不同的作用!尝试使用 cmd='echo "$(pwd)"; cd test2; echo "$(pwd)"' 的每种方式来查看原因,请执行 'bash -x -c $cmd'。 - Bob Kerns
另外:函数并不是解决方案,它们可能在某些情况下是一种替代方案,但你必须构建函数!如果你将$cd的片段作为$1、$2等系列获取,那么将其转换为函数会很有趣,然后更有趣的是不使用eval在另一个shell中使用它。 - Bob Kerns

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接