編碼 · 程式語言
Bash 腳本入門教學
簡答
Bash 是 Linux 伺服器、macOS 終端機與 CI 管線中使用的 Unix 命令列 Shell 與腳本語言。Bash 腳本是由 Shell 指令組成的文字檔,搭配變數、條件、迴圈、函式與管線。本頁說明如何撰寫與執行腳本、安全地為變數加引號、流程控制、使用陣列、重新導向輸出,以及用 set -euo pipefail 處理錯誤。
開啟chmod 權限計算Bash 是什麼、可以拿來做什麼
Bash(Bourne Again SHell)是 GNU 專案的命令列 Shell 與腳本語言。它讀取你輸入的指令、執行程式,並用管線與重新導向把它們串接起來。Bash 腳本就是由這些相同指令組成的文字檔,所以你在終端機中手動做的任何事都可以自動化。
Bash 是大多數 Linux 發行版預設的互動式 Shell,在 macOS 上也有(macOS 現在預設使用 zsh,但仍內建 Bash),在 Windows 上則可透過 WSL 或 Git Bash 使用。開發者、系統管理員與 DevOps 工程師用它撰寫建置腳本、部署、CI 工作、伺服器維護,以及搭配 grep、sed、awk、sort 等工具快速處理資料。
撰寫並執行 Hello, world
腳本以 shebang 行開頭,告訴系統要使用哪個直譯器。用 chmod 讓檔案可執行,然後執行它。
#!/usr/bin/env bash
# hello.sh
echo "Hello, world!"chmod +x hello.sh
./hello.sh
# Hello, world!
bash hello.sh # also works without chmod
bash --version # check which version you have#!/usr/bin/env bash 會在 PATH 中尋找 Bash,比寫死 /bin/bash 更具可攜性。如果腳本只用到 POSIX 功能,可以改用 #!/bin/sh,但此時就不能使用 [[ ]] 與陣列等 Bash 專屬語法。
變數、引號與展開
用 name=value 指派,用 $name 或 ${name} 讀取。每個變數都是字串;Bash 只在 $(( )) 內做整數運算。展開時請加上雙引號,讓空白與特殊字元保持原樣。
name="Ada" # no spaces around =
count=3
echo "Hello, $name" # Hello, Ada
echo "Next: $((count + 1))" # Next: 4 (integer arithmetic)
today=$(date +%Y-%m-%d) # command substitution
echo "Today is $today"
file="my report.txt"
echo "${file%.txt}.pdf" # my report.pdf (strip suffix)
echo "${#name}" # 3 (string length)
echo "${missing:-default}" # default (fallback value)| 語法 | 意義 |
|---|---|
"$var" | 變數的值,保持為單一單字 |
'text' | 純文字,完全不展開 |
$(command) | 指令的輸出 |
$((a + b)) | 整數運算 |
$1, $2, $@, $# | 腳本或函式的引數、所有引數、引數個數 |
$? | 上一個指令的結束狀態(0 代表成功) |
條件與 case
if 會執行一個指令並檢查它的結束狀態。[[ ]] 測試指令用於比較字串與檢查檔案;(( )) 用於比較數字。case 則以萬用字元樣式比對值。
#!/usr/bin/env bash
file="${1:-notes.txt}"
if [[ -f "$file" ]]; then
echo "$file exists"
elif [[ -d "$file" ]]; then
echo "$file is a directory"
else
echo "$file not found"
fi
n=7
if (( n % 2 == 1 )); then echo "$n is odd"; fi
case "$file" in
*.txt) echo "text file" ;;
*.sh) echo "shell script" ;;
*) echo "something else" ;;
esac| 測試 | 為真的條件 |
|---|---|
[[ -f path ]] | 路徑是一般檔案 |
[[ -d path ]] | 路徑是目錄 |
[[ -z "$s" ]] | 字串為空 |
[[ "$a" == "$b" ]] | 字串相等 |
(( a > b )) | 數字 a 大於 b |
[[ $a -lt $b ]] | 數字 a 小於 b(舊式寫法) |
迴圈
Bash 有走訪單字清單的 for、C 風格的 for (( ))、while 與 until。逐行讀取檔案則使用 while read。
for fruit in apple banana cherry; do
echo "fruit: $fruit"
done
for (( i = 1; i <= 3; i++ )); do
echo "i = $i"
done
for f in *.log; do
[[ -e "$f" ]] || continue # skip if no .log files match
echo "log file: $f"
done
while IFS= read -r line; do
echo "line: $line"
done < input.txtIFS= read -r 會完全照原樣讀取每一行:IFS= 保留行首空白,-r 保留反斜線。
函式
用 name() { ... } 定義函式,並像指令一樣呼叫它。引數以 $1、$2 等形式傳入。函式用 return 回傳結束狀態(0 到 255);若要回傳資料,請把資料印出並用 $( ) 擷取輸出。
greet() {
local name="${1:-world}" # local variable, default value
echo "Hello, $name!"
}
is_even() {
(( $1 % 2 == 0 )) # exit status 0 means true
}
greet # Hello, world!
greet "Ann" # Hello, Ann!
msg=$(greet "Bo") # capture output
echo "Got: $msg" # Got: Hello, Bo!
if is_even 4; then echo "4 is even"; fi陣列與關聯式陣列
索引陣列存放字串清單。關聯式陣列將字串鍵對應到值,需要 Bash 4 以上,因此 macOS 內建的舊版 Bash 3.2 不能使用。
langs=("bash" "python" "go")
langs+=("rust")
echo "${langs[0]}" # bash
echo "${#langs[@]}" # 4
for l in "${langs[@]}"; do echo "- $l"; done
# associative array (Bash 4 or later)
declare -A port
port[http]=80
port[https]=443
echo "${port[https]}" # 443
for key in "${!port[@]}"; do
echo "$key -> ${port[$key]}"
done走訪元素時務必寫成加引號的 "${array[@]}";這樣即使元素含有空格,每個元素仍會保持為一個單字。
管線與重新導向
Bash 的招牌特色是組合小型程式。管線 | 把一個指令的輸出送到下一個指令的輸入。重新導向則把輸出送到檔案,或從檔案讀取輸入。檔案描述子 1 是標準輸出,2 是標準錯誤。
ls -l > files.txt # write stdout to a file (overwrite)
echo "more" >> files.txt # append
grep error app.log 2> /dev/null # discard error messages
make > build.log 2>&1 # stdout and stderr to the same file
# count the 3 most common words in a file
tr -cs '[:alpha:]' '\n' < book.txt | tr '[:upper:]' '[:lower:]' |
sort | uniq -c | sort -rn | head -n 3錯誤處理與安全的腳本
預設情況下,指令失敗後 Bash 會繼續執行。許多腳本一開始就寫 set -euo pipefail:-e 在遇到未處理的錯誤時結束,-u 把未設定的變數視為錯誤,-o pipefail 讓管線中任何一個指令失敗時整條管線就算失敗。trap 會在腳本結束時執行清理程式碼。
#!/usr/bin/env bash
set -euo pipefail # stop on errors, unset vars, pipe failures
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT # clean up when the script exits
if ! grep -q "ready" status.txt; then
echo "status is not ready" >&2 # errors go to stderr
exit 1
fi
cp config.txt "$tmp" || { echo "copy failed" >&2; exit 2; }
echo "ok"用 || 在同一行處理失敗,用 && 只在前一個指令成功時才執行下一個。set -e 有例外情況(例如 if 條件中的指令不會觸發它),所以重要的指令請明確檢查。
完整的小腳本:依日期備份
這個腳本會把目錄封存成帶有時間戳記的 .tar.gz 檔,並只保留最新的五個封存檔。它用到引數、預設值、測試、指令替換與管線。
#!/usr/bin/env bash
# backup.sh: copy a directory into a dated .tar.gz archive
set -euo pipefail
usage() { echo "usage: $0 SOURCE_DIR [DEST_DIR]" >&2; exit 1; }
[[ $# -ge 1 ]] || usage
src="$1"
dest="${2:-$HOME/backups}"
[[ -d "$src" ]] || { echo "not a directory: $src" >&2; exit 1; }
mkdir -p "$dest"
stamp=$(date +%Y%m%d-%H%M%S)
archive="$dest/$(basename "$src")-$stamp.tar.gz"
tar -czf "$archive" -C "$(dirname "$src")" "$(basename "$src")"
size=$(du -h "$archive" | cut -f1)
echo "Created $archive ($size)"
# keep only the 5 newest archives for this source
ls -1t "$dest"/"$(basename "$src")"-*.tar.gz | tail -n +6 | while read -r old; do
rm -f -- "$old"
echo "Removed $old"
done./backup.sh ~/projects/site
# Created /home/ann/backups/site-20261031-093000.tar.gz (2.1M)接下來可以學什麼
- GNU Bash Reference Manual,官方文件。
- 終端機中的
man bash與help(例如help read)。 - ShellCheck,會說明常見腳本錯誤的靜態分析工具。
- 如果需要讓腳本也能在
sh、dash或 BusyBox 上執行,請參考 POSIX Shell 規範。
常見問題
Bash 和 sh 有什麼差別?
sh 是 POSIX Shell 標準,由 dash 等程式實作。Bash 與 POSIX sh 相容,但新增了 [[ ]]、陣列、(( )) 與大括號展開等功能。
Bash 和 zsh 有什麼差別?
兩者在大多數日常指令上都相容。zsh 增加了互動式功能,是 macOS 預設的 Shell;以 bash 為 shebang 寫成的腳本仍會用 Bash 執行。
為什麼我的腳本顯示 permission denied?
因為檔案沒有執行權限。請執行 chmod +x script.sh,或改用 bash script.sh 來啟動。
如何把引數傳給 Bash 腳本?
寫在腳本名稱後面,例如 ./script.sh one two。在腳本內它們是 $1 和 $2,$@ 代表全部引數,$# 是引數個數。
應該用 Bash 寫大型程式嗎?
Bash 最適合用來把指令串接在一起。當腳本變得複雜、需要資料結構或細緻的錯誤處理時,Python 這類語言通常更容易維護。