cloverleaf-larry/lib/hl7-diff-set.sh
Bryan Johnson 0046e88a8d v0.9.11: hl7-diff-set left/right identification header + cwd-default docs
Two Bryan UX asks on the batch route-set compare:
1. Identification header now labels which file is left vs right, with the
   REAL matched filenames:
     left  (baseline)  = <windows-prefix>.*
     right (candidate) = <linux-prefix>.*
   Printed to stdout on --format text; to stderr on --format tsv (keeps
   stdout pure tsv). No change to PASS/DIFF/MISSING/EXTRA classification or
   exit codes (0=clean, 1=any non-clean, 2=usage).
2. --dir already defaulted to cwd (DIR="." in v0.9.10) — this only fixes the
   docs (usage header, --help, MANUAL.md) to lead with the no---dir,
   cd-into-the-dir example.

Vera CODE-PASS (agent ade4360f): 0 defects; ran the scripts live against a
4-route fixture (PASS/DIFF/MISSING/EXTRA), verified header/stream-separation,
bash -n clean under /bin/bash 3.2.57, make-manifest --check clean, VERSION +
larry.sh agree at 0.9.11. Live-acceptance owner: Bryan (Gundersen box).
2026-07-23 12:21:56 -07:00

237 lines
9.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# hl7-diff-set.sh — run hl7-diff over a WHOLE SET of paired Windows-vs-Linux
# (or any baseline-vs-candidate) regression output files in one command,
# instead of hand-writing a for-loop with hardcoded route names.
#
# Built for hciroutetest's real on-disk layout (Bryan-direct, 2026-07-23):
# one directory holding paired files named <prefix>.<route>, where TWO
# prefixes (a baseline/"windows" set and a candidate/"linux" set) share a set
# of route suffixes after the last dot, e.g.:
# adt_in_regression_out_windows_single.adt_process_out
# adt_in_regression_out_linux_single.adt_process_out
# adt_in_regression_out_windows_single.lab_adt_out
# adt_in_regression_out_linux_single.lab_adt_out
# ...
#
# Usage:
# hl7-diff-set.sh <baseline-prefix> <candidate-prefix>
# [--dir PATH] # directory holding the paired files.
# # DEFAULTS TO THE CURRENT DIRECTORY (.) if
# # omitted -- --dir is an OVERRIDE, not
# # required. Run from inside the regression
# # directory and just pass the two prefixes.
# [--ignore "FIELD,..."] # forwarded to hl7-diff. Default: MSH.7
# [--include-fields "F,.."] # forwarded to hl7-diff (overrides --ignore for that set)
# [--framing auto|nl|0x1c|len10] # forwarded to hl7-diff. Default: auto
# [--format text|tsv] # how THIS command reports. Default: text
#
# # from inside the regression directory -- no --dir needed:
# cd /opt/cloverleaf/cis2025.01/integrator/adt/data/regression_testing/single
# hl7-diff-set.sh adt_in_regression_out_windows_single adt_in_regression_out_linux_single
#
# # or from anywhere, with an explicit --dir override:
# hl7-diff-set.sh adt_in_regression_out_windows_single adt_in_regression_out_linux_single \
# --dir /opt/cloverleaf/cis2025.01/integrator/adt/data/regression_testing/single
#
# The FIRST prefix argument is always LEFT/baseline; the SECOND is always
# RIGHT/candidate -- consistently, everywhere (the per-run header states this
# explicitly so you never have to remember or guess which is which; v0.9.11).
#
# What it does:
# 1. Globs "<dir>/<baseline-prefix>.*", derives each route suffix (the part
# after "<baseline-prefix>."), and pairs it with "<dir>/<candidate-prefix>.<suffix>".
# Also globs "<dir>/<candidate-prefix>.*" so a candidate-only route (no
# baseline file) is found too — nothing is silently skipped.
# 2. Runs the existing lib/hl7-diff.sh on every matched pair (this does NOT
# reimplement diffing — same engine, same --ignore/--framing semantics).
# 3. Reports PASS/DIFF/MISSING/EXTRA per route, plus a one-line summary.
#
# Per-route outcomes:
# PASS — both files present, 0 field differences (post-ignore)
# DIFF — both files present, N>0 field differences (--format text also
# shows the full hl7-diff --format text detail, indented)
# MISSING — baseline route has no matching candidate file (a dropped output
# IS a regression — this counts as a failure)
# EXTRA — candidate has a route the baseline does not (reported, also
# counts as non-clean — never silently skipped)
#
# --format text (default) prints a human report to stdout: per-route lines,
# DIFF detail indented beneath, then the summary line.
# --format tsv prints ONE machine row per route to stdout:
# route<TAB>status<TAB>diff_count
# (diff_count is "-" for MISSING/EXTRA, since no diff was actually run) —
# the left/right identification header and the summary line still print,
# to STDERR, so stdout stays clean tsv.
# Recommended for scripting: hl7-diff.sh's OWN v0.9.9 text-mode message-
# header mislabel is fixed as of this release (F-8), but tsv remains the
# safer machine-parseable choice regardless.
#
# Exit codes: 0 = every route PASS (all clean); 1 = at least one route is
# DIFF/MISSING/EXTRA; 2 = usage/input error (bad flag, --dir missing, or
# neither prefix matched any file at all).
set -o pipefail
NC_SELF="$0"
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
HL7DIFF="$LIB_DIR/hl7-diff.sh"
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
# shellcheck disable=SC1090,SC1091
. "$LIB_DIR/cygwin-safe.sh"
else
coerce_int() { local r="${1:-}" d="${2:-0}" c; c=$(printf '%s' "$r" | tr -cd '0-9'); printf '%s' "${c:-$d}"; }
fi
die() { printf 'hl7-diff-set: %s\n' "$*" >&2; exit 2; }
DIR="."
IGNORE="MSH.7"
INCLUDE=""
FRAMING="auto"
FORMAT="text"
BASE_PREFIX=""
CAND_PREFIX=""
while [ $# -gt 0 ]; do
case "$1" in
--dir) shift; DIR="$1" ;;
--ignore) shift; IGNORE="$1" ;;
--include-fields) shift; INCLUDE="$1" ;;
--framing) shift; FRAMING="$1" ;;
--format) shift; FORMAT="$1" ;;
-h|--help) sed -n '2,70p' "$NC_SELF"; exit 0 ;;
-*) die "unknown flag: $1" ;;
*)
if [ -z "$BASE_PREFIX" ]; then BASE_PREFIX="$1"
elif [ -z "$CAND_PREFIX" ]; then CAND_PREFIX="$1"
else die "extra arg: $1"
fi
;;
esac
shift
done
[ -x "$HL7DIFF" ] || [ -f "$HL7DIFF" ] || die "backing tool not found: $HL7DIFF"
[ -n "$BASE_PREFIX" ] && [ -n "$CAND_PREFIX" ] || die "usage: hl7-diff-set.sh <baseline-prefix> <candidate-prefix> [--dir PATH] [--ignore FIELDS] [--framing auto|nl|0x1c|len10] [--format text|tsv] (run --help for full usage)"
[ -d "$DIR" ] || die "no such --dir: $DIR"
case "$FORMAT" in text|tsv) ;; *) die "bad --format (want text|tsv)" ;; esac
case "$FRAMING" in auto|nl|0x1c|len10) ;; *) die "bad --framing (want auto|nl|0x1c|len10)" ;; esac
# --- discovery: glob both prefixes, derive route suffixes -------------------
# Portable (bash 3.2 / MobaXterm-safe) glob: a literal no-match pattern stays
# un-expanded, so every candidate is existence-checked with `[ -e ]` before
# use — no reliance on nullglob (bash-4-leaning / shopt-dependent).
_base_suffixes=()
for _f in "$DIR/$BASE_PREFIX".*; do
[ -e "$_f" ] || continue
_bn="$(basename "$_f")"
_base_suffixes+=("${_bn#"$BASE_PREFIX".}")
done
_cand_suffixes=()
for _f in "$DIR/$CAND_PREFIX".*; do
[ -e "$_f" ] || continue
_bn="$(basename "$_f")"
_cand_suffixes+=("${_bn#"$CAND_PREFIX".}")
done
if [ "${#_base_suffixes[@]}" -eq 0 ] && [ "${#_cand_suffixes[@]}" -eq 0 ]; then
die "no files matched '$BASE_PREFIX.*' or '$CAND_PREFIX.*' in $DIR"
fi
# Union of both suffix lists, sorted + de-duped, for a stable/deterministic
# route order regardless of filesystem glob order.
ROUTES="$(printf '%s\n' "${_base_suffixes[@]}" "${_cand_suffixes[@]}" | grep -v '^$' | sort -u)"
# --- run each pair ------------------------------------------------------------
diff_args=(--ignore "$IGNORE" --framing "$FRAMING")
[ -n "$INCLUDE" ] && diff_args+=(--include-fields "$INCLUDE")
n_routes=0 n_pass=0 n_diff=0 n_missing=0 n_extra=0
# v0.9.11 (Bryan UX ask): name which side is which, ONCE, up front — the
# per-diff-line `left=... right=...` labels (F-8, v0.9.10) tell you a value
# came from "left" or "right", but not WHICH FILE PREFIX left/right actually
# is. First positional arg is always left/baseline, second is always
# right/candidate -- consistently, everywhere -- so state it plainly here.
# text mode: part of the normal stdout report. tsv mode: routed to STDERR
# (same as the summary line) so stdout stays pure machine-parseable tsv rows.
if [ "$FORMAT" = "text" ]; then
printf 'hl7-diff-set: baseline=%s candidate=%s dir=%s\n' "$BASE_PREFIX" "$CAND_PREFIX" "$DIR"
printf ' left (baseline) = %s.*\n' "$BASE_PREFIX"
printf ' right (candidate) = %s.*\n' "$CAND_PREFIX"
printf ' ignore: %s framing: %s\n\n' "$IGNORE" "$FRAMING"
else
{
printf 'hl7-diff-set: baseline=%s candidate=%s dir=%s\n' "$BASE_PREFIX" "$CAND_PREFIX" "$DIR"
printf ' left (baseline) = %s.*\n' "$BASE_PREFIX"
printf ' right (candidate) = %s.*\n' "$CAND_PREFIX"
printf ' ignore: %s framing: %s\n' "$IGNORE" "$FRAMING"
} >&2
fi
while IFS= read -r route; do
[ -z "$route" ] && continue
n_routes=$((n_routes + 1))
bfile="$DIR/$BASE_PREFIX.$route"
cfile="$DIR/$CAND_PREFIX.$route"
if [ ! -f "$bfile" ]; then
n_extra=$((n_extra + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tEXTRA\t-\n' "$route"
else
printf ' %-24s EXTRA (candidate-only route — no baseline file: %s)\n' "$route" "$bfile"
fi
continue
fi
if [ ! -f "$cfile" ]; then
n_missing=$((n_missing + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tMISSING\t-\n' "$route"
else
printf ' %-24s MISSING (no candidate output — dropped route: %s)\n' "$route" "$cfile"
fi
continue
fi
count="$("$HL7DIFF" "${diff_args[@]}" --format count "$bfile" "$cfile" 2>&1)"
count="$(coerce_int "$count" 0)"
if [ "$count" -eq 0 ]; then
n_pass=$((n_pass + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tPASS\t0\n' "$route"
else
printf ' %-24s PASS\n' "$route"
fi
else
n_diff=$((n_diff + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tDIFF\t%s\n' "$route" "$count"
else
printf ' %-24s DIFF (%s diff(s))\n' "$route" "$count"
"$HL7DIFF" "${diff_args[@]}" --format text "$bfile" "$cfile" 2>&1 | sed 's/^/ /'
printf '\n'
fi
fi
done <<EOF_ROUTES
$ROUTES
EOF_ROUTES
# --- summary ------------------------------------------------------------------
_summary="$n_routes routes: $n_pass clean, $n_diff differs"
[ "$n_missing" -gt 0 ] && _summary="$_summary, $n_missing missing"
[ "$n_extra" -gt 0 ] && _summary="$_summary, $n_extra extra"
if [ "$FORMAT" = "tsv" ]; then
printf '%s\n' "$_summary" >&2
else
printf '%s\n' "$_summary"
fi
if [ "$n_diff" -eq 0 ] && [ "$n_missing" -eq 0 ] && [ "$n_extra" -eq 0 ] && [ "$n_routes" -gt 0 ]; then
exit 0
fi
exit 1