Script Engineering
Chapter 12: Shell Script Engineering
Between a one-liner that works and a production-ready script lies an entire engineering ecosystem: error handling, signal catching, argument parsing, unit testing, and code quality tooling. This chapter systematically covers how to use set -euo pipefail, trap, shellcheck, bats, and more to elevate shell scripts from "it runs" to "trustworthy production tools."
12.1 Shebang and Script Header Conventions
The shebang (#!) is the first line of a script, telling the kernel which interpreter to use. Choosing the right shebang directly affects script portability and debuggability.
#!/usr/bin/env bash # 推荐:通过 PATH 查找 bash(可移植)
#!/bin/bash # 固定路径:大多数 Linux 可用,macOS 默认 bash 3.x(过旧)
#!/bin/sh # POSIX sh:最大可移植性,但功能受限
# 为何推荐 #!/usr/bin/env bash?
# - /usr/bin/env 在几乎所有 Unix 系统上都存在
# - 自动使用 PATH 中第一个 bash(支持用户自定义安装路径)
# - nix/Homebrew 安装的新版 bash 可以被正确找到
# === 调试选项(shebang 行或 set 命令)===
#!/usr/bin/env bash -x # -x:执行前打印每条命令(xtrace)
#!/usr/bin/env bash -v # -v:读取时打印每一行(verbose)
#!/usr/bin/env bash -xv # 两者结合
# 在脚本内部动态开关调试
set -x # 开启 xtrace
some_command
set +x # 关闭 xtrace
# 只调试某个函数内部
my_function() {
set -x
# ... 需要调试的代码 ...
set +x
}
# 使用 PS4 定制 xtrace 前缀(显示文件名+行号)
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
12.2 Strict Mode: set -euo pipefail
Placing set -euo pipefail at the top of every production script is the first step in shell script engineering. These four options together form "strict mode," turning silent failures into explicit errors.
#!/usr/bin/env bash
set -euo pipefail
# === 逐项解析 ===
# -e (errexit):任何命令非零退出时立即终止脚本
# 没有 -e:
false # 失败但继续
echo "Still running" # 这行会执行!
# 有 -e:
# false # 脚本在此行退出,不打印任何内容
# -e 的陷阱:以下情况 -e 不触发
# 1. if/while/until 条件中的命令
if false; then echo "never"; fi # OK,-e 不触发
# 2. 用 || 连接的命令(右侧运行时左侧失败被容许)
false || echo "fallback" # OK
# 3. 用 ! 否定的命令
! false # OK(反转退出码为 0)
# 局部禁用 -e(处理已知可能失败的命令)
set +e
count=$(grep -c "pattern" file.txt) # grep 未找到时返回 1,不终止脚本
set -e
# 或使用 || true 更简洁
count=$(grep -c "pattern" file.txt) || true
# -u (nounset):引用未定义变量时报错并退出
# 没有 -u:
echo $UNDEFINED_VAR # 输出空字符串,悄悄失败
# 有 -u:
# echo $UNDEFINED_VAR # bash: UNDEFINED_VAR: unbound variable
# -u 的陷阱:使用默认值语法可安全访问可能未定义的变量
echo "${UNDEFINED_VAR:-default_value}" # 安全,输出 default_value
echo "${OPTIONAL_VAR-}" # 未定义时输出空字符串(不报错)
# -o pipefail:管道中任何命令失败则整个管道返回非零退出码
# 没有 pipefail:
cat /nonexistent | wc -l # 整体返回 0(wc 成功),掩盖了 cat 的错误
echo $? # 0
# 有 pipefail:
# cat /nonexistent | wc -l # 整体返回非零,-e 触发脚本退出
# -x (xtrace):打印每条执行的命令(调试用,生产环境慎开)
# set -x # 开启后每行前面会打印 + command
# 推荐的严格模式组合写法
set -euo pipefail
IFS=$'\n\t' # 修改字段分隔符,防止空格分词引起的 bug
| Option | Effect | Pitfall |
|---|---|---|
| -e | Exit on non-zero command | if/ |
| -u | Error on undefined variable | Use ${VAR:-default} to bypass |
| -o pipefail | Propagate pipe failures | Conflicts with grep/head "normal failure" |
| -x | Print each executed command | May leak passwords to logs |
12.3 Complete trap Guide: Signal Catching and Cleanup
trap is the shell's signal-handling mechanism. Production scripts must register an EXIT trap to clean up temporary files and resources — whether the script exits due to success, error, or user interruption (Ctrl+C), the cleanup function is always called.
#!/usr/bin/env bash
set -euo pipefail
# === 基本 trap 语法 ===
# trap 'COMMAND' SIGNAL [SIGNAL...]
# trap FUNCTION_NAME SIGNAL [SIGNAL...]
# === 最重要:EXIT 陷阱(无论如何退出都执行)===
TMPDIR_WORK=""
cleanup() {
local exit_code=$? # 捕获原始退出码
echo "[cleanup] Cleaning up... (exit code: $exit_code)" >&2
# 安全删除临时目录(检查是否为空再删)
if [[ -n "${TMPDIR_WORK}" && -d "${TMPDIR_WORK}" ]]; then
rm -rf "${TMPDIR_WORK}"
echo "[cleanup] Removed temp dir: ${TMPDIR_WORK}" >&2
fi
# 释放锁文件
[[ -f /var/run/myscript.pid ]] && rm -f /var/run/myscript.pid
exit "$exit_code" # 保持原始退出码
}
trap cleanup EXIT # 注册 EXIT 陷阱
# 创建临时目录(trap 注册后创建,确保能被清理)
TMPDIR_WORK=$(mktemp -d)
echo "Working in: $TMPDIR_WORK"
# 脚本主体...
cp /etc/hosts "$TMPDIR_WORK/"
# 即使这里发生错误,cleanup 也会被调用
# === INT / TERM 陷阱 ===
interrupted=false
handle_sigint() {
echo "" >&2
echo "[signal] Caught SIGINT (Ctrl+C), shutting down gracefully..." >&2
interrupted=true
# 不需要显式调用 cleanup,EXIT 陷阱会自动执行
exit 130 # 标准:128 + signal_number(2)
}
handle_sigterm() {
echo "[signal] Caught SIGTERM, shutting down..." >&2
exit 143 # 标准:128 + 15
}
trap handle_sigint INT
trap handle_sigterm TERM
# === ERR 陷阱:在命令失败时调用(配合 -e 使用)===
handle_error() {
local exit_code=$?
local line_no=${BASH_LINENO[0]}
echo "[ERROR] Command failed at line ${line_no} with exit code ${exit_code}" >&2
echo "[ERROR] Command: ${BASH_COMMAND}" >&2
}
trap handle_error ERR
# === 忽略信号(子脚本不被中断)===
trap '' SIGINT # 忽略 Ctrl+C
sleep 60 # 这期间 Ctrl+C 不起作用
trap - SIGINT # 恢复默认处理
# === trap 在子 shell 中的行为 ===
trap 'echo parent EXIT' EXIT
(
trap 'echo child EXIT' EXIT # 子 shell 有自己的 trap
echo "in subshell"
)
# 输出:child EXIT(子 shell 退出时),然后父 shell 正常运行
# === DEBUG 陷阱(高级调试)===
# trap 'echo "Line $LINENO: $BASH_COMMAND"' DEBUG # 每条命令前执行
12.4 Error Handling Patterns
Beyond set -e auto-exit, production scripts need clear error messages and consistent error code conventions. Here are battle-tested error-handling utility functions:
#!/usr/bin/env bash
set -euo pipefail
# === 工具函数:die / err / log ===
readonly SCRIPT_NAME=$(basename "$0")
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*" >&2
}
err() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
}
die() {
err "$@"
exit 1
}
# 使用示例
log "Script started"
err "Something went wrong"
die "Fatal: cannot continue" # 打印错误并退出
# === || 模式:命令失败时显示错误 ===
cp source.txt dest.txt || die "Failed to copy source.txt to dest.txt"
git pull origin main || {
err "Git pull failed"
err "Check network connectivity and repository access"
exit 1
}
# === 检查依赖(脚本开头)===
check_deps() {
local deps=("$@")
for dep in "${deps[@]}"; do
if ! command -v "$dep" &>/dev/null; then
die "Required command not found: '$dep'. Please install it first."
fi
done
}
check_deps curl jq git docker
# === 检查 root 权限 ===
check_root() {
if [[ $EUID -ne 0 ]]; then
die "This script must be run as root (use sudo)"
fi
}
# === 版本检查 ===
check_bash_version() {
local required_major=4
local required_minor=2
if (( BASH_VERSINFO[0] = ${required_major}.${required_minor} required (found: ${BASH_VERSION})"
fi
}
check_bash_version
# === 错误码规范 ===
# 0 : 成功
# 1 : 通用错误
# 2 : 参数错误(命令行解析失败)
# 126 : 权限不足(命令不可执行)
# 127 : 命令不存在
# 128+N : 被信号 N 终止(130=SIGINT, 143=SIGTERM)
12.5 Argument Parsing: getopts vs getopt
getopts is a bash builtin supporting POSIX short options (-h, -v) with no extra dependencies and best portability. getopt is an external program (GNU version) supporting long options (--help, --verbose) with slightly less portability.
getopts: Built-in Short Option Parsing
#!/usr/bin/env bash
set -euo pipefail
# getopts 完整示例
VERBOSE=false
OUTPUT_FILE=""
DRY_RUN=false
usage() {
cat
Process a file with various options.
Options:
-h Show this help message and exit
-v Enable verbose output
-n Dry run (do not modify files)
-o FILE Output file path (default: stdout)
Examples:
$(basename "$0") -v input.txt
$(basename "$0") -o output.txt -v input.txt
$(basename "$0") -n -v input.txt
EOF
}
# getopts 语法:选项字符串中,: 表示该选项需要参数
while getopts ":hvno:" opt; do
case $opt in
h)
usage
exit 0
;;
v)
VERBOSE=true
;;
n)
DRY_RUN=true
;;
o)
OUTPUT_FILE="$OPTARG" # 选项的参数值通过 $OPTARG 获取
;;
:)
echo "Error: Option -${OPTARG} requires an argument" >&2
usage >&2
exit 2
;;
\?)
echo "Error: Unknown option -${OPTARG}" >&2
usage >&2
exit 2
;;
esac
done
# 移除已解析的选项,$@ 变为位置参数
shift $((OPTIND - 1))
# 检查必须的位置参数
if [[ $# -lt 1 ]]; then
echo "Error: is required" >&2
usage >&2
exit 2
fi
INPUT_FILE="$1"
$VERBOSE && echo "[verbose] Input: $INPUT_FILE, Output: ${OUTPUT_FILE:-stdout}, DryRun: $DRY_RUN"
getopt: GNU Long Option Parsing
#!/usr/bin/env bash
set -euo pipefail
# getopt 支持长选项(--help、--verbose、--output FILE)
# 需要 GNU getopt(Linux 默认,macOS 需安装 gnu-getopt)
VERBOSE=false
OUTPUT_FILE=""
DRY_RUN=false
usage() {
cat
Options:
-h, --help Show this help
-v, --verbose Verbose output
-n, --dry-run Dry run
-o, --output FILE Output file
EOF
}
# 解析长选项和短选项
PARSED=$(getopt \
--options hvno: \
--longoptions help,verbose,dry-run,output: \
--name "$(basename "$0")" \
-- "$@") || { usage >&2; exit 2; }
eval set -- "$PARSED"
while true; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--verbose) VERBOSE=true; shift ;;
-n|--dry-run) DRY_RUN=true; shift ;;
-o|--output) OUTPUT_FILE="$2"; shift 2 ;;
--) shift; break ;;
*) echo "Internal error" >&2; exit 2 ;;
esac
done
[[ $# -lt 1 ]] && { echo "Error: input_file required" >&2; exit 2; }
INPUT_FILE="$1"
$VERBOSE && echo "verbose=$VERBOSE dry_run=$DRY_RUN input=$INPUT_FILE output=${OUTPUT_FILE:-stdout}"
12.6 shellcheck: Static Analysis for Shell Scripts
shellcheck is the "compiler warnings" for shell scripts. It catches common bugs, unsafe usage, and portability issues before you run the script — an essential tool for any shell engineering project.
# === 安装 ===
sudo apt install -y shellcheck # Ubuntu/Debian
sudo dnf install -y ShellCheck # Fedora/RHEL
brew install shellcheck # macOS
# === 基本使用 ===
shellcheck script.sh # 检查单个文件
shellcheck *.sh # 检查所有 .sh 文件
shellcheck -s bash script.sh # 明确指定 shell 类型
# === 常见警告及含义 ===
# SC2086: Double quote to prevent globbing and word splitting
# 问题:
echo $VAR # 如果 VAR 包含空格或通配符,会被展开
ls $DIRECTORY # 可能导致意外的文件名拆分
# 修复:
echo "$VAR"
ls "$DIRECTORY"
# SC2046: Quote this to prevent word splitting
# 问题:
command $(generate_args) # 命令替换结果按空格分割
# 修复:
command "$(generate_args)" # 或用数组
# SC2006: Use $(..) instead of legacy `..`
# 问题:
result=`command` # 反引号嵌套困难,已废弃
# 修复:
result=$(command)
# SC2034: VAR appears unused
# 可以用 _ 前缀表明这是故意未使用的变量
_unused_var="intentional"
# SC2155: Declare and assign separately to avoid masking return values
# 问题:
local result=$(possibly_failing_command) # local 使返回码永远为 0
# 修复:
local result
result=$(possibly_failing_command)
# SC2164: Use 'cd ... || exit' in case cd fails
# 问题:
cd /some/dir
rm -rf * # 如果 cd 失败,会删除当前目录下的文件!
# 修复:
cd /some/dir || exit 1
rm -rf *
# === .shellcheckrc 配置文件(项目根目录)===
cat > .shellcheckrc
## 12.7 shfmt: Automatic Shell Code Formatting
**shfmt** is the `gofmt` for shell scripts — it automatically formats code style and eliminates team formatting debates. It supports bash/sh/mksh and integrates seamlessly with editors and CI.
```bash
# === 安装 ===
# 从 GitHub Releases 下载二进制
VERSION="3.7.0"
curl -LO "https://github.com/mvdan/sh/releases/download/v${VERSION}/shfmt_v${VERSION}_linux_amd64"
install -m755 "shfmt_v${VERSION}_linux_amd64" /usr/local/bin/shfmt
# 或通过包管理器
brew install shfmt # macOS
# === 基本使用 ===
shfmt script.sh # 打印格式化后的结果到 stdout
shfmt -w script.sh # 原地修改文件(write)
shfmt -d script.sh # 显示差异(diff 模式,不修改)
shfmt -l *.sh # 列出需要格式化的文件
# === 常用格式化选项 ===
shfmt -i 2 script.sh # 使用 2 空格缩进(默认 tab)
shfmt -i 4 script.sh # 使用 4 空格缩进
shfmt -ci script.sh # case 语句中的 ) 也缩进
shfmt -bn script.sh # 二元运算符放在行尾(不换行前)
shfmt -sr script.sh # 重定向操作符后加空格
# === 推荐的 CI 检查命令(验证格式,不修改)===
shfmt -d -i 2 -ci scripts/
# === 编辑器集成 ===
# VS Code: 安装 "shell-format" 扩展
# Vim: 安装 vim-shfmt 插件
# Neovim: 通过 null-ls / conform.nvim 集成
# 示例 Vim 配置:
# autocmd FileType sh setlocal formatprg=shfmt\ -i\ 2
# === 格式化前后对比示例 ===
# 格式化前(混乱的缩进和间距):
# if [ "$x" = "1" ];then
# echo "yes"
# fi
# 格式化后(shfmt -i 2):
# if [ "$x" = "1" ]; then
# echo "yes"
# fi
12.8 bats: Unit Testing Framework for Shell Scripts
bats (Bash Automated Testing System) is a testing framework designed for shell scripts. Its syntax is concise, output is clear, it supports the TAP protocol, and it integrates seamlessly into CI pipelines.
# === 安装 bats-core ===
git clone https://github.com/bats-core/bats-core.git
cd bats-core && sudo ./install.sh /usr/local
# 或通过包管理器
brew install bats-core # macOS
sudo apt install bats # Ubuntu(可能版本较旧)
# 安装辅助库(推荐)
git clone https://github.com/bats-core/bats-support.git test/test_helper/bats-support
git clone https://github.com/bats-core/bats-assert.git test/test_helper/bats-assert
# === 被测试的脚本:lib/utils.sh ===
# is_number() { [[ "$1" =~ ^[0-9]+$ ]]; }
# greet() { echo "Hello, $1!"; }
# === bats 测试文件:test/utils.bats ===
cat > test/utils.bats
## 12.9 logger: Writing to System Logs
The `logger` command writes messages to the system log (syslog/journald), integrating script logs with the OS logging infrastructure for centralized management and monitoring.
```bash
# === logger 基本用法 ===
logger "Hello from shell script" # 写入 syslog
journalctl -t logger --since "1 min ago" # 查看(systemd 系统)
# === 常用选项 ===
logger -t "myscript" "Application started" # -t:设置标签(tag)
logger -p local0.info "Info message" # -p:facility.level(优先级)
logger -p local0.warning "Warning message"
logger -p local0.err "Error occurred"
logger -s "Also print to stderr" # -s:同时输出到 stderr
logger -i "Include PID in message" # -i:包含进程 ID
# === 日志级别(level)===
# debug &2; }
log_error() { logger -t "$SCRIPT_TAG" -p local0.err "$*"; echo "[ERROR] $*" >&2; }
log_info "Deployment started by user: $USER"
log_warning "Config file missing, using defaults"
log_error "Database connection failed"
# === 查看日志 ===
journalctl -t "deploy-v2" # 按 tag 过滤(systemd)
journalctl -t "deploy-v2" -f # 实时跟踪
grep "deploy-v2" /var/log/syslog # 传统 syslog
# === rsyslog 自定义 facility 配置 ===
# 在 /etc/rsyslog.d/myapp.conf 中添加:
# local0.* /var/log/myapp.log
# 重启 rsyslog:sudo systemctl restart rsyslog
# 之后 local0 的所有消息都会写入 /var/log/myapp.log
12.10 Script Best Practices
The following are production-tested shell script engineering best practices, each with a clear "why":
#!/usr/bin/env bash
set -euo pipefail
# === 1. 幂等性设计:脚本运行多次结果相同 ===
# 坏:
mkdir /opt/myapp # 第二次运行报错:directory exists
# 好:
mkdir -p /opt/myapp # -p 允许目录已存在
# 坏:
useradd myapp # 第二次运行报错:user exists
# 好:
id myapp &>/dev/null || useradd --system --no-create-home myapp
# === 2. 原子操作:mktemp + mv 防止半写状态 ===
# 坏:直接写入目标文件(崩溃会留下损坏文件)
generate_config > /etc/myapp/config.yaml
# 好:写到临时文件,完成后原子替换
tmp=$(mktemp /etc/myapp/config.yaml.XXXXXX)
generate_config > "$tmp"
chmod 640 "$tmp"
mv "$tmp" /etc/myapp/config.yaml # mv 在同一文件系统上是原子操作
# === 3. 加锁防止并发运行 ===
LOCKFILE="/var/run/myscript.lock"
# 方法 1:flock(推荐,自动清理)
(
flock -n 9 || { echo "Another instance is running"; exit 1; }
# --- 受保护的代码块 ---
echo "Running with lock"
sleep 5
) 9>"$LOCKFILE"
# 方法 2:PID 文件
PIDFILE="/var/run/myscript.pid"
if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "Already running (PID $(cat "$PIDFILE"))" >&2
exit 1
fi
echo $$ > "$PIDFILE"
trap 'rm -f "$PIDFILE"' EXIT
# === 4. 配置文件加载 ===
# 默认配置
CONFIG_FILE="${CONFIG_FILE:-/etc/myapp/myapp.conf}"
[[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE"
# 允许用户覆盖
[[ -f ~/.myapp.conf ]] && source ~/.myapp.conf
# === 5. 目录变量:基于脚本位置确定路径 ===
# 不要依赖当前工作目录(cd 后路径会变)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$SCRIPT_DIR/lib/utils.sh"
CONFIG="$PROJECT_ROOT/config/settings.yaml"
# === 6. 数组处理路径中的空格 ===
# 坏:空格路径被拆分
files=$(find /path -name "*.txt")
for f in $files; do ... # 空格导致路径被截断!
# 好:用 mapfile(readarray)或 while read
mapfile -t files
## 12.11 Complete Production-Grade Script Template
The following is a production-grade script template incorporating all the best practices from this chapter, ready to copy and use. Each section is annotated with its purpose and rationale.
```bash
#!/usr/bin/env bash
# ==============================================================================
# script_name.sh — 一句话描述这个脚本的功能
# Author: Your Name
# Version: 1.0.0
# Created: 2026-04-25
# Usage: ./script_name.sh [OPTIONS]
# ==============================================================================
# === 严格模式:失败快、失败早、失败清晰 ===
set -euo pipefail
IFS=$'\n\t'
# === 常量:只读,全大写 ===
readonly SCRIPT_NAME=$(basename "${BASH_SOURCE[0]}")
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_VERSION="1.0.0"
readonly TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# === 日志文件 ===
readonly LOG_DIR="${LOG_DIR:-/var/log}"
readonly LOG_FILE="${LOG_DIR}/${SCRIPT_NAME%.sh}_${TIMESTAMP}.log"
# === 全局状态变量 ===
TMPDIR_WORK=""
LOCKFILE="/var/run/${SCRIPT_NAME%.sh}.lock"
# === 颜色输出(自动检测 tty)===
if [[ -t 2 ]]; then
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; NC='\033[0m'
else
RED=''; YELLOW=''; GREEN=''; NC=''
fi
# === 日志函数 ===
log() { echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${GREEN}[INFO]${NC} $*" | tee -a "$LOG_FILE" >&2; }
warn() { echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${YELLOW}[WARN]${NC} $*" | tee -a "$LOG_FILE" >&2; }
err() { echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${RED}[ERROR]${NC} $*" | tee -a "$LOG_FILE" >&2; }
die() { err "$@"; exit 1; }
# === 清理函数(trap EXIT 保证必定执行)===
cleanup() {
local exit_code=$?
log "Cleanup started (exit code: ${exit_code})"
# 删除临时工作目录
if [[ -n "${TMPDIR_WORK:-}" && -d "${TMPDIR_WORK}" ]]; then
rm -rf "${TMPDIR_WORK}"
log "Removed temp dir: ${TMPDIR_WORK}"
fi
# 释放锁
[[ -f "${LOCKFILE}" ]] && rm -f "${LOCKFILE}"
if [[ $exit_code -eq 0 ]]; then
log "Script completed successfully."
else
err "Script exited with errors. Check log: ${LOG_FILE}"
fi
exit "$exit_code"
}
trap cleanup EXIT
# 捕获 SIGINT / SIGTERM
trap 'err "Interrupted by user (SIGINT)"; exit 130' INT
trap 'err "Terminated by signal (SIGTERM)"; exit 143' TERM
# === 依赖检查 ===
check_deps() {
local missing=()
for dep in "$@"; do
command -v "$dep" &>/dev/null || missing+=("$dep")
done
if [[ ${#missing[@]} -gt 0 ]]; then
die "Missing required commands: ${missing[*]}"
fi
}
# === 参数解析 ===
VERBOSE=false
DRY_RUN=false
OUTPUT_FILE=""
usage() {
cat
Description here.
Options:
-h, --help Show this help and exit
-v, --verbose Enable verbose output
-n, --dry-run Dry run — show what would happen but do nothing
-o, --output FILE Write output to FILE (default: stdout)
--version Print version and exit
Examples:
${SCRIPT_NAME} -v input.txt
${SCRIPT_NAME} -n --output result.txt input.txt
EOF
}
parse_args() {
local parsed
parsed=$(getopt \
--options hvno: \
--longoptions help,verbose,dry-run,output:,version \
--name "$SCRIPT_NAME" \
-- "$@") || { usage >&2; exit 2; }
eval set -- "$parsed"
while true; do
case "$1" in
-h|--help) usage; exit 0 ;;
--version) echo "${SCRIPT_NAME} v${SCRIPT_VERSION}"; exit 0 ;;
-v|--verbose) VERBOSE=true; shift ;;
-n|--dry-run) DRY_RUN=true; shift ;;
-o|--output) OUTPUT_FILE="$2"; shift 2 ;;
--) shift; break ;;
*) die "Internal error parsing arguments" ;;
esac
done
[[ $# -lt 1 ]] && { err "Missing required argument: "; usage >&2; exit 2; }
INPUT_FILE="$1"
}
# === 主逻辑 ===
main() {
# 检查依赖
check_deps curl jq
# 初始化日志目录
mkdir -p "$(dirname "$LOG_FILE")"
log "=== ${SCRIPT_NAME} v${SCRIPT_VERSION} started ==="
log "Log file: ${LOG_FILE}"
$VERBOSE && log "Verbose mode enabled"
$DRY_RUN && warn "DRY RUN — no changes will be made"
# 加锁防止并发运行
(
flock -n 9 || die "Another instance of ${SCRIPT_NAME} is already running"
# 创建临时工作目录(cleanup 会自动删除)
TMPDIR_WORK=$(mktemp -d "/tmp/${SCRIPT_NAME%.sh}.XXXXXX")
log "Working directory: ${TMPDIR_WORK}"
# --- 业务逻辑开始 ---
log "Processing: ${INPUT_FILE}"
if $DRY_RUN; then
log "[dry-run] Would process ${INPUT_FILE}"
else
cp "${INPUT_FILE}" "${TMPDIR_WORK}/"
log "File copied to work dir"
# ... 更多处理步骤 ...
fi
# 输出结果
if [[ -n "${OUTPUT_FILE}" ]]; then
# 原子写入
local tmp_out
tmp_out=$(mktemp "${OUTPUT_FILE}.XXXXXX")
echo "result data" > "$tmp_out"
mv "$tmp_out" "${OUTPUT_FILE}"
log "Output written to: ${OUTPUT_FILE}"
else
echo "result data"
fi
# --- 业务逻辑结束 ---
) 9>"${LOCKFILE}"
}
# === 入口 ===
parse_args "$@"
main
Chapter Summary: This chapter built a complete shell script engineering knowledge system:
set -euo pipefaileliminates silent failures;trap EXITguarantees resource cleanup;getopts/getoptstandardizes the argument interface;shellcheckcatches bugs before committing;shfmtunifies code style;batsprovides unit testing guarantees;loggerintegrates script logs into the system logging infrastructure. These tools and templates combined are sufficient to elevate any script into a production-ready, trustworthy tool.
Previous
← Ch11: Pipes
Next
Ch13: systemd →