强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 $filerm "$file"
SC2006使用反引号`cmd`$(cmd)
SC2046命令替换未加引号rm $(find ...)rm "$(find ...)"
SC2034变量赋值但未使用删除或使用变量
SC2155declare 和赋值分开local x; x=$(cmd)
SC2164cd 没有错误检查cd dir || exit
SC2012用 ls 代替 findls *.txtfind . -name '*.txt'
SC2154引用未定义变量检查拼写或提供默认值
SC2039Bash 特性在 sh 中不可用修改 shebang 或改用 POSIX 语法
SC2162read 未加 -rread lineread -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 影响性能仅在需要时开启

12.8 扩展阅读