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