linux-bash-scripting

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Linux 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
  • -E
    propagates ERR traps into functions
  • inherit_errexit
    propagates errexit into
    $()
    command substitutions
  • Always create temp dirs under the EXIT trap:
    _tmpdir=$(mktemp -d)
  • Wrap body in
    main() { ... }
    with source guard:
    [[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
    — enables sourcing for testing
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
  • -E
    会将 ERR 陷阱传递到函数内部
  • inherit_errexit
    会将 errexit 规则传递到
    $()
    命令替换中
  • 始终在 EXIT 陷阱中声明临时目录:
    _tmpdir=$(mktemp -d)
  • 将脚本主体封装在
    main() { ... }
    中,加上引入保护逻辑:
    [[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
    —— 支持脚本被引入以进行测试

Core Rules

核心规则

  • Quote every expansion:
    "$var"
    ,
    "$(cmd)"
    ,
    "${array[@]}"
  • local
    for function variables,
    local -r
    for function constants,
    readonly
    for script constants
  • printf '%s\n'
    over
    echo
    — predictable behavior, no flag interpretation
  • [[ ]]
    for conditionals;
    (( ))
    for arithmetic;
    $()
    over backticks
  • End options with
    --
    :
    rm -rf -- "$path"
    ,
    grep -- "$pattern" "$file"
  • Require env vars:
    : "${VAR:?must be set}"
  • Never
    eval
    user input; build commands as arrays:
    cmd=("grep" "--" "$pat" "$f"); "${cmd[@]}"
  • Separate
    local
    from assignment to preserve exit codes:
    local val; val=$(cmd)
  • Debug tracing:
    PS4='+${BASH_SOURCE[0]}:${LINENO}: '
    with
    bash -x
    — shows file:line per command
  • Named exit codes:
    readonly EX_USAGE=64 EX_CONFIG=78
    — no magic numbers in
    exit
  • Pipeline diagnostics:
    "${PIPESTATUS[@]}"
    shows exit code of each pipe stage, not just last failure
  • 所有变量扩展都加引号:
    "$var"
    "$(cmd)"
    "${array[@]}"
  • 函数内变量用
    local
    声明,函数内常量用
    local -r
    声明,脚本全局常量用
    readonly
    声明
  • 优先用
    printf '%s\n'
    而非
    echo
    —— 行为可预测,不会被参数标志干扰
  • 条件判断用
    [[ ]]
    ;算术运算用
    (( ))
    ;命令替换优先用
    $()
    而非反引号
  • 选项参数末尾加
    --
    避免歧义:
    rm -rf -- "$path"
    grep -- "$pattern" "$file"
  • 强制校验必填环境变量:
    : "${VAR:?must be set}"
  • 不要对用户输入使用
    eval
    ;用数组构建命令:
    cmd=("grep" "--" "$pat" "$f"); "${cmd[@]}"
  • local
    声明和赋值分开以保留退出码:
    local val; val=$(cmd)
  • 调试追踪配置:
    PS4='+${BASH_SOURCE[0]}:${LINENO}: '
    配合
    bash -x
    使用 —— 会显示每条命令对应的文件和行号
  • 使用命名退出码:
    readonly EX_USAGE=64 EX_CONFIG=78
    —— 不要在
    exit
    中使用魔数
  • 管道诊断:
    "${PIPESTATUS[@]}"
    可以获取管道中每一步的退出码,而不仅仅是最后一步的错误状态

Safe Iteration

安全迭代遍历

bash
undefined
bash
undefined

NUL-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
undefined
for f in *.txt; do [[ -e "$f" ]] || continue; process "$f"; done
undefined

Argument 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
done
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
done

Production Patterns

生产级实践模式

Dependency check:
bash
require() { command -v "$1" &>/dev/null || { printf 'Missing: %s\n' "$1" >&2; exit 1; }; }
require jq; require curl
Dry-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.yml
Retry 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:
[[ "$1" =~ ^[1-9][0-9]*$ ]] || die "Invalid: $1"
— validate at script boundaries with
[[ =~ ]]
  • umask 077
    for scripts creating sensitive files
  • Signal cleanup:
    trap 'cleanup; exit 130' INT TERM
    — preserves correct exit codes for callers
依赖检查:
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

反模式

BadFix
for f in $(ls)
for f in *; do
or
find -print0 | while read
local x=$(cmd)
local x; x=$(cmd)
— preserves exit code
echo "$data"
printf '%s\n' "$data"
cat file | grep
grep pat file
kill -9 $pid
first
kill "$pid"
first,
-9
as last resort
cd dir; cmd
`cd dir
错误写法正确写法
for f in $(ls)
for f in *; do
或者
find -print0 | while read
local x=$(cmd)
local x; x=$(cmd)
—— 保留命令退出码
echo "$data"
printf '%s\n' "$data"
cat file | grep
grep pat file
一开始就用
kill -9 $pid
先使用
kill "$pid"
-9
作为最后手段
cd dir; cmd
`cd dir

Performance

性能优化

  • Parameter expansion over externals:
    ${path%/*}
    not
    dirname
    ,
    ${path##*/}
    not
    basename
    ,
    ${var//old/new}
    not
    sed
  • (( ))
    over
    expr
    ;
    [[ =~ ]]
    over
    echo | grep
  • Cache results:
    val=$(cmd)
    once, reuse
    $val
  • xargs -0 -P "$(nproc)"
    for parallel work
  • declare -A map
    for lookups instead of repeated grep
  • 优先用参数扩展而非外部命令:用
    ${path%/*}
    代替
    dirname
    ${path##*/}
    代替
    basename
    ${var//old/new}
    代替
    sed
  • (( ))
    代替
    expr
    ;用
    [[ =~ ]]
    代替
    echo | grep
  • 缓存结果:只执行一次
    val=$(cmd)
    ,后续复用
    $val
  • 并行任务用
    xargs -0 -P "$(nproc)"
  • 查找场景用
    declare -A map
    实现映射,而非重复执行 grep

Bash 4.4+ / 5.x

Bash 4.4+ / 5.x 特性

  • ${var@Q}
    shell-quoted,
    ${var@U}
    uppercase,
    ${var@L}
    lowercase
  • declare -n ref=varname
    nameref for indirect access
  • wait -n
    wait for any background job
  • $EPOCHSECONDS
    ,
    $EPOCHREALTIME
    — timestamps without forking
    date
  • ${var@Q}
    转义为 shell 安全格式,
    ${var@U}
    转大写,
    ${var@L}
    转小写
  • declare -n ref=varname
    引用实现间接变量访问
  • wait -n
    等待任意一个后台任务完成
  • $EPOCHSECONDS
    $EPOCHREALTIME
    —— 不需要调用
    date
    即可获取时间戳

Linux-Specific

Linux 专属特性

  • /proc/self/status
    ,
    /proc/cpuinfo
    ,
    /proc/meminfo
    for system info
  • systemctl
    for services;
    journalctl -u svc
    for logs
  • GNU coreutils:
    sed -i
    (no
    ''
    ),
    grep -P
    (PCRE),
    readlink -f
  • timeout 30s cmd
    to prevent hangs
  • flock
    for script locking (see above)
  • Package install:
    apt-get install -y
    /
    dnf install -y
    /
    pacman -S --noconfirm
  • 读取
    /proc/self/status
    /proc/cpuinfo
    /proc/meminfo
    获取系统信息
  • 服务管理用
    systemctl
    ;日志查询用
    journalctl -u svc
  • GNU coreutils 特性:
    sed -i
    (不需要加
    ''
    )、
    grep -P
    (PCRE 正则支持)、
    readlink -f
  • timeout 30s cmd
    防止命令挂起
  • flock
    实现脚本锁(见上文)
  • 包安装命令:
    apt-get install -y
    /
    dnf install -y
    /
    pacman -S --noconfirm

ShellCheck

ShellCheck

Run
shellcheck --enable=all script.sh
. Key rules:
  • SC2155: Separate declaration from assignment
  • SC2086: Double-quote variables
  • SC2046: Quote command substitutions
  • SC2164:
    cd dir || exit
  • SC2327/SC2328: Use
    ${BASH_REMATCH[n]}
    not
    $n
    for regex captures
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