常見 Bash 內建變數介紹
目錄
$0
執行 Bash 指令碼時,Bash 會自動將指令碼的名稱儲存在內建變數 $0 中。因為 $0 基於的是實際的指令碼檔名稱,而不是在指令碼中進行硬編碼,所以在重新命名指令碼檔案的名稱後,不需要修改指令碼的內容。比如下面的指令碼片段:
#!/bin/bash ARGS=3 # 這個指令碼需要 3 個引數. E_BADARGS=65 # 傳遞給指令碼的引數個數不對. echo "Args number is : $#" echo $0 if [ $# -ne "$ARGS" ] # 測試指令碼的引數個數。 then echo "Usage: $(basename $0) first-parameter second-parameter third-parameter" exit $E_BADARGS fi # 開始幹正事兒
在上面的程式碼中我們使用了 $(basename $0) 的寫法,這是因為 $0 會包含指令碼檔案的路徑,為了讓輸出看起來清爽一些,我用 $(basename $0) 去掉了指令碼的路徑名稱,下面是執行的結果:
$1, $2 等等
$0, $1,$2... 被稱為位置引數。所謂的位置引數(positional parameter),指的是 Shell 指令碼的命令列引數(argument);同時也表示在 Shell 函式內的函式引數。它們的名稱是以單個的整數來命名。出於歷史的原因,當這個整數大於 9 時,就應該以大括號{} 括起來。下面是一個簡單的 demo:
#!/bin/bash echo $1 echo $2 echo $3
$#
位置引數的個數,具體的用法請參考 $0 中的示例。
$* 與 "$*"
所有的位置引數。但是 $* 與 "$*" 的表現是不一樣的,我們通過下面的 demo 來介紹其異同。
$* 提供分隔後的引數:
for arg in $* do echo $arg done
$* 和 [email protected] 的表現是一樣的。
"$*" 把所有引數看作一個字串:
for arg in "$*" do echo $arg done
[email protected] 與 "[email protected] "
所有的位置引數。[email protected] 和 $* 的表現是一樣的。
"[email protected]" 能夠提供看上去比較合理的結果:
for arg in "[email protected]" do echo $arg done
下面是 "[email protected]" 的一個比較常見的用法如下:
if[ "$1"='node' ]; then SCRIPT_FILE= for ARG in "[email protected]" do if[ "${ARG}"='main.js' ]; then SCRIPT_FILE='main.js' break fi done if[ -z "$SCRIPT_FILE" ]; then exec "[email protected]""main.js" exit 0; fi fi exec"[email protected]"
這是在常見 nodejs 的 docker 映象時經常使用的一段程式碼:
"[email protected]" 還常常與 shift 命令一起使用來丟棄引數 $1 #!/bin/bash # 使用./test.sh 1 2 3 4 5 來呼叫這個指令碼 echo "[email protected]" # 1 2 3 4 5 shift echo "[email protected]" # 2 3 4 5 shift echo "[email protected]" # 3 4 5 # 每次 "shift" 都會丟棄$1. # "[email protected]" 將包含剩下的引數.
還可以使用 set 命令在指令碼中設定位置引數:
#!/bin/bash set -- "First one" "second" "third:one" "" "Fifth: :one" # 設定這個指令碼的引數, $1, $2, 等等. index=1 # 起始計數. echo "Listing args with \"\[email protected]\":" for arg in "[email protected]" do echo "Arg #$index = $arg" let "index+=1" done # [email protected] 把每個引數都看成是單獨的單詞. echo "Arg list seen as separate words."
$!
執行在後臺的最後一個作業的 PID。
$ sleep 60 & [1] 6238 $ echo "$!" 6238
如果有多個在後臺執行的任務,就需要通過 $! 來獲得 PID 並進行 wait:
$ sleep 60 & $ pid1=$! $ sleep 100 & $ pid2=$! $ wait $pid1 # 等待第一個後臺程序結束 $ wait $pid2 # 等待第二個後臺程序結束
$_
這個變數儲存之前執行的命令的最後一個引數的值。
把下面的程式碼儲存在 test.sh 檔案中:
#!/bin/bash echo $_ # ./test.sh du >/dev/null # 這麼做命令列上將沒有輸出. echo $_ # du ls -al >/dev/null # 這麼做命令列上將沒有輸出. echo $_ # -al (這是最後的引數) : echo $_ # :
下面是一個比較常見的用法,可以直接進入建立的目錄:
$ mkdir hello && cd $_
$$
指令碼自身的 PID (當前 bash 程序的 PID):
$PPID
程序的 $PPID 就是這個程序的父程序的 PID。
$?
$? 儲存了最後所執行的命令的退出狀態碼,一般表示命令執行成功或失敗。當函式返回之後,$? 儲存函式中最後所執行的命令的退出狀態碼。這就是 bash 對函式 "返回值" 的處理方法。當一個指令碼退出,$? 儲存了指令碼的退出狀態碼,這個退出狀態碼也就是指令碼中最後一個執行命令的退出狀態碼。 0 表示成功,其它值表示錯誤。
當指令碼以不帶引數的 exit 命令來結束時,指令碼的退出狀態碼就由指令碼中最後執行的命令來決定(就是exit之前的命令)。不帶引數的exit命令與 exit $? 的效果是一樣的,甚至指令碼的結尾不寫 exit,也與前兩者的效果相同。
我們還可以把 $? 儲存到變數中,從而讓指令碼返回其中某個命令的返回值:
#!/bin/bash set -x go get -d -v golang.org/x/net/html go get -u github.com/jstemmer/go-junit-report go test -v 2>&1 > tmp status=$? $GOPATH/bin/go-junit-report < tmp > test_output.xml exit ${status}
上面的程式把 go test 命令的返回值儲存到了變數 status 中,並通過 exit ${status} 作為指令碼的返回值。
關於退出狀態
在 Linux 系統中,程式(包括指令碼)的退出狀態是非常有用的,只要程式執行完成,就會向 Shell 返回一個退出狀態碼。這個狀態碼是一個數值,指明瞭程式是否成功結束。按照慣例,退出狀態碼為 0 表示程式執行成功;非 0 表示程式執行失敗,不同的值對應著不同的失敗原因。
造成程式執行失敗的原因可能是非法引數,也可能是出現了錯誤的條件。比如 cp 命令,退出狀態碼 1 表示檔案沒有找到,2 表示檔案不可讀,3 表示目標目錄沒有找到,4 表示目標目錄不可寫,5 表示一般性錯誤。
$BASH
Bash 的二進位制程式檔案的路徑:
$BASH_VERSION
檢查系統上安裝的 Bash 版本號:
檢查 $BASH_VERSION 對於判斷系統上到底執行的是哪個 shell 來說是一種非常好的方法。變數 $SHELL有時候不能夠給出正確的答案。
$EUID 與 $UID
$EUID 表示 "有效" 使用者 ID。
$UID 表示 使用者ID號,是當前使用者的使用者標識號, 記錄在 /etc/passwd 檔案中。這是當前使用者的真實 id, 即使只是通過使用 su 命令來臨時改變為另一個使用者標識, 這個 id 也不會被改變。$UID 是一個只讀變數,不能在命令列或者指令碼中修改它。
$GROUPS
當前使用者所屬的組。
這是一個當前使用者的組 id 陣列, 與記錄在 /etc/passwd 檔案中的內容一樣:
$HOME
使用者的 home 目錄,一般是 /home/username。
$HOSTNAME
主機名稱。
$IFS
內部域分隔符。這個變數用來決定 Bash 在解釋字串時如何識別域,或者單詞邊界。
$IFS預設為空白(空格, 製表符,和換行符),但這是可以修改的,比如在分析逗號分隔的資料檔案時,就可以設定為逗號。注意 $* 使用的是儲存在 $IFS 中的第一個字元來分隔位置引數的。
$IFS 處理其他字元與處理空白字元不同的 demo:
#!/bin/bash output_args_one_per_line() { for arg do echo "[$arg]" done } echo "IFS=\" \"" echo "-------" IFS=" " var=" a b c " output_args_one_per_line $var echo; echo "IFS=:" echo "-----" IFS=: var=":a::b:c:::" # 與上邊一樣, 但是用" "替換了":". output_args_one_per_line $var # 使用 : 後,冒號前後的空字元也被解析了。 exit 0
執行上面的指令碼,結果如下:
$PATH
可執行檔案的搜尋路徑。
當給出一個命令時,Bash 會自動生成一張雜湊(hash)表,並且在這張雜湊表中按照 PATH 變數中所列出的路徑來搜尋這個可執行命令。路徑會儲存在環境變數中,$PATH 變數本身就一個以冒號分隔的目錄列表。通常情況下,系統都是在 /etc/profile 和 ~/.bashrc 中儲存 $PATH 的定義,Ubuntu 是定義在 /etc/environment 檔案中。
PATH=${PATH}:/opt/bin
將會把目錄 /opt/bin 附加到當前目錄列表中,在指令碼中,這是一種把目錄臨時新增到 $PATH 中的權宜之計。當這個指令碼退出時,$PATH 將會恢復以前的值(一個子程序,比如說一個指令碼,是不能夠修改父程序的環境變數的)。
當前的"工作目錄",通常是不會出現在 $PATH 中的,這樣做的目的是出於安全的考慮。因為當前目錄是不斷變化的,很有可能會存在與系統工具同名的惡意程式(比如你在網上下載了一個叫 cat 的惡意程式)。這時執行 cat 命令,就會運行當前目錄下的 cat 惡意程式(把當前目錄放在 PATH 變數的靠前位置的情況)。
還有一種情況,比如我們經常會自己用c語言或者其它的語言寫一些程式,然後編譯、連結為可執行檔案。假如我們的可執行檔案是做一些不可恢復性的操作,比如刪除檔案,格式化磁碟之類的。而這些檔名字又恰巧和我們系統 $PATH 下的某些常用可執行檔名字相同時,那麼結果會出乎我們的意料。
也就是說當前目錄是總在變化的,一會我們 cd 到這兒了,一會又 cd 到另一個地方去了。這樣的話,當前目錄下有哪些可執行檔案也會隨著改變的。有時候我們不會太在意自己處於的目錄位置,如果當前目錄在 $PATH中,那麼我們也就不清楚自己幹了什麼。
而 $PATH 裡面則放置了一些固定的目錄,這些目錄是不會變化的,這樣的話,當我們輸入命令時,永遠可以保證不會隨著自己的位置改變,而導致出乎意料。
$OLDPWD
前一個工作目錄,可以通過下面的命令快速的回到前一個工作目錄:
$ cd -
$PWD
工作目錄(你當前所在的目錄),這與內建命令 pwd 的作用相同:
下面的指令碼演示瞭如何防止誤刪檔案:
#!/bin/bash E_WRONG_DIRECTORY=73 clear # 清屏. TargetDirectory=/home/nick/testdir cd $TargetDirectory echo "Deleting stale files in $TargetDirectory." if [ "$PWD" != "$TargetDirectory" ] then # 防止偶然刪錯目錄. echo "Wrong directory!" echo "In $PWD, rather than $TargetDirectory!" echo "Bailing out!" exit $E_WRONG_DIRECTORY fi rm -rf * # 刪除檔案 rm .[A-Za-z0-9]* # 刪除點檔案 echo "Done." echo "Old files deleted in $TargetDirectory." exit 0
執行上面的指令碼,顯示的結果如下:
$PS1
這是主提示符,可以在命令列中見到它,筆者的 Ubuntu16.04 中為:
看起來有些複雜,其實是添加了一些字型顏色的設定等內容。
$PS2
第二提示符,當你需要額外輸入的時候,你就會看到它,預設值為 ">":
當我們往命令列上貼上一個多行的命令時就會看到它的身影:
$PS4
第四提示符,當我們使用 -x 選項來呼叫指令碼時,這個提示符會出現在每行輸出的開頭,預設為 "+":
執行下面的指令碼:
set -x echo "Hello nick" echo 'This will show $PS4'
參考:
Bash Internal Variables
《高階 Bash 指令碼程式設計指南》
《Unix/Linux/OS X 中的 Shell 程式設計》