高级脚本技巧

掌握高级 Shell 脚本技巧,编写更强大、更可靠的自动化脚本。

脚本模板

专业脚本框架

#!/bin/bash
#
# Script Name: example.sh
# Description: 脚本功能描述
# Author: Your Name
# Created: 2024-01-01
# Version: 1.0
#

set -euo pipefail  # 严格模式
IFS=$'\n\t'        # 字段分隔符

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

# 全局变量
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"

# 日志函数
log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

info() {
    echo -e "${GREEN}[INFO]${NC} $*" | tee -a "$LOG_FILE"
}

warn() {
    echo -e "${YELLOW}[WARN]${NC} $*" | tee -a "$LOG_FILE"
}

error() {
    echo -e "${RED}[ERROR]${NC} $*" | tee -a "$LOG_FILE"
}

die() {
    error "$*"
    exit 1
}

# 清理函数
cleanup() {
    local exit_code=$?
    log "脚本退出,退出码: $exit_code"
    # 清理临时文件等
    rm -f /tmp/temp_$$_*
}

trap cleanup EXIT

# 显示帮助
usage() {
    cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]

Description:
    脚本功能描述

Options:
    -h, --help          显示帮助信息
    -v, --verbose       详细输出
    -d, --debug         调试模式
    -c, --config FILE   配置文件

Examples:
    $SCRIPT_NAME -v
    $SCRIPT_NAME -c /etc/config.conf

EOF
    exit 0
}

# 参数解析
parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help)
                usage
                ;;
            -v|--verbose)
                VERBOSE=1
                shift
                ;;
            -d|--debug)
                set -x
                shift
                ;;
            -c|--config)
                CONFIG_FILE="$2"
                shift 2
                ;;
            *)
                die "未知选项: $1"
                ;;
        esac
    done
}

# 主函数
main() {
    info "脚本开始执行"
    
    # 检查权限
    [[ $EUID -eq 0 ]] || die "此脚本需要 root 权限"
    
    # 检查依赖
    command -v jq >/dev/null 2>&1 || die "需要安装 jq"
    
    # 主要逻辑
    info "执行主要任务..."
    
    info "脚本执行完成"
}

# 脚本入口
parse_args "$@"
main

错误处理

严格模式

#!/bin/bash

# set -e: 遇到错误立即退出
# set -u: 使用未定义变量时报错
# set -o pipefail: 管道中任一命令失败则失败
# set -x: 打印执行的命令(调试)

set -euo pipefail

# 示例:严格模式的效果
echo "开始执行"

# 这会导致脚本退出(因为 set -e)
# false

# 这不会执行
echo "这行不会打印"

错误捕获和处理

#!/bin/bash

# 捕获错误信息
error_handler() {
    local line_no=$1
    local bash_lineno=$2
    local last_cmd=$3
    local code=$4
    
    echo "错误发生在:"
    echo "  文件: $0"
    echo "  行号: $line_no"
    echo "  命令: $last_cmd"
    echo "  退出码: $code"
    
    # 发送告警
    # send_alert "脚本错误: $0:$line_no"
}

trap 'error_handler ${LINENO} ${BASH_LINENO} "$BASH_COMMAND" $?' ERR

# 重试机制
retry() {
    local max_attempts=$1
    shift
    local cmd="$@"
    local attempt=1
    
    until $cmd; do
        if (( attempt >= max_attempts )); then
            echo "命令失败,已重试 $max_attempts 次"
            return 1
        fi
        echo "命令失败,等待重试... (尝试 $attempt/$max_attempts)"
        sleep 5
        ((attempt++))
    done
    
    return 0
}

# 使用示例
retry 3 curl -f https://example.com/api

并发处理

后台任务管理

#!/bin/bash

# 并发执行任务
parallel_tasks() {
    local max_jobs=4
    local job_count=0
    
    for item in item1 item2 item3 item4 item5 item6; do
        # 等待直到有空闲槽位
        while (( $(jobs -r | wc -l) >= max_jobs )); do
            sleep 0.1
        done
        
        # 后台执行任务
        {
            echo "处理 $item"
            sleep 2
            echo "$item 完成"
        } &
    done
    
    # 等待所有任务完成
    wait
    echo "所有任务完成"
}

parallel_tasks

GNU Parallel

# 安装
sudo apt install parallel

# 基本使用
seq 10 | parallel echo "处理任务 {}"

# 并行运行命令
parallel -j 4 ::: command1 command2 command3 command4

# 处理文件列表
find . -name "*.log" | parallel gzip {}

# 从文件读取任务
parallel -a tasks.txt -j 8 ./process.sh

# 进度显示
seq 100 | parallel --progress sleep 0.1

# 结果收集
parallel -j 4 "echo {}; date" ::: task{1..10} > results.txt

xargs 并发

# 并发执行(4 个进程)
find . -name "*.txt" | xargs -P 4 -I {} gzip {}

# 批量处理
seq 100 | xargs -n 10 -P 4 ./batch_process.sh

# 空格处理
find . -print0 | xargs -0 -P 4 -I {} echo "处理: {}"

进程间通信

命名管道(FIFO)

#!/bin/bash

# 创建命名管道
FIFO="/tmp/my_fifo"
mkfifo "$FIFO"

# 清理函数
cleanup() {
    rm -f "$FIFO"
}
trap cleanup EXIT

# 写入进程
{
    for i in {1..10}; do
        echo "消息 $i"
        sleep 1
    done
} > "$FIFO" &

# 读取进程
while read line; do
    echo "收到: $line"
done < "$FIFO"

信号处理

#!/bin/bash

# 信号处理函数
handle_sigint() {
    echo "收到 SIGINT (Ctrl+C)"
    exit 0
}

handle_sigterm() {
    echo "收到 SIGTERM"
    cleanup
    exit 0
}

handle_sigusr1() {
    echo "收到 SIGUSR1,重新加载配置"
    load_config
}

# 注册信号处理
trap handle_sigint SIGINT
trap handle_sigterm SIGTERM
trap handle_sigusr1 SIGUSR1

# 主循环
while true; do
    echo "运行中... PID: $$"
    sleep 5
done

进程同步

#!/bin/bash

# 使用文件锁
LOCK_FILE="/var/lock/myscript.lock"

acquire_lock() {
    exec 200>"$LOCK_FILE"
    flock -n 200 || {
        echo "无法获取锁,其他实例正在运行"
        exit 1
    }
}

release_lock() {
    flock -u 200
}

trap release_lock EXIT

acquire_lock
echo "获取锁成功,执行任务..."
sleep 10

配置文件处理

读取 INI 配置

#!/bin/bash

# config.ini
# [database]
# host=localhost
# port=3306
# user=admin
# password=secret

read_ini() {
    local file=$1
    local section=$2
    local key=$3
    
    awk -F= -v section="$section" -v key="$key" '
        /^\[.*\]$/ { 
            current_section = substr($0, 2, length($0)-2)
        }
        current_section == section && $1 == key {
            gsub(/^[ \t]+|[ \t]+$/, "", $2)
            print $2
        }
    ' "$file"
}

# 使用
DB_HOST=$(read_ini config.ini database host)
DB_PORT=$(read_ini config.ini database port)
echo "数据库: $DB_HOST:$DB_PORT"

JSON 处理

#!/bin/bash

# 使用 jq 处理 JSON
config='{"server": {"host": "localhost", "port": 8080}}'

# 读取值
host=$(echo "$config" | jq -r '.server.host')
port=$(echo "$config" | jq -r '.server.port')

# 修改值
new_config=$(echo "$config" | jq '.server.port = 9090')

# 从文件读取
jq '.servers[] | select(.active == true)' config.json

# 生成 JSON
jq -n \
    --arg name "test" \
    --argjson port 8080 \
    '{name: $name, port: $port}'

YAML 处理

#!/bin/bash

# 使用 yq 处理 YAML(需要安装 yq)
# pip install yq

# 读取值
host=$(yq -r '.database.host' config.yaml)

# 修改值
yq -y '.database.port = 5432' config.yaml > config_new.yaml

# 合并 YAML
yq -s '.[0] * .[1]' base.yaml override.yaml

数组和关联数组

高级数组操作

#!/bin/bash

# 数组定义
servers=("web1" "web2" "web3")

# 数组长度
echo "服务器数量: ${#servers[@]}"

# 数组切片
echo "前两个: ${servers[@]:0:2}"

# 追加元素
servers+=("web4" "web5")

# 删除元素
unset servers[1]

# 数组遍历
for server in "${servers[@]}"; do
    echo "服务器: $server"
done

# 数组索引遍历
for i in "${!servers[@]}"; do
    echo "[$i]: ${servers[$i]}"
done

# 数组排序
IFS=$'\n' sorted=($(sort <<<"${servers[*]}"))
unset IFS

# 数组去重
unique=($(printf "%s\n" "${servers[@]}" | sort -u))

关联数组应用

#!/bin/bash

# 声明关联数组
declare -A server_status
declare -A server_config

# 设置值
server_status[web1]="running"
server_status[web2]="stopped"
server_status[db1]="running"

# 配置映射
server_config[web1]="192.168.1.10:80"
server_config[web2]="192.168.1.11:80"
server_config[db1]="192.168.1.20:3306"

# 遍历关联数组
for server in "${!server_status[@]}"; do
    echo "$server: ${server_status[$server]} @ ${server_config[$server]}"
done

# 检查键是否存在
if [[ -v server_status[web1] ]]; then
    echo "web1 存在"
fi

# 统计
declare -A count
for item in a b c a b a; do
    ((count[$item]++))
done

for key in "${!count[@]}"; do
    echo "$key: ${count[$key]}"
done

文本处理

sed 高级用法

# 多行替换
sed '/start/,/end/s/old/new/g' file.txt

# 使用变量
var="new_value"
sed "s/old_value/$var/g" file.txt

# 备份并修改
sed -i.bak 's/old/new/g' file.txt

# 删除空行
sed '/^$/d' file.txt

# 提取匹配行
sed -n '/pattern/p' file.txt

# 在匹配行前后插入
sed '/pattern/i\新行内容' file.txt
sed '/pattern/a\新行内容' file.txt

awk 高级用法

# 统计
awk '{sum+=$1; count++} END {print sum/count}' data.txt

# 条件处理
awk '$3 > 100 {print $1, $2}' data.txt

# 数组
awk '{count[$1]++} END {for(i in count) print i, count[i]}' file.txt

# 多文件处理
awk 'FNR==1{print FILENAME} {print}' file1.txt file2.txt

# 格式化输出
awk '{printf "%-10s %5d %8.2f\n", $1, $2, $3}' data.txt

# 函数
awk 'function abs(x){return x<0?-x:x} {print abs($1)}' data.txt

性能优化

避免子进程

# 慢速(每次迭代创建子进程)
for i in {1..1000}; do
    result=$(expr $i + 1)
done

# 快速(使用内置算术)
for i in {1..1000}; do
    result=$((i + 1))
done

# 慢速(使用外部命令)
if [ $(grep -c "pattern" file.txt) -gt 0 ]; then
    echo "找到"
fi

# 快速(使用内置功能)
if grep -q "pattern" file.txt; then
    echo "找到"
fi

批量操作

# 慢速(逐个文件处理)
for file in *.log; do
    cat "$file" | process
done

# 快速(批量处理)
cat *.log | process

# 慢速(多次管道)
cat file.txt | grep pattern | sort | uniq

# 快速(减少管道)
grep pattern file.txt | sort -u

实用脚本示例

系统监控脚本

#!/bin/bash
# monitor.sh - 系统资源监控

THRESHOLD_CPU=80
THRESHOLD_MEM=90
THRESHOLD_DISK=85
EMAIL="admin@example.com"

check_cpu() {
    local cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d% -f1)
    if (( $(echo "$cpu > $THRESHOLD_CPU" | bc -l) )); then
        echo "CPU 使用率过高: ${cpu}%"
        return 1
    fi
    return 0
}

check_memory() {
    local mem=$(free | grep Mem | awk '{printf("%.0f", $3/$2 * 100)}')
    if (( mem > THRESHOLD_MEM )); then
        echo "内存使用率过高: ${mem}%"
        return 1
    fi
    return 0
}

check_disk() {
    df -h | awk 'NR>1 {gsub(/%/,"",$5); if($5>threshold) print $0}' threshold=$THRESHOLD_DISK
}

# 主循环
while true; do
    alert=""
    
    check_cpu || alert+="CPU过高\n"
    check_memory || alert+="内存过高\n"
    disk_alert=$(check_disk)
    [[ -n "$disk_alert" ]] && alert+="磁盘空间不足:\n$disk_alert\n"
    
    if [[ -n "$alert" ]]; then
        echo -e "$alert" | mail -s "系统告警" "$EMAIL"
    fi
    
    sleep 300
done

日志分析脚本

#!/bin/bash
# log_analyzer.sh - 日志分析工具

LOG_FILE="/var/log/nginx/access.log"
REPORT_FILE="/tmp/log_report_$(date +%Y%m%d).txt"

{
    echo "=== 日志分析报告 ==="
    echo "时间: $(date)"
    echo ""
    
    echo "Top 10 访问IP:"
    awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
    echo ""
    
    echo "Top 10 访问页面:"
    awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
    echo ""
    
    echo "HTTP 状态码统计:"
    awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn
    echo ""
    
    echo "流量统计 (MB):"
    awk '{sum+=$10} END {printf "%.2f\n", sum/1024/1024}' "$LOG_FILE"
    
} > "$REPORT_FILE"

echo "报告已生成: $REPORT_FILE"

下一步:学习 系统性能调优 章节。