- Fix bash arithmetic crash on MobaXterm/Cygwin: $(date +%s) was returning CR-tainted values landing in $(( )) operands - Mouse mode off by default; opt in via LARRY_MOUSE=1 or /mouse on - Comprehensive CR-safety sweep across lib/*.sh and larry.sh — every command-substitution result, file read, and user input that feeds an arithmetic context, case dispatcher, or path/header is now CR-stripped at the source New shared helper lib/cygwin-safe.sh defines three primitives: coerce_int VAL [DEFAULT] — for arithmetic / integer-test operands strip_cr VAL — for case patterns, regex tests, paths, headers read_clean VAR [PROMPT] — read -r wrapper that strips CR pre-assign Hardened call sites (14 files, 60+ patch points): - larry.sh: status-line date/tput, 3 y/N approvals, auth menu, API key - lib/oauth.sh: cmd_login + cmd_refresh date+%s captures - lib/nc-engine.sh: 5 y/N action prompts + find|wc arithmetic - lib/nc-msgs.sh: parse_time_ms (4 date sites) + meta-TSV time + MSG_COUNT - lib/nc-regression.sh: tr|wc count + hl7-diff ?-fallback arithmetic - lib/nc-smat-diff.sh: A_COUNT/B_COUNT/DIFFS_TOTAL - lib/nc-insert-protocol.sh: every awk-emitted line number → head/tail math - lib/journal.sh: _next_seq wc -l arithmetic - lib/lessons.sh: _next_id/_count + 2 y/N prompts - lib/hl7-sanitize.sh: cmd_count + clear-table y/N - lib/ssh-helper.sh: 4 local+remote wc -c integer compares - lib/nc-find.sh, lib/nc-table.sh, lib/nc-document.sh, larry-rollback.sh Reproduces the exact error Bryan hit: bash: ...: arithmetic syntax error: invalid arithmetic operator (error token is "") lib/cygwin-safe.sh added to MANIFEST so it auto-syncs on next launch. Co-Authored-By: Clover (Claude Opus 4.7) <noreply@anthropic.com>
227 lines
7.7 KiB
Bash
Executable File
227 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# nc-table.sh — read and modify Cloverleaf lookup tables (.tbl files).
|
|
# Every modification goes through the journal (snapshot + diff + atomic write).
|
|
#
|
|
# Subcommands:
|
|
# list [--site SITE] list .tbl files
|
|
# show <name> [--site SITE] dump the file
|
|
# pairs <name> [--site SITE] [--format csv|tsv] input→output pairs only
|
|
# lookup <name> <input> find output for input
|
|
# reverse-lookup <name> <output> find input(s) for output
|
|
# add <name> <input> <output> [--site SITE] add or update a row (journaled)
|
|
# delete <name> <input> [--site SITE] delete a row by input (journaled)
|
|
# create <name> --from-csv FILE [opts] create a new table from CSV
|
|
# replace <name> --from-csv FILE [opts] replace contents (journaled)
|
|
#
|
|
# Find tables at: $HCISITEDIR/tables/<name>.tbl (or $HCIROOT/Tables/ shared)
|
|
set -o pipefail
|
|
|
|
NC_SELF="$0"
|
|
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
|
|
JOURNAL="$LIB_DIR/journal.sh"
|
|
C2T="$LIB_DIR/csv-to-table.sh"
|
|
T2C="$LIB_DIR/table-to-csv.sh"
|
|
|
|
die() { printf 'nc-table: %s\n' "$*" >&2; exit 1; }
|
|
|
|
[ -f "$JOURNAL" ] && . "$JOURNAL"
|
|
|
|
locate_table() {
|
|
local name="$1" site="${2:-${HCISITE:-}}"
|
|
# Strip .tbl if user gave it
|
|
name="${name%.tbl}"
|
|
for d in \
|
|
"${HCIROOT:-}/$site/tables" \
|
|
"${HCIROOT:-}/$site/Tables" \
|
|
"${HCIROOT:-}/Tables"; do
|
|
[ -f "$d/${name}.tbl" ] && { printf '%s\n' "$d/${name}.tbl"; return 0; }
|
|
done
|
|
return 1
|
|
}
|
|
|
|
cmd_list() {
|
|
local site="${HCISITE:-}"
|
|
while [ $# -gt 0 ]; do case "$1" in --site) shift; site="$1" ;; esac; shift; done
|
|
for d in "${HCIROOT:-}/$site/tables" "${HCIROOT:-}/$site/Tables" "${HCIROOT:-}/Tables"; do
|
|
[ -d "$d" ] || continue
|
|
find "$d" -maxdepth 1 -name '*.tbl' -type f 2>/dev/null | sort
|
|
done | sort -u
|
|
}
|
|
|
|
cmd_show() {
|
|
local name="$1"; shift
|
|
local site="${HCISITE:-}"
|
|
while [ $# -gt 0 ]; do case "$1" in --site) shift; site="$1" ;; esac; shift; done
|
|
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
|
|
cat "$f"
|
|
}
|
|
|
|
cmd_pairs() {
|
|
local name="$1"; shift
|
|
local site="${HCISITE:-}"
|
|
local fmt="csv"
|
|
while [ $# -gt 0 ]; do case "$1" in
|
|
--site) shift; site="$1" ;;
|
|
--format) shift; fmt="$1" ;;
|
|
esac; shift; done
|
|
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
|
|
case "$fmt" in
|
|
csv) "$T2C" --with-header "$f" ;;
|
|
tsv) "$T2C" --with-header --delim $'\t' "$f" ;;
|
|
*) die "bad --format" ;;
|
|
esac
|
|
}
|
|
|
|
cmd_lookup() {
|
|
local name="$1" input="$2"
|
|
local site="${HCISITE:-}"
|
|
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
|
|
"$T2C" "$f" | awk -F',' -v target="$input" '
|
|
BEGIN { found=0 }
|
|
{
|
|
# CSV unquote (basic)
|
|
v=$1; gsub(/^"|"$/, "", v); gsub(/""/, "\"", v)
|
|
if (v == target) { o=$2; gsub(/^"|"$/, "", o); gsub(/""/, "\"", o); print o; found=1; exit }
|
|
}
|
|
END { if (!found) exit 1 }
|
|
'
|
|
}
|
|
|
|
cmd_reverse_lookup() {
|
|
local name="$1" output="$2"
|
|
local site="${HCISITE:-}"
|
|
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
|
|
"$T2C" "$f" | awk -F',' -v target="$output" '
|
|
{
|
|
v=$2; gsub(/^"|"$/, "", v); gsub(/""/, "\"", v)
|
|
if (v == target) { i=$1; gsub(/^"|"$/, "", i); gsub(/""/, "\"", i); print i }
|
|
}
|
|
'
|
|
}
|
|
|
|
# Modification helpers (journal-backed)
|
|
modify_via_csv() {
|
|
local name="$1" csv_path="$2" site="$3" mode="$4" # mode = add|delete|replace|create
|
|
local action="$mode-table"
|
|
local target
|
|
|
|
# Determine target path
|
|
if [ "$mode" = "create" ]; then
|
|
[ -n "$site" ] || die "create: --site required"
|
|
[ -d "${HCIROOT}/$site/tables" ] || mkdir -p "${HCIROOT}/$site/tables"
|
|
target="${HCIROOT}/$site/tables/${name%.tbl}.tbl"
|
|
[ ! -e "$target" ] || die "table already exists: $target (use replace if you want to overwrite)"
|
|
else
|
|
target=$(locate_table "$name" "$site") || die "no such table: $name"
|
|
fi
|
|
|
|
local new; new=$(mktemp)
|
|
"$C2T" --has-header --out "$new" < "$csv_path" 2>/dev/null \
|
|
|| "$C2T" --has-header "$csv_path" > "$new"
|
|
|
|
if declare -f journal_write >/dev/null 2>&1; then
|
|
journal_write "$target" "$new"
|
|
else
|
|
# No journal — direct write with simple backup
|
|
# v0.7.5: strip non-digits from date — Cygwin date.exe could yield
|
|
# "1779999999\r" which would produce a path with embedded CR.
|
|
local _ts; _ts=$(date +%s | tr -cd '0-9')
|
|
[ -f "$target" ] && cp -p "$target" "${target}.larry-bak.${_ts:-0}"
|
|
mv "$new" "$target"
|
|
echo "(no journal available; backup at ${target}.larry-bak.<ts>)"
|
|
fi
|
|
rm -f "$new"
|
|
}
|
|
|
|
cmd_add() {
|
|
local name="$1" input="$2" output="$3"; shift 3
|
|
local site="${HCISITE:-}"
|
|
while [ $# -gt 0 ]; do case "$1" in --site) shift; site="$1" ;; esac; shift; done
|
|
[ -n "$name" ] && [ -n "$input" ] && [ -n "$output" ] || die "usage: add NAME INPUT OUTPUT"
|
|
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
|
|
|
|
# Read current pairs, replace existing input row or append, then rewrite
|
|
local tmp_csv; tmp_csv=$(mktemp)
|
|
{
|
|
"$T2C" --with-header "$f"
|
|
# If the new pair wasn't already in the existing data, it'll be added below
|
|
} > "$tmp_csv"
|
|
|
|
local awk_script='
|
|
BEGIN { added=0 }
|
|
NR==1 { print; next }
|
|
{
|
|
n=split($0, c, ",")
|
|
v=c[1]; gsub(/^"|"$/, "", v); gsub(/""/, "\"", v)
|
|
if (v == NEW_IN) {
|
|
printf "%s,%s\n", NEW_IN, NEW_OUT
|
|
added=1
|
|
} else { print }
|
|
}
|
|
END { if (!added) printf "%s,%s\n", NEW_IN, NEW_OUT }
|
|
'
|
|
local newcsv; newcsv=$(mktemp)
|
|
awk -v NEW_IN="$input" -v NEW_OUT="$output" "$awk_script" "$tmp_csv" > "$newcsv"
|
|
modify_via_csv "$name" "$newcsv" "$site" "add"
|
|
rm -f "$tmp_csv" "$newcsv"
|
|
}
|
|
|
|
cmd_delete() {
|
|
local name="$1" input="$2"; shift 2
|
|
local site="${HCISITE:-}"
|
|
while [ $# -gt 0 ]; do case "$1" in --site) shift; site="$1" ;; esac; shift; done
|
|
[ -n "$name" ] && [ -n "$input" ] || die "usage: delete NAME INPUT"
|
|
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
|
|
local tmp_csv; tmp_csv=$(mktemp)
|
|
"$T2C" --with-header "$f" \
|
|
| awk -F',' -v IN="$input" '
|
|
NR==1 { print; next }
|
|
{
|
|
v=$1; gsub(/^"|"$/, "", v); gsub(/""/, "\"", v)
|
|
if (v == IN) next
|
|
print
|
|
}
|
|
' > "$tmp_csv"
|
|
modify_via_csv "$name" "$tmp_csv" "$site" "delete"
|
|
rm -f "$tmp_csv"
|
|
}
|
|
|
|
cmd_create() {
|
|
local name="$1"; shift
|
|
local from_csv=""
|
|
local site="${HCISITE:-}"
|
|
while [ $# -gt 0 ]; do case "$1" in
|
|
--from-csv) shift; from_csv="$1" ;;
|
|
--site) shift; site="$1" ;;
|
|
esac; shift; done
|
|
[ -f "$from_csv" ] || die "create: --from-csv FILE required"
|
|
modify_via_csv "$name" "$from_csv" "$site" "create"
|
|
}
|
|
|
|
cmd_replace() {
|
|
local name="$1"; shift
|
|
local from_csv=""
|
|
local site="${HCISITE:-}"
|
|
while [ $# -gt 0 ]; do case "$1" in
|
|
--from-csv) shift; from_csv="$1" ;;
|
|
--site) shift; site="$1" ;;
|
|
esac; shift; done
|
|
[ -f "$from_csv" ] || die "replace: --from-csv FILE required"
|
|
modify_via_csv "$name" "$from_csv" "$site" "replace"
|
|
}
|
|
|
|
SUB="${1:-list}"
|
|
case "$SUB" in
|
|
list) shift; cmd_list "$@" ;;
|
|
show) shift; [ $# -ge 1 ] || die "usage: show NAME"; cmd_show "$@" ;;
|
|
pairs) shift; [ $# -ge 1 ] || die "usage: pairs NAME"; cmd_pairs "$@" ;;
|
|
lookup) shift; [ $# -ge 2 ] || die "usage: lookup NAME INPUT"; cmd_lookup "$@" ;;
|
|
reverse-lookup) shift; [ $# -ge 2 ] || die "usage: reverse-lookup NAME OUTPUT"; cmd_reverse_lookup "$@" ;;
|
|
add) shift; cmd_add "$@" ;;
|
|
delete) shift; cmd_delete "$@" ;;
|
|
create) shift; cmd_create "$@" ;;
|
|
replace) shift; cmd_replace "$@" ;;
|
|
help|-h|--help) sed -n '2,20p' "$NC_SELF" ;;
|
|
*) die "unknown subcommand: $SUB" ;;
|
|
esac
|