- hl7-diff-set: compare a whole set of Windows-vs-Linux regression outputs in one command — takes two prefixes (+ optional --dir), auto-discovers routes by glob, runs hl7-diff per pair (--framing auto --ignore MSH.7, overridable), reports per-route PASS/DIFF/MISSING/EXTRA + summary, exit 0 iff all clean (MISSING and EXTRA both count as regressions). No more hand-written for-loop with hardcoded names. - F-8: hl7-diff --format text now shows SEG.FIELD left='...' right='...' with visible (empty)/<absent> tokens so a dropped component (e.g. Linux MSH.9.3 absent vs Windows ADT_A02) is obvious without opening the message files; --------- message N header now uses the real message index (was a stale counter). tsv/count formats unchanged. Consolidated by Clover; Vera CODE-PASS (Deliverables/2026-07-23-cloverleaf-larry-v0910-hl7-diff-set.md) — own-fixture verification of all route classifications + the absent-token render + F-7 no-hang. Live-acceptance = Bryan's regression run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
435 lines
19 KiB
Bash
Executable File
435 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# hl7-diff.sh — HL7-aware diff with field-level normalization.
|
|
# Compares two HL7 message files (or multi-message files), segment-by-segment,
|
|
# field-by-field. Lets you ignore fields that always change (default: MSH.7
|
|
# timestamp) without losing meaningful differences.
|
|
#
|
|
# Usage:
|
|
# hl7-diff.sh [--ignore "FIELD,FIELD,..."] [--include-fields "FIELD,..."]
|
|
# [--separator SEP] [--format text|tsv|count]
|
|
# [--framing auto|nl|0x1c|len10]
|
|
# <left_file> <right_file>
|
|
#
|
|
# Multi-message handling:
|
|
# Files may contain multiple HL7 messages framed one of three ways:
|
|
# - len10: a fixed 10-ASCII-digit byte-length prefix immediately followed
|
|
# by "MSH", with NO delimiter between messages (this is hciroutetest's
|
|
# DEFAULT output framing — e.g. adt_in_regression_out_windows.<route>_out
|
|
# files: "0000001161MSH|...<1161 bytes total, incl. this MSH>...0000000239MSH|...").
|
|
# Segments WITHIN a message are newline-separated; the boundary between
|
|
# messages is defined solely by the byte count, never by a delimiter —
|
|
# do NOT split len10 input on newlines or 0x1c.
|
|
# - the file separator byte 0x1c (the nc_msgs --format raw default), OR
|
|
# - blank lines between MSH-starting blocks (legacy nl-delimited).
|
|
# Default --framing auto detects len10 first (leading run of exactly 10
|
|
# ASCII digits immediately followed by "MSH"), then 0x1c, then falls back to
|
|
# the nl/MSH-block heuristic. Force a specific framing with --framing if a
|
|
# file's shape could be ambiguous (e.g. a len10 file whose first message
|
|
# happens to be exactly readable as 10 digits by coincidence — vanishingly
|
|
# unlikely in practice, but the flag exists for certainty).
|
|
#
|
|
# Defaults:
|
|
# --ignore MSH.7
|
|
#
|
|
# Output (text format):
|
|
# Per-message header (printed once, right before that message's FIRST
|
|
# differing line — a message with no diffs gets no header), then one line
|
|
# per differing field, BOTH sides labeled so you never have to guess which
|
|
# value came from which file:
|
|
# SEG.FIELD[.COMP[.SUB]] left='LEFT_VALUE' right='RIGHT_VALUE'
|
|
# An empty-but-present value renders as the literal token (empty); a
|
|
# component/subcomponent that does not exist AT ALL on that side (the other
|
|
# side has more components, e.g. left MSH-9=ADT^A02^ADT_A02 vs right
|
|
# MSH-9=ADT^A02 — right's 3rd component is not merely blank, it is absent)
|
|
# renders as <absent>, so "blank" and "not there" are never visually the
|
|
# same thing. (F-8, tsk-2026-07-23-hl7diff-readability, Bryan-direct
|
|
# live-feedback: previously the right-hand value printed as a genuinely
|
|
# blank/whitespace field, which looked like only ONE value was shown per
|
|
# diff line — the data was always there in the underlying lv/rv, it just
|
|
# rendered invisibly.)
|
|
#
|
|
# Exit codes: 0 identical (post-ignore), 1 differences found, 2 input error
|
|
set -o pipefail
|
|
|
|
NC_SELF="$0"
|
|
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
|
|
|
|
# v0.8.26: shared control-byte sanitizer. The diff echoes raw HL7 field values,
|
|
# which can carry C0 control bytes that corrupt a terminal when viewed
|
|
# un-redirected. The final awk (sole stdout producer) is piped through the
|
|
# tty-gated sanitizer; on a pipe/redirect it passes through raw. See
|
|
# lib/cygwin-safe.sh.
|
|
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
|
|
# shellcheck disable=SC1090,SC1091
|
|
. "$LIB_DIR/cygwin-safe.sh"
|
|
else
|
|
_sanitize_ctl_tty() { cat; } # degrade safe: raw passthrough if lib missing
|
|
fi
|
|
|
|
die() { printf 'hl7-diff: %s\n' "$*" >&2; exit 2; }
|
|
|
|
IGNORE="MSH.7"
|
|
INCLUDE=""
|
|
FORMAT="text"
|
|
FRAMING="auto"
|
|
LEFT=""
|
|
RIGHT=""
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--ignore) shift; IGNORE="$1" ;;
|
|
--include-fields) shift; INCLUDE="$1" ;;
|
|
--format) shift; FORMAT="$1" ;;
|
|
--framing) shift; FRAMING="$1" ;;
|
|
-h|--help) sed -n '2,33p' "$NC_SELF"; exit 0 ;;
|
|
-*) die "unknown flag: $1" ;;
|
|
*) if [ -z "$LEFT" ]; then LEFT="$1"
|
|
elif [ -z "$RIGHT" ]; then RIGHT="$1"
|
|
else die "extra arg: $1"; fi ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -f "$LEFT" ] || die "no such left file: $LEFT"
|
|
[ -f "$RIGHT" ] || die "no such right file: $RIGHT"
|
|
case "$FORMAT" in text|tsv|count) ;; *) die "bad --format" ;; esac
|
|
case "$FRAMING" in auto|nl|0x1c|len10) ;; *) die "bad --framing (want auto|nl|0x1c|len10)" ;; esac
|
|
|
|
# v0.9.10 (tsk-2026-07-23-len10, Bryan-blocking): len10 framing.
|
|
# hciroutetest's DEFAULT output framing is NOT newline- or 0x1c-delimited: each
|
|
# message is prefixed with a fixed 10-ASCII-digit byte-length, and messages are
|
|
# CONCATENATED WITH NO SEPARATOR — the next message's 10-digit prefix butts
|
|
# directly against the prior message's final byte. Segments WITHIN a message
|
|
# ARE newline-separated (converted to \r below, matching the internal
|
|
# convention the other two framings already use), but the message BOUNDARY is
|
|
# defined solely by the byte count. Splitting this on newlines or 0x1c would
|
|
# silently corrupt every message after the first.
|
|
#
|
|
# looks_like_len10: true if the first 13 bytes of a file are exactly 10 ASCII
|
|
# digits followed immediately by "MSH" (auto-detect signature).
|
|
looks_like_len10() {
|
|
local infile="$1" head13 digits msh
|
|
head13=$(LC_ALL=C head -c 13 -- "$infile" 2>/dev/null)
|
|
[ "${#head13}" -eq 13 ] || return 1
|
|
digits="${head13:0:10}"
|
|
msh="${head13:10:3}"
|
|
case "$digits" in ''|*[!0-9]*) return 1 ;; esac
|
|
[ "$msh" = "MSH" ]
|
|
}
|
|
|
|
# split_messages_len10: read 10 ASCII digits -> N (byte count) -> consume the
|
|
# next N bytes as one message -> repeat until EOF. Uses `tail -c +OFFSET |
|
|
# head -c LEN` (seek-based on regular files, not a byte-at-a-time `dd bs=1`
|
|
# loop) so it stays fast on large regression files, and stays portable to
|
|
# bash 3.2/AIX/Cygwin (no `read -N`, which is bash-4-only) per this codebase's
|
|
# existing cross-shell constraints (see lib/nc-engine.sh:53, lib/cygwin-safe.sh).
|
|
# LC_ALL=C throughout: byte-precise, immune to multi-byte locale mangling.
|
|
split_messages_len10() {
|
|
local infile="$1" outfile="$2"
|
|
local size offset digits len body_offset msg got
|
|
size=$(LC_ALL=C wc -c < "$infile" | tr -d ' ')
|
|
offset=1
|
|
: > "$outfile"
|
|
while [ "$offset" -le "$size" ]; do
|
|
digits=$(LC_ALL=C tail -c +"$offset" -- "$infile" | LC_ALL=C head -c 10)
|
|
[ "${#digits}" -eq 10 ] || {
|
|
# Fewer than 10 bytes left — trailing whitespace/EOF noise, not a new
|
|
# message. Warn only if it's non-blank (possible truncation), never die:
|
|
# a stray trailing newline is common and must not fail the whole diff.
|
|
case "$digits" in *[![:space:]]*) printf 'hl7-diff: len10: %d trailing byte(s) after last complete message in %s (ignored): %q\n' "${#digits}" "$infile" "$digits" >&2 ;; esac
|
|
break
|
|
}
|
|
case "$digits" in
|
|
*[!0-9]*) die "len10: bad length prefix '$digits' at byte offset $offset in $infile (not all-digit — file is not len10-framed, or a prior message miscounted; re-run with an explicit --framing to rule out a mis-detect)" ;;
|
|
esac
|
|
len=$((10#$digits))
|
|
body_offset=$((offset + 10))
|
|
msg=$(LC_ALL=C tail -c +"$body_offset" -- "$infile" | LC_ALL=C head -c "$len" | tr '\n' '\r')
|
|
got=$(LC_ALL=C tail -c +"$body_offset" -- "$infile" | LC_ALL=C head -c "$len" | LC_ALL=C wc -c | tr -d ' ')
|
|
[ "$got" -eq "$len" ] || printf 'hl7-diff: len10: message at offset %d claimed %d bytes but only %d were available in %s (truncated capture?)\n' "$offset" "$len" "$got" "$infile" >&2
|
|
printf '%s\x1e' "$msg" >> "$outfile"
|
|
offset=$((body_offset + len))
|
|
done
|
|
}
|
|
|
|
# Split a file into individual messages. Each MSH-block becomes one message.
|
|
# Output: one message per line, with \r preserved as 0x0d between segments,
|
|
# messages separated by \x1e (record sep). Returns a path.
|
|
split_messages() {
|
|
local infile="$1" outfile="$2" mode="$FRAMING"
|
|
if [ "$mode" = "auto" ]; then
|
|
if looks_like_len10 "$infile"; then mode="len10"
|
|
elif grep -q $'\x1c' "$infile" 2>/dev/null; then mode="0x1c"
|
|
else mode="nl"
|
|
fi
|
|
fi
|
|
case "$mode" in
|
|
len10)
|
|
split_messages_len10 "$infile" "$outfile"
|
|
;;
|
|
0x1c)
|
|
awk -v RS=$'\x1c' 'NF>0 || $0!="" {gsub(/\n$/,""); printf "%s\x1e", $0}' "$infile" > "$outfile"
|
|
;;
|
|
nl)
|
|
# Fallback: each `MSH|` starts a new message; everything until next MSH is one message
|
|
awk '
|
|
/^MSH\|/ {
|
|
if (msg != "") printf "%s\x1e", msg
|
|
msg = $0
|
|
next
|
|
}
|
|
{
|
|
if (msg != "") msg = msg "\r" $0
|
|
else msg = $0
|
|
}
|
|
END {
|
|
if (msg != "") printf "%s\x1e", msg
|
|
}
|
|
' "$infile" > "$outfile"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Build awk script that does the comparison. We feed it both message lists and
|
|
# the ignore/include lists.
|
|
TMP_L=$(mktemp); TMP_R=$(mktemp)
|
|
trap 'rm -f "$TMP_L" "$TMP_R"' EXIT
|
|
|
|
split_messages "$LEFT" "$TMP_L"
|
|
split_messages "$RIGHT" "$TMP_R"
|
|
|
|
# v0.8.26: pipe the comparison output through the tty-gated sanitizer, then
|
|
# propagate ${PIPESTATUS[0]} so awk's exit (0 identical, 1 differs) survives.
|
|
# SQ: a literal single-quote character, passed in via -v (OUTSIDE the
|
|
# single-quoted awk program text below) so the text-mode left/right quoting
|
|
# (F-8) never has to put a bare ' inside the awk source itself -- the whole
|
|
# program is one bash '...'-quoted string, and a stray ' in there would
|
|
# terminate it early and corrupt everything after (bash has no escape inside
|
|
# single quotes).
|
|
awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
|
|
-v LFILE="$LEFT" -v RFILE="$RIGHT" -v SQ="'" '
|
|
# F-7 fix (2026-07-23, tsk-2026-07-23-len10, Bryan-blocking, discovered while
|
|
# validating len10 self-tests -- PRE-EXISTING since v0.3.0 initial release
|
|
# e08f030, unrelated to the len10 change): n, i, ent, arr were used in this
|
|
# function body but were NOT declared in the local-variable parameter list,
|
|
# so they were true GLOBAL awk variables. i and n collide with the i/nm loop
|
|
# variables the top-level END block uses to walk L_MSGS/R_MSGS. Every time
|
|
# ignored() hit the early-return match branch (the NORMAL path -- e.g. a
|
|
# real MSH.7 timestamp diff hitting the default --ignore MSH.7 rule) it left
|
|
# global i at whatever value its OWN internal for-loop had reached, silently
|
|
# resetting the outer per-message loop index in the calling scope. Net
|
|
# effect: on ANY 2+-message file pair where an ignored field actually
|
|
# differs (this is hl7-diff single most common real-world use case -- this
|
|
# is exactly the Windows-vs-Linux MSH.7 regression scenario), the outer
|
|
# message loop got stuck re-comparing the same message forever -- a silent
|
|
# infinite loop / 100 percent CPU hang, not a crash, so it was never caught
|
|
# by a quick eyeball check. Confirmed via git blame still present at HEAD
|
|
# abc0d09 (v0.9.8); reproduced identically on both /usr/bin/awk (BSD /
|
|
# onetrueawk) and gawk 5.4.1, ruling out an implementation quirk -- this is
|
|
# a genuine missing-local bug in the shipped source, fixed by declaring the
|
|
# 4 missing locals below.
|
|
function ignored(seg, field, comp, subc, key, key2, n, i, ent, arr) {
|
|
if (INCLUDE != "") {
|
|
# Inclusion mode: only fields in INCLUDE are checked
|
|
key = seg "." field
|
|
if (comp != "") key = key "." comp
|
|
if (subc != "") key = key "." subc
|
|
key2 = seg "." field
|
|
# Match if exact or a prefix in include list
|
|
n = split(INCLUDE, arr, ",")
|
|
for (i=1; i<=n; i++) {
|
|
ent = arr[i]
|
|
if (ent == key || ent == key2) return 0
|
|
}
|
|
return 1
|
|
}
|
|
key = seg "." field
|
|
n = split(IGNORE, arr, ",")
|
|
for (i=1; i<=n; i++) {
|
|
ent = arr[i]
|
|
if (ent == key) return 1
|
|
if (ent == seg "." field "." comp) return 1
|
|
if (ent == seg "." field "." comp "." subc) return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function parse_msg(msg, out_segs, n, i, seg_name, raw_segs) {
|
|
delete out_segs
|
|
n = split(msg, raw_segs, "\r")
|
|
for (i=1; i<=n; i++) {
|
|
if (raw_segs[i] == "") continue
|
|
seg_name = substr(raw_segs[i], 1, 3)
|
|
out_segs[i] = seg_name "|" raw_segs[i] # prefix with name for easy lookup
|
|
}
|
|
return n
|
|
}
|
|
|
|
# F-8 fix (2026-07-23, tsk-2026-07-23-hl7diff-readability, Bryan-direct live
|
|
# feedback): lv_absent/rv_absent (new formal params, 6th/7th) tell emit()
|
|
# whether a side has NO such component/subcomponent at all (the other side
|
|
# has more components -- e.g. left MSH-9=ADT^A02^ADT_A02, right MSH-9=ADT^A02:
|
|
# component 3 on the right is ABSENT there, not merely blank) so text mode
|
|
# can render <absent> instead of a same-looking-as-blank (empty). Absence
|
|
# cascades down: if the whole field is absent, every component and
|
|
# subcomponent under it is absent too, regardless of its own split count.
|
|
function compare_field(seg, fidx, lv, rv, msg_idx, lv_absent, rv_absent, n_lc, n_rc, lc, rc, lcomp, rcomp, j, k, n_lsc, n_rsc, lsub, rsub, nm, diffs, ls, rs, ns, lc_absent, rc_absent, ls_absent, rs_absent) {
|
|
if (lv == rv) return 0
|
|
# Try component-level if both have ^
|
|
if (lv ~ /\^/ || rv ~ /\^/) {
|
|
n_lc = split(lv, lcomp, "^")
|
|
n_rc = split(rv, rcomp, "^")
|
|
nm = (n_lc > n_rc) ? n_lc : n_rc
|
|
diffs = 0
|
|
for (j=1; j<=nm; j++) {
|
|
lc = (j <= n_lc) ? lcomp[j] : ""
|
|
rc = (j <= n_rc) ? rcomp[j] : ""
|
|
lc_absent = lv_absent || (j > n_lc)
|
|
rc_absent = rv_absent || (j > n_rc)
|
|
if (lc == rc) continue
|
|
# subcomponent
|
|
if (lc ~ /&/ || rc ~ /&/) {
|
|
n_lsc = split(lc, lsub, "&")
|
|
n_rsc = split(rc, rsub, "&")
|
|
ns = (n_lsc > n_rsc) ? n_lsc : n_rsc
|
|
for (k=1; k<=ns; k++) {
|
|
ls = (k <= n_lsc) ? lsub[k] : ""
|
|
rs = (k <= n_rsc) ? rsub[k] : ""
|
|
ls_absent = lc_absent || (k > n_lsc)
|
|
rs_absent = rc_absent || (k > n_rsc)
|
|
if (ls != rs && !ignored(seg, fidx, j, k)) {
|
|
emit(msg_idx, seg "." fidx "." j "." k, ls, rs, ls_absent, rs_absent)
|
|
diffs++
|
|
}
|
|
}
|
|
} else {
|
|
if (!ignored(seg, fidx, j, "")) {
|
|
emit(msg_idx, seg "." fidx "." j, lc, rc, lc_absent, rc_absent)
|
|
diffs++
|
|
}
|
|
}
|
|
}
|
|
return diffs
|
|
}
|
|
if (!ignored(seg, fidx, "", "")) {
|
|
emit(msg_idx, seg "." fidx, lv, rv, lv_absent, rv_absent)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
# F-8 fix: text mode now (a) labels both sides, left=... right=..., so the
|
|
# reader never has to guess which value belongs to which file -- the data
|
|
# was always present in lv/rv, it just rendered invisibly when the
|
|
# right-hand value happened to be empty; (b) renders an empty-but-present
|
|
# value as the literal token (empty) and a component/subcomponent that does
|
|
# not exist AT ALL on that side as <absent> (see compare_field), so blank
|
|
# and not-there are never visually indistinguishable; (c) prints the
|
|
# "----- message N -----" header lazily, right before the first emitted
|
|
# line of a message, keyed off the msg_idx actually passed in -- this
|
|
# replaces the old preemptive header print in END (which used the STALE
|
|
# global DIFF_COUNT to decide whether to print a header for the message
|
|
# about to be processed, so a clean message 1 followed by a differing
|
|
# message 2 got the message-2 diffs mislabeled under a "----- message 1
|
|
# -----" header, or no header at all). tsv/count output is untouched
|
|
# (still raw lv/rv). SQ is the single-quote character passed in via -v.
|
|
function emit(msg_idx, path, lv, rv, lv_absent, rv_absent, lv_disp, rv_disp) {
|
|
# count mode: accumulate only — no per-diff output (final print is in END).
|
|
if (FMT == "tsv") {
|
|
printf "%d\t%s\t%s\t%s\n", msg_idx, path, lv, rv
|
|
} else if (FMT == "text") {
|
|
if (msg_idx != LAST_HDR_MSG) {
|
|
printf "----- message %d -----\n", msg_idx
|
|
LAST_HDR_MSG = msg_idx
|
|
}
|
|
lv_disp = (lv_absent == 1) ? "<absent>" : ((lv == "") ? "(empty)" : lv)
|
|
rv_disp = (rv_absent == 1) ? "<absent>" : ((rv == "") ? "(empty)" : rv)
|
|
printf " %-20s left=%-24s right=%s\n", path, SQ lv_disp SQ, SQ rv_disp SQ
|
|
}
|
|
DIFF_COUNT++
|
|
}
|
|
|
|
function diff_segment(seg_name, lseg, rseg, msg_idx, lf, rf, nl, nr, i, base, nmax, lv, rv, field_num) {
|
|
nl = split(lseg, lf, "|")
|
|
nr = split(rseg, rf, "|")
|
|
nmax = (nl > nr) ? nl : nr
|
|
# MSH field numbering offset
|
|
base = (seg_name == "MSH") ? 0 : 1
|
|
# MSH.1 is the field separator (always "|"); MSH.2 is encoding chars.
|
|
# For seg=MSH, index i in array → MSH.<i> for i>=2 corresponds to lf[i]
|
|
# For other seg, index i (i>=2) corresponds to SEG.(i-1).
|
|
for (i=2; i<=nmax; i++) {
|
|
lv = (i <= nl) ? lf[i] : ""
|
|
rv = (i <= nr) ? rf[i] : ""
|
|
field_num = (seg_name == "MSH") ? i : (i - 1)
|
|
# F-8: field itself absent on a side (that side segment does not have
|
|
# this many pipe-delimited fields) cascades to <absent> for every
|
|
# component/subcomponent under it -- see compare_field.
|
|
compare_field(seg_name, field_num, lv, rv, msg_idx, (i > nl), (i > nr))
|
|
}
|
|
}
|
|
|
|
function diff_message(left_msg, right_msg, msg_idx, l_segs, r_segs, ln, rn, i, l_name, r_name, l, r, mx) {
|
|
ln = parse_msg(left_msg, l_segs)
|
|
rn = parse_msg(right_msg, r_segs)
|
|
mx = (ln > rn) ? ln : rn
|
|
for (i=1; i<=mx; i++) {
|
|
l = (i <= ln) ? l_segs[i] : ""
|
|
r = (i <= rn) ? r_segs[i] : ""
|
|
l_name = (l != "") ? substr(l, 1, 3) : "(none)"
|
|
r_name = (r != "") ? substr(r, 1, 3) : "(none)"
|
|
if (l_name != r_name) {
|
|
if (l == "" && r == "") continue # both ends padded — not a real diff
|
|
emit(msg_idx, "SEGMENT_ORDER", l_name, r_name)
|
|
continue
|
|
}
|
|
if (l_name == "(none)") continue
|
|
# strip "NAME|" prefix we added in parse_msg
|
|
sub(/^[A-Z0-9]+\|/, "", l)
|
|
sub(/^[A-Z0-9]+\|/, "", r)
|
|
diff_segment(l_name, l, r, msg_idx)
|
|
}
|
|
}
|
|
|
|
# F-8: LAST_HDR_MSG tracks which message index the text-mode "----- message
|
|
# N -----" header was last printed for, so emit() can print it lazily (and
|
|
# correctly) right before the first line of that message instead of the
|
|
# old preemptive/stale-DIFF_COUNT guess in the loop below.
|
|
BEGIN { DIFF_COUNT = 0; n_l = 0; n_r = 0; LAST_HDR_MSG = 0 }
|
|
|
|
FNR == NR { L_MSGS[++n_l] = $0; next }
|
|
{ R_MSGS[++n_r] = $0 }
|
|
|
|
END {
|
|
# F-1 fix (2026-06-08): the early-exit `if (FMT=="count")` that used to sit
|
|
# here fired BEFORE the diff loop ran, so DIFF_COUNT was always 0. The loop
|
|
# is now unconditional; count output is emitted AFTER the loop at the bottom.
|
|
nm = (n_l > n_r) ? n_l : n_r
|
|
if (FMT == "text") {
|
|
printf "HL7 diff:\n left: %s (%d messages)\n right: %s (%d messages)\n ignore: %s\n", LFILE, n_l, RFILE, n_r, IGNORE
|
|
if (INCLUDE != "") printf " include-only: %s\n", INCLUDE
|
|
printf "\n"
|
|
}
|
|
if (n_l != n_r) {
|
|
if (FMT == "tsv") printf "0\tMESSAGE_COUNT\t%d\t%d\n", n_l, n_r
|
|
else if (FMT != "count") printf " MESSAGE COUNT mismatch: %d vs %d\n", n_l, n_r
|
|
DIFF_COUNT++
|
|
}
|
|
for (i=1; i<=nm; i++) {
|
|
lm = (i <= n_l) ? L_MSGS[i] : ""
|
|
rm = (i <= n_r) ? R_MSGS[i] : ""
|
|
if (lm == "" || rm == "") {
|
|
emit(i, "MESSAGE_PRESENCE", (lm != "" ? "present" : "missing"), (rm != "" ? "present" : "missing"))
|
|
continue
|
|
}
|
|
# F-8: header printing moved into emit() (lazy, keyed by the real
|
|
# msg_idx) -- no preemptive print here anymore. See emit()/LAST_HDR_MSG.
|
|
diff_message(lm, rm, i)
|
|
}
|
|
if (FMT == "count") { print DIFF_COUNT; exit (DIFF_COUNT > 0 ? 1 : 0) }
|
|
if (FMT == "text") printf "\n%d total field difference(s)\n", DIFF_COUNT
|
|
exit (DIFF_COUNT > 0 ? 1 : 0)
|
|
}
|
|
' RS=$'\x1e' "$TMP_L" "$TMP_R" | _sanitize_ctl_tty
|
|
exit "${PIPESTATUS[0]}"
|