强曰为道

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

19 - 设计模式

19 - 设计模式

19.1 配置管理模式

多层配置文件

#!/bin/bash
# config_manager.sh —— 多层配置管理

declare -A CONFIG

# 默认值
load_defaults() {
    CONFIG=(
        [host]="localhost"
        [port]="8080"
        [log_level]="info"
        [workers]="4"
        [timeout]="30"
        [debug]="false"
    )
}

# 从 INI 文件加载
load_config_file() {
    local file="$1"
    [[ -f "$file" ]] || return 0
    
    local section="global"
    while IFS= read -r line || [[ -n "$line" ]]; do
        line="${line#"${line%%[![:space:]]*}"}"
        line="${line%"${line##*[![:space:]]}"}"
        
        [[ -z "$line" || "$line" == \#* ]] && continue
        
        if [[ "$line" =~ ^\[([^\]]+)\]$ ]]; then
            section="${BASH_REMATCH[1]}"
            continue
        fi
        
        if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
            local key="${BASH_REMATCH[1]}"
            local value="${BASH_REMATCH[2]}"
            key="${key#"${key%%[![:space:]]*}"}"
            key="${key%"${key##*[![:space:]]}"}"
            value="${value#"${value%%[![:space:]]*}"}"
            value="${value%"${value##*[![:space:]]}"}"
            CONFIG["$key"]="$value"
        fi
    done < "$file"
}

# 从环境变量覆盖
load_env_overrides() {
    local prefix="${1:-APP}"
    local key value
    
    for key in "${!CONFIG[@]}"; do
        local env_name="${prefix}_${key^^}"
        value="${!env_name:-}"
        [[ -n "$value" ]] && CONFIG["$key"]="$value"
    done
}

# 完整加载链
load_config() {
    load_defaults
    load_config_file "/etc/myapp/config.ini"
    load_config_file "$HOME/.myapp/config.ini"
    load_config_file "./config.ini"
    load_env_overrides "MYAPP"
}

config_get() {
    echo "${CONFIG[${1}]:-}"
}

# 使用
load_config
echo "Host: $(config_get host)"
echo "Port: $(config_get port)"

19.2 插件系统模式

#!/bin/bash
# plugin_system.sh —— 简易插件系统

readonly PLUGIN_DIR="${PLUGIN_DIR:-./plugins}"

declare -A PLUGINS=()

# 加载所有插件
load_plugins() {
    local plugin_file
    
    for plugin_file in "$PLUGIN_DIR"/*.sh; do
        [[ -f "$plugin_file" ]] || continue
        
        local plugin_name
        plugin_name=$(basename "$plugin_file" .sh)
        
        # 加载插件
        source "$plugin_file"
        
        # 检查插件是否定义了必需函数
        if ! declare -f "plugin_${plugin_name}_init" &>/dev/null; then
            echo "警告: 插件 $plugin_name 缺少 init 函数" >&2
            continue
        fi
        
        PLUGINS["$plugin_name"]="$plugin_file"
        echo "已加载插件: $plugin_name"
    done
}

# 初始化所有插件
init_plugins() {
    local name
    for name in "${!PLUGINS[@]}"; do
        "plugin_${name}_init"
    done
}

# 调用插件的特定钩子
call_hook() {
    local hook="$1"
    shift
    
    local name
    for name in "${!PLUGINS[@]}"; do
        local func="plugin_${name}_${hook}"
        if declare -f "$func" &>/dev/null; then
            "$func" "$@"
        fi
    done
}

插件文件示例:

#!/bin/bash
# plugins/notify.sh —— 通知插件

plugin_notify_init() {
    echo "通知插件已初始化"
}

plugin_notify_on_deploy() {
    local env="$1"
    local version="$2"
    echo "发送部署通知: $env -> $version"
    # curl -X POST "https://hooks.slack.com/..." ...
}

plugin_notify_on_error() {
    local message="$1"
    echo "发送错误通知: $message"
}
# 主程序使用
source plugin_system.sh

load_plugins
init_plugins

# 触发钩子
call_hook "on_deploy" "production" "2.0.0"

19.3 日志框架

#!/bin/bash
# logger.sh —— 结构化日志框架

# 日志级别
readonly _LOG_DEBUG=0
readonly _LOG_INFO=1
readonly _LOG_WARN=2
readonly _LOG_ERROR=3
readonly _LOG_FATAL=4

# 配置
_LOG_LEVEL=${_LOG_LEVEL:-$_LOG_INFO}
_LOG_FILE=""
_LOG_FORMAT="text"  # text | json
_LOG_COLOR=true

# 颜色
readonly _COLOR_RESET="\033[0m"
readonly _COLOR_GRAY="\033[0;37m"
readonly _COLOR_GREEN="\033[0;32m"
readonly _COLOR_YELLOW="\033[0;33m"
readonly _COLOR_RED="\033[0;31m"
readonly _COLOR_MAGENTA="\033[0;35m"

declare -a _LEVEL_NAMES=("DEBUG" "INFO" "WARN" "ERROR" "FATAL")
declare -a _LEVEL_COLORS=("$_COLOR_GRAY" "$_COLOR_GREEN" "$_COLOR_YELLOW" "$_COLOR_RED" "$_COLOR_MAGENTA")

_log() {
    local level=$1
    shift
    local message="$*"
    
    [[ $level -lt $_LOG_LEVEL ]] && return
    
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    local caller="${FUNCNAME[2]:-main}"
    local source="${BASH_SOURCE[2]:-$0}"
    local lineno="${BASH_LINENO[1]:-0}"
    
    if [[ "$_LOG_FORMAT" == "json" ]]; then
        # JSON 格式
        local json="{\"time\":\"$timestamp\",\"level\":\"${_LEVEL_NAMES[$level]}\",\"caller\":\"$caller\",\"file\":\"$source:$lineno\",\"msg\":\"$message\"}"
        echo "$json" >&2
        [[ -n "$_LOG_FILE" ]] && echo "$json" >> "$_LOG_FILE"
    else
        # 文本格式
        local level_name="${_LEVEL_NAMES[$level]}"
        local color="${_LEVEL_COLORS[$level]}"
        
        if [[ "$_LOG_COLOR" == true ]]; then
            printf "${color}[%s] [%-5s] [%s:%d] %s${_COLOR_RESET}\n" \
                "$timestamp" "$level_name" "$source" "$lineno" "$message" >&2
        else
            printf "[%s] [%-5s] [%s:%d] %s\n" \
                "$timestamp" "$level_name" "$source" "$lineno" "$message" >&2
        fi
        
        [[ -n "$_LOG_FILE" ]] && printf "[%s] [%-5s] [%s:%d] %s\n" \
            "$timestamp" "$level_name" "$source" "$lineno" "$message" >> "$_LOG_FILE"
    fi
}

log::debug() { _log $_LOG_DEBUG "$@"; }
log::info()  { _log $_LOG_INFO  "$@"; }
log::warn()  { _log $_LOG_WARN  "$@"; }
log::error() { _log $_LOG_ERROR "$@"; }
log::fatal() { _log $_LOG_FATAL "$@"; }

# 日志配置
log::configure() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --level)   _LOG_LEVEL="$2"; shift 2 ;;
            --file)    _LOG_FILE="$2"; shift 2 ;;
            --format)  _LOG_FORMAT="$2"; shift 2 ;;
            --no-color) _LOG_COLOR=false; shift ;;
            *) shift ;;
        esac
    done
}

# 性能计时
declare -A _TIMERS

log::timer_start() {
    _TIMERS["$1"]=$(date +%s%N)
}

log::timer_end() {
    local name="$1"
    local start="${_TIMERS[$name]}"
    local end=$(date +%s%N)
    local duration=$(( (end - start) / 1000000 ))
    log::info "计时器 [$name]: ${duration}ms"
    unset "_TIMERS[$name]"
}

使用日志框架:

#!/bin/bash
source logger.sh

log::configure --level "$_LOG_DEBUG" --file "/var/log/app.log"

log::debug "调试信息"
log::info "应用启动"
log::warn "配置文件缺失,使用默认值"
log::error "连接数据库失败"
log::fatal "无法恢复的错误"

log::timer_start "operation"
# ... 业务逻辑 ...
sleep 1
log::timer_end "operation"

19.4 参数解析模式

getopts(POSIX 标准)

#!/bin/bash
# getopts 风格参数解析

usage() {
    cat << 'EOF'
用法: script.sh [选项] <参数>

选项:
  -e <env>    目标环境 (dev|prod)
  -v          详细输出
  -f          强制执行
  -h          显示帮助
EOF
}

verbose=false
force=false
env="dev"

while getopts "e:vfh" opt; do
    case "$opt" in
        e) env="$OPTARG" ;;
        v) verbose=true ;;
        f) force=true ;;
        h) usage; exit 0 ;;
        ?) usage; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

echo "环境: $env"
echo "详细: $verbose"
echo "强制: $force"
echo "剩余参数: $*"

长选项解析(手动模式)

#!/bin/bash
# 长选项参数解析

declare -A ARGS=()

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --env|-e)
                ARGS[env]="$2"; shift 2 ;;
            --version|-v)
                ARGS[version]="$2"; shift 2 ;;
            --output|-o)
                ARGS[output]="$2"; shift 2 ;;
            --force|-f)
                ARGS[force]="true"; shift ;;
            --dry-run|-n)
                ARGS[dry_run]="true"; shift ;;
            --help|-h)
                usage; exit 0 ;;
            --)
                shift; break ;;
            -*)
                echo "未知选项: $1" >&2; usage >&2; exit 1 ;;
            *)
                break ;;
        esac
    done
    ARGS[remaining]=("$@")
}

# 使用
parse_args "$@"

echo "环境: ${ARGS[env]:-dev}"
echo "版本: ${ARGS[version]:-latest}"
echo "输出: ${ARGS[output]:-/dev/stdout}"
echo "强制: ${ARGS[force]:-false}"
echo "模拟: ${ARGS[dry_run]:-false}"

19.5 锁文件模式

#!/bin/bash
# flock 文件锁

readonly LOCK_FILE="/var/lock/myapp.lock"

# 获取锁(非阻塞)
acquire_lock() {
    exec 200>"$LOCK_FILE"
    if ! flock -n 200; then
        echo "另一个实例正在运行" >&2
        return 1
    fi
    echo $$ >&200
}

# 释放锁
release_lock() {
    flock -u 200 2>/dev/null
    rm -f "$LOCK_FILE"
}

trap release_lock EXIT

acquire_lock || exit 1
# ... 独占执行 ...

19.6 重试模式

#!/bin/bash
# 通用重试函数

retry() {
    local max_attempts="$1"
    local delay="$2"
    local backoff="${3:-1}"  # 退避因子
    shift 3
    
    local attempt=1
    local current_delay=$delay
    
    while ((attempt <= max_attempts)); do
        if "$@"; then
            return 0
        fi
        
        local exit_code=$?
        
        if ((attempt < max_attempts)); then
            echo "尝试 $attempt/$max_attempts 失败 (退出码: $exit_code),${current_delay}s 后重试..." >&2
            sleep "$current_delay"
            current_delay=$((current_delay * backoff))
        fi
        
        ((attempt++))
    done
    
    echo "所有 $max_attempts 次尝试均失败" >&2
    return 1
}

# 使用
retry 3 5 2 curl -sf "https://api.example.com/health"
# 3次尝试,初始5秒,每次翻倍:5s → 10s → 20s

19.7 临时文件管理模式

#!/bin/bash
# 临时资源管理

declare -a _TEMP_RESOURCES=()

temp::dir() {
    local tmpdir
    tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXX")
    _TEMP_RESOURCES+=("dir:$tmpdir")
    echo "$tmpdir"
}

temp::file() {
    local tmpfile
    tmpfile=$(mktemp "${TMPDIR:-/tmp}/myapp.XXXXXX")
    _TEMP_RESOURCES+=("file:$tmpfile")
    echo "$tmpfile"
}

temp::cleanup() {
    for resource in "${_TEMP_RESOURCES[@]}"; do
        local type="${resource%%:*}"
        local path="${resource#*:}"
        
        case "$type" in
            file) [[ -f "$path" ]] && rm -f "$path" ;;
            dir)  [[ -d "$path" ]] && rm -rf "$path" ;;
        esac
    done
    _TEMP_RESOURCES=()
}

trap temp::cleanup EXIT

# 使用
tmpdir=$(temp::dir)
tmpfile=$(temp::file)
echo "处理中..." > "$tmpfile"
# 脚本退出时自动清理

19.8 业务场景:完整 CLI 框架

#!/bin/bash
# myapp —— CLI 应用框架
set -euo pipefail

readonly VERSION="1.0.0"
readonly SCRIPT_NAME=$(basename "$0")

# ---- 模块加载 ----
source "$(dirname "$0")/lib/logger.sh"
source "$(dirname "$0")/lib/config.sh"

# ---- 全局配置 ----
declare -A CLI_ARGS=(
    [command]=""
    [env]="dev"
    [verbose]="false"
    [dry_run]="false"
)

# ---- 参数解析 ----
parse_args() {
    [[ $# -eq 0 ]] && { usage; exit 1; }
    
    CLI_ARGS[command]="$1"; shift
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --env|-e)      CLI_ARGS[env]="$2"; shift 2 ;;
            --verbose|-v)  CLI_ARGS[verbose]="true"; shift ;;
            --dry-run|-n)  CLI_ARGS[dry_run]="true"; shift ;;
            --version)     echo "$VERSION"; exit 0 ;;
            --help|-h)     usage; exit 0 ;;
            *)             break ;;
        esac
    done
    CLI_ARGS[remaining]=("$@")
}

usage() {
    cat << EOF
用法: $SCRIPT_NAME <命令> [选项]

命令:
  build       构建项目
  test        运行测试
  deploy      部署服务
  status      查看状态

选项:
  -e, --env <env>     环境 (dev|staging|prod)
  -v, --verbose       详细输出
  -n, --dry-run       模拟运行
      --version       显示版本
  -h, --help          显示帮助

示例:
  $SCRIPT_NAME deploy --env production
  $SCRIPT_NAME test --verbose
EOF
}

# ---- 命令实现 ----
cmd_build() {
    log::info "开始构建..."
    [[ "${CLI_ARGS[dry_run]}" == "true" ]] && { log::info "(模拟运行)"; return 0; }
    # build logic
    log::info "构建完成"
}

cmd_test() {
    log::info "运行测试..."
    # test logic
    bats tests/ 2>/dev/null || log::warn "测试框架未安装"
    log::info "测试完成"
}

cmd_deploy() {
    local env="${CLI_ARGS[env]}"
    log::info "部署到 $env 环境..."
    
    if [[ "$env" == "prod" ]]; then
        read -rp "确认部署到生产环境?(yes/no): " confirm
        [[ "$confirm" != "yes" ]] && { log::info "已取消"; return 0; }
    fi
    
    [[ "${CLI_ARGS[dry_run]}" == "true" ]] && { log::info "(模拟运行)"; return 0; }
    # deploy logic
    log::info "部署完成"
}

cmd_status() {
    log::info "查看状态..."
    # status logic
}

# ---- 主入口 ----
main() {
    parse_args "$@"
    
    [[ "${CLI_ARGS[verbose]}" == "true" ]] && _LOG_LEVEL=$_LOG_DEBUG
    
    case "${CLI_ARGS[command]}" in
        build)  cmd_build ;;
        test)   cmd_test ;;
        deploy) cmd_deploy ;;
        status) cmd_status ;;
        *)      log::error "未知命令: ${CLI_ARGS[command]}"; usage; exit 1 ;;
    esac
}

main "$@"

19.9 注意事项

模式适用场景复杂度
配置管理多环境部署
插件系统可扩展工具
日志框架生产级脚本
参数解析CLI 工具
重试模式网络操作
临时文件管理需要清理的资源

19.10 扩展阅读