#!/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 # newest-first # larry-rollback.sh --session # whole session # larry-rollback.sh --entry # one specific write # # Usage: # nc-insert-protocol.sh insert [--mode end|after|before --anchor NAME] # nc-insert-protocol.sh add-route # # path to a file containing the TCL `protocol NAME { … }` block to insert # 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 } } # 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 && NRs && 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 [--mode end|after|before --anchor NAME]" >&2; exit 2; } cmd_insert "$2" "$3" "${@:4}" ;; add-route) [ $# -ge 4 ] || { echo "usage: $0 add-route " >&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