Shell 腳本入門:從第一行開始自動化
為什麼要寫腳本?
Shell 腳本(Shell Script)就是把一系列 Shell 指令寫進一個文字檔,讓電腦自動依序執行。想像你每天早上都要執行同樣 10 個指令來部署程式,腳本讓這件事變成只需一個指令。
- 自動化重複工作:備份、部署、日誌清理
- 減少人為錯誤:複雜的操作流程一次寫好,之後不用再重複輸入
- 排程執行:搭配
cron定時自動執行 - 環境標準化:讓新伺服器的初始設定變成一鍵完成
第一個腳本
先建立一個最簡單的腳本,感受一下完整的流程:
步驟 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