← 中文资源

简体中文版本

Bash getopts 与位置参数校验

解析 -n、-v、-o、-h 短选项,区分缺少参数与未知选项,并在 shift 后校验两个目录参数。

阅读英文原文

功能说明

publish 脚本支持干跑、详细输出、报告目录和帮助四个短选项,随后要求恰好两个位置参数:源目录与目标目录。解析完成后检查源目录存在、报告目录非空;干跑只打印计划,否则创建目录并用 find 逐文件复制,最后把汇总写入报告文件。帮助信息通过 heredoc 输出,未知选项与参数数量错误都返回状态码 2。

关键代码

optstring 以冒号开头,开启静默错误模式:缺少参数时 getopts 返回冒号并把选项名放入 OPTARG,未知选项返回问号,脚本据此输出自定义提示。循环结束后 shift “$((OPTIND - 1))” 移除已解析选项,剩余参数才是位置参数。find 使用 -print0 配合 read -d ‘’,避免文件名中的空格和换行破坏循环。相对路径通过 ${file#“$source_dir”/} 还原后再拼接目标路径。

使用注意

getopts 只支持短选项,长选项需要 getopt 或手工解析。源目录末尾带斜杠时,前缀截取可能留下多余斜杠,调用前应规范化。若目标目录位于源目录内部,逐文件遍历可能把正在复制的文件再次纳入,应该拒绝这种组合。示例未保留权限、时间戳和符号链接语义,cp 与 rsync 的行为不同,需按需求选择。报告 tee 失败会让整个脚本失败。

验证方式

准备一个包含若干文件的源目录,执行 bash publish.sh -n src dst 应只打印计划;去掉 -n 后目标目录出现相同文件,报告写入当前目录。执行 -o /tmp 可把报告放到 /tmp。传入不存在的选项返回 2,-o 后不跟参数返回 2,少给一个位置参数同样返回 2。用带空格的文件名验证复制不丢文件。

代码示例

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

usage() {
    cat <<'EOF'
Usage: publish.sh [-n] [-v] [-o DIR] <source-dir> <destination-dir>

  -n  dry run: print actions without copying
  -v  verbose: list every copied file
  -o  write a summary report into DIR (default: .)
  -h  show this help
EOF
}

dry_run=0
verbose=0
report_dir="."

while getopts ":nvo:h" opt; do
    case "$opt" in
        n) dry_run=1 ;;
        v) verbose=1 ;;
        o) report_dir="$OPTARG" ;;
        h) usage; exit 0 ;;
        :)
            printf 'option -%s requires an argument\n' "$OPTARG" >&2
            usage >&2
            exit 2
            ;;
        \?)
            printf 'unknown option -%s\n' "$OPTARG" >&2
            usage >&2
            exit 2
            ;;
    esac
done
shift "$((OPTIND - 1))"

if (( $# != 2 )); then
    printf 'expected 2 positional arguments, got %d\n' "$#" >&2
    usage >&2
    exit 2
fi

source_dir=$1
destination_dir=$2

if [[ ! -d "$source_dir" ]]; then
    printf 'source directory does not exist: %s\n' "$source_dir" >&2
    exit 1
fi

if [[ -z "$report_dir" ]]; then
    printf 'report directory must not be empty\n' >&2
    exit 2
fi

if (( dry_run )); then
    printf 'would copy %s -> %s\n' "$source_dir" "$destination_dir"
    printf 'would write report to %s\n' "$report_dir"
    exit 0
fi

mkdir -p -- "$destination_dir" "$report_dir"

copied=0
while IFS= read -r -d '' file; do
    relative=${file#"$source_dir"/}
    mkdir -p -- "$destination_dir/$(dirname -- "$relative")"
    cp -- "$file" "$destination_dir/$relative"
    copied=$((copied + 1))
    if (( verbose )); then
        printf 'copied %s\n' "$relative"
    fi
done < <(find "$source_dir" -type f -print0)

printf 'copied %d files to %s\n' "$copied" "$destination_dir" |
    tee -- "$report_dir/publish-report.txt"