脚本规范的"四行宣言"

每个脚本头部建议放这四行:

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

意义:

  • #!/bin/bash — 用 bash 跑(不是 sh)
  • set -e — 任何命令失败立即退出
  • set -u — 引用未定义变量报错
  • set -o pipefail — 管道里任何一段失败就算失败
  • IFS=$'\n\t' — 默认分隔符改成"换行 / Tab"(避免空格分词问题)

合起来叫"unofficial strict mode"——能挡掉 80% 常见 bug。

调试技巧

1. set -x:每行执行前打印

#!/bin/bash
set -x         # 开
echo "hello"
ls /tmp
set +x         # 关

输出:

+ echo hello
hello
+ ls /tmp
...

也可以临时跑:

bash -x script.sh

2. set -v:打印每行(不展开变量)

-x 类似但更原始。

3. trap:错误时的清理

#!/bin/bash
set -euo pipefail

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"; echo "已清理 $tmpdir"' EXIT

# 后续无论怎么退出,trap 都会跑
cp big-files "$tmpdir/"
process "$tmpdir"

可捕获的信号:

信号 时机
EXIT 任何退出
ERR 出错(配合 set -e)
INT Ctrl+C
TERM kill 默认信号

4. 调试错误时回溯

trap 'echo "Error in line $LINENO: $BASH_COMMAND"' ERR

出错时打印行号和具体命令。

shellcheck:静态分析(必装)

sudo apt install shellcheck
shellcheck script.sh

shellcheck 检查脚本里所有潜在 bug 和不良习惯——quotes 没加、变量名拼错、用了已废弃的语法等。写脚本前必跑

VS Code 装 shellcheck 插件,边写边提示。

错误处理模板

#!/bin/bash
set -euo pipefail

# 日志
log() { printf "[%s] %s\n" "$(date +%T)" "$*"; }
err() { printf "[%s] ERROR: %s\n" "$(date +%T)" "$*" >&2; }
die() { err "$@"; exit 1; }

# 清理
tmpdir=$(mktemp -d)
cleanup() {
    log "清理 $tmpdir"
    rm -rf "$tmpdir"
}
trap cleanup EXIT

# 出错时附加信息
trap 'err "脚本在 line $LINENO 失败: $BASH_COMMAND"' ERR

# 主流程
main() {
    log "开始"

    # 前置检查
    [[ "$EUID" -eq 0 ]] || die "需要 root 权限"
    command -v jq >/dev/null || die "缺少 jq"

    # 业务
    do_work

    log "完成"
}

do_work() {
    # ...
    :
}

main "$@"

常见性能优化

1. 避免循环里调外部命令

# ❌ 慢:每次循环 fork 一个 awk
for i in {1..1000}; do
    echo "$i" | awk '{print $1*2}'
done

# ✓ 一次性
seq 1 1000 | awk '{print $1*2}'

2. 用内置而不是外部命令

# ❌ 慢:fork basename
basename "$file"

# ✓ 快:参数展开
"${file##*/}"

3. 大文件用 awk / sed 一次过

# ❌ 逐行处理 100 万行
while read line; do
    [[ "$line" == *ERROR* ]] && ((count++))
done < big.log

# ✓ awk 一次扫
count=$(awk '/ERROR/{c++} END{print c}' big.log)

命名与风格规范

# 变量:小写 + 下划线
backup_dir="/backups"

# 常量 / 环境:大写
readonly MAX_RETRIES=3
export DEBUG=1

# 函数:小写 + 下划线
do_backup() { ... }

# 缩进:4 空格(或 2 空格,统一就好)

# 长选项更可读
rsync --archive --verbose --delete   # 比 -avz --delete 易读

一个真实的最小生产脚本骨架

#!/bin/bash
#
# backup.sh - 每日数据库 + 文件备份
#
# 用法:backup.sh [--dry-run]
#

set -euo pipefail
IFS=$'\n\t'

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/var/log/backup.log"
readonly BACKUP_DIR="/backups/$(date +%F)"

# 日志(同时屏幕和文件)
exec > >(tee -a "$LOG_FILE") 2>&1

log() { printf "[%s] %s\n" "$(date +'%F %T')" "$*"; }
die() { log "ERROR: $*"; exit 1; }

dry_run=false
[[ "${1:-}" == "--dry-run" ]] && dry_run=true

main() {
    log "开始备份(dry-run=$dry_run)"
    mkdir -p "$BACKUP_DIR"

    backup_db
    backup_files
    upload_to_cloud
    cleanup_old

    log "完成"
}

backup_db() {
    log "备份数据库..."
    $dry_run && return
    mysqldump -uroot mydb | gzip > "$BACKUP_DIR/db.sql.gz"
}

backup_files() {
    log "备份文件..."
    $dry_run && return
    tar -czf "$BACKUP_DIR/data.tar.gz" /srv/data
}

upload_to_cloud() {
    log "上传到对象存储..."
    $dry_run && return
    rclone copy "$BACKUP_DIR" "remote:backups/$(basename "$BACKUP_DIR")"
}

cleanup_old() {
    log "清理 30 天前..."
    $dry_run && return
    find /backups -mindepth 1 -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;
}

main "$@"

下一篇起进入模块七:磁盘与存储管理。