介绍
# Shell Scripting
编写可靠、可维护的 bash 脚本。涵盖参数解析、错误处理、可移植性、临时文件、并行执行、进程管理以及自文档化脚本。
## 适用场景
- 编写供他人(或未来的自己)运行的脚本 - 自动化多步骤工作流 - 使用标志和选项解析命令行参数 - 正确处理错误和清理工作 - 并行运行任务 - 使脚本在 Linux 和 macOS 之间可移植 - 用更简单的接口封装复杂命令
## 脚本模板
```bash #!/usr/bin/env bash set -euo pipefail
# Description: What this script does (one line) # Usage: script.sh [options] <required-arg>
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_NAME="$(basename "$0")"
# Defaults VERBOSE=false OUTPUT_DIR="./output"
usage() { cat <<EOF Usage: $SCRIPT_NAME [options] <input-file>
Description: Process the input file and generate output.
Options: -o, --output DIR Output directory (default: $OUTPUT_DIR) -v, --verbose Enable verbose output -h, --help Show this help message
Examples: $SCRIPT_NAME data.csv $SCRIPT_NAME -v -o /tmp/results data.csv EOF }
log() { echo "[$(date '+%H:%M:%S')] $*" >&2; } debug() { $VERBOSE && log "DEBUG: $*" || true; } die() { log "ERROR: $*"; exit 1; }
# Parse arguments while [[ $# -gt 0 ]]; do case "$1" in -o|--output) OUTPUT_DIR="$2"; shift 2 ;; -v|--verbose) VERBOSE=true; shift ;; -h|--help) usage; exit 0 ;; --) shift; break ;; -*) die "Unknown option: $1" ;; *) break ;; esac done
INPUT_FILE="${1:?$(usage >&2; echo "Error: input file required")}" [[ -f "$INPUT_FILE" ]] || die "File not found: $INPUT_FILE"
# Main logic main() { debug "Input: $INPUT_FILE" debug "Output: $OUTPUT_DIR" mkdir -p "$OUTPUT_DIR"
log "Processing $INPUT_FILE..." # ... do work ... log "Done. Output in $OUTPUT_DIR" }
main "$@" ```
## 错误处理
### set 选项
```bash set -e # Exit on any command failure set -u # Error on undefined variables set -o pipefail # Pipe fails if any command in the pipe fails set -x # Debug: print each command before executing (noisy)
# Combined (use this in every script) set -euo pipefail
# Temporarily disable for commands that are allowed to fail set +e some_command_that_might_fail exit_code=$? set -e ```
### Trap 用于清理
```bash # Cleanup on exit (any exit: success, failure, or signal) TMPDIR="" cleanup() { [[ -n "$TMPDIR" ]] && rm -rf "$TMPDIR" } trap cleanup EXIT
TMPDIR=$(mktemp -d) # Use $TMPDIR freely — it's cleaned up automatically
# Trap specific signals trap 'echo "Interrupted"; exit 130' INT # Ctrl+C trap 'echo "Terminated"; exit 143' TERM # kill ```
### 错误处理模式
```bash # Check command exists before using it command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
# Provide default values NAME="${NAME:-default_value}"
# Required variable (fail if unset) : "${API_KEY:?Error: API_KEY environment variable is required}"
# Retry a command retry() { local max_attempts=$1 shift local attempt=1 while [[ $attempt -le $max_attempts ]]; do "$@" && return 0 log "Attempt $attempt/$max_attempts failed. Retrying..." ((attempt++)) sleep $((attempt * 2)) done die "Command failed after $max_attempts attempts: $*" }
retry 3 curl -sf https://api.example.com/health ```
## 参数解析
### 简单:位置参数 + 标志
```bash # Manual parsing (no dependencies) FORCE=false DRY_RUN=false
while [[ $# -gt 0 ]]; do case "$1" in -f|--force) FORCE=true; shift ;; -n|--dry-run) DRY_RUN=true; shift ;; -o|--output) [[ -n "${2:-}" ]] || die "--output requires a value" OUTPUT="$2"; shift 2 ;; --output=*) OUTPUT="${1#*=}"; shift ;; -h|--help) usage; exit 0 ;; --) shift; break ;; # End of options -*) die "Unknown option: $1" ;; *) break ;; # Start of positional args esac done
# Remaining args are positional FILES=("$@") [[ ${#FILES[@]} -gt 0 ]] || die "At least one file is required" ```
### getopts(POSIX,仅限短选项)
```bash while getopts ":o:vhf" opt; do case "$opt" in o) OUTPUT="$OPTARG" ;; v) VERBOSE=true ;; f) FORCE=true ;; h) usage; exit 0 ;; :) die "Option -$OPTARG requires an argument" ;; ?) die "Unknown option: -$OPTARG" ;; esac done shift $((OPTIND - 1)) ```
## 临时文件和目录
```bash # Create temp file (automatically unique) TMPFILE=$(mktemp) echo "data" > "$TMPFILE"
# Create temp directory TMPDIR=$(mktemp -d)
# Create temp with custom prefix/suffix TMPFILE=$(mktemp /tmp/myapp.XXXXXX) TMPFILE=$(mktemp --suffix=.json) # GNU only
# Always clean up with trap trap 'rm -f "$TMPFILE"' EXIT
# Portable pattern (works on macOS and Linux) TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'myapp') trap 'rm -rf "$TMPDIR"' EXIT ```
## 并行执行
### xargs -P
```bash # Run 4 commands in parallel cat urls.txt | xargs -P 4 -I {} curl -sO {}
# Process files in parallel (4 at a time) find . -name "*.csv" | xargs -P 4 -I {} ./process.sh {}
# Parallel with progress indicator find . -name "*.jpg" | xargs -P 8 -I {} sh -c 'convert {} -resize 800x600 resized/{} && echo "Done: {}"' ```
### 后台任务 + wait
```bash # Run tasks in background, wait for all pids=() for file in data/*.csv; do process_file "$file" & pids+=($!) done
# Wait for all and check results failed=0 for pid in "${pids[@]}"; do wait "$pid" || ((failed++)) done [[ $failed -eq 0 ]] || die "$failed jobs failed" ```
### GNU Parallel(如果可用)
```bash # Process files with 8 parallel jobs parallel -j 8 ./process.sh {} ::: data/*.csv
# With progress bar parallel --bar -j 4 convert {} -resize 800x600 resized/{/} ::: *.jpg
# Pipe input lines cat urls.txt | parallel -j 10 curl -sO {} ```
## 进程管理
### 后台进程
```bash # Start in background long_running_command & BG_PID=$!
# Check if still running kill -0 $BG_PID 2>/dev/null && echo "Running" || echo "Stopped"
# Wait for it wait $BG_PID echo "Exit code: $?"
# Kill on script exit trap 'kill $BG_PID 2>/dev/null' EXIT ```
### 进程监管
```bash # Run a command, restart if it dies run_with_restart() { local cmd=("$@") while true; do "${cmd[@]}" & local pid=$! log "Started PID $pid" wait $pid local exit_code=$? log "Process exited with code $exit_code. Restarting in 5s..." sleep 5 done }
run_with_restart ./my-server --port 8080 ```
### 超时
```bash # Kill command after 30 seconds timeout 30 long_running_command
# With custom signal (SIGKILL after SIGTERM fails) timeout --signal=TERM --kill-after=10 30 long_running_command
# Portable (no timeout command) ( sleep 30; kill $ 2>/dev/null ) & TIMER_PID=$! long_running_command kill $TIMER_PID 2>/dev/null ```
## 可移植性(Linux 与 macOS)
### 常见差异
```bash # sed: macOS requires -i '' (empty backup extension) # Linux: sed -i 's/old/new/g' file.txt # macOS: sed -i '' 's/old/new/g' file.txt # Portable: sed -i.bak 's/old/new/g' file.txt && rm file.txt.bak
# date: different flags # GNU (Linux): date -d '2026-02-03' '+%s' # BSD (macOS): date -j -f '%Y-%m-%d' '2026-02-03' '+%s'
# readlink -f: doesn't exist on macOS # Portable alternative: real_path() { cd "$(dirname "$1")" && echo "$(pwd)/$(basename "$1")"; }
# stat: different syntax # GNU: stat -c '%s' file # BSD: stat -f '%z' file
# grep -P: not available on macOS by default # Use grep -E instead, or install GNU grep ```
### POSIX 安全模式
```bash # Use printf instead of echo -e (echo behavior varies) printf "Line 1\nLine 2\n"
# Use $() instead of backticks result=$(command) # Good result=`command` # Bad (deprecated, nesting issues)
# Use [[ ]] for tests (bash), [ ] for POSIX sh [[ -f "$file" ]] # Bash (safer, no word splitting) [ -f "$file" ] # POSIX sh
# Array check (bash only, not POSIX) if [[ ${#array[@]} -gt 0 ]]; then echo "Array has elements" fi ```
## 配置文件解析
### Source(引入)配置文件
```bash # Simple: source a key=value file # config.env: # DB_HOST=localhost # DB_PORT=5432
# Validate before sourcing (security: check for commands) if grep -qP '^[A-Z_]+=.*[;\`\$\(]' config.env; then die "Config file contains unsafe characters" fi source config.env ```
### 解析 INI 风格配置
```bash # config.ini: # [database] # host = localhost # port = 5432 # [app] # debug = true
parse_ini() { local file="$1" section="" while IFS='= ' read -r key value; do [[ -z "$key" || "$key" =~ ^[#\;] ]] && continue if [[ "$key" =~ ^\[(.+)\]$ ]]; then section="${BASH_REMATCH[1]}" continue fi value="${value%%#*}" # Strip inline comments value="${value%"${value##*[![:space:]]}"}" # Trim trailing whitespace printf -v "${section}_${key}" '%s' "$value" done < "$file" }
parse_ini config.ini echo "$database_host" # localhost echo "$app_debug" # true ```
## 常用模式
### 破坏性操作前确认
```bash confirm() { local prompt="${1:-Are you sure?}" read -rp "$prompt [y/N] " response [[ "$response" =~ ^[Yy]$ ]] }
confirm "Delete all files in /tmp/data?" || die "Aborted" rm -rf /tmp/data/* ```
### 进度指示器
```bash # Simple counter total=$(wc -l < file_list.txt) count=0 while IFS= read -r file; do ((count++)) printf "\rProcessing %d/%d..." "$count" "$total" >&2 process "$file" done < file_list.txt echo "" >&2 ```
### 锁文件(防止并发运行)
```bash LOCKFILE="/tmp/${SCRIPT_NAME}.lock"
acquire_lock() { if ! mkdir "$LOCKFILE" 2>/dev/null; then die "Another instance is running (lock: $LOCKFILE)" fi trap 'rm -rf "$LOCKFILE"' EXIT }
acquire_lock # ... safe to proceed, only one instance runs ... ```
### 标准输入或文件参数
```bash # Read from file argument or stdin input="${1:--}" # Default to "-" (stdin) if [[ "$input" == "-" ]]; then cat else cat "$input" fi | while IFS= read -r line; do process "$line" done ```
## 提示
- 始终以 `set -euo pipefail` 开头。它能捕获 80% 的静默 bug。 - 始终使用 `trap cleanup EXIT` 来处理临时文件。不要依赖执行到最后的清理代码。 - 给所有变量扩展加上引号:使用 `"$var"` 而非 `$var`。未加引号的变量在遇到空格和 glob 通配符时会出错。 - 在 bash 中使用 `[[ ]]` 而非 `[ ]`。它在处理空字符串、空格和模式匹配方面表现更好。 - `shellcheck` 是 shell 脚本最好的静态分析工具。运行它:`shellcheck myscript.sh`。如果可用,请安装它。 - 对常量使用 `readonly` 防止意外覆盖:`readonly DB_HOST="localhost"`。 - 编写一个 `usage()` 函数,并在遇到 `-h`/`--help` 或缺少必需参数时调用它。未来的用户(包括你自己)会感谢你的。 - 对于可能包含特殊字符或需要格式化的内容,优先使用 `printf` 而非 `echo`。 - 运行脚本前,先用 `bash -n script.sh`(语法检查)测试脚本。