当脚本出错时,首先使用这三招,能解决80%的问题。
脚本开头必加
#!/bin/bash
# 日常脚本推荐配置
set -euo pipefail
trap 'echo "脚本被用户中断"; exit 130' INT TERM
trap 'echo "错误: 第 $LINENO 行 [$BASH_COMMAND] 失败 (代码: $?)" >&2' ERR
#!/bin/bash
# 严格模式
set -euo pipefail
# 调试信息
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# 错误处理函数
handle_error() {
local exit_code=$?
local line_no=$1
local command=$2
echo "[ERROR] $SCRIPT_NAME 执行失败" >&2
echo " 位置: 第 $line_no 行" >&2
echo " 命令: $command" >&2
echo " 退出码: $exit_code" >&2
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')" >&2
# 可以在这里添加日志记录、清理操作等
exit $exit_code
}
# 设置 trap
trap 'echo "[INFO] 脚本被用户中断"; exit 130' INT TERM
trap 'handle_error $LINENO "$BASH_COMMAND"' ERR
# 主逻辑
echo "脚本开始执行..."
# ... 你的代码 ...
#!/usr/bin/env bash
log() {
echo "$SCRIPT_START_TIME: $1"
}
SCRIPT_START_TIME=$(date +'%Y-%m-%d %H:%M:%S %Z')
readonly LOG_FILE="/var/log/docker_install_$(date +%Y%m%d_%H%M%S).log"
# 颜色定义
readonly COLOR_RESET='\033[0m'
readonly COLOR_RED='\033[0;31m'
readonly COLOR_GREEN='\033[0;32m'
readonly COLOR_YELLOW='\033[1;33m'
readonly COLOR_BLUE='\033[0;34m'
readonly COLOR_CYAN='\033[0;36m'
# 带颜色的日志函数
function log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local color=""
local log_level=""
case "$level" in
"INFO")
color="$COLOR_CYAN"
log_level="INFO"
;;
"ERROR")
color="$COLOR_RED"
log_level="ERROR"
;;
"SUCCESS")
color="$COLOR_GREEN"
log_level="SUCCESS"
;;
"WARNING")
color="$COLOR_YELLOW"
log_level="WARNING"
;;
*)
color="$COLOR_BLUE"
log_level="$level"
;;
esac
# 控制台输出带颜色,文件输出不带颜色
echo -e "${color}[$timestamp] [$log_level] $message${COLOR_RESET}" | tee -a >(sed "s/\x1B\[[0-9;]*[a-zA-Z]//g" >> "$LOG_FILE")
}
function log_info() {
log "INFO" "$1"
}
function log_error() {
log "ERROR" "$1"
}
function log_success() {
log "SUCCESS" "$1"
}
function log_warning() {
log "WARNING" "$1"
}
# 使用示例
log_info "开始安装Docker..."
log_success "Docker安装成功!"
log_error "安装过程中出现错误"
log_warning "这是一个警告信息"
- 启用详细模式:-x (xtrace)
这是最强大、最常用的调试手段。它会在执行命令之前,先打印出扩展后的命令(以 + 开头),让你清晰看到脚本的实际执行流程。
使用方法:
在脚本第一行启用
#!/bin/bash -x
# 或者在运行时启用
bash -x your_script.sh
或者在脚本内部局部启用
set -x
... 需要调试的代码段 ...
set +x # 关闭调试
输出示例:
+ echo 'Hello World'
Hello World
+ count=5
+ echo 'Count is 5'
Count is 5
你可以看到每一行代码的执行顺序和变量的实际值。
- 启用语法检查:-n (noexec)
它只检查脚本语法错误,而不执行任何命令。非常适合在运行脚本前做一次快速“编译检查”。
使用方法:
bash -n your_script.sh
如果语法有误(如 if 忘记 fi, 括号不匹配等),它会立即报错并指出行号。
- 启用错误退出:-e (errexit)
让脚本在任何命令执行失败(返回值非0) 时立即退出。这能防止错误像滚雪球一样扩大,帮你快速定位到第一个出问题的命令。
使用方法:
#!/bin/bash -e
# 或者在脚本内部启用
set -e
注意: 有些命令返回非0值是正常的(比如 grep 没找到内容),这时可以用 || true 来规避:
bash
grep "pattern" file.txt || true # 即使grep失败,脚本也不会退出
二、组合拳与增强型技巧(中级)
将上述基础选项组合起来,并加入更多细节控制。
- 黄金调试组合:-xe
通常将 -x 和 -e 一起使用,既能跟踪执行过程,又能在出错时立即停止。
bash -xe your_script.sh
- 增强组合:-xeo pipefail
-e 有一个缺陷:它不管管道命令的失败。set -o pipefail 可以修复这个问题。
#!/bin/bash
set -xeo pipefail
示例:这个管道命令中,sort失败是看不到的
cat file.txt | grep "something" | sort
有了 pipefail,只要管道中任一命令失败,整个管道就视为失败,脚本会退出。
- 显示未设变量:-u (nounset)
当尝试使用未定义的变量时,脚本会报错并退出。可以有效防止因变量名拼写错误导致的诡异问题。
#!/bin/bash
set -u
echo $MY_VAR # 如果 MY_VAR 未设置,脚本会在这里报错退出
终极调试组合(建议放在所有脚本开头):
#!/bin/bash
set -xeo pipefail
三、精准定位与日志输出(高级实战)
当脚本很复杂时,光有 -x 可能不够,需要更精细的控制。
- 自定义调试信息:PS4 变量
-x 输出的行首 + 是可以定制的,通过 PS4 环境变量可以显示更多有用信息,例如行号、函数名、进程ID。
# 在启用 set -x 之前设置
export PS4='+[${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}()} $BASH_SOURCE ] '
set -x
输出示例:
+[25: main /path/to/script.sh ] echo 'Hello World'
这能让你精确知道输出对应的是脚本的哪一行、哪个函数。
- 使用 trap 捕获信号和退出
trap 可以在脚本退出时、收到中断信号时,自动执行一些命令,非常适合做资源清理和打印最后的调试信息。
#!/bin/bash
# 定义一个调试函数
debug_info() {
echo "=== 调试信息 ==="
echo "最后执行的命令: $?"
echo "当前脚本参数: $@"
echo "当前函数名: ${FUNCNAME[1]}"
}
# 在EXIT和ERR时触发调试函数
trap debug_info EXIT ERR
set -e
... 你的脚本代码 ...
PS4 核心变量解析
首先理解这些常用变量:
$LINENO:当前行号
$FUNCNAME[0]:当前函数名
$BASH_SOURCE:当前脚本路径
$BASH_LINENO:调用栈中的行号
$?:上一条命令的退出状态
🎯 推荐配置方案
方案1:基础调试版(最常用)
export PS4='+[${LINENO}]${FUNCNAME[0]:+ ${FUNCNAME[0]}() }$BASH_SOURCE: '
set -x
输出示例:
+[25]main() /path/script.sh: echo "Hello World"
+[26]/path/script.sh: var=5
方案2:详细调试版(推荐用于复杂脚本)
export PS4='+[$(date +%H:%M:%S)] [${LINENO}]${FUNCNAME[0]:+${FUNCNAME[0]}(): }($BASH_SOURCE) '
set -x
输出示例:
+[14:30:25] [152]process_data(): (/home/user/script.sh) while read line
+[14:30:25] [153]process_data(): (/home/user/script.sh) echo "$line"
方案3:带执行时间和退出状态(高级调试)
export PS4='+[${LINENO}] [\D{%H:%M:%S}] [Exit:$?]${FUNCNAME[0]:+ ${FUNCNAME[0]}(): }'
set -x
输出示例:
+[45] [14:31:10] [Exit:0]validate_input(): grep -q "pattern" file.txt
+[46] [14:31:10] [Exit:1]validate_input(): echo "Pattern not found"
优势: 能看到每条命令的执行时间和退出状态,快速定位失败命令。
🚀 终极智能配置
这是我个人最推荐的「智能 PS4」,能根据上下文自动调整显示内容:
# 智能 PS4 配置
export PS4='\n\033[1;33m[DEBUG]\033[0m \033[1;36mLine:\033[0m \033[1;32m${LINENO}\033[0m\
${FUNCNAME[0]:+ \033[1;35mFunction:\033[0m \033[1;32m${FUNCNAME[0]}()\033[0m}\
\033[1;34mFile:\033[0m \033[1;33m$(basename $BASH_SOURCE)\033[0m\n \033[1;37m→ \033[0m'
set -x
输出效果:
[DEBUG] Line: 25 Function: main() File: script.sh
→ echo "Starting processing"
[DEBUG] Line: 30 File: script.sh
→ ls -la /nonexistent
特点:
✅ 彩色高亮,不同信息用不同颜色
✅ 智能显示,不在函数中时不显示函数名
✅ 清晰排版,命令在下一行显示,易于阅读
✅ 文件简写,只显示文件名而非完整路径
🔧 针对不同场景的专用配置
场景1:性能调试
export PS4='+[LINE:${LINENO}] [TIME:\t] [PID:$$] '
set -x
场景2:函数调用跟踪
export PS4='+[${BASH_SOURCE}:${LINENO}] [${FUNCNAME[0]:-main}] '
set -x
场景3:简单明了版
export PS4='→ [${LINENO}] : '
set -x
💡 实用调试技巧组合
- 局部调试(推荐)
不要在整个脚本中启用 set -x,只在需要的地方使用:
#!/bin/bash
# 正常的脚本逻辑
config_file="app.conf"
# 开始调试问题区域
export PS4='+[${LINENO}]${FUNCNAME[0]:+ ${FUNCNAME[0]}(): }'
set -x # 开启调试
process_data() {
local input=$1
echo "Processing: $input"
# 复杂的数据处理逻辑
}
process_data "$config_file"
set +x # 关闭调试
继续其他逻辑
- 条件调试
#!/bin/bash
# 通过环境变量控制调试
if [[ "${DEBUG:-false}" == "true" ]]; then
export PS4='+[${LINENO}] '
set -x
fi
# 使用:DEBUG=true ./script.sh
- 函数深度调试
#!/bin/bash
# 显示调用栈的 PS4
export PS4='+[BASH_SOURCE:${BASH_SOURCE##*/}:LINENO:${LINENO}:FUNCNAME:${FUNCNAME[0]}] '
set -x
main() {
echo "In main"
helper_function
}
helper_function() {
echo "In helper"
nested_function
}
nested_function() {
echo "In nested"
}
main
🎯 最佳实践总结
按需使用:不要全局启用 set -x,只在问题区域使用
渐进细化:先用简单配置,发现问题后再用详细配置
颜色辅助:在复杂脚本中使用彩色输出提高可读性
环境控制:通过环境变量控制调试开关
组合使用:配合 set -euo pipefail 让脚本更健壮
我的首选推荐:
对于大多数情况,使用这个平衡的方案:
export PS4='+[Line:${LINENO}]${FUNCNAME[0]:+ [Func:${FUNCNAME[0]}()] }→ '
set -x
它提供了足够的信息,又不会过于冗长,能快速帮你定位到问题所在的行和函数。
带颜色
export PS4='+\[\033[1;33m\][Line:${LINENO}]\[\033[0m\]\[\033[1;34m\]${FUNCNAME[0]:+ [Func:${FUNCNAME[0]}()] }\[\033[0m\]→ '
export PS4='+\[\033[1;31m\][\[\033[1;33m\]Line:${LINENO}\[\033[1;31m\]]\[\033[0m\]\[\033[1;36m\]${FUNCNAME[0]:+ [Func:${FUNCNAME[0]}()] }\[\033[1;32m\]→\[\033[0m\] '
export PS4='+\[\033[1;35m\][\[\033[1;33m\]Line:${LINENO}\[\033[1;35m\]]\[\033[1;36m\]${FUNCNAME[0]:+ [Func:${FUNCNAME[0]}()] }\[\033[1;32m\]›\[\033[0m\] '
export PS4='+\[\033[43m\033[30m\][Line:${LINENO}]\[\033[0m\]\[\033[44m\033[37m\]${FUNCNAME[0]:+ [Func:${FUNCNAME[0]}()] }\[\033[0m\]\[\033[1;32m\]→\[\033[0m\] '
显示脚本名和时间
#!/bin/bash
export PS4='+ [$(date +%H:%M:%S)] [${BASH_SOURCE}:${LINENO}] [${FUNCNAME[0]:-main}] '
set -x
echo "Debugging with timestamp"
sleep 1
echo "Done"
显示进程ID和层次关系
#!/bin/bash
export PS4='+ [$$:${BASH_SOURCE}:${LINENO}] '
set -x
function parent() {
echo "Parent function"
child
}
function child() {
echo "Child function"
}
parent
复杂脚本调试
#!/bin/bash
export PS4='+ [${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}] '
set -x
config_file="app.conf"
if [[ -f "$config_file" ]]; then
source "$config_file"
process_config
else
echo "Config file not found" >&2
exit 1
fi
function process_config() {
echo "Processing configuration"
validate_settings
}
function validate_settings() {
echo "Validating settings"
# 验证逻辑
}
增加日志输出功能
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
# 日志函数
log() {
local level=$1
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
#echo -e "${level}: ${message}"
echo -e "[${timestamp}] ${level}: ${message}"
}
info() { log "${BLUE}INFO${NC}" "$@"; }
warn() { log "${YELLOW}WARN${NC}" "$@"; }
error() { log "${RED}ERROR${NC}" "$@"; }
success() { log "${GREEN}SUCCESS${NC}" "$@"; }
check_privileges() {
if [[ $EUID -ne 0 ]]; then
error "此脚本需要 root 权限执行,请使用 sudo 或切换到 root 用户"
exit 1
fi
info "权限检查通过"
}
## 第二种日志输出格式
log() {
local level=$1
local message=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local format="%s [%s] %s\n"
case $level in
info)
printf "$format" "$timestamp" "INFO" "$message" >&1 ;;
error)
printf "$format" "$timestamp" "ERROR" "$message" >&2 ;;
warn)
printf "$format" "$timestamp" "WARN" "$message" >&1 ;;
*)
echo "Invalid log level: $level" >&2
return 1 ;;
esac
}
log_info() {
log "info" "$*"
}
log_error() {
log "error" "$*"
}
log_warn() {
log "warn" "$*"
}
基本要求
#!/usr/bin/env bash
# 错误处理
set -euo pipefail
trap 'log_error "脚本执行失败,退出码: $? (行号: ${LINENO})"' ERR
trap 'log_error "脚本被用户中断"; exit 130' INT TERM
# 导入通用函数库
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
source "${SCRIPT_DIR}/common_functions.sh" 2>/dev/null || {
echo "警告: 无法加载通用函数库,使用简化日志功能"
log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $*"; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $*" >&2; }
log_warn() { echo "[WARN] $(date '+%Y-%m-%d %H:%M:%S') - $*"; }
log_success() { echo "[SUCCESS] $(date '+%Y-%m-%d %H:%M:%S') - $*"; }
}
# 错误处理函数
die() {
log_error "$1"
exit "${2:-1}" # 默认退出码1
}