bash-defensive-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBash Defensive Patterns
Bash防御式编程模式
Comprehensive guidance for writing production-ready Bash scripts using defensive programming techniques, error handling, and safety best practices to prevent common pitfalls and ensure reliability.
本指南全面介绍如何使用防御式编程技术、错误处理和安全最佳实践编写可用于生产环境的Bash脚本,以避免常见陷阱并确保可靠性。
When to Use This Skill
适用场景
- Writing production automation scripts
- Building CI/CD pipeline scripts
- Creating system administration utilities
- Developing error-resilient deployment automation
- Writing scripts that must handle edge cases safely
- Building maintainable shell script libraries
- Implementing comprehensive logging and monitoring
- Creating scripts that must work across different platforms
- 编写生产环境自动化脚本
- 构建CI/CD流水线脚本
- 创建系统管理工具
- 开发具备容错能力的部署自动化脚本
- 编写需要安全处理边缘情况的脚本
- 构建可维护的Shell脚本库
- 实现全面的日志记录与监控
- 创建可跨不同平台运行的脚本
Core Defensive Principles
核心防御原则
1. Strict Mode
1. 严格模式
Enable bash strict mode at the start of every script to catch errors early.
bash
#!/bin/bash
set -Eeuo pipefail # Exit on error, unset variables, pipe failuresKey flags:
- : Inherit ERR trap in functions
set -E - : Exit on any error (command returns non-zero)
set -e - : Exit on undefined variable reference
set -u - : Pipe fails if any command fails (not just last)
set -o pipefail
在每个脚本开头启用Bash严格模式,以便尽早发现错误。
bash
#!/bin/bash
set -Eeuo pipefail # 遇到错误、未定义变量、管道失败时退出关键参数说明:
- : 在函数中继承ERR陷阱
set -E - : 任何命令返回非零值时退出
set -e - : 引用未定义变量时退出
set -u - : 管道中任意命令失败则整个管道失败(而非仅最后一个命令)
set -o pipefail
2. Error Trapping and Cleanup
2. 错误捕获与清理
Implement proper cleanup on script exit or error.
bash
#!/bin/bash
set -Eeuo pipefail
trap 'echo "Error on line $LINENO"' ERR
trap 'echo "Cleaning up..."; rm -rf "$TMPDIR"' EXIT
TMPDIR=$(mktemp -d)在脚本退出或出错时执行适当的清理操作。
bash
#!/bin/bash
set -Eeuo pipefail
trap 'echo "第$LINENO行发生错误"' ERR
trap 'echo "正在清理..."; rm -rf "$TMPDIR"' EXIT
TMPDIR=$(mktemp -d)Script code here
脚本代码写在这里
undefinedundefined3. Variable Safety
3. 变量安全
Always quote variables to prevent word splitting and globbing issues.
bash
undefined始终给变量加引号,防止单词分割和通配符展开问题。
bash
undefinedWrong - unsafe
错误示例 - 不安全
cp $source $dest
cp $source $dest
Correct - safe
正确示例 - 安全
cp "$source" "$dest"
cp "$source" "$dest"
Required variables - fail with message if unset
必填变量 - 未设置时提示错误并退出
: "${REQUIRED_VAR:?REQUIRED_VAR is not set}"
undefined: "${REQUIRED_VAR:?REQUIRED_VAR 未设置}"
undefined4. Array Handling
4. 数组处理
Use arrays safely for complex data handling.
bash
undefined使用数组安全处理复杂数据。
bash
undefinedSafe array iteration
安全的数组迭代
declare -a items=("item 1" "item 2" "item 3")
for item in "${items[@]}"; do
echo "Processing: $item"
done
declare -a items=("item 1" "item 2" "item 3")
for item in "${items[@]}"; do
echo "正在处理: $item"
done
Reading output into array safely
安全地将命令输出读取到数组中
mapfile -t lines < <(some_command)
readarray -t numbers < <(seq 1 10)
undefinedmapfile -t lines < <(some_command)
readarray -t numbers < <(seq 1 10)
undefined5. Conditional Safety
5. 条件判断安全
Use for Bash-specific features, for POSIX.
[[ ]][ ]bash
undefined使用实现Bash特定功能,使用保证POSIX兼容性。
[[ ]][ ]bash
undefinedBash - safer
Bash环境 - 更安全
if [[ -f "$file" && -r "$file" ]]; then
content=$(<"$file")
fi
if [[ -f "$file" && -r "$file" ]]; then
content=$(<"$file")
fi
POSIX - portable
POSIX兼容 - 可移植
if [ -f "$file" ] && [ -r "$file" ]; then
content=$(cat "$file")
fi
if [ -f "$file" ] && [ -r "$file" ]; then
content=$(cat "$file")
fi
Test for existence before operations
操作前检查变量是否存在
if [[ -z "${VAR:-}" ]]; then
echo "VAR is not set or is empty"
fi
undefinedif [[ -z "${VAR:-}" ]]; then
echo "VAR未设置或为空"
fi
undefinedFundamental Patterns
基础模式
Pattern 1: Safe Script Directory Detection
模式1:安全检测脚本目录
bash
#!/bin/bash
set -Eeuo pipefailbash
#!/bin/bash
set -Eeuo pipefailCorrectly determine script directory
正确获取脚本目录
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")"
echo "Script location: $SCRIPT_DIR/$SCRIPT_NAME"
undefinedSCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")"
echo "脚本位置: $SCRIPT_DIR/$SCRIPT_NAME"
undefinedPattern 2: Comprehensive Function Templat
模式2:通用函数模板
bash
#!/bin/bash
set -Eeuo pipefailbash
#!/bin/bash
set -Eeuo pipefailPrefix for functions: handle_, process_, check_, validate_
函数命名前缀:handle_, process_, check_, validate_
Include documentation and error handling
包含文档说明和错误处理
validate_file() {
local -r file="$1"
local -r message="${2:-File not found: $file}"
if [[ ! -f "$file" ]]; then
echo "ERROR: $message" >&2
return 1
fi
return 0}
process_files() {
local -r input_dir="$1"
local -r output_dir="$2"
# Validate inputs
[[ -d "$input_dir" ]] || { echo "ERROR: input_dir not a directory" >&2; return 1; }
# Create output directory if needed
mkdir -p "$output_dir" || { echo "ERROR: Cannot create output_dir" >&2; return 1; }
# Process files safely
while IFS= read -r -d '' file; do
echo "Processing: $file"
# Do work
done < <(find "$input_dir" -maxdepth 1 -type f -print0)
return 0}
undefinedvalidate_file() {
local -r file="$1"
local -r message="${2:-未找到文件: $file}"
if [[ ! -f "$file" ]]; then
echo "ERROR: $message" >&2
return 1
fi
return 0}
process_files() {
local -r input_dir="$1"
local -r output_dir="$2"
# 验证输入参数
[[ -d "$input_dir" ]] || { echo "ERROR: 输入目录不存在" >&2; return 1; }
# 按需创建输出目录
mkdir -p "$output_dir" || { echo "ERROR: 无法创建输出目录" >&2; return 1; }
# 安全处理文件
while IFS= read -r -d '' file; do
echo "正在处理: $file"
# 执行处理逻辑
done < <(find "$input_dir" -maxdepth 1 -type f -print0)
return 0}
undefinedPattern 3: Safe Temporary File Handling
模式3:安全临时文件处理
bash
#!/bin/bash
set -Eeuo pipefail
trap 'rm -rf -- "$TMPDIR"' EXITbash
#!/bin/bash
set -Eeuo pipefail
trap 'rm -rf -- "$TMPDIR"' EXITCreate temporary directory
创建临时目录
TMPDIR=$(mktemp -d) || { echo "ERROR: Failed to create temp directory" >&2; exit 1; }
TMPDIR=$(mktemp -d) || { echo "ERROR: 无法创建临时目录" >&2; exit 1; }
Create temporary files in directory
在临时目录中创建临时文件
TMPFILE1="$TMPDIR/temp1.txt"
TMPFILE2="$TMPDIR/temp2.txt"
TMPFILE1="$TMPDIR/temp1.txt"
TMPFILE2="$TMPDIR/temp2.txt"
Use temporary files
使用临时文件
touch "$TMPFILE1" "$TMPFILE2"
echo "Temp files created in: $TMPDIR"
undefinedtouch "$TMPFILE1" "$TMPFILE2"
echo "临时文件已创建在: $TMPDIR"
undefinedPattern 4: Robust Argument Parsing
模式4:健壮的参数解析
bash
#!/bin/bash
set -Eeuo pipefailbash
#!/bin/bash
set -Eeuo pipefailDefault values
默认值
VERBOSE=false
DRY_RUN=false
OUTPUT_FILE=""
THREADS=4
usage() {
cat <<EOF
Usage: $0 [OPTIONS]
Options:
-v, --verbose Enable verbose output
-d, --dry-run Run without making changes
-o, --output FILE Output file path
-j, --jobs NUM Number of parallel jobs
-h, --help Show this help message
EOF
exit "${1:-0}"
}
VERBOSE=false
DRY_RUN=false
OUTPUT_FILE=""
THREADS=4
usage() {
cat <<EOF
使用方法: $0 [OPTIONS]
选项:
-v, --verbose 启用详细输出
-d, --dry-run 试运行模式(不实际修改)
-o, --output FILE 输出文件路径
-j, --jobs NUM 并行任务数量
-h, --help 显示帮助信息
EOF
exit "${1:-0}"
}
Parse arguments
解析参数
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose)
VERBOSE=true
shift
;;
-d|--dry-run)
DRY_RUN=true
shift
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
-j|--jobs)
THREADS="$2"
shift 2
;;
-h|--help)
usage 0
;;
--)
shift
break
;;
*)
echo "ERROR: Unknown option: $1" >&2
usage 1
;;
esac
done
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose)
VERBOSE=true
shift
;;
-d|--dry-run)
DRY_RUN=true
shift
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
-j|--jobs)
THREADS="$2"
shift 2
;;
-h|--help)
usage 0
;;
--)
shift
break
;;
*)
echo "ERROR: 未知选项: $1" >&2
usage 1
;;
esac
done
Validate required arguments
验证必填参数
[[ -n "$OUTPUT_FILE" ]] || { echo "ERROR: -o/--output is required" >&2; usage 1; }
undefined[[ -n "$OUTPUT_FILE" ]] || { echo "ERROR: -o/--output 为必填选项" >&2; usage 1; }
undefinedPattern 5: Structured Logging
模式5:结构化日志
bash
#!/bin/bash
set -Eeuo pipefailbash
#!/bin/bash
set -Eeuo pipefailLogging functions
日志函数
log_info() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*" >&2
}
log_warn() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*" >&2
}
log_error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
}
log_debug() {
if [[ "${DEBUG:-0}" == "1" ]]; then
echo "[$(date +'%Y-%m-%d %H:%M:%S')] DEBUG: $*" >&2
fi
}
log_info() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*" >&2
}
log_warn() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*" >&2
}
log_error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
}
log_debug() {
if [[ "${DEBUG:-0}" == "1" ]]; then
echo "[$(date +'%Y-%m-%d %H:%M:%S')] DEBUG: $*" >&2
fi
}
Usage
使用示例
log_info "Starting script"
log_debug "Debug information"
log_warn "Warning message"
log_error "Error occurred"
undefinedlog_info "脚本启动"
log_debug "调试信息"
log_warn "警告消息"
log_error "发生错误"
undefinedPattern 6: Process Orchestration with Signals
模式6:信号驱动的进程编排
bash
#!/bin/bash
set -Eeuo pipefailbash
#!/bin/bash
set -Eeuo pipefailTrack background processes
跟踪后台进程
PIDS=()
cleanup() {
log_info "Shutting down..."
# Terminate all background processes
for pid in "${PIDS[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null || true
fi
done
# Wait for graceful shutdown
for pid in "${PIDS[@]}"; do
wait "$pid" 2>/dev/null || true
done}
trap cleanup SIGTERM SIGINT
PIDS=()
cleanup() {
log_info "正在关闭..."
# 终止所有后台进程
for pid in "${PIDS[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null || true
fi
done
# 等待优雅关闭
for pid in "${PIDS[@]}"; do
wait "$pid" 2>/dev/null || true
done}
trap cleanup SIGTERM SIGINT
Start background tasks
启动后台任务
background_task &
PIDS+=($!)
another_task &
PIDS+=($!)
background_task &
PIDS+=($!)
another_task &
PIDS+=($!)
Wait for all background processes
等待所有后台进程完成
wait
undefinedwait
undefinedPattern 7: Safe File Operations
模式7:安全文件操作
bash
#!/bin/bash
set -Eeuo pipefailbash
#!/bin/bash
set -Eeuo pipefailUse -i flag to move safely without overwriting
使用-i参数安全移动文件,避免覆盖
safe_move() {
local -r source="$1"
local -r dest="$2"
if [[ ! -e "$source" ]]; then
echo "ERROR: Source does not exist: $source" >&2
return 1
fi
if [[ -e "$dest" ]]; then
echo "ERROR: Destination already exists: $dest" >&2
return 1
fi
mv "$source" "$dest"}
safe_move() {
local -r source="$1"
local -r dest="$2"
if [[ ! -e "$source" ]]; then
echo "ERROR: 源文件不存在: $source" >&2
return 1
fi
if [[ -e "$dest" ]]; then
echo "ERROR: 目标文件已存在: $dest" >&2
return 1
fi
mv "$source" "$dest"}
Safe directory cleanup
安全删除目录
safe_rmdir() {
local -r dir="$1"
if [[ ! -d "$dir" ]]; then
echo "ERROR: Not a directory: $dir" >&2
return 1
fi
# Use -I flag to prompt before rm (BSD/GNU compatible)
rm -rI -- "$dir"}
safe_rmdir() {
local -r dir="$1"
if [[ ! -d "$dir" ]]; then
echo "ERROR: 不是目录: $dir" >&2
return 1
fi
# 使用-I参数,删除前确认(兼容BSD/GNU)
rm -rI -- "$dir"}
Atomic file writes
原子写入文件
atomic_write() {
local -r target="$1"
local -r tmpfile
tmpfile=$(mktemp) || return 1
# Write to temp file first
cat > "$tmpfile"
# Atomic rename
mv "$tmpfile" "$target"}
undefinedatomic_write() {
local -r target="$1"
local -r tmpfile
tmpfile=$(mktemp) || return 1
# 先写入临时文件
cat > "$tmpfile"
# 原子重命名
mv "$tmpfile" "$target"}
undefinedPattern 8: Idempotent Script Design
模式8:幂等脚本设计
bash
#!/bin/bash
set -Eeuo pipefailbash
#!/bin/bash
set -Eeuo pipefailCheck if resource already exists
检查资源是否已存在
ensure_directory() {
local -r dir="$1"
if [[ -d "$dir" ]]; then
log_info "Directory already exists: $dir"
return 0
fi
mkdir -p "$dir" || {
log_error "Failed to create directory: $dir"
return 1
}
log_info "Created directory: $dir"}
ensure_directory() {
local -r dir="$1"
if [[ -d "$dir" ]]; then
log_info "目录已存在: $dir"
return 0
fi
mkdir -p "$dir" || {
log_error "无法创建目录: $dir"
return 1
}
log_info "已创建目录: $dir"}
Ensure configuration state
确保配置状态
ensure_config() {
local -r config_file="$1"
local -r default_value="$2"
if [[ ! -f "$config_file" ]]; then
echo "$default_value" > "$config_file"
log_info "Created config: $config_file"
fi}
ensure_config() {
local -r config_file="$1"
local -r default_value="$2"
if [[ ! -f "$config_file" ]]; then
echo "$default_value" > "$config_file"
log_info "已创建配置文件: $config_file"
fi}
Rerunning script multiple times should be safe
多次运行脚本应保持安全
ensure_directory "/var/cache/myapp"
ensure_config "/etc/myapp/config" "DEBUG=false"
undefinedensure_directory "/var/cache/myapp"
ensure_config "/etc/myapp/config" "DEBUG=false"
undefinedPattern 9: Safe Command Substitution
模式9:安全命令替换
bash
#!/bin/bash
set -Eeuo pipefailbash
#!/bin/bash
set -Eeuo pipefailUse $() instead of backticks
使用$()替代反引号
name=$(<"$file") # Modern, safe variable assignment from file
output=$(command -v python3) # Get command location safely
name=$(<"$file") # 从文件读取内容的现代安全方式
output=$(command -v python3) # 安全获取命令路径
Handle command substitution with error checking
带错误检查的命令替换
result=$(command -v node) || {
log_error "node command not found"
return 1
}
result=$(command -v node) || {
log_error "未找到node命令"
return 1
}
For multiple lines
处理多行内容
mapfile -t lines < <(grep "pattern" "$file")
mapfile -t lines < <(grep "pattern" "$file")
NUL-safe iteration
基于NUL的安全迭代
while IFS= read -r -d '' file; do
echo "Processing: $file"
done < <(find /path -type f -print0)
undefinedwhile IFS= read -r -d '' file; do
echo "正在处理: $file"
done < <(find /path -type f -print0)
undefinedPattern 10: Dry-Run Support
模式10:试运行支持
bash
#!/bin/bash
set -Eeuo pipefail
DRY_RUN="${DRY_RUN:-false}"
run_cmd() {
if [[ "$DRY_RUN" == "true" ]]; then
echo "[DRY RUN] Would execute: $*"
return 0
fi
"$@"
}bash
#!/bin/bash
set -Eeuo pipefail
DRY_RUN="${DRY_RUN:-false}"
run_cmd() {
if [[ "$DRY_RUN" == "true" ]]; then
echo "[试运行] 将要执行: $*"
return 0
fi
"$@"
}Usage
使用示例
run_cmd cp "$source" "$dest"
run_cmd rm "$file"
run_cmd chown "$owner" "$target"
undefinedrun_cmd cp "$source" "$dest"
run_cmd rm "$file"
run_cmd chown "$owner" "$target"
undefinedAdvanced Defensive Techniques
高级防御技术
Named Parameters Pattern
命名参数模式
bash
#!/bin/bash
set -Eeuo pipefail
process_data() {
local input_file=""
local output_dir=""
local format="json"
# Parse named parameters
while [[ $# -gt 0 ]]; do
case "$1" in
--input=*)
input_file="${1#*=}"
;;
--output=*)
output_dir="${1#*=}"
;;
--format=*)
format="${1#*=}"
;;
*)
echo "ERROR: Unknown parameter: $1" >&2
return 1
;;
esac
shift
done
# Validate required parameters
[[ -n "$input_file" ]] || { echo "ERROR: --input is required" >&2; return 1; }
[[ -n "$output_dir" ]] || { echo "ERROR: --output is required" >&2; return 1; }
}bash
#!/bin/bash
set -Eeuo pipefail
process_data() {
local input_file=""
local output_dir=""
local format="json"
# 解析命名参数
while [[ $# -gt 0 ]]; do
case "$1" in
--input=*)
input_file="${1#*=}"
;;
--output=*)
output_dir="${1#*=}"
;;
--format=*)
format="${1#*=}"
;;
*)
echo "ERROR: 未知参数: $1" >&2
return 1
;;
esac
shift
done
# 验证必填参数
[[ -n "$input_file" ]] || { echo "ERROR: --input 为必填参数" >&2; return 1; }
[[ -n "$output_dir" ]] || { echo "ERROR: --output 为必填参数" >&2; return 1; }
}Dependency Checking
依赖检查
bash
#!/bin/bash
set -Eeuo pipefail
check_dependencies() {
local -a missing_deps=()
local -a required=("jq" "curl" "git")
for cmd in "${required[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
missing_deps+=("$cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
echo "ERROR: Missing required commands: ${missing_deps[*]}" >&2
return 1
fi
}
check_dependenciesbash
#!/bin/bash
set -Eeuo pipefail
check_dependencies() {
local -a missing_deps=()
local -a required=("jq" "curl" "git")
for cmd in "${required[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
missing_deps+=("$cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
echo "ERROR: 缺少必需命令: ${missing_deps[*]}" >&2
return 1
fi
}
check_dependenciesBest Practices Summary
最佳实践总结
- Always use strict mode -
set -Eeuo pipefail - Quote all variables - prevents word splitting
"$variable" - Use [[]] conditionals - More robust than [ ]
- Implement error trapping - Catch and handle errors gracefully
- Validate all inputs - Check file existence, permissions, formats
- Use functions for reusability - Prefix with meaningful names
- Implement structured logging - Include timestamps and levels
- Support dry-run mode - Allow users to preview changes
- Handle temporary files safely - Use mktemp, cleanup with trap
- Design for idempotency - Scripts should be safe to rerun
- Document requirements - List dependencies and minimum versions
- Test error paths - Ensure error handling works correctly
- Use - Safer than
command -vfor checking executableswhich - Prefer printf over echo - More predictable across systems
- 始终使用严格模式 -
set -Eeuo pipefail - 所有变量加引号 - 避免单词分割
"$variable" - 使用[[ ]]进行条件判断 - 比[ ]更健壮
- 实现错误捕获 - 优雅地捕获和处理错误
- 验证所有输入 - 检查文件存在性、权限、格式
- 使用函数提高复用性 - 使用有意义的命名前缀
- 实现结构化日志 - 包含时间戳和日志级别
- 支持试运行模式 - 允许用户预览变更
- 安全处理临时文件 - 使用mktemp,通过trap清理
- 设计幂等脚本 - 脚本可安全重复运行
- 记录依赖要求 - 列出依赖项和最低版本
- 测试错误路径 - 确保错误处理逻辑正常工作
- 使用- 比
command -v更安全的命令检查方式which - 优先使用printf而非echo - 在不同系统上表现更一致
Resources
参考资源
- Bash Strict Mode: http://redsymbol.net/articles/unofficial-bash-strict-mode/
- Google Shell Style Guide: https://google.github.io/styleguide/shellguide.html
- Defensive BASH Programming: https://www.lifepipe.net/
- Bash严格模式: http://redsymbol.net/articles/unofficial-bash-strict-mode/
- Google Shell风格指南: https://google.github.io/styleguide/shellguide.html
- 防御式Bash编程: https://www.lifepipe.net/