- 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>
265 lines
10 KiB
Bash
Executable File
265 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# nc-insert-protocol.sh — write side of Example 1 (and general NetConfig writes).
|
|
#
|
|
# Two operations:
|
|
#
|
|
# 1. insert — append a NEW protocol block to a NetConfig file.
|
|
# Modes: end (default) | after-protocol NAME | before-protocol NAME
|
|
#
|
|
# 2. add-route — splice a NEW route entry into an existing protocol's
|
|
# DATAXLATE block. The route entry is the inner `{ … }`
|
|
# object (with CACHEMSG, ROUTE_DETAILS, TRXID, etc.) that
|
|
# nc-make-jump.sh emits as the route_add snippet.
|
|
#
|
|
# Both operations go through journal.sh: snapshot the original, write a diff
|
|
# entry, then atomically replace the target. Roll back later via:
|
|
# larry-rollback.sh --target <netconfig> # newest-first
|
|
# larry-rollback.sh --session <session-id> # whole session
|
|
# larry-rollback.sh --entry <entry-id> # one specific write
|
|
#
|
|
# Usage:
|
|
# nc-insert-protocol.sh insert <netconfig> <block_file> [--mode end|after|before --anchor NAME]
|
|
# nc-insert-protocol.sh add-route <netconfig> <protocol_name> <route_file>
|
|
#
|
|
# <block_file> path to a file containing the TCL `protocol NAME { … }` block to insert
|
|
# <route_file> path to a file containing the route entry (just the inner `{ … }`)
|
|
#
|
|
# Exit codes: 0 OK, 2 usage, 3 target not found, 4 already exists / wouldn't change
|
|
set -o pipefail
|
|
|
|
NC_SELF="$0"
|
|
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
|
|
NCP="$LIB_DIR/nc-parse.sh"
|
|
JOURNAL="$LIB_DIR/journal.sh"
|
|
|
|
die() { printf 'nc-insert-protocol: %s\n' "$*" >&2; exit 1; }
|
|
|
|
# v0.7.5: shared CR-safety primitives. awk-emitted line numbers feed
|
|
# `$((end_line - 1))` / `head -n $((start_line - 1))` / `tail -n +$((... + 1))`
|
|
# arithmetic and shell positionals; a CR-tainted awk would crash all three.
|
|
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
|
|
|
|
# Source journal so we can call journal_write directly
|
|
# shellcheck disable=SC1090
|
|
. "$JOURNAL"
|
|
|
|
cmd_insert() {
|
|
local nc="$1" block_file="$2"
|
|
shift 2
|
|
local mode="end" anchor=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--mode) shift; mode="$1" ;;
|
|
--anchor) shift; anchor="$1" ;;
|
|
*) die "unknown flag for insert: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -f "$nc" ] || { die "no such NetConfig: $nc"; }
|
|
[ -f "$block_file" ] || { die "no such block file: $block_file"; }
|
|
|
|
# Extract block name to detect collisions
|
|
local block_name
|
|
block_name=$(awk '/^protocol [A-Za-z0-9_]+/ {print $2; exit}' "$block_file")
|
|
[ -n "$block_name" ] || die "block file does not start with 'protocol NAME {' — bad input"
|
|
|
|
if "$NCP" list-protocols "$nc" 2>/dev/null | grep -qx "$block_name"; then
|
|
die "protocol '$block_name' already exists in $nc — refusing to insert. Use a different name or update the existing block via a different tool."
|
|
fi
|
|
|
|
local tmp; tmp=$(mktemp)
|
|
|
|
case "$mode" in
|
|
end)
|
|
cat "$nc" > "$tmp"
|
|
# Ensure trailing newline before appending
|
|
[ -n "$(tail -c1 "$tmp")" ] && printf '\n' >> "$tmp"
|
|
printf '\n' >> "$tmp"
|
|
cat "$block_file" >> "$tmp"
|
|
printf '\n' >> "$tmp"
|
|
;;
|
|
after)
|
|
[ -n "$anchor" ] || die "--mode after needs --anchor NAME"
|
|
local end_line
|
|
end_line=$("$NCP" list-protocols "$nc" >/dev/null 2>&1
|
|
# Compute end line via the same logic as protocol-block
|
|
awk -v target="$anchor" '
|
|
BEGIN { depth=0; in_block=0; name="" }
|
|
/^protocol [A-Za-z0-9_]+ \{$/ && !in_block {
|
|
name=$2; depth=1; in_block=1
|
|
if (name==target) start=NR
|
|
next
|
|
}
|
|
in_block {
|
|
n_open=gsub(/\{/,"{",$0); n_close=gsub(/\}/,"}",$0)
|
|
depth += n_open - n_close
|
|
if (depth==0) {
|
|
if (name==target) { print NR; exit }
|
|
in_block=0; name=""
|
|
}
|
|
}' "$nc")
|
|
[ -n "$end_line" ] || die "anchor protocol not found: $anchor"
|
|
# v0.7.5: coerce_int — awk-emitted NR can have a trailing CR on
|
|
# Cygwin-awk; would crash $((end_line + 1)) arithmetic.
|
|
end_line=$(coerce_int "$end_line" 0)
|
|
head -n "$end_line" "$nc" > "$tmp"
|
|
printf '\n' >> "$tmp"
|
|
cat "$block_file" >> "$tmp"
|
|
printf '\n' >> "$tmp"
|
|
tail -n +$((end_line + 1)) "$nc" >> "$tmp"
|
|
;;
|
|
before)
|
|
[ -n "$anchor" ] || die "--mode before needs --anchor NAME"
|
|
local start_line
|
|
start_line=$("$NCP" protocol-line "$nc" "$anchor" 2>/dev/null)
|
|
[ -n "$start_line" ] || die "anchor protocol not found: $anchor"
|
|
# v0.7.5: coerce_int — protocol-line is awk-based; CR-taint defense.
|
|
start_line=$(coerce_int "$start_line" 0)
|
|
head -n $((start_line - 1)) "$nc" > "$tmp"
|
|
cat "$block_file" >> "$tmp"
|
|
printf '\n' >> "$tmp"
|
|
tail -n +"$start_line" "$nc" >> "$tmp"
|
|
;;
|
|
*) die "bad --mode: $mode (use end|after|before)" ;;
|
|
esac
|
|
|
|
# Hand off to journal for atomic backup+write
|
|
local entry_id
|
|
entry_id=$(journal_write "$nc" "$tmp")
|
|
rm -f "$tmp"
|
|
printf 'inserted protocol %s into %s (mode=%s)\n' "$block_name" "$nc" "$mode"
|
|
printf 'journal entry: %s\n' "$entry_id"
|
|
printf 'rollback: larry-rollback.sh --entry %s OR larry-rollback.sh --target %s\n' "$entry_id" "$nc"
|
|
}
|
|
|
|
cmd_add_route() {
|
|
local nc="$1" prot="$2" route_file="$3"
|
|
[ -f "$nc" ] || die "no such NetConfig: $nc"
|
|
[ -f "$route_file" ] || die "no such route file: $route_file"
|
|
|
|
# Find the protocol's line range
|
|
local start end
|
|
start=$("$NCP" protocol-line "$nc" "$prot" 2>/dev/null)
|
|
[ -n "$start" ] || die "no such protocol: $prot"
|
|
|
|
# Compute end-line of the protocol block
|
|
end=$(awk -v s="$start" '
|
|
NR == s { depth = 1; in_block = 1; next }
|
|
in_block {
|
|
n_open = gsub(/\{/, "{", $0)
|
|
n_close = gsub(/\}/, "}", $0)
|
|
depth += n_open - n_close
|
|
if (depth == 0) { print NR; exit }
|
|
}
|
|
' "$nc")
|
|
[ -n "$end" ] || die "could not determine end of protocol block: $prot"
|
|
|
|
# Find the DATAXLATE inner block boundaries (just inside the protocol).
|
|
# Lines look like:
|
|
# { DATAXLATE {
|
|
# {existing route 1}
|
|
# {existing route 2}
|
|
# } }
|
|
local dx_start dx_end
|
|
dx_start=$(awk -v s="$start" -v e="$end" '
|
|
NR>s && NR<e && /^[[:space:]]+\{ DATAXLATE \{$/ { print NR; exit }
|
|
' "$nc")
|
|
|
|
if [ -z "$dx_start" ]; then
|
|
# Handle the empty case: { DATAXLATE { } } or { DATAXLATE {<blank> } }
|
|
# If empty, replace with a populated block on a single line plus the new route.
|
|
local dx_empty_line
|
|
dx_empty_line=$(awk -v s="$start" -v e="$end" '
|
|
NR>s && NR<e && /^[[:space:]]+\{ DATAXLATE \{[[:space:]]*$/ { print NR; exit }
|
|
NR>s && NR<e && /^[[:space:]]+\{ DATAXLATE \{[[:space:]]*\}[[:space:]]*\}/ { print NR; exit }
|
|
' "$nc")
|
|
[ -n "$dx_empty_line" ] || die "could not locate DATAXLATE block in protocol $prot"
|
|
dx_start="$dx_empty_line"
|
|
fi
|
|
|
|
# Find the matching close of the DATAXLATE block:
|
|
# search forward from dx_start, counting braces to find the line where
|
|
# depth-since-dx_start returns to 0.
|
|
dx_end=$(awk -v s="$dx_start" -v e="$end" '
|
|
NR>=s && NR<=e {
|
|
n_open = gsub(/\{/, "{", $0)
|
|
n_close = gsub(/\}/, "}", $0)
|
|
if (NR == s) {
|
|
# The DATAXLATE-open line; the outer "{ DATAXLATE {" has 2 opens
|
|
depth = 0
|
|
}
|
|
depth += n_open - n_close
|
|
if (NR > s && depth <= -2) { # closing "} }" brings us down 2 levels
|
|
print NR; exit
|
|
}
|
|
}
|
|
' "$nc")
|
|
# If our naive count failed, try a simpler approach: find next line that is
|
|
# exactly the closing pattern of DATAXLATE ( } } at the appropriate indent).
|
|
if [ -z "$dx_end" ]; then
|
|
dx_end=$(awk -v s="$dx_start" -v e="$end" '
|
|
NR>s && NR<=e && /^[[:space:]]+\}[[:space:]]\}[[:space:]]*$/ { print NR; exit }
|
|
' "$nc")
|
|
fi
|
|
[ -n "$dx_end" ] || die "could not locate end of DATAXLATE block in protocol $prot"
|
|
|
|
# v0.7.5: coerce_int on every awk-emitted line number before it lands in
|
|
# arithmetic (head -n / tail -n +N) — Cygwin awk.exe CR-taint defense.
|
|
start=$(coerce_int "$start" 0)
|
|
end=$(coerce_int "$end" 0)
|
|
dx_start=$(coerce_int "$dx_start" 0)
|
|
dx_end=$(coerce_int "$dx_end" 0)
|
|
|
|
# Indent the route content to match the DATAXLATE inner indentation (8 spaces typical).
|
|
local indent=" "
|
|
local indented_route; indented_route=$(awk -v IND="$indent" '{print IND $0}' "$route_file")
|
|
|
|
# Build the new file:
|
|
# lines 1..dx_start
|
|
# route content (indented)
|
|
# lines dx_start+1..end of file
|
|
# Skip the simple "empty" case: if dx_start was a "{ DATAXLATE {} }" single line,
|
|
# we need to split it. Detect by reading that line.
|
|
local dx_start_line; dx_start_line=$(sed -n "${dx_start}p" "$nc")
|
|
local tmp; tmp=$(mktemp)
|
|
|
|
if [[ "$dx_start_line" =~ \{[[:space:]]DATAXLATE[[:space:]]\{[[:space:]]*\}[[:space:]]*\}[[:space:]]*$ ]]; then
|
|
# Single-line "{ DATAXLATE { } }" — replace with multi-line form
|
|
head -n $((dx_start - 1)) "$nc" > "$tmp"
|
|
printf ' { DATAXLATE {\n' >> "$tmp"
|
|
printf '%s\n' "$indented_route" >> "$tmp"
|
|
printf ' } }\n' >> "$tmp"
|
|
tail -n +$((dx_start + 1)) "$nc" >> "$tmp"
|
|
else
|
|
head -n "$dx_start" "$nc" > "$tmp"
|
|
printf '%s\n' "$indented_route" >> "$tmp"
|
|
tail -n +$((dx_start + 1)) "$nc" >> "$tmp"
|
|
fi
|
|
|
|
local entry_id
|
|
entry_id=$(journal_write "$nc" "$tmp")
|
|
rm -f "$tmp"
|
|
printf 'added route to protocol %s in %s (DATAXLATE block at line %s)\n' "$prot" "$nc" "$dx_start"
|
|
printf 'journal entry: %s\n' "$entry_id"
|
|
printf 'rollback: larry-rollback.sh --entry %s OR larry-rollback.sh --target %s\n' "$entry_id" "$nc"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
SUB="${1:-help}"
|
|
case "$SUB" in
|
|
insert)
|
|
[ $# -ge 3 ] || { echo "usage: $0 insert <netconfig> <block_file> [--mode end|after|before --anchor NAME]" >&2; exit 2; }
|
|
cmd_insert "$2" "$3" "${@:4}" ;;
|
|
add-route)
|
|
[ $# -ge 4 ] || { echo "usage: $0 add-route <netconfig> <protocol_name> <route_file>" >&2; exit 2; }
|
|
cmd_add_route "$2" "$3" "$4" ;;
|
|
help|-h|--help) sed -n '2,30p' "$NC_SELF" ;;
|
|
*) echo "unknown subcommand: $SUB" >&2; exit 2 ;;
|
|
esac
|