前言
本文從 shell (POSIX shell) 的觀點來看待 shell script 如何處理資料。
Shell Script 的資料型態
除了少數整數運算語境外,大部分 shell script 的資料都視為字串。Shell 的確有一些內建的語法可以處理字串,除此之外,shell 做的事就是把字串丟給命令列工具,再等著命令列工具運算完後把結果傳回 shell。
除此之外,shell 也沒有資料結構的概念。在 POSIX shell 中,迴圈可以走訪以分隔符 (預設為空白) 隔開的字串,但沒有真正的陣列型態。在新近版本的 Bash 中,引入了陣列 (array) 和關連式陣列 (associative array) 等基礎的資料結構。如果不在意 shell script 的相容性的話,倒是可以考慮使用。
為什麼 shell 會這樣設計呢?因為 shell script 本質上只是一個串連命令列工具的媒介 (glue)。當你發現 shell script 需要資料結構、在實作某種複雜的演算法、在進行複雜的數字運算時,這都是用錯工具的徵兆。
宣告變數
Shell 變數不需宣告,直接使用即可。為了讓程式碼易讀,會用賦值替代宣告。以下指令宣告變數 var
,並將其賦值為 value
:
var=value;
請注意 =
(等號) 兩邊不可以留空白。若 =
旁邊有空白,會視為相等,而非賦值。
如果 var
的值為空 (null),則使用以下寫法:
var=;
如果沒寫這行,var
的狀態為未設定 (unset)。雖然未設定和空值的值為皆空,但在 shell script 中,這兩種情境還是有差異的。另外一個使用情境是清空 var
原本的值。
建議賦值時將值以一對 "
(雙引號) 包住:
var="value";
若值有空白,使用雙引號可以保持值的完整性:
greet="Hello World";
為什麼要保存空白呢?因為 shell script 以空白區隔指令和參數。用一對 "
包住值,shell 才不會將值拆開。
值可以是整數:
num=12345;
請注意這時候值仍然視為字串。只有將值放入整數運算語境時,值才視為整數。
呼叫變數
在 shell 變數前加上 $
即可呼叫該變數。像是 $var
即為呼叫 var
。
以下 shell script 建立兩個變數,接著用 echo
指令將變數合併後輸出:
#!/bin/sh
greet="Hello";
target="World";
echo "$greet $target";
exit 0;
如果 shell 變數出現在一長串字串之中,使用 {
和 }
包住變數名稱即可。像是 "${var}"
。
以下 shell script 會輸出系統上的 GCC 內的標頭檔的位置:
#!/bin/sh
gcc_ver=$(gcc --version | head -n1 | grep -oP "\d.\d.\d$");
echo "/usr/lib/gcc/x86_64-linux-gnu/${gcc_ver}/include";
exit 0;
GCC 的版本號存在變數 gcc_ver
中。這個變數不是寫死的,會以系統上實際的 GCC 的版本號來取得其值。這裡可以看到使用管線 (pipe) 串連命令列工具的實例。
由於 gcc_ver
出現在一個代表路徑 (path) 的長字串中,所以用 {
和 }
包起來。
移除變數
使用 unset
指令即可移除變數。以下指令移除變數 var
:
unset var;
命令列可視為 shell 的 REPL 互動式環境,所以會有 unset
這種控制變數的指令。
變數擴張
變數擴張是相對進階的特性,一開始沒用到的話可以先略過不讀。
取代變數
進行變數擴張時,可以將 variable
的值以 value
取代。其形式有以下四種:
${variable:-value}
:當variable
存在值且該值不為空,則回傳variable
;反之,則回傳value
${variable:=value}
:當variable
存在值且該值不為空,則回傳variable
;反之,則回傳value
並以value
對variable
賦值${variable:?value}
:當variable
存在值且該值不為空,則回傳variable
:反之,則將value
傳至標準錯誤並中止程式${variable:+value}
:當variable
存在值且該值不為空,則回傳value
:反之,則回傳空值 (null)
取代變數時,其變體為移除 :
(冒號),保留其他符號。當 :
移除時,shell 僅會檢查 variable
是否存在,但不檢查 variable
是否為空值。
處理子字串
進行變數擴張時,會以 pattern
的形式從 variable
的值移除子字串後取出剩餘的部分。其形式有以下四種:
${variable%pattern}
:根據pattern
移除尾端字串,僅移除最短字串 (non-greedy)${variable%%pattern}
:根據pattern
移除尾端字串,移除最長字串 (greedy)${variable#pattern}
:根據pattern
移除頭端字串,僅移除最短字串 (non-greedy)${variable##pattern}
:根據pattern
移除頭端字串,移除最長字串 (greedy)
這裡的 pattern
不是一般字串,也不是常規表示式 (regular expression),而是 shell 特有的樣式語法。
變數的可視域
變數僅限於 shell script 本身
shell script 中,變數的可視域僅限於該 shell script 本身。
以下 shell script 將變數 greet
賦值為 "Hello World"
:
greet="Hello World";
使用 sh(1)
執行該 shell script 後,變數 greet
的值仍為空值:
$ sh greet
$ echo $greet
執行 greet 腳本時,相當於開啟新的 shell 行程,該行程所賦值的變數不會影響原行程。
相對來說,使用 .
(點號) 吃入 greet 腳本時,該腳本的變數就會影響當下的 shell 行程:
$ . greet
$ echo $greet
Hello World
用 export
將變數輸出到子 Shell
如果需要將變數輸出到子行程中,需使用 export
指令。
從 parent 腳本輸出 greet
變數:
#!/bin/sh
# parent
export greet="Hello World";
sh child;
另外注意 parent 腳本呼叫了 child 腳本。
但 child 腳本本身並未對變數 greet
賦值:
#!/bin/sh
# child
echo $greet;
執行 parent 腳本時,會間接執行 child 腳本,這時候變數 greet
的值如同預期:
$ sh parent
Hello World
Shell 控制結構的變數為全域可視
shell script 的控制結構沒有獨立的可視域,而是整個程式共用全域命名空間。看一下以下例子:
#!/bin/sh
# `while` is a shell control structure.
while true;
do
# `var` is global.
var="Hello World";
break;
done
# `var` is set now.
[ "Hello World" = "$var" ] || (
echo "Wrong value";
exit 1;
)
在 while
迴圈運行時,對 var
賦值。在 while
結束後,var
的值仍存在。由此可知,shell 控制結構沒有獨立的可視域,直接使用全域命名空間。
Shell 函式的變數為全域可視
承上,shell 函式也沒有獨立的可視域。看一下以下例子:
#!/bin/sh
# `hello` is a shell function.
hello ()
{
# `var` is global if `hello` is called.
var="Hello World";
}
# Call `hello`.
hello;
# `var` is set now.
[ "Hello World" = "$var" ] || (
echo "Wrong value";
exit 1;
)
hello
函式對 var
賦值。呼叫 hello
函式時執行賦值的動作。在函式呼叫後,var
仍是存在的。由此可知,shell 函式沒有獨立的可視域,直接使用全域命名空間。
shell script 的數字運算
數字運算不是 shell script 的目標,所以 shell script 只有整數運算,當成計數器 (counter) 來用。以 $((
和 ))
包起來即為 shell script 的整運運算語境。
以下是實例:
#!/bin/sh
num=$(( 3 + 4 ));
echo $num; # 7
exit 0;
使用 expr(1)
也可以進行一些運算:
#!/bin/sh
num=$(expr 3 + 4);
echo $num; # 7
exit 0;
由於 expr(1)
不需要整數運算語境,這裡使用 subshell 即可。
如果需要在命令列執行複雜的運算,可以看一下 bc(1)
。限於篇幅,這裡不說明該指令的用法。