前言
Shell Script 想必大家都有使用過,即使沒有真的寫 script,多少也會有在 command line 中透過 pipe 或是 and 條件句來將不同的指令結合在一起的經驗。
也因為 Shell 如此平凡,貼近一般環境,反而很少有人花時間去認真了解過 Shell。我自己也是這種人,於是近期讀了《精通 shell 程式設計 第四版》後,記錄了一些自己從來沒有了解過的細節。
檔案名稱替換
* 字元
在 Shell 中(此篇文章用 Bash 來當例子),*
會被轉譯成該目錄下所有檔案的名稱。(這件事是由 Shell 預先處理好。)
當 echo *
時,實際上會先把 * 替換成檔名(假如有 a b c 三個檔案),然後執行 echo a b c
印出 a b c
。
以 echo 指令的角度,是沒有辦法辨別 a b c 是由 *
替換而來,echo 只知道自己收到三個參數分別為 a, b, c。
? 字元
? 可以對應單一字元,例如 ?? 可以對應出全部剛好兩字元的檔名,如果想要匹配兩個字元以上(包含兩個字元)的檔名可以使用 ??*
該配對方式與 Regular Expression 類似,但並非 Regular Expression。
輸入重導向
在一行指令開始執行時,其實 Shell 已經默默幫我們做了一些事情。
1 | wc -l users # 5 s.sh |
以上兩者的輸出並不相同,差異在於後者是透過標準輸入傳入,wc 程式並無法得知是哪一個檔案傳進來的內容,所以無從得知檔名。
變數相關
變數
- 變數型態只有字串變數
- 宣告變數時 = 號左右不能有空格
1 | a = hello # 錯誤 |
- 當字串代有空格時,要用
'
或"
來夾住字串 - 單引號夾住的內容不會再進行轉譯,雙引號的內容會再進行轉譯
- $ 字號用來取值(shell 會進行替換)
1 | var1=hi |
unset
unset 是一個好用的指令,用來把變數取消設置,也可以將 function, alias 的設定取消:
1 | msg=hello |
還有一種取消的方式為指定空值:
1 | msg=hello |
- 取消 alias :
unset -a
- 取消 function :
unset -f
“$var” 與 $var 的差異
(假設目錄下有 a b c 三個檔案)
1 | var=* |
因為 $var
會替換成 *
,所以 echo $var
會先被 Shell 替換成 echo *
再替換成 echo a b c
。
然而 echo "$var"
最後僅會被替換成 echo "*"
而印出 *
。
(雙引號只會轉譯變數,不會轉譯 *
。)
由上述例子可知,為何比較好的寫法是在印出變數時外面再包一層雙引號。
命令替換:`` 與 $()
這兩個包裹字串的方式會讓字串內容當作指令來執行:
1 | var=`date` |
差異在於 $()
是新的方式,兩個 ` 則是舊的方式
常用指令以及被忽略的事
echo
echo 指令會無視空格(除非你用引號夾住字串)將空格當作分離不同參數的分隔符號:
1 | echo a b c d e # a b c d e |
-e
參數用來開啟轉譯功能
1 | echo -e "123\n" # 123 |
ls
使用 ls 指令,會列出該目錄下所有檔案,但不是一個檔案一行,想要一個檔案一行印出,可以使用 ls -1
wc
用來查看檔案的內容包行幾列、幾個單字、幾個字元。不幸的是 wc 的結果開頭都會有一個空白,可以使用 sed 將它剔除:
1 | wc backup.sh |
sort 覆蓋自己
sort 千萬不要直接重新導入自己,否則會清除該檔案內容:
1 | cat a.txt |
如果有這個需求,可以加上 -o
參數:
1 | cat a.txt |
參數
特殊變數
$#
會替換成參數數量$*
會替換成全部參數- 我們都知道 $1, $2 用來指定不同參數,但假如是參數 10,$10 會被轉譯成
$1
以及0
,這時候就需要${n}
格式來指定參數 10,${10}
。
決策
退出狀態
- 退出狀態為 0 代表成功,非 0 代表失敗
- 這個退出狀態並不是回傳值(這邊跟 C 語言不太一樣)
- 可以用
$?
查看上一個指令的退出狀態
test 指令
- 在 if 句中可以用 [] 來取代 test 兩者並無差別:
1 |
|
上述兩種寫法是完全相同的,[] 也可以直接在 Shell 中使用:
1 | [ a = a ] |
- 有個指令 exit 也可以自行設定退出狀態,若沒有指定,則會使用上一個指令的退出狀態當作退出狀態,即
exit $?
Shell Script 也可以 debug
- -v 參數可以先印出 script 內容,在執行 shell
- -x 參數可以 debug,會逐行指示過程
空命令:
在 if (或是 else, elif)中若沒有內容需要執行,需要加一個空命令 :
,否則會報錯誤(類似 Python 中的 pass。):
1 |
|
&& 與 ||
- 在單行指令中使用 if 並不直覺,我們可以使用 && 與 || 來強化我們的指令
- A && B : 當 A 指令成功則執行 B 指令(A 的退出狀態不為 0 時則不執行 B)
- A || B : 當 A 指令執行失敗時,才執行 B
例如:
1 | grep -irn "target_str" dic.txt || echo "Couldn't not find" |
僅有在 grep 未搜尋到內容時,才會印出 Couldn’t not find。
迴圈
$* 與 $@
我們有時候會透過 $*
取得全部的參數並且使用迴圈迭代,但如果我們的參數中是 a b 'c d'
三個參數,會因為 $*
是把 $1, $2, … 取出來,最後變成 a b c d 丟給迴圈迭代:
1 | for arg in $* |
以上的程式碼會印出
1 | a |
此時我們可以用 "$@"
來改善,shell 會將其替換為 “$1”, “$2” …
1 | for arg in "$@" |
以上的程式碼會印出:
1 | a |
切記!若 “$@” 沒有加上雙引號,那麼 $@ 以及 $* 的結果是一樣的。
break n
- Shell 的 break 可以一次跳出不止一層迴圈,在 break n 指定即可
- 若沒有指定 n 即跳出一層迴圈
迴圈次數的幾種指定方式
- $(seq minimum maximum)
- {minimum..maximum}
- {minimum..maximum..step}
- (( EXP1; EXP2; EXP3 ))
讀寫資料
$$ 變數
$$
變數用來顯示當前 PID。
寫 script 時可能會產生一些暫存檔,如果大家都同時在用這支 script,那麼 race condition 可能會發生。要避免的方式就是讓每一次執行所產生的暫存檔名稱都不一樣,那麼使用 $$
變數就會是一個好方法。
產生暫存檔較好的方式:
1 | echo "This is tmp file" > /tmp/tmpfile_$$ |
環境
區域變數
在 Shell 的環境中,每執行一支 script 都會產生一個屬於該 script 自己的空間(可以想成是產生一個新的 process),該空間與外界互不干擾:
1 | ☁ cat s.sh |
s.sh
腳本看不到外界的變數,外界也看不到它的變數。
輸出變數
承上述,如果我們想要將目前的變數與 子 Shell
共享時,我們可以使用 export
指令
1 | ☁ export x |
- export -p :查看目前 export 的變數列表
目錄以及環境
有了 子 Shell
的概念後,不難理解在 script 中變換目錄對於外界的 Shell 來說是沒有影響力的,因為彼此之間擁有獨立的空間。
那如果想要共享環境,用當前的 Shell 去執行 script 時呢?
.
指令提供了這件事(其實就是 source 指令)。
1 | 03:11:58 yiyu@afra shell → pwd |
() 與 {}
這兩種包裹指令的方式可以讓指令一個接著一個執行下去,差異在於前者是在子 shell 中執行,後者是在當前 shell 中執行。
- ():產生新的 Shell 環境來執行
1 | 03:27:54 yiyu@afra shell → pwd |
- {}:使用當前環境執行。切記,指令前後必須要空格,最後一個指令後面必須要有分號
1 | 03:28:29 yiyu@afra shell → pwd |
再論參數
字串格式
1 | ${#string} |
可以用來計算字串,非常實用
- $0 可以用來顯示當前的程式名稱,時常被用來顯示當前 Shell 為何
樣式配對結構
1 | var=hel___lo~ |
後記
熟悉 Shell 的一些知識與概念之後,寫 script 時不但能用較為優雅的方式來撰寫,也能增加 script 的靈活度。對於現在 CI/CD 流行的時代,Shell 幾乎是不可或缺的技術之一。這篇文章除了提供自己在日後可以快速方便的查閱以外,也提供有使用過 Shell 但是一直沒有深入了解過的各位朋友們!