跳转到内容

Bash Shell 脚本/环境

来自 Wikibooks,开放的书籍,开放的世界
注意

花些时间研究这一部分。一旦理解了这些概念,它们就比较简单了,但它们在重要方面与其他编程语言中的类似概念有所不同。许多程序员和系统管理员,包括一些在 Bash 上经验丰富的程序员,一开始会发现它们很不直观。

子 Shell

[编辑 | 编辑源代码]

在 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 中执行。这将“隔离”这些修改,防止它们影响周围的执行环境。(也就是说,如果可能的话,最好以一种不会出现此问题的方式编写函数。正如我们很快将看到的,local 关键字可以帮助解决这个问题。)

函数定义也是如此;就像一个普通的变量一样,在子 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,如果可能的话。否则,在阅读脚本时,很难跟踪工作目录。(或者,pushdpopd 内置命令可以用于类似的效果。)

子 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 barexport -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.shbash ./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 var2local var1=val1 var2=val2。(还有其他方法,例如 declare 内置命令具有相同的效果,但这可能是最常用的方法。)
    • 它们与非局部变量的不同之处在于,它们在函数调用结束时会消失。特别是,它们仍然对子 shell 和子函数调用可见。此外,与非局部变量一样,它们可以导出到环境中,以便子进程也可以看到它们。

实际上,使用 local 将变量局部化到函数调用就像将函数调用放入子 shell 中一样,只是它只影响一个变量;其他变量仍然可以保持非“局部”。

提示

在函数内部设置的变量(通过赋值、for 循环或其他内置命令)应该使用内置命令 local 标记为“局部”,以避免意外影响函数外部的代码,除非明确希望调用者看到新的值。

需要注意的是,虽然 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
华夏公益教科书