位元詩人 [Shell Scripting] 教學:撰寫第一隻程式

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在本文中,我們以兩個簡單的例子來看如何撰寫 POSIX shell script ,並會介紹一些和撰寫 shell script 相關的開發工具及開發實務。

撰寫第一隻程式

按造傳統,我們以經典的 Hello World 程式來看 shell script 的寫法。用編輯器建立空白檔案 hello ,不需加上附檔名,加入以下內容:

#!/bin/sh
# hello - Classic "Hello World" program in POSIX shell.

echo "Hello World";

exit 0;

將該命令稿加上可執行的權限:

$ chmod +x hello

由於 hello 不是位在 PATH 變數所在的路徑,執行 hello 時要加上 ./ 表示相對路徑:

$ ./hello
Hello World

也可以直接使用 sh 指令來執行 hello 命令稿:

$ sh hello
Hello World

如果系統沒有 sh,或是該命令稿和 sh 不相容,可改用 bash 執行:

$ bash hello
Hello World

這是因為 Bash 向後相容 POSIX shell。

shell script 的組成

我們再展示一次 Hello World 程式的程式碼:

#!/bin/sh                                                #  1
# hello - Classic "Hello World" program in POSIX shell.  #  2

echo "Hello World";                                      #  3

exit 0;                                                  #  4

第 1 行為 shebang。這個寫法是 Unix 或類 Unix 系統的特性,用來表示執行該命令稿的直譯器的路徑。以本例來說,此命令稿指定直譯器為 /bin/sh ,即位於 /binsh shell。

第 2 行為註解。如同其他的編譯器或直譯器, sh 會略過註解所在的文字。這行不是必需的,只是用來註記程式的用途。日後要閱讀程式碼時,可以大略了解此命令稿的主要用途。

第 3 行使用 echo 指令輸出 "Hello World" 字串。echo 指令會在字串尾端自動加上換行符號。該指令尾端的分號 ; 是選擇性的,可加可不加。

第 4 行使用 exit 指令離開此命令稿。exit 指令可加上選擇性的回傳值,用來表示此命令稿的離開狀態 (exit status)。shell script 承襲 C 語言的傳統,0 表示正常離開,非零值 1 表示異常離開。

第 4 行是選擇性的。可加可不加。筆者會建議在 shell 應用程式中明確地指定程式的離開狀態。

寫第一隻函式庫

雖然 shell script 沒有真正的函式庫,但我們可以在 shell 命令稿中撰寫 shell 函式,再由外部程式吃入該命令稿。這個行為類似於撰寫和使用 shell 的函式庫。我們在本節中展示如何撰寫和使用 shell 函式庫。

用編輯器建立空白檔案 lib.sh ,加入以下內容:

hello ()
{
    echo "Hello World";
}

我們還沒正式學到 shell 函式的寫法,先照抄這段程式碼即可。

在撰寫 shell 函式庫時,我們會在命令稿的尾端加上 .sh 附檔名,用來和可執行命令稿區分。此外,shell 函式庫不會加上 shebang 行,不會加上可執行的權限,也不會用 exit 指令離開 shell 命令稿。

接下來,我們撰寫一個吃入 lib.sh 的 shell script :

#!/bin/sh

. ./lib.sh;   # 1
hello;        # 2
exit 0;       # 3

這個 shell script 的關鍵在第 1 行。此程式在該行吃入 lib.sh 。所以,即使該程式本身沒有實作 hello 指令,我們在第 2 行仍然可以呼叫 hello 指令。

以下是執行該 shell 命令稿的方式:

$ chmod +x libTest
$ ./libTest
Hello World

由此可知,我們可以用這種方式撰寫可重覆利用的 shell 函式庫。

筆者自已就寫了一個小型的 shell 函式庫 shlibs。該函式庫主要用在雲端 IDE,可 clone 該 repo 後快速地設置自已常用的 shell 別名和函式。

讀者不一定要去用 shlibs ,因為 shell 別名和函式會因個人需求而相異。但可在寫熟 shell script 後,試著建立自已的通用 shell 函式庫。

對 shell script 除錯

不論是 POSIX shell 或 Bash,本身都內建一些和開發 shell script 相關的功能。善用這些功能,可以讓撰寫 shell script 的過程更順利。

使用 -n 參數時,不執行 shell script ,只檢查是否有語法錯誤:

$ sh -n path/to/script

使用 -x 參數時,會逐一印出執行指令的過程。我們用來執行上一節的 libTest 命令稿:

$ sh -x libTest
+ . ./lib.sh
+ hello
+ echo Hello World
Hello World
+ exit 0

Shell 實際執行的指令會用 + 做為前綴,用來和一般的標準輸出入區隔。由此可知,此 shell 命令稿執行了四行指令。

當 shell 有條件句或迴圈時,使用 -x 參數可以觀察 shell 實際執行的過程,對於除錯相當有幫助。

合併使用 -v -x 參數時,可以同時印出程式碼和執行指令的過程:

$ sh -v -x libTest
#!/bin/sh

. ./lib.sh;
+ . ./lib.sh
hello ()
{
    echo "Hello World";
}

hello;
+ hello
+ echo Hello World
Hello World

exit 0;
+ exit 0

由這個例子可看出,shell script 在吃入 shell 函式庫時,相當於把整個 shell 函式庫貼到 shell script 內,所以可以使用 shell 函式庫內的 shell 函式。

Shell 撰碼風格

shell script 算是易學難精的語言,用撰碼風格來改善 shell script 碼的品質是可行的方向。但 shell script 算是用來自動化特定任務的腳本,不是用來寫應用程式的語言,沒什麼商業公司會特地去推廣 shell script 。

即使如此,網路上還是可以找到一些非官方的撰碼風格建議:

除此之外,還有一些個人開發者自行撰寫的撰碼風格。但那些資料參考性較低,故不列出,有興趣的讀者可自行上網搜尋。

使用 POSIX Shell 的替代品

我們在先前的文章中講過,在撰寫 shell 腳本時,應儘量地讓 shell 腳本遵守 POSIX 標準,避免使用 Bash 特有的特性。如果只以人工檢查,有時候會不經意地引入 Bash 的特性而不自知。最好還是使用軟體自動檢查。

若要使用軟體來檢查 shell 命令稿,最直接的方式就是使用 POSIX shell 來檢查及執行 shell 命令稿。有什麼軟體會比 shell 本身更懂 shell script 呢?

但不是每種 Unix 或類 Unix 系統都有 POSIX shell,有些系統為了便宜行事,直接把 sh(1) 設為指向 Bash 的連結。在這時候,我們不自覺用 Bash 執行 shell script 。

如果讀者使用的系統不提供 POSIX shell,一個替代的方式是在 Bash 中以 POSIX 模式檢查和執行 shell 命令稿。在命令列加上 --posix 即可以 POSIX 模式執行 shell 命令稿:

$ bash --posix path/to/script

或者在 shell 命令稿開頭的地方設置 POSIX 模式:

#!/bin/bash

set -o posix

# Write your code here.

即使如此,Bash 的 POSIX 相容模式並沒有完全符合 POSIX 標準,而會稍稍容許少數 Bash 特有的語法。

如果真的很在意 shell script 的相容性的話,最好也在 GNU/Linux 以外的 Unix 或類 Unix 系統測 shell script ,詳見下一節。

測試 Shell 命令稿的相容性

要讓 shell 命令稿在各 Unix 或類 Unix 系統間相容並不是太容易,一方面來自 shell 本身的異質性,一方面來自命令列工具的異質性。像是 Ubuntu 上的 grep(1) 和 FreeBSD 上的 grep(1) 可用參數就相差很多。

如果只用查手冊的方式來確認相容性,沒有程式運行的真正感覺,效率也很差。比較好的方式是直接在真正的類 Unix 系統上跑看看目標 shell 命令稿,馬上就可以確認和修正不相容的部分。

一般來說,會建議在以下 Unix 或類 Unix 系統檢查 shell script 相容性:

  • GNU/Linux 系統
    • Debian 或 Ubuntu
    • CentOS
    • (選擇性) openSUSE
  • FreeBSD 或 TrueOS
  • (選擇性) Solaris

一般來說,Unix 或類 Unix 系統上的命令列工具可分為 BSD 和 GNU 兩種風格,主要影響命令列工具的可用參數,也會影響這些工具可用的功能。通常 BSD 風格的指令比較接近 Unix 的原味,而 GNU 風格的指令加入比較多的延伸功能。所以,應該至少在 GNU/Linux 和 BSD 中各選一個系統來測。

GNU/Linux 的主流系統又可再分為 Debian 系列和 RHEL (或 CentOS) 系列。如果時間允許的話,可以各選一個系統來測。至於 openSUSE 主打歐洲市場,在台灣比較少人用,行有餘力再測即可。

Solaris 算是商用 Unix,家用電腦不太會接觸到。但 Solaris 算是蠻常見的商用系統,且現在可在 Oracle 官網免費下載 x86-64 架構的版本,有興趣可以抓來用看看。

至於 macOS 也是相當流行的 BSD 系統,要不要測呢?雖然 macOS 市佔率還不錯,但 macOS 主機較貴,很少人會特地買來當 Unix 系統用,平常沒在用 macOS 的話不用刻意去買來測試。

讀者可能會對要如何在多種系統上寫程式感到疑惑。由於類 Unix 系統大抵上遵守 POSIX 標準,雖然不一定會 100% 相容,但差異性沒想像中那麼大。

在撰寫 shell 或其他 Unix 程式時,通常會先在自己最常用的主力系統上寫完程式,確認程式可順利執行後,再移到其他系統上去試跑。這時候,只要微幅修改系統間不相容的部分即可。

例如,可用 uname(1) 等程式判斷 shell script 執行的宿主系統。可參考以下的 shell script 虛擬碼:

if [ "$(uname)" = "Linux" ];
then
    # Write GNU/Linux specific code here.
elif [ "$(uname)" = "Darwin" ];
then
    # Write macOS specific code here.
else
    # Write Unix specific code here.
fi

由於本段虛擬碼的條件句中沒有指令,這段程式碼無法執行,只是給讀者看區隔系統差異的方式。藉由條件敘述,我們可以把不相容的程式碼區隔開來,其餘部分共用程式碼即可。

對 shell script 做靜態程式碼檢查

程式碼的撰碼風格或最佳實務算是靜態的文件,程式人需要自己閱讀後再自行更正 shell script 的程式碼。但也可以直接把對 shell script 的建議寫在軟體裡,透過這些軟體自動檢查 shell script 碼,這種動作就是靜態程式碼檢查。

POSIX shell 或 Bash 本身沒有靜態程式碼檢查的功能,但可考慮使用 shellcheck 來檢查 shell script 碼。這是少數針對 shell script 的靜態程式碼軟體。

shellcheck 的確可以改善我們撰寫 shell script 碼的習慣,但這類軟體本身不是完美的,偶爾會有誤判的情形。靜態程式碼檢查無法取代我們對 shell script 的了解,算是輔助開發的工具。

自動排列 shell script 碼

適當地排列程式碼,有助於維護程式。shfmt 是少數用來自動排版 shell script 碼的軟體,目前支援 POSIX shell、Bash、mksh (Korn shell 的後繼者)。

但筆者不喜歡 shfmt 排出來的風格,目前看起來也沒有參數可以調整,所以筆者目前沒有使用此軟體整理自己的程式碼。有興趣的讀者可以自行參考。

結語

在本文中,我們以兩個簡短的範例來展示如何寫 shell script 。此外,本文介紹了數個和撰寫 shell script 相關的開發工具。希望本文的內容能讓讀者撰寫 shell script 的過程更加順利。

關於作者

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

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