Shell脚本错误处理#

错误处理是Shell脚本开发中的重要组成部分,良好的错误处理可以提高脚本的健壮性和可维护性。本教程将详细介绍Shell中的各种错误处理技术,从基础的退出状态码检查到高级的调试和错误恢复策略。

入门#

基本错误检查#

在Shell中,每个命令执行后都会返回一个退出状态码(0-255),其中0表示成功,非0表示失败。

# 检查命令执行结果
ls -la
if [ $? -eq 0 ]; then
    echo "命令执行成功"
else
    echo "命令执行失败"
fi

# 直接在if语句中检查
if ls -la; then
    echo "命令执行成功"
else
    echo "命令执行失败"
fi

# 使用逻辑运算符
ls -la && echo "成功" || echo "失败"

简单错误处理#

# 检查文件是否存在
if [ ! -f "file.txt" ]; then
    echo "错误: 文件不存在"
    exit 1
fi

# 检查目录是否存在
if [ ! -d "dir" ]; then
    echo "错误: 目录不存在"
    exit 1
fi

# 检查参数是否提供
if [ $# -eq 0 ]; then
    echo "错误: 请提供参数"
    exit 1
fi

基本调试技巧#

# 使用 -x 选项启用调试模式
# 在脚本开头添加
#!/bin/bash -x

# 或者在执行时指定
bash -x script.sh

# 使用 -v 选项显示脚本内容
bash -v script.sh

# 使用 -n 选项检查语法错误
bash -n script.sh

中级#

错误捕获和处理#

set 命令#

set命令可以控制Shell的行为,包括错误处理模式。

# 遇到错误时立即退出
set -e

# 遇到未定义变量时出错
set -u

# 管道中任何命令失败时整个管道失败
set -o pipefail

# 组合使用
set -euo pipefail

# 临时禁用错误检查
set +e
dangerous_command
set -e

错误处理函数#

# 基本错误处理函数
error_exit() {
    echo "错误: $1"
    exit 1
}

# 使用错误处理函数
if [ ! -f "file.txt" ]; then
    error_exit "文件不存在"
fi

# 带错误码的错误处理函数
error_exit() {
    local message=$1
    local code=${2:-1}
    echo "错误: $message"
    exit $code
}

# 使用带错误码的函数
if [ ! -d "dir" ]; then
    error_exit "目录不存在" 2
fi

日志记录#

# 基本日志函数
log_info() {
    echo "[INFO] $1"
}

log_error() {
    echo "[ERROR] $1" >&2
}

log_debug() {
    if [ "$DEBUG" = "true" ]; then
        echo "[DEBUG] $1"
    fi
}

# 使用日志函数
log_info "开始执行脚本"

if [ ! -f "file.txt" ]; then
    log_error "文件不存在"
    exit 1
fi

log_debug "文件存在,继续执行"
log_info "脚本执行完成"

错误恢复#

# 临时文件清理
cleanup() {
    rm -f tempfile.txt
    echo "清理完成"
}

# 注册退出时执行清理
trap cleanup EXIT

# 执行可能失败的操作
cp large_file.txt tempfile.txt

# 错误恢复示例
backup_file() {
    local file=$1
    local backup=${file}.bak
    
    # 备份文件
    if cp "$file" "$backup"; then
        echo "备份成功: $backup"
        return 0
    else
        echo "备份失败"
        return 1
    fi
}

# 使用备份进行恢复
if ! backup_file "important.txt"; then
    echo "尝试使用现有备份恢复"
    if [ -f "important.txt.bak" ]; then
        cp "important.txt.bak" "important.txt"
        echo "恢复成功"
    else
        echo "恢复失败: 无备份文件"
        exit 1
    fi
fi

高级#

高级调试技巧#

断点调试#

# 添加断点
debug() {
    echo "断点: $1"
    read -p "按Enter继续..."
}

# 在脚本中使用断点
log_info "开始处理"
debug "处理前"
process_data
debug "处理后"
log_info "处理完成"

详细调试日志#

# 详细调试函数
debug() {
    if [ "$DEBUG" = "true" ]; then
        local func=${FUNCNAME[1]:-main}
        local line=${BASH_LINENO[0]}
        echo "[DEBUG] $func:$line $1"
    fi
}

# 使用详细调试
process_file() {
    local file=$1
    debug "开始处理文件: $file"
    
    if [ ! -f "$file" ]; then
        debug "文件不存在"
        return 1
    fi
    
    debug "文件存在,大小: $(stat -c %s "$file")"
    # 处理文件
    debug "处理完成"
    return 0
}

# 启用调试
DEBUG=true
process_file "data.txt"

追踪执行流程#

# 追踪函数调用
trace() {
    local func=${FUNCNAME[1]}
    local args="$@"
    echo "[TRACE] 调用 $func($args)"
}

# 示例函数
func1() {
    trace "$@"
    func2 "$@"
}

func2() {
    trace "$@"
    echo "执行中..."
}

# 调用函数
func1 "参数1" "参数2"

错误处理策略#

防御性编程#

# 防御性编程示例
safe_mkdir() {
    local dir=$1
    
    # 检查参数
    if [ -z "$dir" ]; then
        echo "错误: 目录名不能为空"
        return 1
    fi
    
    # 检查是否存在
    if [ -d "$dir" ]; then
        echo "警告: 目录已存在"
        return 0
    fi
    
    # 尝试创建
    if mkdir -p "$dir"; then
        echo "成功: 创建目录 $dir"
        return 0
    else
        echo "错误: 无法创建目录 $dir"
        return 1
    fi
}

# 使用安全函数
safe_mkdir "path/to/dir"

事务处理#

# 简单事务处理
transaction() {
    local temp_file=$(mktemp)
    local success=0
    
    # 操作1
    if ! operation1 > "$temp_file"; then
        echo "操作1失败"
        success=1
    fi
    
    # 操作2
    if [ $success -eq 0 ] && ! operation2 < "$temp_file"; then
        echo "操作2失败"
        success=1
    fi
    
    # 清理
    rm -f "$temp_file"
    
    # 提交或回滚
    if [ $success -eq 0 ]; then
        echo "事务成功"
        return 0
    else
        echo "事务失败,已回滚"
        return 1
    fi
}

# 执行事务
transaction

错误报告#

# 错误报告函数
report_error() {
    local message=$1
    local exit_code=${2:-1}
    local script=${0##*/}
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    
    # 输出到控制台
    echo "[$timestamp] ERROR: $message" >&2
    
    # 记录到日志文件
    echo "[$timestamp] ERROR: $message" >> "${script}.log"
    
    # 退出
    exit $exit_code
}

# 使用错误报告
if [ ! -f "config.txt" ]; then
    report_error "配置文件不存在"
fi

# 带上下文的错误报告
report_error_with_context() {
    local message=$1
    local exit_code=${2:-1}
    local script=${0##*/}
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    local line=${BASH_LINENO[0]}
    local func=${FUNCNAME[1]}
    
    local error_msg="[$timestamp] ERROR in $script:$line ($func): $message"
    echo "$error_msg" >&2
    echo "$error_msg" >> "${script}.log"
    
    exit $exit_code
}

# 使用带上下文的错误报告
validate_input() {
    local input=$1
    if [ -z "$input" ]; then
        report_error_with_context "输入为空"
    fi
}

validate_input ""

大师#

全面错误处理策略#

错误处理框架#

# 错误处理框架

# 配置
DEBUG=${DEBUG:-false}
LOG_FILE=${LOG_FILE:-"$(basename $0 .sh).log"}

# 日志函数
log() {
    local level=$1
    shift
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    local message="[$timestamp] [$level] $@"
    
    # 输出到控制台
    echo "$message"
    
    # 记录到日志文件
    echo "$message" >> "$LOG_FILE"
}

log_info() {
    log "INFO" "$@"
}

log_error() {
    log "ERROR" "$@"
}

log_debug() {
    if [ "$DEBUG" = "true" ]; then
        log "DEBUG" "$@"
    fi
}

# 错误处理函数
error_exit() {
    local message=$1
    local code=${2:-1}
    log_error "$message"
    exit $code
}

# 检查函数
check_file() {
    local file=$1
    local description=${2:-"文件"}
    
    log_debug "检查 $description: $file"
    
    if [ ! -e "$file" ]; then
        error_exit "$description 不存在: $file"
    fi
    
    if [ ! -f "$file" ]; then
        error_exit "$description 不是文件: $file"
    fi
    
    if [ ! -r "$file" ]; then
        error_exit "$description 不可读: $file"
    fi
    
    log_debug "$description 检查通过"
    return 0
}

# 清理函数
cleanup() {
    log_info "执行清理操作"
    # 清理代码
    rm -f temp_*.txt
    log_info "清理完成"
}

# 注册信号处理
trap cleanup EXIT

# 主函数
main() {
    log_info "开始执行脚本"
    
    # 检查参数
    if [ $# -eq 0 ]; then
        error_exit "请提供参数"
    fi
    
    # 检查文件
    check_file "$1" "输入文件"
    
    # 处理逻辑
    log_info "处理文件: $1"
    # 处理代码
    
    log_info "脚本执行完成"
    return 0
}

# 执行主函数
main "$@"

异常处理模拟#

# 异常处理模拟

try() {
    local cmd="$@"
    local errfile=$(mktemp)
    
    # 执行命令并捕获错误
    if $cmd 2>"$errfile"; then
        rm -f "$errfile"
        return 0
    else
        local error=$(cat "$errfile")
        rm -f "$errfile"
        echo "$error"
        return 1
    fi
}

catch() {
    local error=$1
    local handler=$2
    
    if [ -n "$error" ]; then
        $handler "$error"
        return 1
    fi
    
    return 0
}

# 示例
my_handler() {
    echo "捕获到错误: $1"
}

try ls non_existent_file
error=$?
catch "$error" my_handler

try echo "正常执行"
error=$?
catch "$error" my_handler

高级调试技术#

交互式调试#

# 交互式调试函数
interactive_debug() {
    local prompt="debug> "
    local cmd
    
    echo "进入调试模式,输入 'continue' 继续执行"
    
    while true; do
        read -p "$prompt" cmd
        
        case $cmd in
            continue)
                echo "退出调试模式"
                break
                ;;
            quit|exit)
                echo "退出脚本"
                exit 1
                ;;
            variables|vars)
                set
                ;;
            *)
                # 执行任意命令
                eval "$cmd"
                ;;
        esac
    done
}

# 在脚本中使用
process_data() {
    echo "处理数据..."
    # 处理代码
    interactive_debug
    echo "继续执行..."
}

process_data

性能分析#

# 性能分析函数
profile() {
    local func=$1
    local args="$@"
    shift
    
    local start=$(date +%s.%N)
    "$func" "$@"
    local end=$(date +%s.%N)
    local duration=$(echo "$end - $start" | bc)
    
    echo "函数 $func 执行时间: $duration 秒"
}

# 示例函数
slow_function() {
    echo "执行缓慢操作..."
    sleep 1
    echo "操作完成"
}

# 分析性能
profile slow_function

内存使用分析#

# 内存使用分析
mem_usage() {
    local pid=$$
    local mem=$(ps -o rss= -p $pid)
    echo "当前内存使用: $mem KB"
}

# 在关键点检查内存使用
start_operation() {
    mem_usage
    echo "开始操作..."
    # 操作代码
    mem_usage
    echo "操作完成"
}

start_operation

无敌#

高级错误处理系统#

错误管理框架#

#!/bin/bash

# 高级错误管理框架

# 配置
DEBUG=${DEBUG:-false}
VERBOSE=${VERBOSE:-false}
LOG_FILE=${LOG_FILE:-"$(basename $0 .sh).log"}
ERROR_FILE=${ERROR_FILE:-"$(basename $0 .sh).err"}

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# 日志函数
log() {
    local level=$1
    shift
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    local message="[$timestamp] [$level] $@"
    
    # 控制台输出
    case $level in
        ERROR) echo -e "${RED}$message${NC}" >&2 ;;
        WARNING) echo -e "${YELLOW}$message${NC}" ;;
        INFO) echo -e "${GREEN}$message${NC}" ;;
        DEBUG) if [ "$DEBUG" = "true" ]; then echo -e "${BLUE}$message${NC}" ; fi ;;
        *) echo "$message" ;;
    esac
    
    # 日志文件记录
    echo "$message" >> "$LOG_FILE"
    
    # 错误文件记录
    if [ "$level" = "ERROR" ]; then
        echo "$message" >> "$ERROR_FILE"
    fi
}

# 错误处理函数
error() {
    local message=$1
    local code=${2:-1}
    log "ERROR" "$message"
    exit $code
}

# 警告函数
warning() {
    log "WARNING" "$1"
}

# 信息函数
info() {
    log "INFO" "$1"
}

# 调试函数
debug() {
    log "DEBUG" "$1"
}

# 详细信息函数
verbose() {
    if [ "$VERBOSE" = "true" ]; then
        log "INFO" "$1"
    fi
}

# 检查函数
check() {
    local condition=$1
    local message=$2
    local code=${3:-1}
    
    if [ ! $condition ]; then
        error "$message" $code
    fi
}

# 安全执行函数
safe_exec() {
    local cmd="$1"
    local description=${2:-"命令"}
    
    debug "执行: $cmd"
    
    if eval "$cmd"; then
        verbose "$description 执行成功"
        return 0
    else
        error "$description 执行失败"
    fi
}

# 清理函数
cleanup() {
    info "执行清理操作"
    # 清理代码
    rm -f temp_*.txt
    info "清理完成"
}

# 注册信号处理
trap cleanup EXIT

# 注册错误处理
trap 'error "脚本意外退出"' ERR

# 主函数
main() {
    info "开始执行脚本: $(basename $0)"
    
    # 检查参数
    check [ $# -ge 1 ] "请提供输入文件"
    
    local input_file=$1
    
    # 检查文件
    check [ -f "$input_file" ] "输入文件不存在: $input_file"
    check [ -r "$input_file" ] "输入文件不可读: $input_file"
    
    info "处理文件: $input_file"
    
    # 处理逻辑
    safe_exec "cp "$input_file" "${input_file}.bak"" "备份文件"
    safe_exec "sed -i 's/old/new/g' "$input_file"" "修改文件"
    
    info "脚本执行完成"
    return 0
}

# 执行主函数
main "$@"

分布式错误处理#

# 分布式错误处理

# 节点错误处理
node_error() {
    local node=$1
    local error=$2
    log "ERROR" "节点 $node: $error"
    # 错误恢复逻辑
    if [ "$error" = "timeout" ]; then
        log "INFO" "尝试重新连接节点 $node"
        # 重新连接代码
    fi
}

# 执行分布式命令
distributed_exec() {
    local nodes=($1)
    local cmd=$2
    local errors=0
    
    for node in "${nodes[@]}"; do
        log "INFO" "在节点 $node 执行: $cmd"
        if ssh "$node" "$cmd"; then
            log "INFO" "节点 $node 执行成功"
        else
            node_error "$node" "命令执行失败"
            errors=$((errors + 1))
        fi
    done
    
    if [ $errors -eq 0 ]; then
        log "INFO" "所有节点执行成功"
        return 0
    else
        log "ERROR" "$errors 个节点执行失败"
        return 1
    fi
}

# 使用分布式执行
nodes=("node1" "node2" "node3")
distributed_exec "${nodes[*]}" "uptime"

实用脚本示例#

系统备份工具#

#!/bin/bash

# 系统备份工具

# 配置
BACKUP_DIR="/backup"
LOG_FILE="$BACKUP_DIR/backup.log"
ERROR_FILE="$BACKUP_DIR/backup.err"
SOURCES=("/etc" "/home" "/var/www")

# 日志函数
log() {
    local level=$1
    shift
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    local message="[$timestamp] [$level] $@"
    
    echo "$message"
    echo "$message" >> "$LOG_FILE"
    
    if [ "$level" = "ERROR" ]; then
        echo "$message" >> "$ERROR_FILE"
    fi
}

# 错误处理函数
error_exit() {
    log "ERROR" "$1"
    exit 1
}

# 检查函数
check() {
    local condition=$1
    local message=$2
    
    if [ ! $condition ]; then
        error_exit "$message"
    fi
}

# 清理函数
cleanup() {
    log "INFO" "执行清理操作"
    # 清理代码
    rm -f "$BACKUP_DIR"/temp_*.tar.gz
    log "INFO" "清理完成"
}

# 注册信号处理
trap cleanup EXIT

# 主函数
main() {
    log "INFO" "开始系统备份"
    
    # 检查备份目录
    check [ -d "$BACKUP_DIR" ] "备份目录不存在"
    check [ -w "$BACKUP_DIR" ] "备份目录不可写"
    
    # 创建日期目录
    local date=$(date +%Y-%m-%d)
    local backup_dir="$BACKUP_DIR/$date"
    
    if [ -d "$backup_dir" ]; then
        log "WARNING" "备份目录已存在,使用现有目录"
    else
        if mkdir -p "$backup_dir"; then
            log "INFO" "创建备份目录: $backup_dir"
        else
            error_exit "无法创建备份目录"
        fi
    fi
    
    # 执行备份
    for source in "${SOURCES[@]}"; do
        if [ -d "$source" ]; then
            local backup_file="$backup_dir/$(basename $source).tar.gz"
            log "INFO" "备份 $source$backup_file"
            
            if tar -czf "$backup_file" "$source"; then
                log "INFO" "备份成功"
            else
                log "ERROR" "备份失败"
            fi
        else
            log "WARNING" "源目录不存在: $source"
        fi
    done
    
    # 清理旧备份(保留30天)
    log "INFO" "清理30天前的旧备份"
    find "$BACKUP_DIR" -type d -mtime +30 -exec rm -rf {} \;
    
    log "INFO" "系统备份完成"
    return 0
}

# 执行主函数
main

日志分析工具#

#!/bin/bash

# 日志分析工具

# 配置
LOG_FILE=${1:-"/var/log/syslog"}
OUTPUT_DIR="/tmp/log_analysis"

# 日志函数
log() {
    local level=$1
    shift
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    echo "[$timestamp] [$level] $@"
}

# 错误处理函数
error_exit() {
    log "ERROR" "$1"
    exit 1
}

# 检查函数
check() {
    local condition=$1
    local message=$2
    
    if [ ! $condition ]; then
        error_exit "$message"
    fi
}

# 清理函数
cleanup() {
    log "INFO" "执行清理操作"
    # 清理代码
    rm -f "$OUTPUT_DIR"/*.txt
    log "INFO" "清理完成"
}

# 注册信号处理
trap cleanup EXIT

# 分析函数
analyze() {
    log "INFO" "开始分析日志: $LOG_FILE"
    
    # 检查日志文件
    check [ -f "$LOG_FILE" ] "日志文件不存在"
    check [ -r "$LOG_FILE" ] "日志文件不可读"
    
    # 创建输出目录
    if [ ! -d "$OUTPUT_DIR" ]; then
        if mkdir -p "$OUTPUT_DIR"; then
            log "INFO" "创建输出目录: $OUTPUT_DIR"
        else
            error_exit "无法创建输出目录"
        fi
    fi
    
    # 分析错误
    log "INFO" "分析错误日志"
    grep "ERROR" "$LOG_FILE" > "$OUTPUT_DIR/errors.txt"
    local error_count=$(wc -l < "$OUTPUT_DIR/errors.txt")
    log "INFO" "找到 $error_count 条错误"
    
    # 分析警告
    log "INFO" "分析警告日志"
    grep "WARNING" "$LOG_FILE" > "$OUTPUT_DIR/warnings.txt"
    local warning_count=$(wc -l < "$OUTPUT_DIR/warnings.txt")
    log "INFO" "找到 $warning_count 条警告"
    
    # 分析服务状态
    log "INFO" "分析服务状态"
    grep "service" "$LOG_FILE" > "$OUTPUT_DIR/services.txt"
    
    # 生成报告
    log "INFO" "生成分析报告"
    cat > "$OUTPUT_DIR/report.txt" << EOF
日志分析报告
=============

分析日期: $(date)
日志文件: $LOG_FILE

错误统计: $error_count 条
警告统计: $warning_count 条

详细信息:
- 错误: $OUTPUT_DIR/errors.txt
- 警告: $OUTPUT_DIR/warnings.txt
- 服务: $OUTPUT_DIR/services.txt
EOF
    
    log "INFO" "分析完成,报告保存到: $OUTPUT_DIR/report.txt"
    return 0
}

# 主函数
main() {
    log "INFO" "启动日志分析工具"
    analyze
    log "INFO" "工具执行完成"
    return 0
}

# 执行主函数
main

总结#

错误处理是Shell脚本开发中的关键环节,通过掌握各种错误处理技术,您可以创建更加健壮、可靠的Shell脚本。从基础的退出状态码检查到高级的调试和错误恢复策略,本教程涵盖了Shell脚本错误处理的各个方面。

良好的错误处理不仅可以提高脚本的可靠性,还可以改善用户体验和简化维护工作。通过采用防御性编程、全面的错误检查、详细的日志记录和适当的清理操作,您可以构建能够应对各种异常情况的Shell脚本。

记住,错误处理应该是脚本设计的一部分,而不是事后添加的功能。在编写脚本时,始终考虑可能的错误情况,并为它们提供适当的处理机制。