前言

最近寫了很多腳本,但是有太多參數會忘記了,這篇文章主要是整理了一些常用的 bash 腳本技巧,包括變數、邏輯判斷、迴圈、重導向、子殼層、Here Documents 等等。

1. Built-in Variables

1
2
3
4
5
6
7
$0, $1, $2 # 第 n 個參數,即 argv[n]
$# # 參數數量,即 argc
$@ # 所有的參數
$* # 所有的參數,但是會把所有參數當成一個字串 可以搭配 IFS (Internal Field Separator) 使用來分割
$? # 上一個指令的回傳值
$$ # 目前程式的 pid
$! # 上一個在背景執行的程式的 pid

2. Bash Builins

數學運算,這兩個寫法等價。

1
2
echo $((1+1))
echo $[1+1]

變數,{} 可省略

1
2
3
4
5
6
name="Shannon"
echo $name
echo ${name}

# 字串長度
echo ${#name} # 7

指令替換,兩個寫法等價

1
2
echo $(whoami)
echo `whoami`

展開

1
2
echo {1..5} // 1 2 3 4 5
echo {1..10..2} // 1 3 5 7 9

3. Logic Statement

binary comparison operator 常見可以參考 Advanced Bash-Scripting Guide

1
2
3
4
# 前面成功執行的話,後面才會執行
echo hi && echo john
# 前面指令失敗的話,後面才會執行
grep 'notfound' /dev/null || echo 'Not found'

字串用 ><= 一類的符號,數字則是用英文縮寫.詳細可以參考 3.1, 3.2 章節

1
2
3
if [ $age -ge 65 ] && [ $name == "John" ]; then
echo '...'
fi

3.1 Boolean Operation

1
2
3
!     #not
-a #and
-o #or
1
2
3
4
5
6
7
8
# 變數 num 比 10 小 『或是』 比 100 大
if [ $num -lt 10 -o $num -gt 100 ]
then
echo "Number $num is out of range"
elif [ ! -w $filename ]
then
echo "Cannot write to $filename"
fi

3.2 Integer Opteraion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -eq: is equal to
if [ "$a" -eq "$b" ]

# -ne: is not equal to
if [ "$a" -ne "$b" ]

# -gt: is greater than
if [ "$a" -gt "$b" ]

# -ge: is greater than or equal to
if [ "$a" -ge "$b" ]

# -lt: is less than
if [ "$a" -lt "$b" ]

# -le: is less than or equal to
if [ "$a" -le "$b" ]

# < is less than (within double parentheses) 要使用雙括弧
(("$a" < "$b"))

# <= is less than or equal to (within double parentheses)
(("$a" <= "$b"))

# > is greater than (within double parentheses)
(("$a" > "$b"))

# >= is greater than or equal to (within double parentheses)
(("$a" >= "$b"))

3.3 String Comparision

在 Bash 中,=== 都用來進行字串比較,但它們之間有一些微妙的差異,特別是在單中括號([ ])和雙中括號([[ ]])中的行為有所不同。

= 比較符號

單中括號 [ "$a" = "$b" ]

  • 語法:[ "$a" = "$b" ]
  • 功能:這是最基本的字串比較,用於檢查兩個字串是否相等。
  • 注意事項:一定要注意空格。在 [ "$a" = "$b" ] 中,= 兩側必須有空格。如果缺少空格,如 [ "$a"="$b" ],這樣的比較會導致錯誤,因為 = 會被誤認為是命令的一部分。

範例:

1
2
3
4
5
a="apple"
b="apple"
if [ "$a" = "$b" ]; then
echo "a 和 b 相等"
fi

== 比較符號

單中括號 [ "$a" == "$b" ] 和雙中括號 [[ "$a" == "$b" ]]

  • 語法:[ "$a" == "$b" ][[ "$a" == "$b" ]]
  • 功能:== 在某些情況下可以用來比較字串,但在單中括號([ ])中,=== 基本上是等效的。唯一的區別在於,== 可以在雙中括號([[ ]])中使用更多的功能,並且會支援模式匹配(pattern matching)。
  • 注意事項:
    • 在單中括號中,=== 是相同的,兩者都用來比較字串。
    • 在雙中括號中,== 支援字串的模式匹配(例如通配符匹配),這是 = 所不支援的。
    • 單中括號中的 == 行為類似於 =,但在雙中括號中,== 可以進行模式匹配。

範例(單中括號):

1
2
3
4
5
a="apple"
b="apple"
if [ "$a" == "$b" ]; then
echo "a 和 b 相等"
fi

範例(雙中括號,模式匹配):

1
2
3
4
a="apple"
if [[ "$a" == a* ]]; then
echo "a 以 'a' 開頭"
fi

=== 的差異

  • 單中括號 [ ]
    • [ "$a" == "$b" ][ "$a" = "$b" ] 中,=== 基本上是等效的。
    • 它們都比較字串是否相等。
  • 雙中括號 [[ ]]
    • [[ "$a" == "$b" ]] 中,== 會進行模式匹配,因此支持通配符(如 *?)。
    • = 只是做簡單的字串比較,不支持模式匹配。

4. Loop

for each,這兩種 array 存取值的寫法是等價的

1
2
3
4
5
6
7
8
9
10
11
arr=(Alice Bob Oscar)

# method 1
for i in {0..2}; do
echo ${arr[i]}
done

# method 2
for i in "${arr[@]}"; do
echo $i
done

也可以使用 C-style for loop

1
2
3
for (( i=1; i<=5; i++ )); do
echo $i
done

while Loop

1
2
3
4
5
i=0
while [ $i -lt 5 ]; do
echo $i
((i++)) # 雙括號裡面可以寫 C-style 的運算式
done

5. Redirection

1
2
3
4
5
6
7
8
echo john | grep 'john' # pipe,第一個指令的 stdout 作為第二個指令的 stdin
echo hi > /tmp/text # stdout 輸出導向檔案並『覆寫』
echo hi >> /tmp/text # stdout 輸出導向檔案,『附加』在該檔案內容後
echo hi 2>&1 # stderr 導向 stdout
echo hi 1>&2 # stdout 導向 stderr
echo hi &> /tmp/text # stdout 與 stderr 都導向檔案
cat < /tmp/text # stdin 改從檔案讀入:這個命令使用了輸入重定向(input redirection),將 /tmp/text 檔案的內容作為標準輸入(stdin)傳遞給 cat 命令。輸出結果與cat /tmp/text 無異
cat <(echo hi) # 把括號中的輸出丟到 cat 指令當 stdin

6. Subshell (bash parallel script)

這段內容主要解釋了 子殼層 (subshell)父殼層 (parent shell) 之間的執行行為,並說明了如何使用 & 來達到並行執行的效果。

基本的子殼層 (() 的用法)

1
2
(sleep 5)
echo done
  • (sleep 5) 這段程式碼會在一個 子殼層 中執行。子殼層是一個新的 shell 進程,它會從父殼層繼承環境變數等設定,但執行的指令會在這個子殼層中處理。
  • sleep 5 會讓這個子殼層暫停 5 秒。
  • 然後,echo done 在父殼層中執行。

注意:

  • 如果你只是寫 (sleep 5),父殼層會等待這個子殼層完成後再繼續執行下面的指令(即 echo done)。這是因為沒有使用 &,父殼層會「等待」子殼層執行完成。
  • 所以,這段程式碼的結果是:子殼層執行 sleep 5,父殼層會等待 5 秒,然後才顯示 done

使用 & 讓父殼層不等待子殼層

1
2
(sleep 2; echo child) &
echo parent
  • (sleep 2; echo child) 這段程式碼放在子殼層中執行,並且加上了 &,表示 將子殼層的執行放到背景進行
  • & 讓父殼層不會等子殼層執行完成,而是會繼續執行後續的指令。
  • echo parent 在父殼層中立即執行。

結果:

  • 子殼層會執行 sleep 2 然後顯示 child,這需要 2 秒鐘。
  • 父殼層會立即執行 echo parent,所以會先顯示 parent
  • 兩個 echo 命令是並行執行的,這就達到了平行處理的效果。

7. Here Documents

什麼是 “Here Document”?
這個名稱的概念來自:「這裡 (here) 就有一份文件 (document)」 → 你不需要從外部讀取,而是直接在腳本內提供多行內容。對應傳統 Shell Script 的標準輸入 (stdin) → 通常程式需要用 < file.txt 讀取外部檔案,但 Here Document 讓你直接內嵌資料。

📌 具體比較

方法 輸入來源 描述
< file.txt 外部檔案 讀取 file.txt 內容當作標準輸入
<<EOF ... EOF 內嵌內容 直接在腳本內提供多行內容

傳統方式(讀取外部檔案)

1
cat < myfile.txt

Here Document(直接提供內容)

1
2
3
cat <<EOF
這是一段內嵌的內容
EOF

總結來說:
Here Document 讓 Shell Script 不需要額外的檔案,而可以直接在 “這裡 (here)” 定義輸入,這就是它名稱的由來!

什麼是EOF?
EOF 的全名是 “End of File”,意思是「檔案結束」。在 Here Document 的語境中,EOF 只是個常見的 標記 (delimiter),它本身沒有特別的語法意義。你可以用任何字串作為結束標記,但 EOF 是最常見的約定俗成的標記,因為它表示「這段輸入的結束」。

例如:

1
2
3
4
5
6
7
8
9
10
cat <<MYMARKER
這是 Here Document 的內容
可以寫多行
MYMARKER

# 或是使用 <<- 可以使用縮排來提高可讀性
cat <<-MYMARKER
這是 Here Document 的內容
可以寫多行
MYMARKER

Refernece