Linux: Bash 脚本进阶——函数 / 数组 / 错误处理

基础脚本能完成简单任务,但生产级脚本需要更健壮的代码——函数拆分逻辑、错误处理防止"静默失败"、参数解析让脚本更易用。

📋 前置知识:需要先掌握以下内容

1. 你将学到


2. 小明的脚本在服务器上乱跑的故事

(1) 痛点:脚本中途出错后继续乱跑

小明写了一个部署脚本:

BASH
#!/bin/bash
cd /var/www/myapp
git pull origin main
npm install            # ❌ 这里失败了
npm run build          # 但脚本继续往下跑
pm2 restart app        # 用旧的代码和破损的 build 重启动了服务

(2) 加一行就变安全

Bob 说:"在脚本开头加一行 set -e,遇到任何错误立即退出。"

TEXT 📖 仅展示
#!/bin/bash
set -e                 # 出错即停
cd /var/www/myapp
git pull origin main
npm install
npm run build
pm2 restart app

(3) 收益:安全第一

加上 set -e 后,如果 npm install 失败,脚本立刻退出,不会用坏代码重启服务。


3. 知识点讲解

(1) 函数

TEXT 📖 仅展示
# 定义函数
greet() {
    echo "Hello, $1!"
    echo "今天 $(date)"
}

# 另一种语法
function greet {
    echo "Hello, $1!"
}

# 调用
greet "Alice"

# 局部变量
myfunc() {
    local x=10        # 局部变量,不影响外部
    echo "内部: $x"
}
x=5
myfunc                # 输出:内部: 10
echo "外部: $x"       # 输出:外部: 5

(2) 数组

TEXT 📖 仅展示
# 索引数组
fruits=("apple" "banana" "orange")
fruits[3]="grape"     # 添加元素

echo ${fruits[0]}     # apple
echo ${fruits[@]}     # 所有元素
echo ${#fruits[@]}    # 数组长度

# 遍历数组
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done

# 关联数组(类似字典)
declare -A config
config[host]="localhost"
config[port]="8080"
config[user]="admin"

echo ${config[host]}  # localhost

(3) 错误处理

TEXT 📖 仅展示
# set -e:遇到错误立即退出
set -e

# set -u:使用未定义变量时报错
set -u

# set -o pipefail:管道中任何命令失败都返回非零
set -o pipefail

# 常见的组合
set -euo pipefail

> 💡 **提示**:`set -euo pipefail` 是生产级脚本的"安全三件套"——`-e` 遇错即停、`-u` 使用未定义变量时报错、`-o pipefail` 管道中任何命令失败都导致整体失败。所有非玩具脚本都应加上这一行。

# trap:捕获信号或退出时执行清理
trap 'echo "脚本被中断"; exit 1' INT TERM
trap 'rm -f /tmp/tempfile; echo "清理完成"' EXIT

> 💡 **提示**:`trap` 可以在脚本退出时自动执行清理操作——无论脚本是正常结束还是被 Ctrl+C 中断,`trap EXIT` 都会被触发。常用场景:删除临时文件、释放文件锁、恢复配置等。在脚本开头写 `trap 'rm -f $TMPFILE' EXIT` 就不用在每个退出点手动清理了。

# 自定义错误处理
error_handler() {
    echo "❌ 第 $1 行出错"
    echo "命令: $2"
    exit 1
}
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR

(4) getopts——参数解析

TEXT 📖 仅展示
# getopts 语法
while getopts "f:o:hv" opt; do
    case $opt in
        f) INPUT_FILE="$OPTARG" ;;   # -f filename
        o) OUTPUT_DIR="$OPTARG" ;;   # -o output
        h) show_help; exit 0 ;;      # -h help
        v) VERBOSE=true ;;           # -v verbose
        *) echo "未知参数"; exit 1 ;;
    esac
done

> ℹ️ **说明**:`getopts` 是 bash 内建的参数解析工具——选项字符串 `"f:o:hv"` 中,冒号 `:` 表示该选项需要参数(如 `-f filename`),没有冒号表示开关选项(如 `-v`)。它不支持长选项(如 `--help`),如需长选项可用外部命令 `getopt` 或手动解析。

(5) 调试技巧

TEXT 📖 仅展示
# 调试模式:显示每一行执行的命令
set -x

# 关闭调试
set +x

# 在某一段启用调试
echo "正常执行"
set -x
echo "这段会被追踪"
for i in 1 2 3; do echo "$i"; done
set +x
echo "这里又恢复正常"

# 或者启动脚本时调试
# bash -x script.sh

▶ 示例:函数封装

TEXT 📖 仅展示
#!/bin/bash
# functions-demo.sh - 函数示例

log() {
    local level="${1:-INFO}"
    local message="$2"
    echo "[$(date '+%H:%M:%S')] [$level] $message"
}

die() {
    log "ERROR" "$1"
    exit 1
}

check_dependency() {
    if ! command -v "$1" &> /dev/null; then
        die "所需命令 '$1' 未找到"
    fi
}

# 使用函数
log "INFO" "开始检查依赖..."
check_dependency "git"
check_dependency "node"
log "INFO" "依赖检查通过"

▶ 示例:数组操作

TEXT 📖 仅展示
#!/bin/bash
# array-demo.sh - 数组示例

# 服务器列表
servers=("web01" "web02" "db01" "cache01")

echo "服务器总数: ${#servers[@]}"

echo "--- 遍历服务器 ---"
for server in "${servers[@]}"; do
    echo "检查: $server"
done

echo "--- 关联数组示例 ---"
declare -A disk_usage
disk_usage[/]="45%"
disk_usage[/home]="62%"
disk_usage[/var]="78%"

for mount in "${!disk_usage[@]}"; do
    echo "$mount: ${disk_usage[$mount]}"
done

输出:

TEXT 📖 仅展示
[10:23:01] [INFO] 开始检查依赖...
[10:23:01] [INFO] 依赖检查通过
服务器总数: 4
--- 遍历服务器 ---
检查: web01
检查: web02
检查: db01
检查: cache01
--- 关联数组示例 ---
/: 45%
/home: 62%
/var: 78%

▶ 示例:set -e / trap 错误处理

BASH
#!/bin/bash
# safe-script.sh - 健壮的脚本

set -euo pipefail

cleanup() {
    echo "清理临时文件..."
    rm -f /tmp/temp_*.txt
    echo "完成"
}

error_handler() {
    echo "❌ 错误发生在第 $1 行"
    cleanup
    exit 1
}

trap error_handler ERR
trap cleanup EXIT

echo "开始执行..."
mkdir -p /tmp/test
touch /tmp/test/file.txt
cat /tmp/test/file.txt
# 如果 cat 一个不存在的文件,脚本会停在行并触发 ERR trap

输出:

TEXT 📖 仅展示
开始执行...
清理临时文件...
完成

▶ 示例:getopts 参数解析

BASH
#!/bin/bash
# backup-tool.sh - 带参数解析的备份工具

usage() {
    echo "用法: $0 -s <源目录> -d <目标目录> [-c] [-v]"
    echo "  -s  要备份的源目录"
    echo "  -d  备份目标目录"
    echo "  -c  压缩备份"
    echo "  -v  详细输出"
    echo "  -h  显示帮助"
    exit 0
}

# 默认值
COMPRESS=false
VERBOSE=false

while getopts "s:d:cvh" opt; do
    case $opt in
        s) SOURCE="$OPTARG" ;;
        d) DEST="$OPTARG" ;;
        c) COMPRESS=true ;;
        v) VERBOSE=true ;;
        h) usage ;;
        *) usage ;;
    esac
done

# 验证必需参数
if [ -z "$SOURCE" ] || [ -z "$DEST" ]; then
    echo "错误: 必须指定 -s 和 -d"
    usage
fi

if [ ! -d "$SOURCE" ]; then
    echo "错误: 源目录 '$SOURCE' 不存在"
    exit 1
fi

# 执行备份
mkdir -p "$DEST"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_${TIMESTAMP}"

if [ "$COMPRESS" = true ]; then
    tar -czf "${DEST}/${BACKUP_NAME}.tar.gz" "$SOURCE"
    echo "✅ 压缩备份完成: ${DEST}/${BACKUP_NAME}.tar.gz"
else
    cp -r "$SOURCE" "${DEST}/${BACKUP_NAME}"
    echo "✅ 备份完成: ${DEST}/${BACKUP_NAME}"
fi

if [ "$VERBOSE" = true ]; then
    echo "源: $SOURCE"
    echo "目标: $DEST"
    echo "大小: $(du -sh "$SOURCE" | cut -f1)"
fi

输出:

TEXT 📖 仅展示
用法: ./backup-tool.sh -s <源目录> -d <目标目录> [-c] [-v]
  -s  要备份的源目录
  -d  备份目标目录
  -c  压缩备份
  -v  详细输出
  -h  显示帮助
✅ 压缩备份完成: /backup/backup_20260707_102345.tar.gz
源: /var/www/myapp
目标: /backup
大小: 12M

▶ 示例:调试技巧

TEXT 📖 仅展示
#!/bin/bash
# debug-demo.sh - 调试示例

# 只在需要调试的部分开启追踪
echo "脚本启动 PID: $$"

for i in 1 2 3; do
    set -x              # 开始追踪
    echo "循环第 $i 次"
    result=$((i * 10))
    echo "结果: $result"
    set +x              # 停止追踪
done

echo "脚本执行完毕"

▶ 综合示例:生产级部署脚本

BASH
#!/bin/bash
# deploy.sh - 生产级部署脚本

set -euo pipefail

APP_NAME="webapp"
APP_DIR="/var/www/$APP_NAME"
BACKUP_DIR="/backup/$APP_NAME"
LOG_FILE="/var/log/deploy.log"

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

log() {
    local level="$1"
    local msg="$2"
    echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] ${msg}" | tee -a "$LOG_FILE"
}

info()  { log "INFO" "$1"; }
success() { log "INFO" "${GREEN}$1${NC}"; }
error() { log "ERROR" "${RED}$1${NC}"; }

cleanup() {
    info "清理部署临时文件..."
}

error_handler() {
    error "部署在第 $1 行失败"
    error "命令: $BASH_COMMAND"
    cleanup
    exit 1
}

usage() {
    echo "用法: $0 [选项]"
    echo "  -b   指定 Git 分支 (默认: main)"
    echo "  -e   指定环境 (dev/staging/prod)"
    echo "  -n   不执行备份"
    echo "  -h   显示帮助"
    exit 0
}

trap error_handler ERR
trap cleanup EXIT

# --- 参数解析 ---
BRANCH="main"
ENV="prod"
SKIP_BACKUP=false

while getopts "b:e:nh" opt; do
    case $opt in
        b) BRANCH="$OPTARG" ;;
        e) ENV="$OPTARG" ;;
        n) SKIP_BACKUP=true ;;
        h) usage ;;
        *) usage ;;
    esac
done

# --- 检查环境 ---
info "开始部署 $APP_NAME (分支: $BRANCH, 环境: $ENV)"

if [ ! -d "$APP_DIR" ]; then
    error "应用目录 $APP_DIR 不存在"
    exit 1
fi

# --- 备份 ---
if [ "$SKIP_BACKUP" = false ]; then
    info "备份当前版本..."
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    mkdir -p "$BACKUP_DIR"
    tar -czf "${BACKUP_DIR}/${TIMESTAMP}.tar.gz" \
        --exclude=node_modules \
        --exclude=.git \
        "$APP_DIR"
    success "备份完成: ${BACKUP_DIR}/${TIMESTAMP}.tar.gz"
fi

# --- 部署 ---
cd "$APP_DIR"

info "拉取最新代码..."
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"

info "安装依赖..."
npm ci --production

info "构建应用..."
if [ "$ENV" = "production" ]; then
    npm run build:prod
else
    npm run build
fi

info "重启服务..."
pm2 restart "$APP_NAME" || pm2 start npm --name "$APP_NAME" -- start

# --- 健康检查 ---
info "执行健康检查..."
sleep 3
if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health | grep -q "200"; then
    success "✅ 部署成功!健康检查通过"
else
    error "❌ 健康检查失败,请手动检查"
    exit 1
fi

❓ 常见问题

Q: 函数和别名有什么区别? A: 函数比别名更强大:支持参数、局部变量、返回值、复用。别名只是简单的命令替换。复杂逻辑用函数,简单缩写用别名。

Q: set -e 什么情况下不生效? A: set -e 在以下情况不生效:1)在 if/while/until 的条件表达式中。2)在 &&|| 后面。3)在管道中(需要加 set -o pipefail)。4)命令以 ! 取反时。

Q: trap 能捕获哪些信号? A: 常见的:EXIT(脚本退出)、ERR(命令失败)、INT(Ctrl+C)、TERMkill 默认信号)。其他还有 HUPQUITUSR1USR2

Q: getopts 和 getopt 有什么区别? A: getopts 是 bash 内建命令,更简单,不支持长选项(如 --help)。getopt 是外部命令(util-linux 包),支持长选项和短选项混合。推荐用 getopts 保持兼容性。

Q: 脚本里怎么生成临时文件? A:mktemptempfile=$(mktemp) 创建临时文件,tempdir=$(mktemp -d) 创建临时目录。临时文件通常创建在 /tmp 下,用完要删除。


📖 小节


📝 作业

  1. 基础题(难度⭐):将第 20 课的备份脚本用函数重构(log/die/check_dependency 等函数),并添加 set -euo pipefailtrap 错误处理
  2. 进阶题(难度⭐⭐):用 getopts 添加 -s(源目录)和 -d(目标目录)参数,并创建一个关联数组存储配置,用遍历输出所有配置项
  3. 挑战题(难度⭐⭐⭐):用 mktemp 创建临时目录,在处理完成后自动清理,并写一个生产级部署脚本——支持参数解析、彩色日志、错误处理、健康检查
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏