#!/usr/bin/env bash # nc-xlate.sh — visualize and explore Cloverleaf xlate (.xlt) files. # Parses the TCL nested-block format and renders operation flows. # # Subcommands: # list [--site SITE] list .xlt files # show [--site SITE] dump the raw file # ops [--site SITE] list operations as TSV (op, in, out, err) # tree [--site SITE] ASCII tree by op type # summary [--site SITE] counts by operation + segments touched # diff [--site SITE] diff two xlates (semantic, sorted-by-op) set -o pipefail NC_SELF="$0" die() { printf 'nc-xlate: %s\n' "$*" >&2; exit 1; } locate_xlate() { local name="$1" site="${2:-${HCISITE:-}}" name="${name%.xlt}" for d in "${HCIROOT:-}/$site/Xlate" "${HCIROOT:-}/$site/xlate" "${HCIROOT:-}/Xlate"; do [ -f "$d/${name}.xlt" ] && { printf '%s\n' "$d/${name}.xlt"; 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/Xlate" "${HCIROOT:-}/$site/xlate"; do [ -d "$d" ] || continue find "$d" -maxdepth 1 -name '*.xlt' -type f 2>/dev/null | sort done } 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_xlate "$name" "$site") || die "no such xlate: $name" cat "$f" } # Parse the .xlt operations. Each top-level block looks like: # { { OP } # { ERR } # { IN } # { OUT } # [ { TABLE } ] # [ ... ] # } # Emit TSV: op_num \t op \t in \t out \t err \t extra parse_ops() { local f="$1" awk ' BEGIN { depth=0; in_block=0; n=0 } /^end_prologue$/ { in_pre=0; next } /^prologue$/ { in_pre=1; next } in_pre { next } /^[[:space:]]*\{ \{ OP / { depth=1; in_block=1 op=""; in_path=""; out_path=""; err=""; extra="" # First line has "{ { OP }" match($0, /\{ OP [A-Z]+ \}/) if (RSTART) { op = substr($0, RSTART+5, RLENGTH-7) } next } in_block { # Track depth via gsub-of-self trick no = gsub(/\{/, "{", $0) nc = gsub(/\}/, "}", $0) depth += no - nc if (match($0, /\{ IN [^}]+\}/)) { in_path = substr($0, RSTART+5, RLENGTH-6); gsub(/[[:space:]]+$/, "", in_path) } if (match($0, /\{ OUT [^}]+\}/)) { out_path = substr($0, RSTART+6, RLENGTH-7); gsub(/[[:space:]]+$/, "", out_path) } if (match($0, /\{ ERR [^}]+\}/)) { err = substr($0, RSTART+6, RLENGTH-7); gsub(/[[:space:]]+$/, "", err) } if (match($0, /\{ TABLE [^}]+\}/)) { extra = "TABLE=" substr($0, RSTART+8, RLENGTH-9); gsub(/[[:space:]]+$/, "", extra) } if (depth == 0) { n++ printf "%d\t%s\t%s\t%s\t%s\t%s\n", n, op, in_path, out_path, err, extra in_block = 0 } } ' "$f" } cmd_ops() { 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_xlate "$name" "$site") || die "no such xlate: $name" printf 'num\top\tin\tout\terr\textra\n' parse_ops "$f" } cmd_tree() { 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_xlate "$name" "$site") || die "no such xlate: $name" printf 'Xlate: %s\n' "$name" parse_ops "$f" | awk -F'\t' ' { op=$2; in_path=$3; out_path=$4; extra=$6 if (in_path == "" && out_path == "") { printf " %d. %s\n", $1, op; next } arrow = "→" if (op == "MOVE") arrow = "↦" if (op == "DELETE") arrow = "✗" if (extra != "") arrow = "↦(" extra ")→" printf " %2d. %-12s %-30s %s %s\n", $1, op, in_path, arrow, out_path } ' } cmd_summary() { 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_xlate "$name" "$site") || die "no such xlate: $name" printf 'Xlate: %s (path: %s)\n\n' "$name" "$f" printf 'Operations by type:\n' parse_ops "$f" | awk -F'\t' 'NR>0 {c[$2]++} END {for (k in c) printf " %-12s %d\n", k, c[k]}' | sort -k2 -rn printf '\nSegments touched (IN side):\n' parse_ops "$f" | awk -F'\t' '{ if (match($3, /\.[A-Z][A-Z0-9]+\(/)) { seg = substr($3, RSTART+1, RLENGTH-2) c[seg]++ } } END {for (k in c) printf " %-6s %d\n", k, c[k]}' | sort printf '\nTables referenced:\n' parse_ops "$f" | awk -F'\t' '$6 ~ /TABLE=/ { t=substr($6,7); print t }' | sort -u | sed 's/^/ /' } cmd_diff() { # Accept --site like every other subcommand. Without it, site-scoped xlates # (the common case — xlates live under /Xlate/) could not be located # and diff always died with "no such xlate". --site applies to BOTH names. local site="${HCISITE:-}" local positional=() while [ $# -gt 0 ]; do case "$1" in --site) shift; site="$1" ;; *) positional+=("$1") ;; esac shift done local n1="${positional[0]:-}" n2="${positional[1]:-}" [ -n "$n1" ] && [ -n "$n2" ] || die "usage: diff NAME1 NAME2 [--site SITE]" local f1 f2 f1=$(locate_xlate "$n1" "$site") || die "no such xlate: $n1" f2=$(locate_xlate "$n2" "$site") || die "no such xlate: $n2" diff -u <(parse_ops "$f1" | sort -k2) <(parse_ops "$f2" | sort -k2) } SUB="${1:-list}" case "$SUB" in list) shift; cmd_list "$@" ;; show) shift; [ $# -ge 1 ] || die "usage: show NAME"; cmd_show "$@" ;; ops) shift; [ $# -ge 1 ] || die "usage: ops NAME"; cmd_ops "$@" ;; tree) shift; [ $# -ge 1 ] || die "usage: tree NAME"; cmd_tree "$@" ;; summary) shift; [ $# -ge 1 ] || die "usage: summary NAME"; cmd_summary "$@" ;; diff) shift; [ $# -ge 2 ] || die "usage: diff NAME1 NAME2"; cmd_diff "$@" ;; help|-h|--help) sed -n '2,11p' "$NC_SELF" ;; *) die "unknown subcommand: $SUB" ;; esac