Linux: Bash 脚本进阶——函数 / 数组 / 错误处理
基础脚本能完成简单任务,但生产级脚本需要更健壮的代码——函数拆分逻辑、错误处理防止"静默失败"、参数解析让脚本更易用。
📋 前置知识:需要先掌握以下内容
- 第20课:Bash 脚本基础
1. 你将学到
- 函数定义和返回值
- 索引数组和关联数组
- set -e 和 trap 错误处理
- getopts 命令行参数
- set -x 调试技巧
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)、TERM(kill默认信号)。其他还有HUP、QUIT、USR1、USR2。
Q: getopts 和 getopt 有什么区别? A:
getopts是 bash 内建命令,更简单,不支持长选项(如--help)。getopt是外部命令(util-linux包),支持长选项和短选项混合。推荐用getopts保持兼容性。
Q: 脚本里怎么生成临时文件? A: 用
mktemp:tempfile=$(mktemp)创建临时文件,tempdir=$(mktemp -d)创建临时目录。临时文件通常创建在/tmp下,用完要删除。
📖 小节
- 函数
func() { local vars; return N; }封装逻辑 - 数组
${arr[@]}遍历,${#arr[@]}长度,declare -A关联数组 set -euo pipefail让脚本健壮,trap ERR EXIT捕获错误和清理getopts解析命令行参数set -x/+x调试,bash -x script.sh启动调试模式
📝 作业
- 基础题(难度⭐):将第 20 课的备份脚本用函数重构(log/die/check_dependency 等函数),并添加
set -euo pipefail和trap错误处理 - 进阶题(难度⭐⭐):用
getopts添加-s(源目录)和-d(目标目录)参数,并创建一个关联数组存储配置,用遍历输出所有配置项 - 挑战题(难度⭐⭐⭐):用
mktemp创建临时目录,在处理完成后自动清理,并写一个生产级部署脚本——支持参数解析、彩色日志、错误处理、健康检查