位元詩人 [Shell Scripting] 教學:資料型態 (Data Type) 和變數 (Variable)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

本文從 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 並以 valuevariable 賦值
  • ${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)。限於篇幅,這裡不說明該指令的用法。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。