调试shell脚本的技巧

当脚本出错时,首先使用这三招,能解决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 "这是一个警告信息"
  1. 启用详细模式:-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

你可以看到每一行代码的执行顺序和变量的实际值。

  1. 启用语法检查:-n (noexec)
    它只检查脚本语法错误,而不执行任何命令。非常适合在运行脚本前做一次快速“编译检查”。

使用方法:

bash -n your_script.sh

如果语法有误(如 if 忘记 fi, 括号不匹配等),它会立即报错并指出行号。

  1. 启用错误退出:-e (errexit)
    让脚本在任何命令执行失败(返回值非0) 时立即退出。这能防止错误像滚雪球一样扩大,帮你快速定位到第一个出问题的命令。

使用方法:

#!/bin/bash -e
# 或者在脚本内部启用
set -e

注意: 有些命令返回非0值是正常的(比如 grep 没找到内容),这时可以用 || true 来规避:

bash

grep "pattern" file.txt || true  # 即使grep失败,脚本也不会退出

二、组合拳与增强型技巧(中级)
将上述基础选项组合起来,并加入更多细节控制。

  1. 黄金调试组合:-xe
    通常将 -x 和 -e 一起使用,既能跟踪执行过程,又能在出错时立即停止。
bash -xe your_script.sh
  1. 增强组合:-xeo pipefail
    -e 有一个缺陷:它不管管道命令的失败。set -o pipefail 可以修复这个问题。
#!/bin/bash
set -xeo pipefail

示例:这个管道命令中,sort失败是看不到的

cat file.txt | grep "something" | sort

有了 pipefail,只要管道中任一命令失败,整个管道就视为失败,脚本会退出。

  1. 显示未设变量:-u (nounset)
    当尝试使用未定义的变量时,脚本会报错并退出。可以有效防止因变量名拼写错误导致的诡异问题。
#!/bin/bash
set -u

echo $MY_VAR  # 如果 MY_VAR 未设置,脚本会在这里报错退出

终极调试组合(建议放在所有脚本开头):

#!/bin/bash
set -xeo pipefail

三、精准定位与日志输出(高级实战)
当脚本很复杂时,光有 -x 可能不够,需要更精细的控制。

  1. 自定义调试信息: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'

这能让你精确知道输出对应的是脚本的哪一行、哪个函数。

  1. 使用 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

💡 实用调试技巧组合

  1. 局部调试(推荐)
    不要在整个脚本中启用 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  # 关闭调试

继续其他逻辑

  1. 条件调试
#!/bin/bash

# 通过环境变量控制调试
if [[ "${DEBUG:-false}" == "true" ]]; then
    export PS4='+[${LINENO}] '
    set -x
fi

# 使用:DEBUG=true ./script.sh
  1. 函数深度调试
#!/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
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容