Shell 腳本入門:從第一行開始自動化

為什麼要寫腳本?

Shell 腳本(Shell Script)就是把一系列 Shell 指令寫進一個文字檔,讓電腦自動依序執行。想像你每天早上都要執行同樣 10 個指令來部署程式,腳本讓這件事變成只需一個指令。

第一個腳本

先建立一個最簡單的腳本,感受一下完整的流程:

步驟 1:建立腳本檔案

$ vim hello.sh

輸入以下內容:

#!/bin/bash
# 這是一個說明注釋(# 開頭的行都是注釋)

echo "哈囉,$(whoami)!現在是 $(date)。"
echo "你在 $(pwd) 目錄。"

步驟 2:給予執行權限

$ chmod +x hello.sh

步驟 3:執行腳本

$ ./hello.sh
哈囉,alice!現在是 Tue May 20 10:30:00 CST 2026。
你在 /home/alice 目錄。

Shebang(#!)是什麼?

第一行 #!/bin/bash 叫做 shebang,告訴系統用哪個程式來執行這個腳本。

Shebang用途
#!/bin/bash用 bash 執行(最常用)
#!/bin/sh用 POSIX sh 執行(跨平台,功能較少)
#!/usr/bin/env python3用系統中找到的 python3 執行
#!/usr/bin/env bash推薦寫法:在 PATH 中找 bash,可攜性更好

變數

定義與使用變數

#!/usr/bin/env bash

# 定義變數:= 兩側不能有空格
NAME="Alice"
AGE=30
PI=3.14

# 使用變數:加上 $
echo "姓名:$NAME"
echo "年齡:$AGE"

# 複雜情況加大括號,避免歧義
echo "我叫 ${NAME}Chen"   # 大括號區分變數名稱和後面的文字

# 指令替換:把指令的輸出存進變數
TODAY=$(date +%Y-%m-%d)
HOSTNAME=$(hostname)
echo "今天是 $TODAY,主機是 $HOSTNAME"

變數的種類

種類說明例子
本地變數只在當前 Shell 有效NAME="Alice"
環境變數可被子程序繼承export PATH="$PATH:/opt/bin"
唯讀變數設定後不可修改readonly VERSION="1.0"

引數與特殊變數

腳本可以接受外部傳入的引數:

#!/usr/bin/env bash
# 執行:./greet.sh Alice 30

echo "第一個引數:$1"      # Alice
echo "第二個引數:$2"      # 30
echo "所有引數:$@"        # Alice 30
echo "引數數量:$#"        # 2
echo "腳本名稱:$0"        # ./greet.sh
echo "上一個指令的回傳碼:$?"
echo "目前腳本的 PID:$$"
特殊變數說明
$0腳本名稱(或命令名稱)
$1$2、...第 1、2... 個引數
$#引數的數量
$@所有引數(各自為獨立字串)
$*所有引數(合併為一個字串)
$?上一個指令的回傳碼(0=成功,非0=失敗)
$$目前腳本的 PID

條件判斷(if)

#!/usr/bin/env bash

# 基本 if 語法
if [ 條件 ]; then
    # 條件成立時執行
elif [ 其他條件 ]; then
    # 其他條件成立時執行
else
    # 都不成立時執行
fi

# 實際例子:檢查引數
if [ $# -eq 0 ]; then
    echo "用法:$0 <使用者名稱>"
    exit 1
fi

# 字串比較
if [ "$1" = "admin" ]; then
    echo "歡迎,管理員!"
else
    echo "您好,$1!"
fi

# 數字比較
if [ $AGE -ge 18 ]; then
    echo "成年人"
fi

常用的測試條件

類別條件說明
數字比較-eq等於(equal)
-ne不等於(not equal)
-gt大於(greater than)
-lt小於(less than)
-ge / -le大於等於 / 小於等於
字串-z "$str"字串為空
-n "$str"字串非空
"$a" = "$b"字串相等
檔案-f "$file"存在且是普通檔案
-d "$dir"存在且是目錄
-e "$path"路徑存在(任何類型)
-r / -w / -x "$file"可讀 / 可寫 / 可執行

迴圈(for、while)

for 迴圈

#!/usr/bin/env bash

# 遍歷清單
for fruit in apple banana cherry; do
    echo "水果:$fruit"
done

# 遍歷目錄中的所有 .log 檔案
for logfile in /var/log/*.log; do
    echo "處理:$logfile(大小:$(du -sh "$logfile" | cut -f1))"
done

# 數字範圍
for i in $(seq 1 5); do
    echo "數字:$i"
done

# bash 原生語法(推薦)
for ((i=1; i<=5; i++)); do
    echo "數字:$i"
done

while 迴圈

#!/usr/bin/env bash

# 計數迴圈
count=1
while [ $count -le 5 ]; do
    echo "第 $count 次"
    count=$((count + 1))
done

# 逐行讀取檔案
while IFS= read -r line; do
    echo "讀到:$line"
done < servers.txt

函數

#!/usr/bin/env bash

# 定義函數
log_info() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
}

log_error() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
}

check_service() {
    local service="$1"   # local 讓變數只在函數內有效
    if systemctl is-active --quiet "$service"; then
        log_info "$service 正在執行"
        return 0
    else
        log_error "$service 未執行!"
        return 1
    fi
}

# 呼叫函數
log_info "腳本開始"
check_service nginx || {
    log_error "嘗試啟動 nginx..."
    sudo systemctl start nginx
}
log_info "腳本結束"

腳本撰寫最佳實踐

#!/usr/bin/env bash
# ─── 最佳實踐示範 ───────────────────────────────────

# 1. 安全模式:讓腳本在錯誤時立即停止
set -euo pipefail
# -e: 任何指令失敗就停止
# -u: 使用未定義的變數就停止(避免 $TYPO 展開為空)
# -o pipefail: 管線中任何指令失敗都算失敗

# 2. 明確定義常數
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/var/log/myscript.log"

# 3. 統一的 log 函數
log() { echo "[$(date '+%T')] $*" | tee -a "$LOG_FILE"; }

# 4. 確認是否以 root 執行
if [[ $EUID -ne 0 ]]; then
    echo "錯誤:此腳本需要 root 權限" >&2
    exit 1
fi

# 5. 退出時清理(trap)
cleanup() {
    log "清理暫存檔..."
    rm -f /tmp/myscript_*
}
trap cleanup EXIT

log "腳本開始"
# ... 主要邏輯 ...
log "腳本結束"

實用腳本範本

範本一:備份腳本

#!/usr/bin/env bash
set -euo pipefail

BACKUP_SRC="/var/www/html"
BACKUP_DST="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DST}/backup_${DATE}.tar.gz"

echo "開始備份 $BACKUP_SRC..."
tar -czf "$BACKUP_FILE" "$BACKUP_SRC"
echo "備份完成:$BACKUP_FILE($(du -sh "$BACKUP_FILE" | cut -f1))"

# 刪除 30 天前的舊備份
find "$BACKUP_DST" -name "backup_*.tar.gz" -mtime +30 -delete
echo "已清除 30 天前的舊備份"

範本二:檢查多個服務狀態

#!/usr/bin/env bash
SERVICES=(nginx mysql redis)
FAILED=()

for svc in "${SERVICES[@]}"; do
    if systemctl is-active --quiet "$svc"; then
        echo "✅ $svc 正常"
    else
        echo "❌ $svc 異常!"
        FAILED+=("$svc")
    fi
done

if [ ${#FAILED[@]} -gt 0 ]; then
    echo "異常服務:${FAILED[*]}"
    exit 1
fi