From 8ffdeb4f5dfbc1937d2536109413aaf46afb80ac Mon Sep 17 00:00:00 2001 From: Bryan Johnson Date: Tue, 26 May 2026 10:35:46 -0700 Subject: [PATCH] v0.3.4: field-name aliases, dot/dash syntax, ops (=, !=, ~, !~), new formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Field path improvements (hl7-field.sh + every tool that uses it): - Accept both `.` and `-` as separators: PID.3 == PID-3 PV1.3.4 == PV1-3.4 == PV1-3-4 == PV1.3-4 - Field-name aliases (case-insensitive): mrn → PID.3 account / account_number → PID.18 name / patient_name → PID.5 dob / birthdate → PID.7 ssn → PID.19 visit / encounter / csn → PV1.19 attending → PV1.7 event → MSH.9.2 control_id / msgid → MSH.10 ...and ~40 more covering MSH/PID/PV1/EVN/NK1/GT1/IN1/OBR/OBX/DG1/ORC - Aliases also accept component/subcomponent suffixes: name.2 → PID.5.2 mrn.1 → PID.3.1 Filter operators (nc-msgs.sh --field): PATH=VALUE exact equality PATH!=VALUE not equal PATH~VALUE contains (case-insensitive) PATH!~VALUE does not contain (case-insensitive) PATH=NULL /= null / empty / absent PATH!=NULL present (any non-empty rep) PATH=* wildcard — any non-empty value Multiple --field flags AND; for OR, run two queries. New output formats for nc-msgs.sh: text (default) segments per line + metadata header per message oneline one message per line, segments joined with a ⏎ marker fields each non-empty field on its own line: "SEG.N: value" mp alias for fields (matches v1 `mp` semantic) labeled fields with friendly aliases: "MSH.9 (msg_type): ADT^A08" raw, json, count — unchanged MANUAL.md updated with the full operator + format reference. Co-Authored-By: Claude Opus 4.7 --- MANUAL.md | 42 ++++++++-- VERSION | 2 +- larry.sh | 2 +- lib/hl7-field.sh | 93 ++++++++++++++++++++- lib/nc-msgs.sh | 206 +++++++++++++++++++++++++++++++++++++++++------ 5 files changed, 312 insertions(+), 33 deletions(-) diff --git a/MANUAL.md b/MANUAL.md index 960d7bb..bbdde9b 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -155,18 +155,50 @@ Smat databases are **SQLite 3**. Reads via native `sqlite3 -ascii` — no Clover # Count messages in a thread's smat lib/nc-msgs.sh ADTto_3m --format count -# Recent 5 messages (text format with metadata + parsed segments) +# Recent 5 messages (text format = segments per line, with metadata header) lib/nc-msgs.sh ADTto_3m --limit 5 --format text +# OUTPUT FORMATS: +# text (default) segments per line + metadata header per message +# oneline one message per line; segments separated by ⏎ marker +# fields each non-empty field on its own line: "SEG.N: value" +# mp alias for fields (v1 `mp`-style) +# labeled fields with alias names where known: "MSH.9 (msg_type): ADT^A08" +# raw raw bytes; messages separated by 0x1c (FS) — for piping +# json structured JSON +# count just the count +lib/nc-msgs.sh ADTto_3m --limit 3 --format oneline +lib/nc-msgs.sh ADTto_3m --limit 1 --format fields +lib/nc-msgs.sh ADTto_3m --limit 1 --format labeled # adds friendly aliases + # Time range — supports human expressions lib/nc-msgs.sh ADTto_3m --after "3 days ago" --format count lib/nc-msgs.sh ADTto_3m --after "2026-05-20" --before "2026-05-26 12:00:00" -# Filter by HL7 field — find messages where PID.3 equals an MRN -lib/nc-msgs.sh ADTto_3m --field PID.3=5720501458 --limit 20 --format text +# Filter operators (paths accept either . or - separators; same field name aliases everywhere): +# PATH=VALUE exact equality (any repetition) +# PATH!=VALUE not equal (no repetition matches) +# PATH~VALUE contains, case-insensitive +# PATH!~VALUE does not contain, case-insensitive +# PATH=NULL empty / absent / "" — any of those +# PATH= same as =NULL +# PATH=* wildcard — any non-empty value +# PATH!=NULL present (any non-empty repetition) +# Multiple --field flags AND together. For OR, run two queries. -# Account-number search at PID.18 -lib/nc-msgs.sh ADTto_3m --field PID.18=623000286 --format text +# Examples using field-name ALIASES (case-insensitive; auto-translates to SEG.N) +lib/nc-msgs.sh ADTto_3m --field 'mrn=5720501458' --format count +lib/nc-msgs.sh ADTto_3m --field 'account_number=623000286' --format text +lib/nc-msgs.sh ADTto_3m --field 'event=A08' --format count +lib/nc-msgs.sh ADTto_3m --field 'visit=*' --format count # any non-empty +lib/nc-msgs.sh ADTto_3m --field 'ssn=NULL' --format count # missing SSN +lib/nc-msgs.sh ADTto_3m --field 'name~smith' --format text # contains +lib/nc-msgs.sh ADTto_3m --field 'name!~test' --format count # production-looking +lib/nc-msgs.sh ADTto_3m --field 'event=A08' --field 'visit=*' # AND of both +# Component access: name.2 (PID.5 component 2 = given name) +lib/nc-msgs.sh ADTto_3m --field 'name.2=SALLY' --format count +# Dash syntax (cheat-sheet style): +lib/nc-msgs.sh ADTto_3m --field 'PV1-3.4=100200' --format count # JSON output for piping lib/nc-msgs.sh ADTto_3m --field PID.3=5720501458 --format json | jq diff --git a/VERSION b/VERSION index 1c09c74..42045ac 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.3 +0.3.4 diff --git a/larry.sh b/larry.sh index 90ec751..50cbda8 100755 --- a/larry.sh +++ b/larry.sh @@ -32,7 +32,7 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.3.3" +LARRY_VERSION="0.3.4" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/larry.sh}" LARRY_AGENTS_URL="${LARRY_AGENTS_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/agents}" diff --git a/lib/hl7-field.sh b/lib/hl7-field.sh index 59d95be..8e1f0db 100755 --- a/lib/hl7-field.sh +++ b/lib/hl7-field.sh @@ -2,10 +2,13 @@ # hl7-field.sh — extract a specific field from an HL7 v2 message. Native v3. # # Field path: SEG[.FIELD[.COMPONENT[.SUBCOMPONENT]]] +# Both `.` and `-` are accepted as separators (cheat-sheet flexibility): # PID — return the whole PID segment # PID.3 — return PID field 3 +# PID-3 — same as PID.3 # PID.3.1 — return PID field 3, component 1 -# PID.3.1.1 — return PID field 3, component 1, subcomponent 1 +# PID-3.1 — same as PID.3.1 +# PV1-3-4 — PV1 segment, field 3, component 4 # MSH.10 — special: MSH numbering accounts for the encoding chars # (MSH.1 = field separator char, MSH.2 = encoding chars, # MSH.3+ = subsequent fields). @@ -37,7 +40,93 @@ fi [ -n "$MSG" ] || { echo "hl7-field: empty message" >&2; exit 3; } # Parse path: SEG, optional .FIELD, .COMPONENT, .SUBCOMPONENT -IFS='.' read -r SEG FNUM CNUM SCNUM <<< "$PATH_SPEC" +# Accept both `.` and `-` as separators (PID.3.1 == PID-3.1 == PID-3-1 == PID.3-1). +# Normalize first separator-after-segment to `.` then split. +NORMALIZED=$(printf '%s' "$PATH_SPEC" | sed 's/[.\-]/./g') + +# Resolve common HL7 field-name aliases. The first token may be an alias like +# MRN, NAME, ACCOUNT, VISIT, etc. — translate to its SEG.FIELD form, preserving +# any component/subcomponent suffix the user passed. +# MRN → PID.3 +# NAME.2 → PID.5.2 +# account_no.1 → PID.18.1 +resolve_hl7_alias() { + local norm; norm=$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]' | tr ' ' '_') + case "$norm" in + MRN|PATIENT_ID|PT_ID|PATIENTID) echo "PID.3" ;; + ALT_ID|ALTID|ALT_PATIENT_ID|ALT_PT_ID) echo "PID.4" ;; + NAME|PATIENT_NAME|PT_NAME) echo "PID.5" ;; + MAIDEN|MOTHER_MAIDEN) echo "PID.6" ;; + DOB|BIRTHDATE|BIRTH_DATE|BIRTHDAY) echo "PID.7" ;; + SEX|GENDER) echo "PID.8" ;; + ALIAS) echo "PID.9" ;; + ADDR|ADDRESS|PT_ADDRESS) echo "PID.11" ;; + PHONE|HOME_PHONE) echo "PID.13" ;; + WORK_PHONE|BUSINESS_PHONE) echo "PID.14" ;; + ACCT|ACCOUNT|ACCOUNT_NUMBER|ACCOUNTNUM|ACCT_NUM|ACCOUNT_NO) echo "PID.18" ;; + SSN) echo "PID.19" ;; + LIC|LICENSE|DRIVER_LICENSE|DL) echo "PID.20" ;; + DOD|DEATH_DATE|DATE_OF_DEATH) echo "PID.29" ;; + PATIENT_CLASS|PT_CLASS) echo "PV1.2" ;; + LOCATION|ASSIGNED_LOCATION|PT_LOCATION|BED_LOCATION) echo "PV1.3" ;; + ATTENDING|ATTENDING_DR|ATTENDING_DOCTOR|ATTENDING_PROVIDER) echo "PV1.7" ;; + REFERRING|REFERRING_DR|REFERRING_DOCTOR|REFERRING_PROVIDER) echo "PV1.8" ;; + CONSULTING|CONSULTING_DR|CONSULTING_DOCTOR) echo "PV1.9" ;; + ADMITTING|ADMITTING_DR|ADMITTING_DOCTOR|ADMITTING_PROVIDER) echo "PV1.17" ;; + PT_TYPE|PATIENT_TYPE) echo "PV1.18" ;; + VISIT|VISIT_NUMBER|VISIT_NO|ENCOUNTER|CSN|ENC|ENC_NUM) echo "PV1.19" ;; + ALT_VISIT|ALT_VISIT_ID) echo "PV1.50" ;; + EVENT_DT|EVN_DATE) echo "EVN.2" ;; + REASON_FOR_EVENT|EVN_REASON) echo "EVN.4" ;; + OPERATOR|EVN_OPERATOR|RESPONSIBLE_OPERATOR) echo "EVN.5" ;; + CONTROL_ID|MSG_CONTROL_ID|MSG_CTL_ID|CTLID|MSGID|MESSAGE_ID) echo "MSH.10" ;; + MSG_TYPE|MESSAGE_TYPE) echo "MSH.9" ;; + EVENT|EVENT_CODE|TRIGGER_EVENT|TRIGGER) echo "MSH.9.2" ;; + TIMESTAMP|MSG_TIMESTAMP|MSG_TIME|SENT_TIME) echo "MSH.7" ;; + SENDING_APP|SENDING_APPLICATION) echo "MSH.3" ;; + SENDING_FACILITY|SENDING_FAC) echo "MSH.4" ;; + RECEIVING_APP|RECEIVING_APPLICATION) echo "MSH.5" ;; + RECEIVING_FACILITY|RECEIVING_FAC) echo "MSH.6" ;; + PROCESSING_ID|PROC_ID) echo "MSH.11" ;; + VERSION|HL7_VERSION|VER) echo "MSH.12" ;; + NK|NK_NAME|NEXT_OF_KIN) echo "NK1.2" ;; + NK_RELATIONSHIP|RELATIONSHIP) echo "NK1.3" ;; + NK_ADDRESS) echo "NK1.4" ;; + NK_PHONE) echo "NK1.5" ;; + GUARANTOR|GUARANTOR_NAME|GT_NAME) echo "GT1.4" ;; + GUARANTOR_ADDRESS|GT_ADDRESS) echo "GT1.5" ;; + GUARANTOR_PHONE|GT_PHONE) echo "GT1.6" ;; + GUARANTOR_SSN|GT_SSN) echo "GT1.12" ;; + INSURANCE|INSURANCE_PLAN|INS_PLAN) echo "IN1.2" ;; + INSURED_NAME|POLICY_HOLDER|INSURED) echo "IN1.16" ;; + INSURED_DOB) echo "IN1.17" ;; + POLICY|POLICY_NUMBER|INS_POLICY) echo "IN1.36" ;; + DIAGNOSIS|DX|DIAGNOSIS_CODE) echo "DG1.3" ;; + DIAGNOSIS_DESC|DX_DESC|DIAGNOSIS_DESCRIPTION) echo "DG1.4" ;; + PLACER|PLACER_ORDER|ORDER_NUMBER|ORDER_NO) echo "OBR.2" ;; + FILLER|FILLER_ORDER) echo "OBR.3" ;; + TEST_CODE|UNIVERSAL_SERVICE_ID|SERVICE_CODE) echo "OBR.4" ;; + SPECIMEN|SPECIMEN_SOURCE) echo "OBR.15" ;; + ORDERING|ORDERING_PROVIDER|ORDERING_DR) echo "OBR.16" ;; + OBS_VALUE|RESULT_VALUE|OBX_VALUE) echo "OBX.5" ;; + OBS_STATUS|RESULT_STATUS|OBX_STATUS) echo "OBX.11" ;; + *) echo "" ;; + esac +} + +# Split the normalized path. If the first token is an alias, replace it. +IFS='.' read -ra _parts <<< "$NORMALIZED" +_first="${_parts[0]:-}" +_aliased=$(resolve_hl7_alias "$_first") +if [ -n "$_aliased" ]; then + # Replace first token with alias expansion; keep remaining components + if [ ${#_parts[@]} -gt 1 ]; then + NORMALIZED="${_aliased}.$(IFS=. ; echo "${_parts[*]:1}")" + else + NORMALIZED="$_aliased" + fi +fi +IFS='.' read -r SEG FNUM CNUM SCNUM <<< "$NORMALIZED" [ -n "$SEG" ] || { echo "hl7-field: bad path: $PATH_SPEC" >&2; exit 2; } # Detect encoding characters from MSH diff --git a/lib/nc-msgs.sh b/lib/nc-msgs.sh index 8f8d465..157bde9 100755 --- a/lib/nc-msgs.sh +++ b/lib/nc-msgs.sh @@ -67,7 +67,7 @@ while [ $# -gt 0 ]; do done [ -n "$THREAD" ] || die "usage: nc-msgs.sh [...flags]" -case "$FORMAT" in text|json|count|raw) ;; *) die "bad --format: $FORMAT" ;; esac +case "$FORMAT" in text|json|count|raw|oneline|fields|mp|labeled) ;; *) die "bad --format: $FORMAT" ;; esac command -v sqlite3 >/dev/null 2>&1 || die "sqlite3 not on PATH (universally available on Cloverleaf hosts; install via your distro otherwise)" # Locate smatdb @@ -144,14 +144,22 @@ if [ -n "$TYPE" ]; then WHERE="$WHERE AND Type = '$ESC_TYPE'" fi -# Coarse LIKE pre-filter for any --field VALUEs (substring presence) -# This is just an SQL fast-path; the precise field match happens via hl7-field.sh below. +# Coarse LIKE pre-filter — only safe for positive exact/contains operators. +# Negation ops, null/wildcard, and absences require post-filter. +prefilter_op_path=""; prefilter_op_kind=""; prefilter_op_val="" for filt in "${FILTERS[@]}"; do - val="${filt#*=}" - if [ -n "$val" ] && [ "$val" != "$filt" ]; then - ESC_VAL=$(printf '%s' "$val" | sed "s/'/''/g") - WHERE="$WHERE AND MessageContent LIKE '%${ESC_VAL}%'" - fi + # Skip negation, null, wildcard — those need every message to be inspected + if [[ "$filt" == *"!="* ]] || [[ "$filt" == *"!~"* ]]; then continue; fi + # Extract value portion regardless of op + if [[ "$filt" == *"~"* ]] && [[ "$filt" != *"!~"* ]]; then + val="${filt#*~}" + elif [[ "$filt" == *"="* ]]; then + val="${filt#*=}" + else continue; fi + norm_val=$(printf '%s' "$val" | tr '[:upper:]' '[:lower:]') + if [ "$norm_val" = "null" ] || [ -z "$val" ] || [ "$val" = "*" ]; then continue; fi + ESC_VAL=$(printf '%s' "$val" | sed "s/'/''/g") + WHERE="$WHERE AND MessageContent LIKE '%${ESC_VAL}%'" done SMATDB=$(locate_smatdb) @@ -184,22 +192,100 @@ awk -v RS=$'\x1e' -v FS=$'\x1f' -v outdir="$TMP_OUT" ' MSG_COUNT=$(ls "$TMP_OUT"/msg_*.bin 2>/dev/null | wc -l | tr -d ' ') KEPT=0 -# Apply --field filters precisely via hl7-field.sh +# Parse a single filter expression: returns path / op / expected via globals. +# Supported operators (longest-first match): +# !~ does not contain (case-insensitive substring) +# != not equal +# ~ contains (case-insensitive substring) +# = exact equal (or NULL keyword, empty, or * wildcard — see match_filters) +parse_filter() { + local filt="$1" + FP_OP=""; FP_PATH=""; FP_EXPECTED="" + if [[ "$filt" == *"!~"* ]]; then + FP_PATH="${filt%%!~*}"; FP_EXPECTED="${filt#*!~}"; FP_OP="!~" + elif [[ "$filt" == *"!="* ]]; then + FP_PATH="${filt%%!=*}"; FP_EXPECTED="${filt#*!=}"; FP_OP="!=" + elif [[ "$filt" == *"~"* ]]; then + FP_PATH="${filt%%~*}"; FP_EXPECTED="${filt#*~}"; FP_OP="~" + elif [[ "$filt" == *"="* ]]; then + FP_PATH="${filt%%=*}"; FP_EXPECTED="${filt#*=}"; FP_OP="=" + fi +} + +# Returns 0 if the (op, expected) check matches the actual field value(s). +field_matches() { + local actual="$1" op="$2" expected="$3" + local expected_lc; expected_lc=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]') + local actual_lc; actual_lc=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]') + local is_null=0 + if [ -z "$expected" ] || [ "$expected_lc" = "null" ]; then is_null=1; fi + + case "$op" in + "=") + if [ "$is_null" = "1" ]; then + # Null match: actual empty or all-empty reps + [ -z "$actual" ] && return 0 + while IFS= read -r rep; do + [ -n "$rep" ] && [ "$rep" != "\"\"" ] && return 1 + done <<< "$actual" + return 0 + elif [ "$expected" = "*" ]; then + # Wildcard — any non-empty rep + while IFS= read -r rep; do + [ -n "$rep" ] && [ "$rep" != "\"\"" ] && return 0 + done <<< "$actual" + return 1 + fi + while IFS= read -r rep; do + [ "$rep" = "$expected" ] && return 0 + done <<< "$actual" + return 1 + ;; + "!=") + if [ "$is_null" = "1" ]; then + # Not-null: at least one rep is non-empty + while IFS= read -r rep; do + [ -n "$rep" ] && [ "$rep" != "\"\"" ] && return 0 + done <<< "$actual" + return 1 + elif [ "$expected" = "*" ]; then + # Not-any-value = null + [ -z "$actual" ] && return 0 + while IFS= read -r rep; do + [ -n "$rep" ] && [ "$rep" != "\"\"" ] && return 1 + done <<< "$actual" + return 0 + fi + # Not-equal: NO repetition equals expected + while IFS= read -r rep; do + [ "$rep" = "$expected" ] && return 1 + done <<< "$actual" + return 0 + ;; + "~") + # Contains, case-insensitive + [ "$is_null" = "1" ] && return 1 # contains-nothing is meaningless + [[ "$actual_lc" == *"$expected_lc"* ]] && return 0 + return 1 + ;; + "!~") + # Does not contain, case-insensitive + [ "$is_null" = "1" ] && return 0 # always passes "doesn't contain (nothing)" + [[ "$actual_lc" == *"$expected_lc"* ]] && return 1 + return 0 + ;; + *) return 1 ;; + esac +} + +# Apply all --field filters; AND semantics. match_filters() { local msg_file="$1" for filt in "${FILTERS[@]}"; do - path="${filt%%=*}" - expected="${filt#*=}" - [ "$path" = "$expected" ] && continue # skip if "=" missing - # exact match: any repetition equal to expected - actual=$("$HL7F" "$path" "$msg_file" 2>/dev/null) - matched=0 - if [ -n "$actual" ]; then - while IFS= read -r rep; do - [ "$rep" = "$expected" ] && { matched=1; break; } - done <<< "$actual" - fi - [ "$matched" = "1" ] || return 1 + parse_filter "$filt" + [ -z "$FP_OP" ] && continue + local actual; actual=$("$HL7F" "$FP_PATH" "$msg_file" 2>/dev/null) + field_matches "$actual" "$FP_OP" "$FP_EXPECTED" || return 1 done return 0 } @@ -225,7 +311,7 @@ case "$FORMAT" in fi done ;; - text) + text|oneline|fields|mp|labeled) i=0 for f in "$TMP_OUT"/msg_*.bin; do i=$((i+1)) @@ -236,14 +322,86 @@ case "$FORMAT" in typ=$(printf '%s' "$meta" | awk -F'\t' '{print $2}') src=$(printf '%s' "$meta" | awk -F'\t' '{print $3}') dst=$(printf '%s' "$meta" | awk -F'\t' '{print $4}') - # Render time if [ "$tm" -gt 100000000000 ] 2>/dev/null; then tm_h=$(date -r $((tm/1000)) 2>/dev/null || date -d "@$((tm/1000))" 2>/dev/null || echo "$tm") else tm_h="$tm" fi printf '===== msg %d time=%s type=%s src=%s dst=%s =====\n' "$KEPT" "$tm_h" "$typ" "$src" "$dst" - tr '\r' '\n' < "$f" + case "$FORMAT" in + text) + tr '\r' '\n' < "$f" + ;; + oneline) + # Compact: single line, segments separated by visible '⏎' marker + tr '\r' '\037' < "$f" | sed 's/\x1f/ ⏎ /g' + printf '\n' + ;; + fields|mp) + # Each field on its own line: SEG.N: value (skips empty) + tr '\r' '\n' < "$f" | awk -F'|' ' + NF > 0 { + seg = substr($1, 1, 3) + if (seg == "") next + is_msh = (seg == "MSH") + for (k=2; k<=NF; k++) { + val = $k + if (val == "" || val == "\"\"") continue + fnum = is_msh ? k : (k - 1) + printf "%s.%d: %s\n", seg, fnum, val + } + }' + ;; + labeled) + # Same as fields but adds the friendly alias when known. + tr '\r' '\n' < "$f" | awk -F'|' ' + BEGIN { + # Reverse alias lookup table (alias for SEG.N → label) + a["PID.3"]="mrn"; a["PID.4"]="alt_id" + a["PID.5"]="name"; a["PID.6"]="mothers_maiden" + a["PID.7"]="dob"; a["PID.8"]="sex" + a["PID.11"]="address"; a["PID.13"]="phone" + a["PID.14"]="work_phone"; a["PID.18"]="account" + a["PID.19"]="ssn"; a["PID.20"]="license" + a["PID.29"]="dod" + a["PV1.2"]="patient_class"; a["PV1.3"]="location" + a["PV1.7"]="attending"; a["PV1.8"]="referring" + a["PV1.9"]="consulting"; a["PV1.17"]="admitting" + a["PV1.18"]="patient_type"; a["PV1.19"]="visit" + a["PV1.50"]="alt_visit" + a["MSH.3"]="sending_app"; a["MSH.4"]="sending_facility" + a["MSH.5"]="receiving_app"; a["MSH.6"]="receiving_facility" + a["MSH.7"]="timestamp"; a["MSH.9"]="msg_type" + a["MSH.10"]="control_id"; a["MSH.11"]="processing_id" + a["MSH.12"]="hl7_version" + a["EVN.1"]="trigger_event"; a["EVN.2"]="event_dt" + a["EVN.4"]="evn_reason"; a["EVN.5"]="operator" + a["NK1.2"]="next_of_kin"; a["NK1.3"]="relationship" + a["NK1.4"]="nk_address"; a["NK1.5"]="nk_phone" + a["GT1.4"]="guarantor"; a["GT1.5"]="gt_address" + a["GT1.6"]="gt_phone"; a["GT1.12"]="gt_ssn" + a["IN1.2"]="insurance"; a["IN1.16"]="insured" + a["IN1.17"]="insured_dob"; a["IN1.36"]="policy" + a["DG1.3"]="diagnosis"; a["DG1.4"]="dx_desc" + a["OBR.2"]="placer_order"; a["OBR.3"]="filler_order" + a["OBR.4"]="test_code"; a["OBR.16"]="ordering" + a["OBX.5"]="value"; a["OBX.11"]="status" + } + NF > 0 { + seg = substr($1, 1, 3) + if (seg == "") next + is_msh = (seg == "MSH") + for (k=2; k<=NF; k++) { + val = $k + if (val == "" || val == "\"\"") continue + fnum = is_msh ? k : (k - 1) + key = seg "." fnum + if (key in a) printf "%s (%s): %s\n", key, a[key], val + else printf "%s: %s\n", key, val + } + }' + ;; + esac printf '\n' fi done