#!/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 [--site SITE] dump the file # pairs [--site SITE] [--format csv|tsv] input→output pairs only # lookup find output for input # reverse-lookup find input(s) for output # add [--site SITE] add or update a row (journaled) # delete [--site SITE] delete a row by input (journaled) # create --from-csv FILE [opts] create a new table from CSV # replace --from-csv FILE [opts] replace contents (journaled) # # Find tables at: $HCISITEDIR/tables/.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 [ -f "$target" ] && cp -p "$target" "${target}.larry-bak.$(date +%s)" mv "$new" "$target" echo "(no journal available; backup at ${target}.larry-bak.)" 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