2. 用 Shell 脚本封装单步计算命令并统一参数入口

为什么单步计算也需要统一入口

单步科学计算程序往往通过位置参数或选项参数接收输入,例如一个用于计算一维扩散方程显式差分解的可执行程序 diff1d,它需要网格点数、时间步长、扩散系数、总步数和输出文件名。直接调用时,每次都要重复敲一长串参数,容易漏项、写错单位或把默认值记混。封装脚本的核心任务不是"再包一层",而是把参数入口固定下来:位置参数只保留最必要的输入,其余通过带默认值的选项提供,并在真正调用计算程序之前完成类型、范围和组合校验。

下面这个脚本 run_diff1d.sh 采用 getopts 解析选项,把 -n(网格点数)、-t(时间步长)、-d(扩散系数)、-s(总步数)、-o(输出文件)统一为可选参数,并给出稳定的默认值。校验失败时以非零码退出且不启动计算;校验通过时把完整参数、时间戳和退出码写入日志。

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

PROG="diff1d"
N=100
DT=0.001
DIFF=0.1
STEPS=500
OUT="diff1d_out.dat"
LOG="run_diff1d.log"

usage() {
  echo "用法: $0 [-n 网格点数] [-t 时间步长] [-d 扩散系数] [-s 总步数] [-o 输出文件]" >&2
}

while getopts ":n:t:d:s:o:h" opt; do
  case "$opt" in
    n) N="$OPTARG" ;;
    t) DT="$OPTARG" ;;
    d) DIFF="$OPTARG" ;;
    s) STEPS="$OPTARG" ;;
    o) OUT="$OPTARG" ;;
    h) usage; exit 0 ;;
    \?) echo "未知选项: -$OPTARG" >&2; usage; exit 2 ;;
    :)  echo "选项 -$OPTARG 缺少参数" >&2; usage; exit 2 ;;
  esac
done

is_pos_int() { [[ "$1" =~ ^[1-9][0-9]*$ ]]; }
is_pos_num() { [[ "$1" =~ ^[0-9]+([.][0-9]+)?$ ]] && awk "BEGIN{exit !($1>0)}"; }

fail=0
is_pos_int "$N"     || { echo "错误: -n 必须为正整数, 当前 '$N'" >&2; fail=1; }
is_pos_num "$DT"    || { echo "错误: -t 必须为正数, 当前 '$DT'" >&2; fail=1; }
is_pos_num "$DIFF"  || { echo "错误: -d 必须为正数, 当前 '$DIFF'" >&2; fail=1; }
is_pos_int "$STEPS" || { echo "错误: -s 必须为正整数, 当前 '$STEPS'" >&2; fail=1; }
[[ -n "$OUT" ]]     || { echo "错误: -o 不能为空" >&2; fail=1; }
[ "$fail" -eq 0 ] || exit 3

# 显式差分稳定性约束: DIFF*DT/(dx*dx) <= 0.5, dx 取单位长度
awk "BEGIN{exit !($DIFF*$DT <= 0.5)}" || {
  echo "错误: 稳定性条件不满足, 需 DIFF*DT <= 0.5" >&2; exit 4;
}

{
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始执行"
  echo "参数: N=$N DT=$DT DIFF=$DIFF STEPS=$STEPS OUT=$OUT"
} >> "$LOG"

if "$PROG" -n "$N" -t "$DT" -d "$DIFF" -s "$STEPS" -o "$OUT" >> "$LOG" 2>&1; then
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] 成功, 输出 $OUT" >> "$LOG"
else
  rc=$?
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] 失败, 退出码 $rc" >> "$LOG"
  exit "$rc"
fi

参数入口与校验的分层关系

下图把脚本内部的处理顺序画成一条带分支的参数处理链:左侧是用户可传入的五个选项,中间是默认值填充与逐项校验,右侧是稳定性约束和最终调用。注意校验失败与稳定性失败分别以不同退出码结束,便于在批处理中区分"参数写错"和"参数组合不可用"。

run_diff1d.sh 参数入口与校验链 命令行选项 -n 网格点数 -t 时间步长 -d 扩散系数 -s 总步数 -o 输出文件 未传入则使用默认值 N=100 DT=0.001 DIFF=0.1 STEPS=500 默认值填充 getopts 解析 未给选项取默认 未知选项报错 逐项类型校验 -n 正整数 -t 正数 -d 正数 -s 正整数 -o 非空 任一失败 → 退出码 3 不启动计算程序 稳定性约束 DIFF·DT ≤ 0.5 不满足 → 退出码 4 调用 diff1d -n -t -d -s -o stdout/stderr → 日志 记录时间戳与退出码 参数错误终止 exit 3 约束失败 exit 4 默认值 → 类型校验 → 稳定性约束 → 调用计算 → 日志记录;失败路径以不同退出码区分

验证统一入口是否真的可重复

封装完成后,用三组调用验证行为是否符合预期。第一组只给必要选项,检查默认值是否生效;第二组故意传入非法值,检查是否在启动计算前被拦截;第三组传入违反稳定性条件的组合,检查退出码是否为 4。以下命令假定 diff1d 已在 PATH 中,且脚本已加执行权限。

chmod +x run_diff1d.sh

# 1) 仅指定输出文件,其余走默认值
./run_diff1d.sh -o case_default.dat
echo "退出码: $?"

# 2) 非法网格点数,应在校验阶段被拦截
./run_diff1d.sh -n 0 -o case_bad.dat
echo "退出码: $?"

# 3) 稳定性条件不满足: 0.1 * 10 = 1.0 > 0.5
./run_diff1d.sh -d 0.1 -t 10 -o case_unstable.dat
echo "退出码: $?"

# 查看日志中记录的参数与结果
tail -n 5 run_diff1d.log

预期判读:第 1 组退出码为 0,日志中出现 N=100 DT=0.001 DIFF=0.1 STEPS=500 与"成功"记录;第 2 组退出码为 3,日志中没有"开始执行"之后的计算调用记录;第 3 组退出码为 4,同样不产生输出文件。若第 2 组仍生成了 case_bad.dat,说明校验被放在了调用之后,需要把校验块整体前移。

把默认值集中写在脚本顶部,而不是散落在 getopts 分支里,可以让"当前默认配置"一眼可见;后续若要把默认值改为从配置文件读取,也只需替换这一处。
使用 set -euo pipefail 时,awk 判断稳定性条件的写法要保证条件为假时返回非零码,否则 || 分支不会触发。示例中通过 exit !(条件) 让不满足约束时 awk 返回 1,从而进入错误分支。

可直接套用的参数入口检查清单

检查项本脚本中的对应实现通过判据
每个选项都有默认值顶部集中定义 N/DT/DIFF/STEPS/OUT只传 -o 也能完整运行
未知选项与缺参有明确提示getopts 的 \? 与 : 分支退出码 2,stderr 有用法提示
类型与范围校验先于调用is_pos_int / is_pos_num非法值退出码 3,无输出文件
物理/数值约束单独校验DIFF·DT ≤ 0.5违反时退出码 4
参数与结果可追溯日志记录时间戳、完整参数、退出码run_diff1d.log 可还原每次调用

完成以上封装后,同一计算任务只需记住一个入口和五个选项名,默认值、校验规则与日志格式都固定在脚本内部,后续接入批处理或调度系统时可以直接复用这个入口,而不必重新拼装命令行。