#!/usr/bin/env bash # nc-diff-interface.sh — diff one Cloverleaf interface across two environments. # # Use case: "I made a change in test to interface X, forgot what, need to move to prod. # Tell me exactly what differs." # # Compares: # 1. The protocol block (TCL definition in NetConfig). # 2. Every xlate (.xlt) file referenced by the protocol. # 3. Every tclproc (.tcl) file referenced by the protocol. # 4. (Optional) related tables (.tbl) — references found inside xlates/tclprocs. # # Usage: # nc-diff-interface.sh --interface NAME --left NC_PATH_A --right NC_PATH_B # [--out PATH] # markdown report (default: stdout) # [--include-tables] # also diff .tbl files referenced by the xlates/tclprocs # [--left-label LBL] # e.g. "TEST" # [--right-label LBL] # e.g. "PROD" # # The two NetConfig paths must each be a site root NetConfig — site root is # `dirname ` and that's where Xlate/, tclprocs/, tables/ are looked up. set -o pipefail # Note: not using `set -u` — the connected-cluster traversal uses associative # arrays whose emptiness shouldn't bash-fault. NC_SELF="$0" LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)" NCP="$LIB_DIR/nc-parse.sh" die() { printf 'nc-diff-interface: %s\n' "$*" >&2; exit 1; } INTERFACE="" NC_A="" NC_B="" OUT="" INCLUDE_TABLES=0 LABEL_A="A" LABEL_B="B" DEPTH=1 # how many hops out from the named interface to also diff while [ $# -gt 0 ]; do case "$1" in --interface) shift; INTERFACE="$1" ;; --left) shift; NC_A="$1" ;; --right) shift; NC_B="$1" ;; --out) shift; OUT="$1" ;; --include-tables) INCLUDE_TABLES=1 ;; --left-label) shift; LABEL_A="$1" ;; --right-label) shift; LABEL_B="$1" ;; --depth) shift; DEPTH="$1" ;; -h|--help) sed -n '2,22p' "$NC_SELF"; exit 0 ;; -*) die "unknown flag: $1" ;; *) die "extra arg: $1" ;; esac shift done [ -n "$INTERFACE" ] || die "missing --interface NAME" [ -n "$NC_A" ] || die "missing --left NC_PATH" [ -n "$NC_B" ] || die "missing --right NC_PATH" [ -f "$NC_A" ] || die "no such file: $NC_A" [ -f "$NC_B" ] || die "no such file: $NC_B" [[ "$DEPTH" =~ ^[0-9]+$ ]] || die "--depth must be a non-negative integer" SITE_A="$(dirname "$NC_A")" SITE_B="$(dirname "$NC_B")" out_target() { if [ -n "$OUT" ]; then mkdir -p "$(dirname "$OUT")" 2>/dev/null; cat > "$OUT" else cat fi } # Helper: print a diff between two files in fenced code block, or a clear message # if one or both are missing. emit_file_diff() { local title="$1" a="$2" b="$3" printf '### %s\n\n' "$title" if [ ! -e "$a" ] && [ ! -e "$b" ]; then printf '_(missing on both sides)_\n\n'; return elif [ ! -e "$a" ]; then printf '**Only on %s**: `%s`\n\n' "$LABEL_B" "$b" printf '```\n'; head -50 "$b"; printf '```\n\n'; return elif [ ! -e "$b" ]; then printf '**Only on %s**: `%s`\n\n' "$LABEL_A" "$a" printf '```\n'; head -50 "$a"; printf '```\n\n'; return fi local sha_a sha_b sha_a=$(shasum "$a" 2>/dev/null | awk '{print $1}' || md5 "$a") sha_b=$(shasum "$b" 2>/dev/null | awk '{print $1}' || md5 "$b") if [ "$sha_a" = "$sha_b" ]; then printf '_identical (sha1 `%s`)_\n\n' "$sha_a" else printf '%s: `%s` (%s)\n' "$LABEL_A" "$a" "$sha_a" printf '%s: `%s` (%s)\n\n' "$LABEL_B" "$b" "$sha_b" printf '```diff\n' diff -u "$a" "$b" 2>/dev/null || true printf '```\n\n' fi } # Same shape but for two strings (in-memory) emit_text_diff() { local title="$1" text_a="$2" text_b="$3" printf '### %s\n\n' "$title" local ta tb; ta=$(mktemp); tb=$(mktemp) printf '%s\n' "$text_a" > "$ta" printf '%s\n' "$text_b" > "$tb" if cmp -s "$ta" "$tb"; then printf '_identical_\n\n' else printf '```diff\n' diff -u "$ta" "$tb" 2>/dev/null || true printf '```\n\n' fi rm -f "$ta" "$tb" } # Find referenced files in a NetConfig+site root, for a given interface collect_xlates() { local nc="$1" site="$2" "$NCP" xlate-refs "$nc" "$INTERFACE" 2>/dev/null \ | awk -v site="$site" '{print site"/Xlate/"$0}' } collect_tclprocs() { local nc="$1" site="$2" "$NCP" tclproc-refs "$nc" "$INTERFACE" 2>/dev/null \ | awk -v site="$site" '{print site"/tclprocs/"$0".tcl"}' } # Tables — referenced inside xlates and tclprocs (look for *.tbl references) collect_table_refs() { local site="$1" shift local files=("$@") for f in "${files[@]}"; do [ -f "$f" ] || continue grep -hoE '[A-Za-z0-9_]+\.tbl' "$f" 2>/dev/null done | sort -u | awk -v site="$site" '$0 != "" {print site"/tables/"$0}' } # Walk the connected graph N hops out from the named interface, combining # sources and destinations from BOTH NetConfigs. Result: deduplicated thread set. build_cluster() { local depth="$1" declare -A visited visited["$INTERFACE"]=1 local frontier=("$INTERFACE") local d for ((d=0; d/dev/null) \ $("$NCP" destinations "$nc" "$f" 2>/dev/null); do [ -z "$rel" ] && continue if [ -z "${visited[$rel]:-}" ]; then visited[$rel]=1 next_frontier+=("$rel") fi done done done [ ${#next_frontier[@]} -eq 0 ] && break frontier=("${next_frontier[@]}") done printf '%s\n' "${!visited[@]}" | sort } # Diff one thread's protocol block + its xlates + its tclprocs. emit_thread_section() { local iface="$1" idx="$2" total="$3" printf '## [%d/%d] Thread `%s`\n\n' "$idx" "$total" "$iface" local BLOCK_A BLOCK_B BLOCK_A=$("$NCP" protocol-block "$NC_A" "$iface" 2>/dev/null || echo "") BLOCK_B=$("$NCP" protocol-block "$NC_B" "$iface" 2>/dev/null || echo "") if [ -z "$BLOCK_A" ] && [ -z "$BLOCK_B" ]; then printf '_absent on both sides — referenced as DEST but block not present_\n\n' return elif [ -z "$BLOCK_A" ]; then printf '**Only on %s.** Block on %s:\n\n```tcl\n%s\n```\n\n' "$LABEL_B" "$LABEL_B" "$BLOCK_B" return elif [ -z "$BLOCK_B" ]; then printf '**Only on %s.** Block on %s:\n\n```tcl\n%s\n```\n\n' "$LABEL_A" "$LABEL_A" "$BLOCK_A" return fi emit_text_diff "Protocol block" "$BLOCK_A" "$BLOCK_B" # Xlates referenced local X_A=() X_B=() while IFS= read -r l; do X_A+=("$l"); done < <(collect_xlates_for "$NC_A" "$SITE_A" "$iface") while IFS= read -r l; do X_B+=("$l"); done < <(collect_xlates_for "$NC_B" "$SITE_B" "$iface") declare -A XSET local p for p in "${X_A[@]}" "${X_B[@]}"; do [ -n "$p" ] && XSET[$(basename "$p")]=1; done if [ ${#XSET[@]} -gt 0 ]; then printf '#### Xlates referenced by `%s`\n\n' "$iface" local x for x in "${!XSET[@]}"; do emit_file_diff "Xlate \`$x\`" "$SITE_A/Xlate/$x" "$SITE_B/Xlate/$x" done fi # Tclprocs referenced local T_A=() T_B=() while IFS= read -r l; do T_A+=("$l"); done < <(collect_tclprocs_for "$NC_A" "$SITE_A" "$iface") while IFS= read -r l; do T_B+=("$l"); done < <(collect_tclprocs_for "$NC_B" "$SITE_B" "$iface") declare -A TSET for p in "${T_A[@]}" "${T_B[@]}"; do [ -n "$p" ] && TSET[$(basename "$p")]=1; done if [ ${#TSET[@]} -gt 0 ]; then printf '#### Tclprocs referenced by `%s`\n\n' "$iface" local t for t in "${!TSET[@]}"; do emit_file_diff "Tclproc \`$t\`" "$SITE_A/tclprocs/$t" "$SITE_B/tclprocs/$t" done fi } # Per-iface collectors (versions of the originals scoped to a specific iface) collect_xlates_for() { "$NCP" xlate-refs "$1" "$3" 2>/dev/null | awk -v site="$2" '{print site"/Xlate/"$0}' } collect_tclprocs_for() { "$NCP" tclproc-refs "$1" "$3" 2>/dev/null | awk -v site="$2" '{print site"/tclprocs/"$0".tcl"}' } # ───────────────────────────────────────────────────────────────────────────── # Compose report # ───────────────────────────────────────────────────────────────────────────── { printf '# Interface diff: `%s` + connected (depth %d)\n\n' "$INTERFACE" "$DEPTH" printf '_%s_ → `%s`\n' "$LABEL_A" "$NC_A" printf '_%s_ → `%s`\n\n' "$LABEL_B" "$NC_B" # Build the cluster CLUSTER=() while IFS= read -r t; do CLUSTER+=("$t"); done < <(build_cluster "$DEPTH") TOTAL=${#CLUSTER[@]} printf '## Cluster (%d threads)\n\n' "$TOTAL" printf 'These are the threads that will be diffed, starting from `%s` and walking %d hop(s) outward via sources/destinations on BOTH sides.\n\n' "$INTERFACE" "$DEPTH" for t in "${CLUSTER[@]}"; do printf -- '- `%s`\n' "$t"; done printf '\n' # Diff each thread in the cluster IDX=0 for t in "${CLUSTER[@]}"; do IDX=$((IDX+1)) emit_thread_section "$t" "$IDX" "$TOTAL" done # Optional tables section if [ "$INCLUDE_TABLES" = "1" ]; then printf '## Tables referenced (across the whole cluster)\n\n' declare -A TBL_SEEN for t in "${CLUSTER[@]}"; do while IFS= read -r f; do [ -f "$f" ] && for tbl in $(grep -hoE '[A-Za-z0-9_]+\.tbl' "$f" 2>/dev/null); do TBL_SEEN[$tbl]=1 done done < <(collect_xlates_for "$NC_A" "$SITE_A" "$t"; \ collect_tclprocs_for "$NC_A" "$SITE_A" "$t"; \ collect_xlates_for "$NC_B" "$SITE_B" "$t"; \ collect_tclprocs_for "$NC_B" "$SITE_B" "$t") done if [ ${#TBL_SEEN[@]} -eq 0 ]; then printf '_no .tbl references found inside the cluster_\n\n' else for tbl in "${!TBL_SEEN[@]}"; do emit_file_diff "Table \`$tbl\`" "$SITE_A/tables/$tbl" "$SITE_B/tables/$tbl" done fi fi printf '---\n\n' printf '_Generated %s by Larry-Anywhere nc-diff-interface.sh (depth=%d)._\n' \ "$(date -Iseconds 2>/dev/null || date)" "$DEPTH" } | out_target [ -n "$OUT" ] && printf 'nc-diff-interface: wrote %s\n' "$OUT" >&2