Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
..
Failed to load latest commit information.
README.md

README.md

bash-handbook CC 4.0

這份文件是為想要學習 Bash 但是又不想要專研太深的人。

Tip:不妨嘗試 learnyoubash — 一個基於本文件的互動教學平台!

Node 套件手稿

你可以使用 npm 來安裝這份手冊。只要執行:

$ npm install -g bash-handbook

你現在可以在命令執行 bash-handbook。它會開啟你所選的 $PAGER。反之,你也可以繼續在這裡閱讀。

來源是這裡:https://github.com/denysdovhan/bash-handbook

翻譯

目前翻譯這個 bash-handbook 的版本有:

徵求其他語言版本翻譯

目錄

簡介

如果你是一個開發者,你一定知道時間的寶貴,那麼使工作流程更快速簡單便是一件很重要的事情。

為了讓工作更有效率及更高的產能,常常會有些重複繁瑣的事情,像是:

  • 截圖並把圖片上傳到伺服器。
  • 需要處理各式各樣有著不同格式的文件。
  • 在不同檔案格式間進行轉換。
  • 解析程式的輸出。

進入 Bash 的世界,coder 們的救星!

Bash 是一個由 Brain Fox 為 GNU 專案開發的 Unix Shell,這是一個為取代 Bourne shell 而開發的自由軟體,最初在 1989 年被發布,並且 Bash 也作為 Linux 和 macOS 的 default shell 很長一段時間了。

那麼我們為什麼需要學習有著 30 年歷史的東西呢?答案很簡單:這個東西是當今最強大的且易於攜帶的工具,在所有 Unix-based 的作業系統撰寫有效率的的腳本程式。而這就是你需要學習 bash 的原因。

在這個手冊中,我會透過範例去說明 bash 最重要的概念。而我也希望這篇概略性的文章對你會有幫助。

關於 Shell 以及 Shell 的模式

使用者 bash shell 可以工作在兩種模式 - 互動模式及非互動模式。

互動模式

如果你是在 Ubuntu 上工作,你有七個虛擬終端機可以讓你使用。 桌面環境是被放在第七個終端機中, 所以你可以使用 Ctrl-Alt-F7 快捷鍵來回到友善的 GUI 介面。

你可以使用 Ctrl-Alt-F1 來開始使用 shell。在那之後,熟悉的 GUI 介面將會消失,取而代之的則是虛擬終端機。

如果你看到以下的東西,代表你處在互動模式:

user@host:~$

你可以輸入各種 Unix 的命令,像是 lsgrepcdmkdirrm,然後看看他們執行後的結果。

我們稱這個為互動模式,因為 shell 是直接與使用者作互動。

但使用虛擬終端機實在是不怎麼方便。例如,假設你想要同時編輯文件和執行其他命令,在這樣的情況下,使用虛擬終端機模擬器(virtual terminal emulators)會比較好,像是:

非互動模式

在非互動模式中,shell 從檔案或是 pipe 讀取到命令然後執行他們。當直譯器到達檔案的末端(EOF, End Of Line)時,shell 程式終止 session 並返回到父程式。

使用以下的命令來執行,使 shell 在非互動模式中執行:

sh /path/to/script.sh
bash /path/to/script.sh

在上面的例子中,script.sh 其實只是一個普通的檔案,而 shell 直譯器可以解析檔案中的命令;shbash 是 shell 的直譯程式。你可以使用你喜歡的編輯器來建立 script.sh(例如:vim、nano、Sublime Text、Atom 等等)。

除此之外,你還可以透過 chmod 命令給予檔案執行權限,使檔案可以執行:

chmod +x /path/to/script.sh

另外,在這個 script 第一行必須指定應該使用哪個程式來執行這個檔案,像是:

#!/bin/bash
echo "Hello, world!"

或者,如果你偏好使用 sh 而不是 bash,將 #!/bin/bash 改成 #!/bin/sh。這個 #! 字元序列被稱為 shebang。現在你可以這樣執行這個 script:

/path/to/script.sh

我們使用上面簡單方便的技巧使用 echo 將文字印到螢幕終端。

另一個使用 sheban 的方式:

#!/usr/bin/env bash
echo "Hello, world!"

使用 shebang 的好處是可以基於環境變數 PATH 來尋找程式(這個例子是 bash)。相較於上面的第一種方法,應該使用這個方法比較好,因為我們不知道程式被放在系統的什麼地方。這樣好處是,如果 PATH 變數在系統上可能被設定到其他不同版本的程式上。例如,安裝一個新版本的 bash 同時保留原來版本的 bash,然後把新版本的路徑加入環境變數 PATH 中。使用 #!/bin/bash 會導致使用到原來的 bash,而使用 #!/usr/bin/env bash 會使用到新的版本。

Exit codes

每個命令都會回傳一個 exit code回傳狀態結束狀態)。一個成功的命令始終回傳 0(零代碼),而失敗總是回傳非零值(錯誤代碼)。錯誤代碼必須介於 1 到 255 的正整數。

撰寫 script 時,我們可以使用另一個方便的 exit 命令。使用這個命令來終止目前的執行,並向 shell 提供一個 exit code。執行一個不帶有任何參數的 exit code,會終止執行目前的 script 並回傳在 exit 之前的最後一個命令的 exit code。

當程式終止時,shell 會把 exit code 指派給環境變數 $?$? 變數是我們測試一個 script 是否成功執行。

用同樣的方式,我們可以使用 exit 來停止一個 script,我們能在函式中使用 return 來退出函式並回傳 exit code 給呼叫者,又或者你可以在函式中使用 exit,這會退出函式 結束程式。

註解

Script 中可以包含註解。註解是一個特殊的語句,會被 shell 直譯器忽略。它們以一個 # 符號為開頭。

例如:

#!/bin/bash
# 這個 script 將會列印你的使用者名稱。
whoami

Tip: 使用註解可以解釋你的 script 做了什麼,以及為什麼這麼做。

變數

與大部分的程式語言一樣,你可以在 bash 內建立變數。 Bash 中沒有資料類型的概念。變數只能是純數字或是一或多個字元所組成的字串。你可以建立三種類型的變數:區域變數、環境變數、以及作為命令列引數的變數。

區域變數

區域變數是只存在於單一 script 的變數,它們是無法讓其他程式或 script 存取的。 一個變數可以使用 = 來宣告(根據規則,在變數名稱、 = 、變數的值之間不應有空格的存在),你可以透過 $ 來取得這個變數的值。例如:

username="denysdovhan"  # 宣告變數
echo $username          # 顯示變數
unset username          # 刪除變數

我們也可以使用 local keyword 在一個 function 中宣告一個區域變數。當 function 結束時,這個區域變數就會消失。

local local_var="I'm a local value"

環境變數

環境變數執行在目前 shell session 可以讓任何程式或 script 存取的變數。建立它們與區域變數類似,但使用 export 作為前綴

export GLOBAL_VAR="I'm a global variable"

bash 中有許多全域變數。你會經常遇到這些全域變數,以下是最常使用到的變數查閱表︰

變數 描述
$HOME 目前使用者的家目錄
$PATH shell 以這些冒號分隔的目錄清單尋找命令
$PWD 目前的工作目錄
$RANDOM 0 到 32767 之間的隨機整數
$UID 數字類型,使用者的 ID
$PS1 主要的提示字串
$PS2 次要的提示字串

根據這個連結,可以查看更多在 Bash 內的環境變數列表。

命令列引數

命令列引數是當程式執行時,變數根據執行程式時,程式名稱後所跟隨的參數而得。下面表格列出了當你在程式執行時,命令列引數和其他特殊變數它們所代表的意義。

參數 描述
$0 Script 的名稱
$1 … $9 從第 1 個到第 9 個參數
${10} … ${N} 從第 10 個到第 N 個參數
$*$@ 除了 $0 以外的所有參數
$# 不包含 $0的參數數量
$FUNCNAME function 名稱(只有一個變數在 function 內部)

在下面的範例,命令列引數會是:$0='./script.sh'$1='foo'$2='bar'

./script.sh foo bar

變數也可以有預設值。我們可以使用以下的語法來定義預設值:

 # 如果變數是空值,分配一個預設值。
: ${VAR:='default'}
: ${$1:='first'}
#
FOO=${FOO:-'default'}

Shell expansion

Expansion 被劃分為 token 後,會在命令列上執行。換句話說,這些 expansion(展開) 是一種機制來計算算術運算,以儲存命令的執行結果等等。

如果你有興趣的話,你可以閱讀更多關於 shell expansions

括號展開

括號展開讓我們可以產生任意字串。它類似於檔案名稱展開。例如:

echo beg{i,a,u}n # begin began begun

括號展開也可以用在建立一個 loop iterated 的範圍。

echo {0..5} # 0 1 2 3 4 5
echo {00..8..2} # 00 02 04 06 08

命令替代

命令替代允許我們對執行指令,並將指令的回傳值作為參數放進其他命令中,或是將回傳值指派到變數當中,當命令被 ``$() 包住時,命令替代將會執行。例如,我們可以使用它如下所示:

now=`date +%T`
#
now=$(date +%T)

echo $now # 19:08:26

算術代入展開

在 bash 裡我們可以很輕鬆的做到任何算術運算。但是表達式必須放在 $(( )) 裡面,算術代入展開的格式為:

result=$(( ((10 + 5*3) - 7) / 2 ))
echo $result # 9

在算術代入展開內,變數不需要 $ 前綴:

x=4
y=7
echo $(( x + y ))     # 11
echo $(( ++x + y++ )) # 12
echo $(( x + y ))     # 13

單引號和雙引號

單引號和雙引號有很重要的區別。在雙引號內的變數或是命令替代會被代入展開。在單引號內則不會。例如:

echo "Your home: $HOME" # Your home: /Users/<username>
echo 'Your home: $HOME' # Your home: $HOME

區域變數和環境變數包含空格時,它們在引號內的展開要格外小心。隨便舉個例子,使用 echo 來印出一些使用者的輸入:

INPUT="A string  with   strange    whitespace."
echo $INPUT   # A string with strange whitespace.
echo "$INPUT" # A string  with   strange    whitespace.

第一個 echo 調用了 5 個獨立的參數 — $INPUT 被拆成獨立的單字,echo 在每個單字之間列印一個空白字元。在第二種情況中,echo 調用了一個參數(整個 $INPUT,包含空白)。

現在來看看一個更嚴重的例子吧:

FILE="Favorite Things.txt"
cat $FILE   # 嘗試印出兩個檔案的內容:`Favorite` 和 `Things.txt`。
cat "$FILE" # 列印一個檔案的內容: `Favorite Things.txt`。

在這個範例的問題可以透過把 FILE 重新命名成 Favorite-Things.txt 來解決,考慮輸入進來的環境變數、位置參數,或是命令的輸出(findcat 等等)。如果輸入可能包含空格,請記得使用引號包起來。

陣列

與大部分的程式語言一樣,在 bash 一個陣列是一個變數,讓你可以指向多個數值。在 Bash 中,陣列是從 0 開始,意思是陣列第一個元素的索引值是 0 。

當處理陣列時,我們應該要知道一個特殊的環境變數 IFSIFSInput Field Separator,是陣列中的元素之間的分隔符號。預設的 IFS 是一個空格 IFS=' '

陣列宣告

在 Bash 你可以通過簡單地賦值給陣列變數的索引來建立陣列。

fruits[0]=Apple
fruits[1]=Pear
fruits[2]=Plum

也可以使用複合賦值建立陣列變數,像是:

fruits=(Apple Pear Plum)

陣列展開

個別陣列元素可以像其他變數一樣展開:

echo ${fruits[1]} # Pear

可以把 *@ 放在陣列索引值的地方將陣列展開:

echo ${fruits[*]} # Apple Pear Plum
echo ${fruits[@]} # Apple Pear Plum

以上兩行有很重要且微妙的差別,假設陣列的元素值中包含空白:

fruits[0]=Apple
fruits[1]="Desert fig"
fruits[2]=Plum

我們想要將陣列中的元素隔行印出,所以我們試著用看看內建函數 printf

printf "+ %s\n" ${fruits[*]}
# + Apple
# + Desert
# + fig
# + Plum

為什麼 Desertfig 被分開了?讓我們嘗試使用雙引號括起來:

printf "+ %s\n" "${fruits[*]}"
# + Apple Desert fig Plum

現在所有元素都被放在同一行了 - 這不是我們想要的!為了解決這個問題,接著試試 ${fruits[@]} 吧:

printf "+ %s\n" "${fruits[@]}"
# + Apple
# + Desert fig
# + Plum

在雙引號中,${fruits[@]} 將陣列每個元素視為成獨立的參數;保留了陣列元素中的空白。

切割陣列

除此之外,我們可以使用 slice 運算符來取出陣列中某個部份:

echo ${fruits[@]:0:2} # Apple Desert fig

在上面的範例,${fruits[@]} expands 整個陣列的內容,而 :0:2 取出從索引為 0,長度為 2 的元素。 (譯者註:因此會從陣列位置 0 開始取 2 個元素)

把一個物件加入到陣列中

在一個陣列中新增元素也是相單簡單的。在這種情況下複合賦值特別有用。我們可以像這樣使用︰ (譯者註:複合賦值-例如在 a = a + 1 中,就是將 a 的值帶入運算式中,再將值指派回原本的變數)

fruits=(Orange "${fruits[@]}" Banana Cherry)
echo ${fruits[@]} # Orange Apple Desert fig Plum Banana Cherry

上面的範例中,${fruits[@]} 展開整個陣列的內容,並替換 fruits 然後帶入複合賦值,分配新的值到 fruits 陣列,修改原始的值。

把一個物件從陣列中移除

如果要從一個陣列中刪除一個元素,使用 unset 命令:

unset fruits[0]
echo ${fruits[@]} # Apple Desert fig Plum Banana Cherry

Streams、pipes 和 lists

Bash 在處理程式及輸出有非常強大的功能可以用。使用 streams 可以把程式的輸出導向到另一個程式的 input 或是一個檔案中,這讓我們可以方便的寫入任何我們想要的東西,或者是程式的紀錄檔。

Pipes 給我們建立 conveyor 的機會和控制命令的執行。(譯者註:converyor 為輸送的概念)

最重要的是我們瞭解如何使用這個強大和複雜的工具。

Streams

Bash 接收輸入並將輸出作為序列或字元的 streams 。這些 streams 可能會被重導向到檔案或其他 streams。

這裡有三個描述:

代碼 描述符 描述
0 stdin 標準輸入
1 stdout 標準輸出
2 stderr 錯誤輸出

重導讓我們可以控制命令的輸入來自哪裡,和輸出到哪去。在重導向 streams 時會用到以下運算子:

運算子 描述
> 重新導向輸出
&> 重新導向輸出和錯誤輸出
&>> 以附加形式重新導向輸出和錯誤輸出
< 重新導向輸入
<< Here 文件語法
<<< Here 字串

這裡有一些重導向的範例:

# 將 ls 的輸出寫入到 list.txt。
ls -l > list.txt

# 將輸出附加到 list.txt。
ls -a >> list.txt

# 所有錯誤將會被寫入到 error.txt。
grep da * 2> errors.txt

# 從 errors.txt 讀取。
less < errors.txt

Pipes

我們不只可以在檔案可以重導 streams,也可以在其他的程式中。Pipes 也能將輸出作為其他程式的輸入。

在下方的範例,command1 把輸出導向到 command2,然後 command2 再將輸出作為 command3 的輸入:

command1 | command2 | command3

這樣的結構稱為 pipelines

實際上,這可以用來在多個程式中依序處理資料。例如,ls -l 的輸出傳送到 grep,只列印出附檔名為 .md 的檔案,然後這個輸出最終傳到 less

ls -l | grep .md$ | less

pipeline 的 exit status 通常是 pipeline 中最後一個命令的 exit status。shell 不會回傳狀態,直到所有在 pipeline 的命令都完成。如果任何在 pipeline 內的命令失敗,你應該設定 pipefail 選項:

set -o pipefail

命令序列

命令序列是由 ;&&&|| 運算符分隔一個或多個 pipeline 序列。

如果一個命令是以 & 作為結尾,shell 會在 subshell 中非同步執行命令。換句話說,這個命令會在背景執行。

命令透過 ; 隔開依序執行:一個接著一個指令執行直到所有命令完成。

# command2 會在 command1 執行完後被執行。
command1 ; command2

# 等同於這個方式。
command1
command2

&&|| 分別稱為 ANDOR 序列。

AND 序列 看起來像是:

# 只有在 command1 結束成功時(回傳 exit status 為 0 ),command2 才會被執行。
command1 && command2

OR 序列 的形式:

# 只有在 command1 未成功完成時(回傳錯誤代碼 ),command2 才會被執行。
command1 || command2

一個 ANDOR 序列回傳的狀態碼,是最後一個執行命令的狀態碼。

條件陳述式

與大部分的程式語言一樣,Bash 中的條件陳述讓我們決定是否執行某些程式。結果取決於在 [[ ]] 內的運算式。

條件表達式可以包含 &&|| 運算符,就是 ANDOR。除此之外,這裡還有許多其他方便的表達式

有兩種不同條件陳述:ifcase

Primary 和組合表達式

[[ ]](或 sh 中的 [ ])中的表達式稱為 測試命令primaries。這些表達式幫助我們表明條件的結果。在下表中,我們使用 [ ],因為它也可以在 sh 執行。這裡有關於在 bash 中,雙引號和單引號的差別的一些回答。

檢查檔案系統中的檔案或目錄的狀態:

Primary 涵義
[ -e FILE ] 如果 FILE 存在(exists),為 Ture
[ -f FILE ] 如果 FILE 存在且為一個普通的文件(file),為 True
[ -d FILE ] 如果 FILE 存在且為一個目錄(directory),為 True
[ -s FILE ] 如果 FILE 存在且不為空(size 大於 0),為 True
[ -r FILE ] 如果 FILE 存在且有讀取權限(readable),為 True
[ -w FILE ] 如果 FILE 存在且有寫入權限(writable),為 True
[ -x FILE ] 如果 FILE 存在且有執行權限(executable),為 True
[ -L FILE ] 如果 FILE 存在且為符號連結(link),為 True
[ FILE1 -nt FILE2 ] FILE1 比 FILE2 新(newer than)
[ FILE1 -ot FILE2 ] FILE1 比 FILE2 舊(older than)

檢查字串狀態:

Primary 涵義
[ -z STR ] STR 為空(長度為 0,zero)
[ -n STR ] STR 不為空 (長度不為 0,non-zero)
[ STR1 == STR2 ] STR1STR2 相等
[ STR1 != STR2 ] STR1STR2 不相等

二進制的算術運算符:

Primary 涵義
[ ARG1 -eq ARG2 ] ARG1ARG2 相等(equal)
[ ARG1 -ne ARG2 ] ARG1ARG2 不相等(not equal)
[ ARG1 -lt ARG2 ] ARG1 小於 ARG2less than)
[ ARG1 -le ARG2 ] ARG1 小於等於 ARG2less than or equal)
[ ARG1 -gt ARG2 ] ARG1 大於 ARG2greater than)
[ ARG1 -ge ARG2 ] ARG1 大於等於 ARG2greater than or equal)

條件式也可以 組合 在一起使用:

運算符 效果
[ ! EXPR ] 如果 EXPR 為 false,則為 True
[ (EXPR) ] 回傳 EXPR 的值
[ EXPR1 -a EXPR2 ] AND 邏輯。如果 EXPR1 and EXPR2 為 Ture,則為 True
[ EXPR1 -o EXPR2 ] OR 邏輯。如果 EXPR1 or EXPR2 為 Ture,則為 True

還有許多有用的 primariy 你可以在 Bash man 網頁輕鬆的找到它們。

使用 if 陳述

if 使用方式就像其他程式語言一樣。如果表達式括號內為 true,在 thenfi 之間的程式碼會被執行。fi 說明根據條件所執行的程式碼已經結束。

# 單行
if [[ 1 -eq 1 ]]; then echo "true"; fi

# 多行
if [[ 1 -eq 1 ]]; then
  echo "true"
fi

同樣的,我們可以使用 if..else,像是:

# 單行
if [[ 2 -ne 1 ]]; then echo "true"; else echo "false"; fi

# 多行
if [[ 2 -ne 1 ]]; then
  echo "true"
else
  echo "false"
fi

有時候 if..else 不能滿足我們的需要。在這個情況下,別忘了 if..elif..else,使用起來也是很方便。

看看下面的例子︰

if [[ `uname` == "Adam" ]]; then
  echo "Do not eat an apple!"
elif [[ `uname` == "Eva" ]]; then
  echo "Do not take an apple!"
else
  echo "Apples are delicious!"
fi

使用一個 case 陳述

如果你面對的是好幾個不同情況,需要採取不同的行動,那麼使用 case 陳述或許會比巢狀化的 if 陳述來的有用。下方是使用 case 來處理更多複雜的條件:

case "$extension" in
  "jpg"|"jpeg")
    echo "It's image with jpeg extension."
  ;;
  "png")
    echo "It's image with png extension."
  ;;
  "gif")
    echo "Oh, it's a giphy!"
  ;;
  *)
    echo "Woops! It's not image!"
  ;;
esac

每個 case 都會對應到一個條件。| 符號是被用來分離多個條件,而 ) 運算符是條件的結尾。第一個符合的命令會被執行。* 代表不對應以上所給的任一條件。每個命令區塊之間需要透過 ;; 隔開。

迴圈

這裡我們不必感到驚訝。與任何程式語言一樣,在 bash 之中,如果判斷式結果為 true,就會迭代迴圈。

在 Bash 有四種類型的迴圈:forwhileuntilselect

for 迴圈

for 非常相似於 C 的 for 迴圈。它看起來像是:

for arg in elem1 elem2 ... elemN
do
  # statements
done

在每個迴圈中,arg 會依序被賦予 elem1elemN 的值。值可能是萬用字元或括號展開

當然,我們也可以將 for 迴圈寫成一行,但在這個情況我們需要在 do 之前放一個分號,像是這樣:

for i in {1..5}; do echo $i; done

順便一提,如果你覺得 for..in..do 看起來很奇怪,你也可以撰寫 C 語言風格的 for 迴圈,像是:

for (( i = 0; i < 10; i++ )); do
  echo $i
done

當我們想要將目錄下每個文件做相同的操作時,for 是相當方便的。例如,如果我們需要移動所有的 *.bash 檔到 script 資料夾,並給予它們可執行的權限,我們的 script 可以像是這樣:

#!/bin/bash

for FILE in $HOME/*.bash; do
  mv "$FILE" "${HOME}/scripts"
  chmod +x "${HOME}/scripts/${FILE}"
done

while 迴圈

只要該條件為 truewhile 迴圈測試一個條件,並遍歷序列的命令。被檢測的條件跟 if.. then 中使用的 primary 並沒有差別。所以 while 迴圈看起來會像是:

while [[ condition ]]
do
  # statements
done

就像 for 迴圈一樣,如果我們想要將 do 和條件寫成一行的話,我們必須在 do 之前加上分號。

一個處理範例如下所示:

#!/bin/bash

# 0 到 9 每個數字的平方。
x=0
while [[ $x -lt 10 ]]; do # x 小於 10。
  echo $(( x * x ))
  x=$(( x + 1 )) # x 加 1。
done

until 迴圈

until 迴圈和 while 迴圈完全相反的。它會像 while 一樣檢查條件,迴圈持續到表達式為 False 停止:

until [[ condition ]]; do
  #statements
done

select 迴圈

select 迴圈幫助我們組織一個使用者功能表。它和 for 迴圈語法幾乎相同:

select answer in elem1 elem2 ... elemN
do
  # statements
done

select 會將 elem1..elemN 跟隨著序號列印出來提示使用者。然後將選單的提示文字設定在 PS3 當中。在上面的例子當中,使用者的輸入會存於 answer 中。如果 answer 是介於 1..N 的數字,那敘述句就會被執行且 select 會因為 do-loop 的關係而循環,因此我們應該使用 break 來避免無窮迴圈。

一個處理範例如下所示:

#!/bin/bash

PS3="Choose the package manager: "
select ITEM in bower npm gem pip
do
  echo -n "Enter the package name: " && read PACKAGE
  case $ITEM in
    bower) bower install $PACKAGE ;;
    npm)   npm   install $PACKAGE ;;
    gem)   gem   install $PACKAGE ;;
    pip)   pip   install $PACKAGE ;;
  esac
  break # 避免無窮迴圈。
done

在這個範例中,詢問使用者想要使用哪套 package manager 安裝套件,並詢問我們需要安裝什麼套件,最後會執行安裝:

如果我們執行,我們可以得到:

$ ./my_script
1) bower
2) npm
3) gem
4) pip
Choose the package manager: 2
Enter the package name: bash-handbook
<installing bash-handbook>

迴圈控制

在某些情況下,我們需要在正常結束前或跳過某次迭代來結束迴圈。在這個情況下,我們可以使用 shell 內建的 breakcontinue 語句。這兩個語法都可以在前述的每一種迴圈執行。

break 語句被用於在迴圈結束之前,退出目前迴圈。我們之前已經看過了。

continue 語句跳過一個迭代。我們可以像這樣使用它:

for (( i = 0; i < 10; i++ )); do
  if [[ $(( i % 2 )) -eq 0 ]]; then continue; fi
  echo $i
done

如果我們執行上方的範例,我們可以列印出 0 到 9 間的奇數。

函式

在 script 中,就像任何其他程式語言一樣,我們有能力定義 function 和呼叫 function。在 Bash 中的 function 是一個程式碼區塊,但是有些不同。

在 Bash 中的 function 就是將一堆指令包裝成一個 name,而這個 name 就是 function name,呼叫一個 function 就像在其他程式語言一樣,你只要執行 function 的名稱,function 就會被 調用

我們可以用這種方式宣告我們的 function:

my_func () {
  # statements
}

my_func # 呼叫 my_func。

我們在調用 function 前,必須要先宣告。

Function 可以接受參數並回傳一個結果 — exit code。在 function 內,與非互動模式下的 script 參數處理方式相同 — 使用位置參數。結果代碼可以使用 return 命令來 回傳

以下的 function 需要一個名稱並回傳 0,表示成功執行。

# 帶參數的 function。
greeting () {
  if [[ -n $1 ]]; then
    echo "Hello, $1!"
  else
    echo "Hello, unknown!"
  fi
  return 0
}

greeting Denys  # Hello, Denys!
greeting        # Hello, unknown!

我們已經討論過 exit codesreturn 命令不帶任何最後一個執行命令 exit code 的回傳參數。上面的範例,return 0 會回傳一個成功的 exit code 0

Debug

shell 提供了 debug script 的工具。如果我們想要在 debug 模式執行 script,我們在 script 的 shebang 使用一個特別的選項:

#!/bin/bash options

這個選項是改變 shell 行為的設定。下面是一些可能對你有幫助的選項清單:

簡寫 名稱 描述
-f noglob 禁止檔案名 expansion(globbing)     
-i interactive Script 執行在互動模式
-n noexec 讀取命令,但不執行它們(確認語法是否正確)
pipefail 如果任何命令失敗使 pipeline 失敗,不只是最後一個命令失敗
-t 在第一個命令完成後退出
-v verbose 在執行前,列印每個命令到 stderr
-x xtrace 在執行前,列印每個命令和他的參數到 stderr

例如,我們有個 script 有 -x 選項像是:

#!/bin/bash -x

for (( i = 0; i < 3; i++ )); do
  echo $i
done

這將會列印變數的值到 stdout 以及其他有用的資訊:

$ ./my_script
+ (( i = 0 ))
+ (( i < 3 ))
+ echo 0
0
+ (( i++  ))
+ (( i < 3 ))
+ echo 1
1
+ (( i++  ))
+ (( i < 3 ))
+ echo 2
2
+ (( i++  ))
+ (( i < 3 ))

有時候我們需要 debug script 某個部份。在這種情況下使用 set 命令是很方便的,使用 - 啟用選項,+ 停用選項。

#!/bin/bash

echo "xtrace is turned off"
set -x
echo "xtrace is enabled"
set +x
echo "xtrace is turned off again"

後記

我希望這麼小手冊會是有趣並有幫助的。老實說,我撰寫這個手冊是為了幫助自己不忘記這些基本的 bash。我嘗試透過簡單但有意義的方式,希望你們會喜歡。

這本手冊講述我自己與 Bash 的經驗。它並不全面,所以如果你還需要更多資訊的話,請執行 man bash 並從那裡開始。

非常歡迎你的貢獻,任何指正或問題我都非常感激。這些都可以透過建立一個新 issue 來進行。

感謝你閱讀這本手冊!

想要了解更多嗎?

這裡有一些涵蓋 Bash 的相關資料:

其他資源

  • awesome-bash 是一個 Bash script 和其他資源的列表。
  • awesome-shell 是另一個 Shell 及其他資源的列表。
  • bash-it 為你日常工作使用、 開發和維護 shell script 和自訂命令提供了可靠的框架。
  • dotfiles.github.io 是一個很棒的指南,蒐集了多個 dotfiles 及可用在 bash 及其他 shell 的開發框架。
  • learnyoubash 幫助你撰寫你的第一個 bash script。
  • shellcheck 是一個 shell script 的靜態分析工具。你可以從在 www.shellcheck.net 網頁上或從指令來執行。安裝說明在 koalaman/shellcheck 的 Github repo 頁面。

最後,Stack Overflow 有許多標記為 bash 的問題,當你在卡住時,你可以從那些地方學習或是提問。

License

CC 4.0

© Denys Dovhan