OA0 = Omni AI 0
OA0 是一个探索 AI 的论坛
现在注册
已注册用户请  登录
OA0  ›  技能包  ›  shell-scripting-master:编写健壮、可移植且专业的 Shell 自动化脚本

shell-scripting-master:编写健壮、可移植且专业的 Shell 自动化脚本

 
  plugin ·  2026-02-04 17:59:45 · 3 次点击  · 0 条评论  

名称: shell-scripting
描述: 编写健壮、可移植的 Shell 脚本。适用于解析参数、正确处理错误、编写 POSIX 兼容脚本、管理临时文件、并行运行命令、管理后台进程或为脚本添加 --help 帮助信息。
元数据: {"clawdbot":{"emoji":"🐚","requires":{"bins":["bash"]},"os":["linux","darwin","win32"]}}


Shell 脚本编程

编写可靠、可维护的 Bash 脚本。涵盖参数解析、错误处理、可移植性、临时文件、并行执行、进程管理和脚本自文档化。

适用场景

  • 编写供他人(或未来的自己)运行的脚本
  • 自动化多步骤工作流
  • 解析带标志和选项的命令行参数
  • 正确处理错误和清理工作
  • 并行运行任务
  • 确保脚本在 Linux 和 macOS 间的可移植性
  • 为复杂命令提供更简单的包装接口

脚本模板

#!/usr/bin/env bash
set -euo pipefail

# 描述:本脚本的功能(一行概括)
# 用法:script.sh [选项] <必需参数>

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"

# 默认值
VERBOSE=false
OUTPUT_DIR="./output"

usage() {
    cat <<EOF
用法:$SCRIPT_NAME [选项] <输入文件>

描述:
  处理输入文件并生成输出。

选项:
  -o, --output DIR    输出目录(默认:$OUTPUT_DIR)
  -v, --verbose       启用详细输出
  -h, --help          显示此帮助信息

示例:
  $SCRIPT_NAME data.csv
  $SCRIPT_NAME -v -o /tmp/results data.csv
EOF
}

log() { echo "[$(date '+%H:%M:%S')] $*" >&2; }
debug() { $VERBOSE && log "DEBUG: $*" || true; }
die() { log "ERROR: $*"; exit 1; }

# 解析参数
while [[ $# -gt 0 ]]; do
    case "$1" in
        -o|--output) OUTPUT_DIR="$2"; shift 2 ;;
        -v|--verbose) VERBOSE=true; shift ;;
        -h|--help) usage; exit 0 ;;
        --) shift; break ;;
        -*) die "未知选项:$1" ;;
        *) break ;;
    esac
done

INPUT_FILE="${1:?$(usage >&2; echo "错误:需要输入文件")}"
[[ -f "$INPUT_FILE" ]] || die "文件未找到:$INPUT_FILE"

# 主逻辑
main() {
    debug "输入:$INPUT_FILE"
    debug "输出:$OUTPUT_DIR"
    mkdir -p "$OUTPUT_DIR"

    log "正在处理 $INPUT_FILE..."
    # ... 执行实际工作 ...
    log "完成。输出位于 $OUTPUT_DIR"
}

main "$@"

错误处理

set 标志

set -e          # 任何命令失败时立即退出
set -u          # 使用未定义变量时报错
set -o pipefail # 管道中任意命令失败则整个管道失败
set -x          # 调试:执行前打印每条命令(输出较多)

# 组合使用(建议每个脚本都加上)
set -euo pipefail

# 临时禁用,以允许某些命令失败
set +e
some_command_that_might_fail
exit_code=$?
set -e

使用 trap 进行清理

# 退出时清理(任何退出:成功、失败或信号)
TMPDIR=""
cleanup() {
    [[ -n "$TMPDIR" ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT

TMPDIR=$(mktemp -d)
# 可自由使用 $TMPDIR —— 它会自动清理

# 捕获特定信号
trap 'echo "已中断"; exit 130' INT    # Ctrl+C
trap 'echo "已终止"; exit 143' TERM    # kill

错误处理模式

# 使用前检查命令是否存在
command -v jq >/dev/null 2>&1 || die "需要 jq 但未安装"

# 提供默认值
NAME="${NAME:-default_value}"

# 必需变量(未设置则失败)
: "${API_KEY:?错误:需要 API_KEY 环境变量}"

# 重试命令
retry() {
    local max_attempts=$1
    shift
    local attempt=1
    while [[ $attempt -le $max_attempts ]]; do
        "$@" && return 0
        log "第 $attempt/$max_attempts 次尝试失败。正在重试..."
        ((attempt++))
        sleep $((attempt * 2))
    done
    die "命令在 $max_attempts 次尝试后失败:$*"
}

retry 3 curl -sf https://api.example.com/health

参数解析

简单方法:位置参数 + 标志

# 手动解析(无依赖)
FORCE=false
DRY_RUN=false

while [[ $# -gt 0 ]]; do
    case "$1" in
        -f|--force) FORCE=true; shift ;;
        -n|--dry-run) DRY_RUN=true; shift ;;
        -o|--output)
            [[ -n "${2:-}" ]] || die "--output 需要一个值"
            OUTPUT="$2"; shift 2 ;;
        --output=*)
            OUTPUT="${1#*=}"; shift ;;
        -h|--help) usage; exit 0 ;;
        --) shift; break ;;  # 选项结束
        -*) die "未知选项:$1" ;;
        *) break ;;  # 位置参数开始
    esac
done

# 剩余参数为位置参数
FILES=("$@")
[[ ${#FILES[@]} -gt 0 ]] || die "至少需要一个文件"

getopts(仅限 POSIX,短选项)

while getopts ":o:vhf" opt; do
    case "$opt" in
        o) OUTPUT="$OPTARG" ;;
        v) VERBOSE=true ;;
        f) FORCE=true ;;
        h) usage; exit 0 ;;
        :) die "选项 -$OPTARG 需要一个参数" ;;
        ?) die "未知选项:-$OPTARG" ;;
    esac
done
shift $((OPTIND - 1))

临时文件和目录

# 创建临时文件(自动保证唯一性)
TMPFILE=$(mktemp)
echo "data" > "$TMPFILE"

# 创建临时目录
TMPDIR=$(mktemp -d)

# 使用自定义前缀/后缀创建临时文件
TMPFILE=$(mktemp /tmp/myapp.XXXXXX)
TMPFILE=$(mktemp --suffix=.json)  # 仅限 GNU

# 始终使用 trap 清理
trap 'rm -f "$TMPFILE"' EXIT

# 可移植模式(适用于 macOS 和 Linux)
TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'myapp')
trap 'rm -rf "$TMPDIR"' EXIT

并行执行

xargs -P

# 并行运行 4 个命令
cat urls.txt | xargs -P 4 -I {} curl -sO {}

# 并行处理文件(每次 4 个)
find . -name "*.csv" | xargs -P 4 -I {} ./process.sh {}

# 带进度指示器的并行处理
find . -name "*.jpg" | xargs -P 8 -I {} sh -c 'convert {} -resize 800x600 resized/{} && echo "完成:{}"'

后台作业 + wait

# 在后台运行任务,等待所有完成
pids=()
for file in data/*.csv; do
    process_file "$file" &
    pids+=($!)
done

# 等待所有任务并检查结果
failed=0
for pid in "${pids[@]}"; do
    wait "$pid" || ((failed++))
done
[[ $failed -eq 0 ]] || die "$failed 个作业失败"

GNU Parallel(如果可用)

# 使用 8 个并行作业处理文件
parallel -j 8 ./process.sh {} ::: data/*.csv

# 带进度条
parallel --bar -j 4 convert {} -resize 800x600 resized/{/} ::: *.jpg

# 管道输入行
cat urls.txt | parallel -j 10 curl -sO {}

进程管理

后台进程

# 在后台启动
long_running_command &
BG_PID=$!

# 检查是否仍在运行
kill -0 $BG_PID 2>/dev/null && echo "运行中" || echo "已停止"

# 等待它完成
wait $BG_PID
echo "退出码:$?"

# 脚本退出时终止
trap 'kill $BG_PID 2>/dev/null' EXIT

进程监控

# 运行命令,如果退出则重启
run_with_restart() {
    local cmd=("$@")
    while true; do
        "${cmd[@]}" &
        local pid=$!
        log "已启动 PID $pid"
        wait $pid
        local exit_code=$?
        log "进程退出,代码 $exit_code。5 秒后重启..."
        sleep 5
    done
}

run_with_restart ./my-server --port 8080

超时

# 30 秒后终止命令
timeout 30 long_running_command

# 使用自定义信号(SIGTERM 失败后发送 SIGKILL)
timeout --signal=TERM --kill-after=10 30 long_running_command

# 可移植方法(无 timeout 命令)
( sleep 30; kill $$ 2>/dev/null ) &
TIMER_PID=$!
long_running_command
kill $TIMER_PID 2>/dev/null

可移植性(Linux vs macOS)

常见差异

# sed:macOS 要求 -i ''(空备份扩展名)
# Linux:
sed -i 's/old/new/g' file.txt
# macOS:
sed -i '' 's/old/new/g' file.txt
# 可移植:
sed -i.bak 's/old/new/g' file.txt && rm file.txt.bak

# date:不同标志
# GNU(Linux):
date -d '2026-02-03' '+%s'
# BSD(macOS):
date -j -f '%Y-%m-%d' '2026-02-03' '+%s'

# readlink -f:macOS 上不存在
# 可移植替代方案:
real_path() { cd "$(dirname "$1")" && echo "$(pwd)/$(basename "$1")"; }

# stat:不同语法
# GNU:stat -c '%s' file
# BSD:stat -f '%z' file

# grep -P:macOS 默认不可用
# 使用 grep -E 替代,或安装 GNU grep

POSIX 安全模式

# 使用 printf 替代 echo -e(echo 行为不一致)
printf "第 1 行\n第 2 行\n"

# 使用 $() 替代反引号
result=$(command)   # 推荐
result=`command`    # 不推荐(已弃用,嵌套有问题)

# 测试时使用 [[ ]](bash)或 [ ](POSIX sh)
[[ -f "$file" ]]   # Bash(更安全,无单词分割)
[ -f "$file" ]     # POSIX sh

# 数组检查(仅限 bash,非 POSIX)
if [[ ${#array[@]} -gt 0 ]]; then
    echo "数组有元素"
fi

配置文件解析

直接加载配置文件

# 简单方法:加载 key=value 文件
# config.env:
# DB_HOST=localhost
# DB_PORT=5432

# 加载前验证(安全:检查命令)
if grep -qP '^[A-Z_]+=.*[;\`\$\(]' config.env; then
    die "配置文件包含不安全字符"
fi
source config.env

解析 INI 风格配置

# config.ini:
# [database]
# host = localhost
# port = 5432
# [app]
# debug = true

parse_ini() {
    local file="$1" section=""
    while IFS='= ' read -r key value; do
        [[ -z "$key" || "$key" =~ ^[#\;] ]] && continue
        if [[ "$key" =~ ^\[(.+)\]$ ]]; then
            section="${BASH_REMATCH[1]}"
            continue
        fi
        value="${value%%#*}"     # 去除行内注释
        value="${value%"${value##*[![:space:]]}"}"  # 去除尾部空白
        printf -v "${section}_${key}" '%s' "$value"
    done < "$file"
}

parse_ini config.ini
echo "$database_host"  # localhost
echo "$app_debug"      # true

实用模式

执行破坏性操作前确认

confirm() {
    local prompt="${1:-确定吗?}"
    read -rp "$prompt [y/N] " response
    [[ "$response" =~ ^[Yy]$ ]]
}

confirm "删除 /tmp/data 中的所有文件?" || die "已中止"
rm -rf /tmp/data/*

进度指示器

# 简单计数器
total=$(wc -l < file_list.txt)
count=0
while IFS= read -r file; do
    ((count++))
    printf "\r正在处理 %d/%d..." "$count" "$total" >&2
    process "$file"
done < file_list.txt
echo "" >&2

锁文件(防止并发运行)

LOCKFILE="/tmp/${SCRIPT_NAME}.lock"

acquire_lock() {
    if ! mkdir "$LOCKFILE" 2>/dev/null; then
        die "另一个实例正在运行(锁:$LOCKFILE)"
    fi
    trap 'rm -rf "$LOCKFILE"' EXIT
}

acquire_lock
# ... 安全继续,只有一个实例运行 ...

标准输入或文件参数

# 从文件参数或标准输入读取
input="${1:--}"   # 默认为 "-"(标准输入)
if [[ "$input" == "-" ]]; then
    cat
else
    cat "$input"
fi | while IFS= read -r line; do
    process "$line"
done

提示

  • 始终以 set -euo pipefail 开头。它能捕获 80% 的静默错误。
  • 始终使用 trap cleanup EXIT 处理临时文件。切勿依赖脚本末尾的清理代码。
  • 引用所有变量扩展:用 "$var" 而非 $var。未引用的变量会在空格和通配符处出错。
  • 在 bash 中使用 [[ ]] 而非 [ ]。它能更好地处理空字符串、空格和模式匹配。
  • shellcheck 是 Shell 脚本的最佳检查工具。运行:shellcheck myscript.sh。如果可用请安装它。
  • 对常量使用 readonly 防止意外覆盖:readonly DB_HOST="localhost"
  • 编写 usage() 函数,并在 -h/--help 或缺少必需参数时调用它。未来的用户(包括你自己)会感谢你。
  • 对于可能包含特殊字符或需要格式化的内容,优先使用 printf 而非 echo
  • 运行前使用 bash -n script.sh 测试脚本语法。
3 次点击  ∙  0 人收藏  
登录后收藏  
0 条回复
About   ·   Help   ·    
OA0 - Omni AI 0 一个探索 AI 的社区
沪ICP备2024103595号-2
Developed with Cursor