调试是Shell脚本开发中至关重要的环节。通过有效的调试技术,我们可以快速定位和修复脚本中的错误,提高代码质量和开发效率。
使用echo、set命令等基本方法进行调试。
使用trap、调试模式等高级技术。
使用ShellCheck、bashdb等专业工具。
预防和处理各种常见错误。
诊断和解决复杂问题的方法。
调试和错误处理的最佳实践。
有效的调试不仅可以快速定位问题,还能帮助理解代码执行流程,提高代码质量。掌握调试技巧是每个Shell脚本开发者的必备技能。
使用简单有效的方法进行脚本调试。
最简单直接的调试方法,通过输出变量值和执行状态来了解脚本运行情况。
#!/bin/bash
# 使用echo进行调试的示例
echo "=== 脚本开始执行 ==="
# 调试变量赋值
name="Alice"
echo "DEBUG: name变量值为: $name"
# 调试函数调用
process_user() {
local username=$1
echo "DEBUG: 进入process_user函数,参数: $username"
# 模拟处理过程
sleep 1
echo "DEBUG: 用户 $username 处理完成"
}
# 调试循环
echo "DEBUG: 开始处理用户列表"
users=("Alice" "Bob" "Charlie")
for user in "${users[@]}"; do
echo "DEBUG: 正在处理用户: $user"
process_user "$user"
done
# 调试条件判断
age=25
echo "DEBUG: age变量值为: $age"
if [ $age -ge 18 ]; then
echo "DEBUG: 进入成年分支"
echo "已成年"
else
echo "DEBUG: 进入未成年分支"
echo "未成年"
fi
# 调试命令执行结果
echo "DEBUG: 执行ls命令"
result=$(ls -la)
echo "DEBUG: ls命令输出行数: $(echo "$result" | wc -l)"
# 带时间戳的调试信息
debug_echo() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] DEBUG: $1"
}
debug_echo "这是带时间戳的调试信息"
debug_echo "脚本执行完成"
echo "=== 脚本执行结束 ==="
DEBUG:便于区分正常输出和调试信息DEBUG: 变量名=值Bash的set命令提供了强大的调试选项,可以控制脚本的执行行为。
#!/bin/bash
# set命令调试示例
echo "=== set命令调试演示 ==="
# 启用调试模式
echo "1. 启用调试模式: set -x"
set -x
# 这行命令会被显示(前面有+号)
name="Debug User"
echo "用户名: $name"
# 关闭调试模式
echo "2. 关闭调试模式: set +x"
set +x
echo "调试模式已关闭,后续命令不会显示"
# 启用严格模式
echo "3. 启用严格模式"
set -euo pipefail
# -e: 遇到错误立即退出
# -u: 使用未定义变量时报错
# -o pipefail: 管道中任意命令失败则整个管道失败
echo "严格模式已启用"
# 演示错误处理
handle_error() {
echo "错误处理函数被调用"
echo "错误代码: $?"
echo "错误命令: $BASH_COMMAND"
}
# 设置错误处理
trap handle_error ERR
echo "4. 演示未定义变量错误"
# 这行会触发错误,因为undefined_var未定义
# echo "未定义变量: $undefined_var"
echo "5. 演示命令失败错误"
# 这行会触发错误,因为目录不存在
# ls /nonexistent_directory
echo "6. 演示管道错误"
# 这个管道会失败
# cat /nonexistent_file | grep "something"
echo "=== 脚本结束 ==="
# 恢复默认设置
set +euo pipefail
trap - ERR
| set选项 | 说明 | 使用场景 |
|---|---|---|
-x |
显示执行的命令及其参数 | 跟踪脚本执行流程 |
-v |
显示执行的命令(原始格式) | 查看命令展开前的样子 |
-e |
命令失败时立即退出 | 严格错误处理 |
-u |
使用未定义变量时报错 | 避免变量拼写错误 |
-o pipefail |
管道中任意命令失败则失败 | 严格的管道错误处理 |
-n |
只检查语法,不执行 | 语法检查 |
使用更高级的技术进行复杂脚本的调试。
trap命令可以捕获信号和特殊事件,用于实现高级调试和错误处理。
#!/bin/bash
# trap命令调试示例
# 调试函数
debug_trap() {
echo "=== 调试信息 ==="
echo "当前时间: $(date)"
echo "最后命令: $BASH_COMMAND"
echo "退出状态: $?"
echo "当前函数: ${FUNCNAME[1]}"
echo "当前行号: $LINENO"
echo "脚本名称: $0"
echo "进程ID: $$"
echo "=== 调试结束 ==="
echo
}
# 设置调试陷阱
set_debug_trap() {
# 在每个命令执行前调用调试函数
trap 'debug_trap' DEBUG
}
# 移除调试陷阱
remove_debug_trap() {
trap - DEBUG
}
# 错误处理函数
error_handler() {
local exit_code=$?
local command=$BASH_COMMAND
echo "❌ 错误发生!"
echo " 命令: $command"
echo " 退出码: $exit_code"
echo " 行号: $LINENO"
echo " 函数: ${FUNCNAME[1]}"
# 可以选择退出或继续执行
# exit $exit_code
}
# 退出处理函数
exit_handler() {
echo "=== 脚本退出 ==="
echo "退出状态: $?"
echo "执行时间: $SECONDS 秒"
echo "=== 感谢使用 ==="
}
# 信号处理函数
signal_handler() {
local signal=$1
echo "收到信号: $signal"
echo "正在清理资源..."
# 执行清理操作
exit 1
}
# 设置陷阱
set_traps() {
# 错误陷阱
trap error_handler ERR
# 退出陷阱
trap exit_handler EXIT
# 信号陷阱
trap 'signal_handler SIGINT' SIGINT
trap 'signal_handler SIGTERM' SIGTERM
echo "陷阱设置完成"
}
# 演示函数
demo_function() {
local input=$1
echo "处理输入: $input"
# 模拟处理
sleep 1
# 模拟错误(取消注释测试)
# ls /nonexistent_directory
echo "处理完成"
}
# 主函数
main() {
echo "=== 开始执行脚本 ==="
# 设置陷阱
set_traps
# 启用调试陷阱(取消注释启用详细调试)
# set_debug_trap
echo "1. 正常操作演示"
demo_function "测试数据1"
echo "2. 第二个操作"
demo_function "测试数据2"
# 模拟长时间运行
echo "3. 模拟工作..."
for i in {1..3}; do
echo " 工作进度: $i/3"
sleep 1
done
echo "4. 所有操作完成"
# 移除调试陷阱
remove_debug_trap
}
# 运行主函数
main
EXIT - 脚本退出时触发ERR - 命令返回非零状态时触发DEBUG - 每个命令执行前触发SIGINT - Ctrl+C中断时触发SIGTERM - 终止信号时触发RETURN - 函数返回时触发创建可重用的调试函数和工具,提高调试效率。
#!/bin/bash
# 自定义调试函数示例
# 调试级别
DEBUG_LEVEL=${DEBUG_LEVEL:-0} # 0=关闭, 1=基础, 2=详细, 3=全部
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# 调试日志函数
debug() {
local level=$1
local message=$2
if [ $DEBUG_LEVEL -ge $level ]; then
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local level_name=""
local color=$NC
case $level in
1) level_name="INFO"; color=$BLUE ;;
2) level_name="DEBUG"; color=$CYAN ;;
3) level_name="TRACE"; color=$PURPLE ;;
esac
echo -e "${color}[$timestamp] [$level_name] $message${NC}" >&2
fi
}
# 变量检查函数
check_variable() {
local var_name=$1
local var_value=$2
if [ -z "$var_value" ]; then
debug 1 "变量 $var_name 为空或未设置"
return 1
else
debug 3 "变量 $var_name = $var_value"
return 0
fi
}
# 函数跟踪
function_trace() {
local function_name=$1
local action=$2 # enter or exit
if [ $DEBUG_LEVEL -ge 2 ]; then
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
if [ "$action" = "enter" ]; then
echo -e "${PURPLE}[$timestamp] [TRACE] → 进入函数: $function_name${NC}" >&2
else
echo -e "${PURPLE}[$timestamp] [TRACE] ← 退出函数: $function_name (返回值: $?)${NC}" >&2
fi
fi
}
# 性能测量
start_timer() {
local timer_name=$1
TIMER_START_$timer_name=$(date +%s%N)
debug 2 "计时器 $timer_name 启动"
}
end_timer() {
local timer_name=$1
local start_var=TIMER_START_$timer_name
local start_time=${!start_var}
if [ -n "$start_time" ]; then
local end_time=$(date +%s%N)
local duration=$(( (end_time - start_time) / 1000000 )) # 毫秒
debug 1 "计时器 $timer_name: ${duration}ms"
unset $start_var
else
debug 1 "错误: 计时器 $timer_name 未启动"
fi
}
# 内存检查(简单版本)
check_memory() {
if command -v free >/dev/null 2>&1; then
local memory_info=$(free -h | grep Mem | awk '{print $3 "/" $2}')
debug 2 "内存使用: $memory_info"
fi
}
# 演示函数
demo_processing() {
function_trace "${FUNCNAME[0]}" "enter"
local input=$1
debug 1 "开始处理: $input"
start_timer "processing"
check_memory
# 模拟处理
debug 3 "步骤1: 验证输入"
check_variable "input" "$input"
debug 3 "步骤2: 处理数据"
sleep 0.5
debug 3 "步骤3: 生成结果"
local result="处理结果: ${input^^}" # 转换为大写
end_timer "processing"
debug 1 "处理完成: $result"
function_trace "${FUNCNAME[0]}" "exit"
echo "$result"
}
# 主函数
main() {
echo "=== 自定义调试工具演示 ==="
# 设置调试级别
if [ -n "$1" ]; then
DEBUG_LEVEL=$1
fi
debug 1 "调试级别: $DEBUG_LEVEL"
debug 2 "脚本启动: $0"
debug 3 "所有参数: $*"
start_timer "total_execution"
# 处理多个输入
inputs=("测试数据1" "测试数据2" "另一个测试")
for input in "${inputs[@]}"; do
debug 1 "=== 处理输入: $input ==="
result=$(demo_processing "$input")
echo "结果: $result"
echo
done
end_timer "total_execution"
debug 1 "所有处理完成"
echo "=== 演示结束 ==="
}
# 运行主函数
main "$@"
使用专业工具提高调试效率和质量。
Shell脚本静态分析工具,可以检测语法错误和常见问题。
#!/bin/bash
# 有问题的脚本示例(ShellCheck会检测到问题)
# 问题1: 未引用的变量
name=John
echo Hello $name
# 问题2: 文件不存在检查方式不对
if [ -f /tmp/*.log ]; then
echo "找到日志文件"
fi
# 问题3: 循环中的命令替换
for file in $(ls *.txt); do
echo "处理文件: $file"
done
# 问题4: 测试表达式中的常见错误
[ $var = "value" ]
# 问题5: 函数中未使用local变量
func() {
counter=0
counter=$((counter + 1))
}
shellcheck script.sh - 检查脚本shellcheck -s bash script.sh - 指定shell类型shellcheck -x script.sh - 检查源命令shellcheck -e SC2034 script.sh - 忽略特定警告
Bash调试器,提供类似GDB的调试体验。
#!/bin/bash
# 安装bashdb
# Ubuntu/Debian: sudo apt-get install bashdb
# CentOS/RHEL: sudo yum install bashdb
# macOS: brew install bashdb
# 使用bashdb调试
# bashdb --debug script.sh
# 常用命令:
# break [行号] - 设置断点
# run - 运行脚本
# next - 执行下一行
# step - 进入函数
# print 变量 - 打印变量值
# watch 变量 - 监视变量变化
# backtrace - 显示调用栈
# quit - 退出调试器
# 示例调试会话:
# $ bashdb --debug example.sh
# bashdb(0)> break 10
# bashdb(0)> run
# bashdb(1)> print $variable
# bashdb(1)> next
# bashdb(1)> quit
通过实际脚本示例展示调试技术的综合应用。
创建一个可重用的调试框架,应用于各种脚本项目。
#!/bin/bash
# 完整的调试框架
# 配置
DEBUG_LEVEL=${DEBUG_LEVEL:-0}
LOG_FILE=${LOG_FILE:-"/var/tmp/debug_$$.log"}
ENABLE_COLOR=${ENABLE_COLOR:-true}
ENABLE_TIMESTAMP=${ENABLE_TIMESTAMP:-true}
# 颜色支持
if [ "$ENABLE_COLOR" = "true" ] && [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; BLUE=''; PURPLE=''; CYAN=''; NC=''
fi
# 日志级别
declare -A LOG_LEVELS=(
[0]="FATAL"
[1]="ERROR"
[2]="WARN"
[3]="INFO"
[4]="DEBUG"
[5]="TRACE"
)
# 初始化日志系统
init_logging() {
local log_dir=$(dirname "$LOG_FILE")
mkdir -p "$log_dir"
> "$LOG_FILE"
log "INFO" "日志系统初始化完成" "日志文件: $LOG_FILE"
}
# 日志函数
log() {
local level=$1
local message=$2
local context=$3
# 检查日志级别
local level_num=0
case $level in
"FATAL") level_num=0 ;;
"ERROR") level_num=1 ;;
"WARN") level_num=2 ;;
"INFO") level_num=3 ;;
"DEBUG") level_num=4 ;;
"TRACE") level_num=5 ;;
*) level_num=3 ;;
esac
if [ $level_num -le $DEBUG_LEVEL ]; then
local timestamp=""
if [ "$ENABLE_TIMESTAMP" = "true" ]; then
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
fi
local color=$NC
case $level in
"FATAL") color=$RED ;;
"ERROR") color=$RED ;;
"WARN") color=$YELLOW ;;
"INFO") color=$BLUE ;;
"DEBUG") color=$CYAN ;;
"TRACE") color=$PURPLE ;;
esac
local log_entry=""
if [ -n "$timestamp" ]; then
log_entry="[$timestamp]"
fi
log_entry="$log_entry [$level] $message"
if [ -n "$context" ]; then
log_entry="$log_entry ($context)"
fi
# 输出到控制台
echo -e "${color}${log_entry}${NC}" >&2
# 写入日志文件(无颜色)
echo "${log_entry}" >> "$LOG_FILE"
fi
}
# 错误处理
set_error_handling() {
set -o errtrace
set -o nounset
set -o pipefail
trap 'error_handler $LINENO' ERR
trap 'exit_handler' EXIT
trap 'signal_handler SIGINT' SIGINT
}
# 错误处理函数
error_handler() {
local line_num=$1
local exit_code=$?
local command=$BASH_COMMAND
log "ERROR" "脚本执行错误" "行号: $line_num, 命令: $command, 退出码: $exit_code"
# 显示调用栈
if [ $DEBUG_LEVEL -ge 4 ]; then
log "DEBUG" "调用栈:" "$(print_stack_trace)"
fi
exit $exit_code
}
# 退出处理函数
exit_handler() {
local exit_code=$?
log "INFO" "脚本退出" "退出码: $exit_code, 运行时间: ${SECONDS}秒"
}
# 信号处理函数
signal_handler() {
local signal=$1
log "WARN" "收到信号" "信号: $signal"
exit 1
}
# 打印调用栈
print_stack_trace() {
local frame=0
local stack_trace=""
while caller $frame; do
((frame++))
done | awk '{
if (NR == 1) {
stack_trace = $2 ":" $1 " in " $3
} else {
stack_trace = stack_trace "\n" $2 ":" $1 " in " $3
}
} END {
print stack_trace
}'
}
# 性能监控
declare -A TIMERS=()
start_timer() {
local timer_name=$1
TIMERS[$timer_name]=$(date +%s%N)
log "DEBUG" "启动计时器" "名称: $timer_name"
}
end_timer() {
local timer_name=$1
local start_time=${TIMERS[$timer_name]}
if [ -n "$start_time" ]; then
local end_time=$(date +%s%N)
local duration_ns=$((end_time - start_time))
local duration_ms=$((duration_ns / 1000000))
log "INFO" "计时器结束" "名称: $timer_name, 耗时: ${duration_ms}ms"
unset TIMERS[$timer_name]
else
log "WARN" "计时器未找到" "名称: $timer_name"
fi
}
# 资源监控
check_resources() {
if command -v free >/dev/null 2>&1; then
local memory=$(free -h | awk '/^Mem:/ {print $3 "/" $2}')
log "DEBUG" "内存使用" "$memory"
fi
if command -v df >/dev/null 2>&1; then
local disk=$(df -h / | awk 'NR==2 {print $5}')
log "DEBUG" "磁盘使用" "$disk"
fi
}
# 变量检查
validate_variable() {
local var_name=$1
local var_value=$2
local required=${3:-false}
if [ -z "$var_value" ]; then
if [ "$required" = "true" ]; then
log "ERROR" "必需变量为空" "变量名: $var_name"
return 1
else
log "DEBUG" "变量为空" "变量名: $var_name"
fi
else
log "TRACE" "变量检查" "$var_name=$var_value"
fi
return 0
}
# 函数跟踪
trace_function() {
local function_name=$1
local action=$2
if [ $DEBUG_LEVEL -ge 5 ]; then
if [ "$action" = "enter" ]; then
log "TRACE" "进入函数" "$function_name"
else
log "TRACE" "退出函数" "$function_name (返回值: $?)"
fi
fi
}
# 演示使用
demo_function() {
trace_function "${FUNCNAME[0]}" "enter"
local input=$1
log "INFO" "开始处理" "输入: $input"
start_timer "demo_processing"
check_resources
# 验证输入
validate_variable "input" "$input" true
# 模拟处理
log "DEBUG" "处理步骤1" "转换大小写"
local processed=$(echo "$input" | tr '[:lower:]' '[:upper:]')
log "DEBUG" "处理步骤2" "添加前缀"
processed="PROCESSED: $processed"
end_timer "demo_processing"
log "INFO" "处理完成" "结果: $processed"
trace_function "${FUNCNAME[0]}" "exit"
echo "$processed"
}
# 主函数
main() {
local args=("$@")
# 初始化
init_logging
set_error_handling
log "INFO" "脚本启动" "参数: ${args[*]}, PID: $$"
start_timer "script_execution"
# 处理参数
if [ ${#args[@]} -eq 0 ]; then
log "WARN" "没有提供参数" "使用默认值"
args=("默认数据1" "默认数据2")
fi
# 处理每个参数
local results=()
for arg in "${args[@]}"; do
log "INFO" "处理参数" "值: $arg"
result=$(demo_function "$arg")
results+=("$result")
log "INFO" "得到结果" "$result"
done
# 显示最终结果
log "INFO" "所有处理完成" "结果数量: ${#results[@]}"
for result in "${results[@]}"; do
echo "最终结果: $result"
done
end_timer "script_execution"
log "INFO" "脚本执行完成" "总耗时"
}
# 运行主函数(如果直接执行)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
# 调试代码混乱
x=5
y=10
# 这里有问题
result=$x+$y
echo $result # 输出 5+10 而不是 15
# 清晰的调试代码
x=5
y=10
debug "变量值" "x=$x, y=$y"
# 使用正确的算术运算
result=$((x + y))
debug "计算结果" "x+y=$result"
echo "$result # 输出 15