linux-bash-scripting
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLinux Bash Scripting
Linux Bash 脚本编写
Target: GNU Bash 4.4+ on Linux. No macOS/BSD workarounds, no Windows paths, no POSIX-only restrictions.
目标:适用于 Linux 环境下的 GNU Bash 4.4+ 版本。不提供 macOS/BSD 兼容方案、不支持 Windows 路径、不限制为仅 POSIX 规范语法。
Script Foundation
脚本基础框架
bash
#!/usr/bin/env bash
set -Eeuo pipefail
shopt -s inherit_errexit
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
trap 'printf "Error at %s:%d\n" "${BASH_SOURCE[0]}" "$LINENO" >&2' ERR
trap 'rm -rf -- "${_tmpdir:-}"' EXIT- propagates ERR traps into functions
-E - propagates errexit into
inherit_errexitcommand substitutions$() - Always create temp dirs under the EXIT trap:
_tmpdir=$(mktemp -d) - Wrap body in with source guard:
main() { ... }— enables sourcing for testing[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
bash
#!/usr/bin/env bash
set -Eeuo pipefail
shopt -s inherit_errexit
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
trap 'printf "Error at %s:%d\n" "${BASH_SOURCE[0]}" "$LINENO" >&2' ERR
trap 'rm -rf -- "${_tmpdir:-}"' EXIT- 会将 ERR 陷阱传递到函数内部
-E - 会将 errexit 规则传递到
inherit_errexit命令替换中$() - 始终在 EXIT 陷阱中声明临时目录:
_tmpdir=$(mktemp -d) - 将脚本主体封装在 中,加上引入保护逻辑:
main() { ... }—— 支持脚本被引入以进行测试[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
Core Rules
核心规则
- Quote every expansion: ,
"$var","$(cmd)""${array[@]}" - for function variables,
localfor function constants,local -rfor script constantsreadonly - over
printf '%s\n'— predictable behavior, no flag interpretationecho - for conditionals;
[[ ]]for arithmetic;(( ))over backticks$() - End options with :
--,rm -rf -- "$path"grep -- "$pattern" "$file" - Require env vars:
: "${VAR:?must be set}" - Never user input; build commands as arrays:
evalcmd=("grep" "--" "$pat" "$f"); "${cmd[@]}" - Separate from assignment to preserve exit codes:
locallocal val; val=$(cmd) - Debug tracing: with
PS4='+${BASH_SOURCE[0]}:${LINENO}: '— shows file:line per commandbash -x - Named exit codes: — no magic numbers in
readonly EX_USAGE=64 EX_CONFIG=78exit - Pipeline diagnostics: shows exit code of each pipe stage, not just last failure
"${PIPESTATUS[@]}"
- 所有变量扩展都加引号:、
"$var"、"$(cmd)""${array[@]}" - 函数内变量用 声明,函数内常量用
local声明,脚本全局常量用local -r声明readonly - 优先用 而非
printf '%s\n'—— 行为可预测,不会被参数标志干扰echo - 条件判断用 ;算术运算用
[[ ]];命令替换优先用(( ))而非反引号$() - 选项参数末尾加 避免歧义:
--、rm -rf -- "$path"grep -- "$pattern" "$file" - 强制校验必填环境变量:
: "${VAR:?must be set}" - 不要对用户输入使用 ;用数组构建命令:
evalcmd=("grep" "--" "$pat" "$f"); "${cmd[@]}" - 将 声明和赋值分开以保留退出码:
locallocal val; val=$(cmd) - 调试追踪配置:配合
PS4='+${BASH_SOURCE[0]}:${LINENO}: '使用 —— 会显示每条命令对应的文件和行号bash -x - 使用命名退出码:—— 不要在
readonly EX_USAGE=64 EX_CONFIG=78中使用魔数exit - 管道诊断:可以获取管道中每一步的退出码,而不仅仅是最后一步的错误状态
"${PIPESTATUS[@]}"
Safe Iteration
安全迭代遍历
bash
undefinedbash
undefinedNUL-delimited file processing
NUL 分隔的文件处理
while IFS= read -r -d '' f; do
process "$f"
done < <(find /path -type f -name '*.log' -print0)
while IFS= read -r -d '' f; do
process "$f"
done < <(find /path -type f -name '*.log' -print0)
Array from command output
从命令输出构建数组
readarray -t lines < <(command)
readarray -d '' files < <(find . -print0)
readarray -t lines < <(command)
readarray -d '' files < <(find . -print0)
Glob with no-match guard
带无匹配保护的通配符遍历
for f in *.txt; do [[ -e "$f" ]] || continue; process "$f"; done
undefinedfor f in *.txt; do [[ -e "$f" ]] || continue; process "$f"; done
undefinedArgument Parsing
参数解析
bash
verbose=false; output=""
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) verbose=true; shift ;;
-o|--output) output="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) printf 'Unknown: %s\n' "$1" >&2; exit 1 ;;
*) break ;;
esac
donebash
verbose=false; output=""
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) verbose=true; shift ;;
-o|--output) output="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) printf 'Unknown: %s\n' "$1" >&2; exit 1 ;;
*) break ;;
esac
doneProduction Patterns
生产级实践模式
Dependency check:
bash
require() { command -v "$1" &>/dev/null || { printf 'Missing: %s\n' "$1" >&2; exit 1; }; }
require jq; require curlDry-run wrapper:
bash
run() { if [[ "${DRY_RUN:-}" == "1" ]]; then printf '[dry] %s\n' "$*" >&2; else "$@"; fi; }
run cp "$src" "$dst"Atomic file write — write to temp, rename into place:
bash
atomic_write() { local tmp; tmp=$(mktemp); cat >"$tmp"; mv -- "$tmp" "$1"; }
generate_config | atomic_write /etc/app/config.ymlRetry with backoff:
bash
retry() { local n=0 max=5 delay=1; until "$@"; do ((++n>=max)) && return 1; sleep $delay; ((delay*=2)); done; }
retry curl -fsSL "$url"Script locking — prevent concurrent runs:
bash
exec 9>/var/lock/"${0##*/}".lock
flock -n 9 || { printf 'Already running\n' >&2; exit 1; }Idempotent operations — safe to rerun:
bash
ensure_dir() { [[ -d "$1" ]] || mkdir -p -- "$1"; }
ensure_link() { [[ -L "$2" ]] || ln -s -- "$1" "$2"; }Input validation: — validate at script boundaries with
[[ "$1" =~ ^[1-9][0-9]*$ ]] || die "Invalid: $1"[[ =~ ]]- for scripts creating sensitive files
umask 077 - Signal cleanup: — preserves correct exit codes for callers
trap 'cleanup; exit 130' INT TERM
依赖检查:
bash
require() { command -v "$1" &>/dev/null || { printf 'Missing: %s\n' "$1" >&2; exit 1; }; }
require jq; require curl试运行封装:
bash
run() { if [[ "${DRY_RUN:-}" == "1" ]]; then printf '[dry] %s\n' "$*" >&2; else "$@"; fi; }
run cp "$src" "$dst"原子文件写入 —— 先写入临时文件,再重命名到目标路径:
bash
atomic_write() { local tmp; tmp=$(mktemp); cat >"$tmp"; mv -- "$tmp" "$1"; }
generate_config | atomic_write /etc/app/config.yml退避重试:
bash
retry() { local n=0 max=5 delay=1; until "$@"; do ((++n>=max)) && return 1; sleep $delay; ((delay*=2)); done; }
retry curl -fsSL "$url"脚本锁 —— 防止脚本并发运行:
bash
exec 9>/var/lock/"${0##*/}".lock
flock -n 9 || { printf 'Already running\n' >&2; exit 1; }幂等操作 —— 重复执行也安全:
bash
ensure_dir() { [[ -d "$1" ]] || mkdir -p -- "$1"; }
ensure_link() { [[ -L "$2" ]] || ln -s -- "$1" "$2"; }输入校验: —— 在脚本入口处用 校验输入
[[ "$1" =~ ^[1-9][0-9]*$ ]] || die "Invalid: $1"[[ =~ ]]- 创建敏感文件的脚本设置
umask 077 - 信号清理:—— 为调用方保留正确的退出码
trap 'cleanup; exit 130' INT TERM
Logging
日志规范
bash
log() { printf '[%s] [%s] %s\n' "$(date -Iseconds)" "$1" "${*:2}" >&2; }
info() { log INFO "$@"; }
warn() { log WARN "$@"; }
error() { log ERROR "$@"; }
die() { error "$@"; exit 1; }bash
log() { printf '[%s] [%s] %s\n' "$(date -Iseconds)" "$1" "${*:2}" >&2; }
info() { log INFO "$@"; }
warn() { log WARN "$@"; }
error() { log ERROR "$@"; }
die() { error "$@"; exit 1; }Anti-Patterns
反模式
| Bad | Fix |
|---|---|
| |
| |
| |
| |
| |
| `cd dir |
| 错误写法 | 正确写法 |
|---|---|
| |
| |
| |
| |
一开始就用 | 先使用 |
| `cd dir |
Performance
性能优化
- Parameter expansion over externals: not
${path%/*},dirnamenot${path##*/},basenamenot${var//old/new}sed - over
(( ));exprover[[ =~ ]]echo | grep - Cache results: once, reuse
val=$(cmd)$val - for parallel work
xargs -0 -P "$(nproc)" - for lookups instead of repeated grep
declare -A map
- 优先用参数扩展而非外部命令:用 代替
${path%/*},dirname代替${path##*/},basename代替${var//old/new}sed - 用 代替
(( ));用expr代替[[ =~ ]]echo | grep - 缓存结果:只执行一次 ,后续复用
val=$(cmd)$val - 并行任务用
xargs -0 -P "$(nproc)" - 查找场景用 实现映射,而非重复执行 grep
declare -A map
Bash 4.4+ / 5.x
Bash 4.4+ / 5.x 特性
- shell-quoted,
${var@Q}uppercase,${var@U}lowercase${var@L} - nameref for indirect access
declare -n ref=varname - wait for any background job
wait -n - ,
$EPOCHSECONDS— timestamps without forking$EPOCHREALTIMEdate
- 转义为 shell 安全格式,
${var@Q}转大写,${var@U}转小写${var@L} - 引用实现间接变量访问
declare -n ref=varname - 等待任意一个后台任务完成
wait -n - 、
$EPOCHSECONDS—— 不需要调用$EPOCHREALTIME即可获取时间戳date
Linux-Specific
Linux 专属特性
- ,
/proc/self/status,/proc/cpuinfofor system info/proc/meminfo - for services;
systemctlfor logsjournalctl -u svc - GNU coreutils: (no
sed -i),''(PCRE),grep -Preadlink -f - to prevent hangs
timeout 30s cmd - for script locking (see above)
flock - Package install: /
apt-get install -y/dnf install -ypacman -S --noconfirm
- 读取 、
/proc/self/status、/proc/cpuinfo获取系统信息/proc/meminfo - 服务管理用 ;日志查询用
systemctljournalctl -u svc - GNU coreutils 特性:(不需要加
sed -i)、''(PCRE 正则支持)、grep -Preadlink -f - 用 防止命令挂起
timeout 30s cmd - 实现脚本锁(见上文)
flock - 包安装命令:/
apt-get install -y/dnf install -ypacman -S --noconfirm
ShellCheck
ShellCheck
Run . Key rules:
shellcheck --enable=all script.sh- SC2155: Separate declaration from assignment
- SC2086: Double-quote variables
- SC2046: Quote command substitutions
- SC2164:
cd dir || exit - SC2327/SC2328: Use not
${BASH_REMATCH[n]}for regex captures$n
Pre-commit:
shellcheck *.sh && shfmt -i 2 -ci -d *.sh执行 进行校验,核心规则:
shellcheck --enable=all script.sh- SC2155:变量声明和赋值分开
- SC2086:变量加双引号
- SC2046:命令替换加引号
- SC2164:处理切换目录失败的情况
cd dir || exit - SC2327/SC2328:正则捕获结果用 而非
${BASH_REMATCH[n]}$n
Pre-commit 检查:
shellcheck *.sh && shfmt -i 2 -ci -d *.sh