Bash 脚本编写教程 / 12 - 调试技术
12 - 调试技术
12.1 echo 调试法
最简单但有效的调试方法:
# 基本 echo 调试
echo "DEBUG: 变量 x = $x" >&2
echo "DEBUG: 到达第 ${LINENO} 行" >&2
# 调试函数
debug() {
[[ "${DEBUG:-0}" == "1" ]] && echo "[DEBUG] $*" >&2
}
# 使用
DEBUG=1
debug "开始处理文件: $filename"
debug "计数器: $count"
# 生产环境关闭
DEBUG=0
debug "这条不会显示"
12.2 set -x:执行追踪
#!/bin/bash
set -x # 开启执行追踪
name="World"
echo "Hello, $name"
# 输出:
# + name=World
# + echo 'Hello, World'
# Hello, World
# 局部开启/关闭
set +x # 关闭追踪
echo "这段不追踪"
set -x # 重新开启
# 条件追踪
DEBUG=${DEBUG:-0}
if [[ "$DEBUG" == "1" ]]; then
set -x
fi
PS4 自定义追踪前缀
# 默认 PS4 是 "+ "
set -x
# 自定义 PS4 显示更多信息
export PS4='+[${BASH_SOURCE}:${LINENO}] ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
# 更详细的 PS4
export PS4='+$(date +%H:%M:%S) ${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:-main}: '
# 带颜色的 PS4
export PS4='\033[0;36m+ \033[0m' # 青色
# 彩色带行号
export PS4='\033[0;33m[${BASH_SOURCE}:${LINENO}]\033[0m '
12.3 ShellCheck 静态分析
ShellCheck 是 Bash 脚本的 linter 工具,能检测数百种常见错误。
# 安装
sudo apt-get install shellcheck # Debian/Ubuntu
brew install shellcheck # macOS
pacman -S shellcheck # Arch Linux
# 基本使用
shellcheck myscript.sh
# 指定 Shell 类型
shellcheck -s bash myscript.sh
shellcheck -s sh myscript.sh
# 输出格式
shellcheck -f json myscript.sh # JSON 格式
shellcheck -f gcc myscript.sh # GCC 格式(CI 集成)
shellcheck -f tty myscript.sh # 终端格式(默认)
# 忽略特定警告
shellcheck -e SC2086 myscript.sh # 忽略 "Double quote to prevent globbing"
# 在脚本内忽略
# shellcheck disable=SC2086
echo $variable # 不再警告
常见 ShellCheck 规则
| 编号 | 说明 | 示例 |
|---|---|---|
| SC2086 | 变量未加引号 | rm $file → rm "$file" |
| SC2006 | 使用反引号 | `cmd` → $(cmd) |
| SC2046 | 命令替换未加引号 | rm $(find ...) → rm "$(find ...)" |
| SC2034 | 变量赋值但未使用 | 删除或使用变量 |
| SC2155 | declare 和赋值分开 | local x; x=$(cmd) |
| SC2164 | cd 没有错误检查 | cd dir || exit |
| SC2012 | 用 ls 代替 find | ls *.txt → find . -name '*.txt' |
| SC2154 | 引用未定义变量 | 检查拼写或提供默认值 |
| SC2039 | Bash 特性在 sh 中不可用 | 修改 shebang 或改用 POSIX 语法 |
| SC2162 | read 未加 -r | read line → read -r line |
# 在 CI 中使用 ShellCheck
if ! shellcheck -f gcc scripts/*.sh; then
echo "ShellCheck 检查失败" >&2
exit 1
fi
12.4 bashdb 调试器
bashdb 是 Bash 的交互式调试器,类似 GDB。
# 安装
sudo apt-get install bashdb
# 启动调试
bashdb myscript.sh
# 常用命令
# (bashdb) break 10 — 在第10行设置断点
# (bashdb) break myfunc — 在函数处设置断点
# (bashdb) run — 运行程序
# (bashdb) next — 执行下一行
# (bashdb) step — 步入函数
# (bashdb) continue — 继续执行
# (bashdb) print $var — 打印变量
# (bashdb) watch $var — 监视变量变化
# (bashdb) backtrace — 查看调用栈
# (bashdb) list — 列出源码
# (bashdb) quit — 退出
# 非交互式调试
bashdb -c 'break 10; run; print $x; quit' myscript.sh
12.5 Bash 内置调试技巧
# 1. 使用 declare -p 打印变量属性
declare -p my_var
declare -p my_array
declare -A my_map
declare -p my_map
# 2. 使用 FUNCNAME 查看调用栈
foo() { bar; }
bar() { baz; }
baz() {
echo "调用栈:"
for ((i = 0; i < ${#FUNCNAME[@]}; i++)); do
echo " ${FUNCNAME[$i]}() 在 ${BASH_SOURCE[$i]}:${BASH_LINENO[$i]}"
done
}
foo
# 3. 使用 BASH_COMMAND 获取当前执行的命令
trap 'echo "执行: $BASH_COMMAND"' DEBUG
# 4. 使用 caller 查看调用位置
my_function() {
caller 0 # 当前函数的调用位置
caller 1 # 调用者的调用位置
}
# 5. 详细错误信息函数
error_detail() {
echo "========== 错误详情 ==========" >&2
echo "退出码: $?" >&2
echo "行号: ${BASH_LINENO[0]}" >&2
echo "函数: ${FUNCNAME[1]:-main}" >&2
echo "源文件: ${BASH_SOURCE[1]:-$0}" >&2
echo "命令: $BASH_COMMAND" >&2
echo "================================" >&2
}
trap error_detail ERR
12.6 调试脚本模板
#!/bin/bash
# debug_template.sh —— 带调试功能的脚本模板
set -euo pipefail
# ---- 调试配置 ----
DEBUG="${DEBUG:-0}"
VERBOSE="${VERBOSE:-0}"
# 调试等级
readonly LOG_DEBUG=0
readonly LOG_INFO=1
readonly LOG_WARN=2
readonly LOG_ERROR=3
LOG_LEVEL=${LOG_LEVEL:-$LOG_INFO}
# ---- 调试函数 ----
log() {
local level=$1
local level_name="$2"
shift 2
[[ $level -lt $LOG_LEVEL ]] && return
local color=""
local reset="\033[0m"
case $level in
$LOG_DEBUG) color="\033[0;36m" ;; # 青色
$LOG_INFO) color="\033[0;32m" ;; # 绿色
$LOG_WARN) color="\033[0;33m" ;; # 黄色
$LOG_ERROR) color="\033[0;31m" ;; # 红色
esac
printf "${color}[%s] [%-5s] %s${reset}\n" \
"$(date '+%H:%M:%S')" "$level_name" "$*" >&2
}
log::debug() { log $LOG_DEBUG "DEBUG" "$@"; }
log::info() { log $LOG_INFO "INFO" "$@"; }
log::warn() { log $LOG_WARN "WARN" "$@"; }
log::error() { log $LOG_ERROR "ERROR" "$@"; }
# 执行追踪
if [[ "$DEBUG" == "1" ]]; then
LOG_LEVEL=$LOG_DEBUG
export PS4='+\033[0;33m[${BASH_SOURCE}:${LINENO}]\033[0m '
set -x
fi
# 变量检查工具
dump_var() {
local var_name="$1"
log::debug "变量 $var_name = $(declare -p "$var_name" 2>/dev/null || echo '<未定义>')"
}
# 性能计时
declare -A TIMERS
timer_start() {
TIMERS["$1"]=$SECONDS
}
timer_end() {
local name="$1"
local elapsed=$(( SECONDS - TIMERS["$name"] ))
log::debug "计时器 [$name]: ${elapsed}s"
}
# ---- 业务逻辑 ----
main() {
log::info "脚本启动"
timer_start "total"
log::debug "参数列表: $*"
# 你的代码...
sleep 1
timer_end "total"
log::info "脚本完成"
}
main "$@"
12.7 注意事项
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| set -x 输出太多 | 大脚本输出爆炸 | 局部开启 set +x / set -x |
| ShellCheck 误报 | 有时规则过于严格 | 使用 # shellcheck disable= |
| bashdb 不支持某些特性 | 关联数组调试有限 | 使用 declare -p 辅助 |
| 调试性能开销 | set -x 和 trap DEBUG 影响性能 | 仅在需要时开启 |