Bash Shell 脚本/环境
注意 花些时间研究这一部分。一旦理解了这些概念,它们就比较简单了,但它们在重要方面与其他编程语言中的类似概念有所不同。许多程序员和系统管理员,包括一些在 Bash 上经验丰富的程序员,一开始会发现它们很不直观。 |
在 Bash 中,可以用括号将一个或多个命令括起来,这将导致这些命令在“子 Shell”中执行。(还有一些方法可以隐式创建子 Shell,我们稍后会看到。)子 Shell 会收到周围上下文“执行环境”的副本,其中包括所有变量,以及其他东西;但子 Shell 对执行环境所做的任何更改都不会在子 Shell 完成时复制回来。例如,此脚本
#!/bin/bash
foo=bar
echo "$foo" # prints 'bar'
# subshell:
(
echo "$foo" # prints 'bar' - the subshell inherits its parents' variables
baz=bip
echo "$baz" # prints 'bip' - the subshell can create its own variables
foo=foo
echo "$foo" # prints 'foo' - the subshell can modify inherited variables
)
echo "$baz" # prints nothing (just a newline) - the subshell's new variables are lost
echo "$foo" # prints 'bar' - the subshell's changes to old variables are lost
打印如下内容
bar bar bip foo bar
提示 如果需要调用修改一个或多个变量的函数,但实际上不想修改这些变量,可以将函数调用括在括号中,以便它在子 Shell 中执行。这将“隔离”这些修改,防止它们影响周围的执行环境。(也就是说,如果可能的话,最好以一种不会出现此问题的方式编写函数。正如我们很快将看到的, |
函数定义也是如此;就像一个普通的变量一样,在子 Shell 中定义的函数在子 Shell 外部不可见。
子 Shell 还限制了对执行环境其他方面的更改;特别是,cd
(“更改目录”)命令只影响子 Shell。例如,此脚本
#!/bin/bash
cd /
pwd # prints '/'
# subshell:
(
pwd # prints '/' - the subshell inherits the working directory
cd home
pwd # prints '/home' - the subshell can change the working directory
) # end of subshell
pwd # prints '/' - the subshell's changes to the working directory are lost
打印如下内容
/ / /home /
提示 如果脚本需要在运行给定命令之前更改工作目录,最好使用子 Shell,如果可能的话。否则,在阅读脚本时,很难跟踪工作目录。(或者, |
子 Shell 中的 exit
语句只终止该子 Shell。例如,此脚本
#!/bin/bash
( exit 0 ) && echo 'subshell succeeded'
( exit 1 ) || echo 'subshell failed'
打印如下内容
subshell succeeded subshell failed
与整个脚本一样,exit
默认返回最后运行命令的退出状态,没有显式 exit
语句的子 Shell 将返回最后运行命令的退出状态。
我们已经看到,当调用一个程序时,它会收到一个在命令行中显式列出的参数列表。我们还没有提到的是,它还会收到一个称为“环境变量”的名称-值对列表。不同的编程语言为程序提供不同的方式来访问环境变量;C 程序可以使用 getenv("variable_name")
(和/或作为 main
的第三个参数接收它们),Perl 程序可以使用 $ENV{'variable_name'}
,Java 程序可以使用 System.getenv().get("variable_name")
,等等。
在 Bash 中,环境变量只是被转换成普通的 Bash 变量。例如,以下脚本打印 HOME
环境变量的值
#!/bin/bash
echo "$HOME"
然而,反之则不然:普通的 Bash 变量不会自动变成环境变量。例如,此脚本
#!/bin/bash
foo=bar
bash -c 'echo $foo'
不会打印 bar
,因为变量 foo
没有作为环境变量传递给 bash
命令。(bash -c script arguments…
运行单行 Bash 脚本 script
。)
要将一个普通的 Bash 变量变成环境变量,必须将其“导出”到环境中。以下脚本会打印 bar
#!/bin/bash
export foo=bar
bash -c 'echo $foo'
请注意,export
不仅仅是创建环境变量;它实际上将 Bash 变量标记为导出变量,以后对 Bash 变量的赋值也会影响环境变量。这个效果由以下脚本说明
#!/bin/bash
foo=bar
bash -c 'echo $foo' # prints nothing
export foo
bash -c 'echo $foo' # prints 'bar'
foo=baz
bash -c 'echo $foo' # prints 'baz'
export
命令也可以用来从环境中删除一个变量,方法是包含 -n
选项;例如,export -n foo
取消了 export foo
的效果。多个变量可以在一条命令中导出或取消导出,例如 export foo bar
或 export -n foo bar
。
重要的是要注意,环境变量只能传递到命令中;它们永远不会从命令中接收回来。在这方面,它们类似于普通的 Bash 变量和子 Shell。例如,此命令
#!/bin/bash
export foo=bar
bash -c 'foo=baz' # has no effect
echo "$foo" # print 'bar'
打印 bar
;对单行脚本中 $foo
的更改不会影响调用它的进程。(但是,它会影响被该脚本依次调用的任何脚本。)
如果一个给定的环境变量只对一个命令有用,可以使用语法 var=value command
,其中变量赋值(或多个变量赋值)的语法在同一行上位于命令之前。(请注意,尽管使用了变量赋值的语法,但这与普通的 Bash 变量赋值非常不同,因为变量会自动导出到环境中,并且它只对一个命令存在。如果你想避免类似语法做不同事情带来的混乱,可以使用常用的 Unix 工具 env
来获得相同的效果。该工具还可以用来删除一个命令的一个环境变量——甚至可以用来删除一个命令的所有环境变量。)如果 $var
已经存在,并且希望将它的实际值包含在对一个命令的环境中,可以写成 var="$var" command
。
题外话:有时将变量定义——或函数定义——放在一个 Bash 脚本(例如,header.sh
)中,另一个 Bash 脚本(例如,main.sh
)可以调用它。我们可以看到,简单地调用另一个 Bash 脚本,例如 ./header.sh
或 bash ./header.sh
,是行不通的:header.sh
中的变量定义将不会被 main.sh
看到,即使我们“导出”了这些定义。(这是一个常见的误解:export
将变量导出到环境中,以便其他进程可以看到它们,但它们仍然只能被子进程看到,而不能被父进程看到。)但是,我们可以使用 Bash 内置命令 .
(“点”)或 source
,它运行外部文件,几乎就像它是一个 shell 函数一样。如果 header.sh
看起来像这样
foo=bar
function baz ()
{
echo "$@"
}
那么这个脚本
#!/bin/bash
. header.sh
baz "$foo"
将打印 'bar'
。
我们现在已经看到了 Bash 中变量范围的一些怪癖。为了总结我们到目前为止所看到的
- 普通的 Bash 变量的作用域是包含它们的 shell,包括该 shell 中的任何子 Shell。
- 它们对任何子进程(即外部程序)不可见。
- 如果它们是在子 Shell 中创建的,它们对父 Shell 不可见。
- 如果它们是在子 Shell 中修改的,这些修改对父 Shell 不可见。
- 函数也是如此,在许多方面,它们与普通的 Bash 变量类似。
- 函数调用不是本质上在子 Shell 中运行的。
- 函数中的变量修改通常对调用该函数的代码可见。
- 导出到环境中的 Bash 变量的作用域是包含它们的 shell,包括该 shell 中的任何子 Shell或子进程。
export
内置命令可以用来将变量导出到环境中。(还有其他方法,但这最常见。)- 它们与未导出的变量不同,只是它们对子进程可见。特别是,它们仍然对父 Shell 或父进程不可见。
- 外部 Bash 脚本,就像其他外部程序一样,在子进程中运行。
.
或source
内置命令可以用来在内部运行这样的脚本,在这种情况下,它不是本质上在子 Shell 中运行的。
现在我们再加上
- 本地化到函数调用的 Bash 变量的作用域是包含它们的函数,包括被该函数调用的任何函数。
local
内置命令可用于将一个或多个变量局部化到函数调用中,语法为local var1 var2
或local var1=val1 var2=val2
。(还有其他方法,例如declare
内置命令具有相同的效果,但这可能是最常用的方法。)- 它们与非局部变量的不同之处在于,它们在函数调用结束时会消失。特别是,它们仍然对子 shell 和子函数调用可见。此外,与非局部变量一样,它们可以导出到环境中,以便子进程也可以看到它们。
实际上,使用 local
将变量局部化到函数调用就像将函数调用放入子 shell 中一样,只是它只影响一个变量;其他变量仍然可以保持非“局部”。
提示 在函数内部设置的变量(通过赋值、for 循环或其他内置命令)应该使用内置命令 |
需要注意的是,虽然 Bash 中的局部变量非常有用,但它们不像大多数其他编程语言中的局部变量那样局部,因为子函数调用可以访问它们。例如,这段脚本
#!/bin/bash
foo=bar
function f1 ()
{
echo "$foo"
}
function f2 ()
{
local foo=baz
f1 # prints 'baz'
}
f2
实际上会打印 baz
而不是 bar
。这是因为 $foo
的原始值在 f2
返回之前被隐藏了。(在编程语言理论中,像 $foo
这样的变量被称为“动态作用域”而不是“词法作用域”。)
local
和子 shell 之间的一个区别是,子 shell 最初从其父 shell 获取其变量,而像 local foo
这样的语句会立即隐藏 $foo
的先前值;也就是说,$foo
成为局部未设置。如果希望将局部 $foo
初始化为现有 $foo
的值,则必须使用 local foo="$foo"
这样的语句显式指定。
当函数退出时,变量会恢复其在 local
声明之前的值(或者如果它们之前是未设置的,它们会简单地变为未设置)。有趣的是,这意味着像这样的一段脚本
#!/bin/bash
function f ()
{
foo=baz
local foo=bip
}
foo=bar
f
echo "$foo"
实际上会打印 baz
:函数中的 foo=baz
语句在变量局部化之前生效,因此在函数返回时恢复的值是 baz
。
由于 local
只是一个可执行命令,因此函数可以在执行时决定是否将给定变量局部化,因此这段脚本
#!/bin/bash
function f ()
{
if [[ "$1" == 'yes' ]] ; then
local foo
fi
foo=baz
}
foo=bar
f yes # modifies a localized $foo, so has no effect
echo "$foo" # prints 'bar'
f # modifies the non-localized $foo, setting it to 'baz'
echo "$foo" # prints 'baz'
实际上会打印
bar baz