执行时是否应将变量加引号?


18

Shell脚本编写的一般规则是,除非有明显的理由,否则应始终对变量加引号。要获得比您可能想知道的更多的详细信息,请查看以下精彩问答:忘记在bash / POSIX shell中引用变量的安全隐患

但是,请考虑以下功能:

run_this(){
    $@
}

是否应该$@在那儿引用?我玩了一段时间,找不到引号不足引起问题的情况。另一方面,使用引号会使传递包含空格作为引号变量的命令时中断:

#!/usr/bin/sh
set -x
run_this(){
    $@
}
run_that(){
    "$@"
}
comm="ls -l"
run_this "$comm"
run_that "$comm"

运行上面的脚本将返回:

$ a.sh
+ comm='ls -l'
+ run_this 'ls -l'
+ ls -l
total 8
-rw-r--r-- 1 terdon users  0 Dec 22 12:58 da
-rw-r--r-- 1 terdon users 45 Dec 22 13:33 file
-rw-r--r-- 1 terdon users 43 Dec 22 12:38 file~
+ run_that 'ls -l'
+ 'ls -l'
/home/terdon/scripts/a.sh: line 7: ls -l: command not found

如果我使用run_that $comm而不是,我可以解决这个问题run_that "$comm",但是由于run_this(unquoted)函数可以同时使用这两者,因此似乎更安全。

因此,在特定情况下使用$@要执行$@作为命令的功能的函数时,应$@加引号吗?请解释为什么/不应该引用它,并举例说明可以破坏它的数据。


6
run_that的行为肯定是我所期望的(命令路径中是否有空格?)。如果您想要其他行为,请确保在您知道数据是什么的呼叫站点取消报价。我希望将此函数称为run_that ls -l,在任何一个版本中其效果都相同。有没有让您期望不同的情况?
Michael Homer 2015年

@MichaelHomer我猜这里的编辑提示了这一点:unix.stackexchange.com/a/250985/70524
muru

@MichaelHomer由于某种原因(可能是因为我还没有第二杯咖啡),我没有在命令的参数或路径中考虑空格,而只在命令本身(选项)中考虑了空格。回想起来,这种情况很常见。
terdon

Shell仍然支持功能,而不是简单地将命令填充到数组中并使用来执行,这是有原因的${mycmd[@]}
chepner 2015年

Answers:


20

问题在于命令如何传递给函数:

$ run_this ls -l Untitled\ Document.pdf 
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_that ls -l Untitled\ Document.pdf 
-rw------- 1 muru muru 33879 Dec 20 11:09 Untitled Document.pdf

"$@"通常在您的run_this函数以正常编写的命令为前缀的情况下使用。run_this导致引用地狱:

$ run_this 'ls -l Untitled\ Document.pdf'
ls: cannot access Untitled\: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l "Untitled\ Document.pdf"'
ls: cannot access "Untitled\: No such file or directory
ls: cannot access Document.pdf": No such file or directory
$ run_this 'ls -l Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l' 'Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory

我不确定如何将带有空格的文件名传递给run_this


1
确实是您的编辑才提示了这一点。由于某种原因,我没有想到要使用带空格的文件名进行测试。我完全不知道为什么不这样做,但是你去了。您当然是对的,但我都没有一种方法可以正确地做到这一点run_this
terdon

@terdon引用已成为一种习惯,以至于我以为您会$@意外地不加引用。我应该留下一个例子。:D
muru 2015年

2
不,这确实是一个习惯,我(错误地)对其进行了测试并得出结论:“呵呵,也许这个人不需要引号”。一种通常称为“脑袋”的程序。
terdon

1
您不能将带有空格的文件名传递给run_this。这与在Bash FAQ 050中讨论的将复杂命令填充到字符串中时遇到的问题基本相同。
Etan Reisner,2015年

9

可能是:

interpret_this_shell_code() {
  eval "$1"
}

要么:

interpret_the_shell_code_resulting_from_the_concatenation_of_those_strings_with_spaces() {
  eval "$@"
}

要么:

execute_this_simple_command_with_these_arguments() {
  "$@"
}

但:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

没有多大意义。

如果要执行ls -l命令(而不是ls带有ls-l作为参数的命令),则可以执行以下操作:

interpret_this_shell_code '"ls -l"'
execute_this_simple_command_with_these_arguments 'ls -l'

但是,如果(更有可能)是ls带有ls-l作为参数的命令,则可以运行:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

现在,如果您要执行的命令不只是一个简单的命令,那么如果要执行变量分配,重定向,管道...,则只能interpret_this_shell_code执行以下操作:

interpret_this_shell_code 'ls -l 2> /dev/null'

当然,您可以随时执行以下操作:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'

5

看着它从在bash / KSH / zsh的角度来看, $*$@是一般阵列扩展的一种特殊情况。数组扩展不同于普通的变量扩展:

$ a=("a b c" "d e" f)
$ printf ' -> %s\n' "${a[*]}"
 -> a b c d e f
$ printf ' -> %s\n' "${a[@]}"
-> a b c
-> d e
-> f
$ printf ' -> %s\n' ${a[*]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f
$ printf ' -> %s\n' ${a[@]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f

使用$*/ ${a[*]}扩展,您可以将数组的第一个值IFS(默认为空格)连接到一个巨型字符串中。如果不引用它,它将像普通字符串一样被拆分。

使用$@/ ${a[@]}扩展名,其行为取决于是否使用$@/ ${a[@]}扩展名:

  1. 如果引用("$@""${a[@]}"),你会得到相当于 "$1" "$2" "$3" #..."${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. 如果没有加引号($@${a[@]})你得到相当于 $1 $2 $3 #...${a[1]} ${a[2]} ${a[3]} # ...

对于包装命令,您绝对希望使用引号@扩展(1.)。


有关bash(和类似bash)数组的更多信息:https : //lukeshu.com/blog/bash-arrays.html


1
刚刚意识到,我指的是戴维达面具时以卢克开头的链接。这个职位的力量很强。
PSkocik 2015年

4

因为当您不使用双引号时$@,您将所有的问题都留在给函数的链接中

您如何运行名​​为的命令*?您不能使用run_this

$ ls
1 2
$ run_this '*'
dash: 2: 1: not found
$ run_that '*'
dash: 3: *: not found

您会看到,即使发生错误,也会run_that给您带来更有意义的信息。

扩展$@到单个单词的唯一方法是用双引号引起来。如果要作为命令运行它,则应将命令及其参数作为单独的单词传递。那是您在调用方执行的操作,而不是在函数内部执行的操作。

$ cmd=ls
$ param1=-l
$ run_that "$cmd" "$param1"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

是更好的选择。或者,如果您的外壳支持数组:

$ cmd=(ls -l)
$ run_that "${cmd[@]}"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

即使shell根本不支持数组,您仍然可以使用来使用它"$@"


3

在in中执行变量bash是一种容易失败的技术。编写一个run_this能够正确处理所有边缘情况的函数是根本不可能的,例如:

  • 管道(例如ls | grep filename
  • 输入/输出重定向(例如ls > /dev/null
  • 贝壳之类的语句if while等。

如果您要做的就是避免代码重复,那么最好使用函数。例如,代替:

run_this(){
    "$@"
}
command="ls -l"
...
run_this "$command"

你应该写

command() {
    ls -l
}
...
command

如果命令仅在运行时可用,则应使用eval,该命令专门用于处理所有会使run_this失败的怪癖:

command="ls -l | grep filename > /dev/null"
...
eval "$command"

请注意,这eval众所周知的安全问题,但是如果您将变量从不受信任的来源传递到run_this,您也会面临任意代码执行的麻烦。


1

这是你的选择。如果您不引用$@任何值,则将对其进行进一步的扩展和解释。如果确实引用了它,则传递给该函数的所有参数都将按其扩展原样复制。如果&>|不自己解析参数,您将永远无法可靠地处理诸如此类之类的shell语法标记-因此,您将只有更合理的选择来处理函数之一:

  1. 确切地说,是在执行单个简单命令时使用的单词"$@"

...要么...

  1. 参数的进一步扩展和解释的版本,然后将这些参数作为简单命令一起应用$@

如果这是故意的,并且您所选择的效果得到了很好的理解,那么这两种方法都不对。尽管第二种方法的优点很少特别有用,但这两种方法都具有另一种优点。仍然...

(run_this(){ $@; }; IFS=@ run_this 'ls@-dl@/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

...这不是没有用的,只是很少会有。而在一个bash壳,因为bash在默认情况下不,即使所述限定前置到一个特殊的内置的命令行或函数的变量定义它的环境,为全球价值$IFS不受影响,它的声明是本地只给run_this()电话。

类似地:

(run_this(){ $@; }; set -f; run_this ls -l \*)

ls: cannot access *: No such file or directory

...也可以配置。行情是有目的的-它们并非一无是处。没有它们,外壳扩展将进行额外的解释-可配置的解释。它曾经是-有一些非常古老贝壳-这$IFS全球范围内应用到所有的输入,而不仅仅是扩展。实际上,所说的shell的行为非常相似run_this(),因为它们破坏了值的所有输入单词$IFS。因此,如果您要查找的是旧的shell行为,则应使用run_this()

我不是在寻找它,现在我很难找到一个有用的例子。我通常更喜欢我的shell运行的命令是我键入的命令。因此,只要有选择,我几乎都会run_that()。除了那个...

(run_that(){ "$@"; }; IFS=l run_that 'ls' '-ld' '/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

几乎任何东西都可以引用。命令将用引号引起来。之所以起作用,是因为到命令实际运行时,所有输入词都已经被删除引号 -这是Shell输入解释过程的最后阶段。因此,'ls'和之间的区别ls仅在解释外壳程序时才重要-这就是为什么引用ls确保任何命名的别名ls都不能代替我引用的ls命令字的原因。除此之外,引用的唯一影响是单词的定界(这是变量/输入空格引用的工作方式和原因)以及元字符和保留字的解释。

所以:

'for' f in ...
 do   :
 done

bash: for: command not found
bash:  do: unexpected token 'do'
bash:  do: unexpected token 'done'

您将永远无法使用run_this()或来做到这一点run_that()

但是函数名称,$PATH'd命令或内建函数将只用引号引起来或不加引号,这就是方法run_this()run_that()工作的最开始。您将无法对$<>|&(){}任何这些做任何有用的事情。不到eval,是。

(run_that(){ "$@"; }; run_that eval printf '"%s\n"' '"$@"')

eval
printf
"%s\n"
"$@"

但是如果没有它,您将由于使用的引号而受制于一个简单命令的限制(即使您不这样做,因为$@当解析元字符的命令时,其行为就像过程开始时的引号一样)。命令行分配和重定向受到相同的约束,它们仅限于函数的命令行。但这没什么大不了的:

(run_that(){ "$@";}; echo hey | run_that cat)

hey

我可以像打开管道一样轻松地<重定向输入或>输出。

无论如何,在这里没有回旋的对与错的方法-每种方法都有其用途。只是您应该在打算使用它时编写它,并且应该知道要做什么。省略引号可以有目的-否则根本就不会有引号-但是如果您出于与目的不相关的原因而省略了引号,则您只是在编写错误的代码。按照你的意思去做 我还是尝试。

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.