Ran every read/analysis tool against the real 24-site integrator (lib + wired dispatch). Fixed: nc-find --name (GNU sed \+ → POSIX; 0 rows on BSD/macOS), nc-find tsv/jsonl exit-1-on-success, nc-parse tclproc-refs dropping digit-leading procs (3M_check_ack), nc-xlate diff missing --site, nc-diff-interface + nc-smat-diff printf '-'-leading option-injection dropping output, nc-status not-up crashing on --format, and nc-status not-up's gawk-only \<up\> word-boundary → portable form (BSD/macOS). Test matrix in Deliverables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
244 lines
12 KiB
Bash
Executable File
244 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# nc-find.sh — cross-site Cloverleaf thread/protocol search. Native v3.
|
|
#
|
|
# Replaces (in v3 terms) the v1 family `tbn`, `tbp`, `tbh`, `tbpr`, plus the
|
|
# v1 `<thread> where` command. Searches every NetConfig under $HCIROOT (or a
|
|
# passed list) without invoking v1/v2 wrappers, and emits site, thread, port,
|
|
# host, process, NetConfig path, and line number for each match.
|
|
#
|
|
# Usage:
|
|
# nc-find.sh --name PATTERN # like tbn: case-insensitive substring on thread name
|
|
# nc-find.sh --port PORT # like tbp: exact port match
|
|
# nc-find.sh --host HOST # like tbh: substring on host
|
|
# nc-find.sh --process PROC # like tbpr: substring on PROCESSNAME
|
|
# nc-find.sh --where THREAD # like `<thread> where`: file:line of the thread's declaration
|
|
# nc-find.sh --xlate XLATENAME # threads referencing a specific .xlt file
|
|
# nc-find.sh --tclproc TCLPROC # threads referencing a specific tclproc
|
|
#
|
|
# Common flags:
|
|
# --hciroot DIR # default $HCIROOT
|
|
# --netconfigs PATHS # colon-separated explicit list (overrides --hciroot)
|
|
# --format tsv|table|jsonl # default: table
|
|
# --case-sensitive # default: case-insensitive for name/host/process
|
|
set -o pipefail
|
|
|
|
NC_SELF="$0"
|
|
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
|
|
NCP="$LIB_DIR/nc-parse.sh"
|
|
|
|
die() { printf 'nc-find: %s\n' "$*" >&2; exit 1; }
|
|
|
|
MODE=""
|
|
QUERY=""
|
|
HCIROOT_OVERRIDE=""
|
|
NETCONFIGS_OVERRIDE=""
|
|
FORMAT="table"
|
|
CASE_SENSITIVE=0
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--name) shift; MODE="name"; QUERY="$1" ;;
|
|
--port) shift; MODE="port"; QUERY="$1" ;;
|
|
--host) shift; MODE="host"; QUERY="$1" ;;
|
|
--process) shift; MODE="process"; QUERY="$1" ;;
|
|
--where) shift; MODE="where"; QUERY="$1" ;;
|
|
--xlate) shift; MODE="xlate"; QUERY="$1" ;;
|
|
--tclproc) shift; MODE="tclproc"; QUERY="$1" ;;
|
|
--hciroot) shift; HCIROOT_OVERRIDE="$1" ;;
|
|
--netconfigs) shift; NETCONFIGS_OVERRIDE="$1" ;;
|
|
--format) shift; FORMAT="$1" ;;
|
|
--case-sensitive) CASE_SENSITIVE=1 ;;
|
|
-h|--help) sed -n '2,22p' "$NC_SELF"; exit 0 ;;
|
|
-*) die "unknown flag: $1" ;;
|
|
*) die "extra arg: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -n "$MODE" ] || die "specify a search mode: --name | --port | --host | --process | --where | --xlate | --tclproc"
|
|
case "$FORMAT" in tsv|table|jsonl) ;; *) die "bad --format: $FORMAT" ;; esac
|
|
|
|
# Resolve NetConfig list
|
|
NCONFIGS=()
|
|
if [ -n "$NETCONFIGS_OVERRIDE" ]; then
|
|
IFS=':' read -ra NCONFIGS <<< "$NETCONFIGS_OVERRIDE"
|
|
else
|
|
ROOT="${HCIROOT_OVERRIDE:-${HCIROOT:-}}"
|
|
[ -n "$ROOT" ] || die "no \$HCIROOT and no --hciroot/--netconfigs"
|
|
while IFS= read -r nc; do NCONFIGS+=("$nc"); done < <(find "$ROOT" -maxdepth 2 -name NetConfig -type f 2>/dev/null)
|
|
fi
|
|
[ ${#NCONFIGS[@]} -gt 0 ] || die "no NetConfig files found"
|
|
|
|
# Use a temp file to collect results, then format
|
|
RESULTS=$(mktemp)
|
|
trap 'rm -f "$RESULTS"' EXIT
|
|
|
|
# Helper: emit one result row to $RESULTS
|
|
emit() {
|
|
# cols: site \t thread \t port \t host \t process \t direction \t file \t line
|
|
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "$@" >> "$RESULTS"
|
|
}
|
|
|
|
GREP_FLAGS=""
|
|
[ "$CASE_SENSITIVE" = "0" ] && GREP_FLAGS="-i"
|
|
|
|
# Per-NetConfig scanning
|
|
for nc in "${NCONFIGS[@]}"; do
|
|
site=$(basename "$(dirname "$nc")")
|
|
|
|
case "$MODE" in
|
|
name|where)
|
|
# Find protocol declarations matching the pattern
|
|
if [ "$MODE" = "where" ]; then
|
|
# Exact match (one specific thread name)
|
|
line=$(grep -nE "^protocol[[:space:]]+${QUERY}[[:space:]]+\{" "$nc" 2>/dev/null | head -1 | cut -d: -f1)
|
|
if [ -n "$line" ]; then
|
|
pname=$("$NCP" protocol-field "$nc" "$QUERY" PROCESSNAME 2>/dev/null | head -1)
|
|
pport=$("$NCP" protocol-nested "$nc" "$QUERY" PROTOCOL.PORT 2>/dev/null | head -1 | sed 's/^{}$//')
|
|
phost=$("$NCP" protocol-nested "$nc" "$QUERY" PROTOCOL.HOST 2>/dev/null | head -1 | sed 's/^{}$//')
|
|
isserver=$("$NCP" protocol-nested "$nc" "$QUERY" PROTOCOL.ISSERVER 2>/dev/null | head -1)
|
|
obib=$("$NCP" protocol-field "$nc" "$QUERY" OBWORKASIB 2>/dev/null | head -1)
|
|
outonly=$("$NCP" protocol-field "$nc" "$QUERY" OUTBOUNDONLY 2>/dev/null | head -1)
|
|
direction="?"
|
|
[ "$isserver" = "1" ] && direction="inbound-tcp"
|
|
[ "$obib" = "1" ] && [ "$direction" = "?" ] && direction="inbound-icl"
|
|
[ "$outonly" = "1" ] && [ "$direction" = "?" ] && direction="outbound"
|
|
emit "$site" "$QUERY" "${pport:-—}" "${phost:-—}" "${pname:-?}" "$direction" "$nc" "$line"
|
|
fi
|
|
else
|
|
# Partial match (substring)
|
|
while IFS= read -r raw; do
|
|
line=$(printf '%s' "$raw" | cut -d: -f1)
|
|
# PORTABILITY: POSIX [[:space:]][[:space:]]* / [A-Za-z0-9_][A-Za-z0-9_]*
|
|
# instead of the GNU-only `\+`. BSD sed (macOS) treats `\+` as a
|
|
# literal `+`, so the GNU form extracted an empty name and every
|
|
# --name match was silently dropped on macOS/BSD hosts.
|
|
thread_name=$(printf '%s' "$raw" | sed -n 's/^[0-9]*:protocol[[:space:]][[:space:]]*\([A-Za-z0-9_][A-Za-z0-9_]*\)[[:space:]]*{.*$/\1/p')
|
|
[ -z "$thread_name" ] && continue
|
|
pname=$("$NCP" protocol-field "$nc" "$thread_name" PROCESSNAME 2>/dev/null | head -1)
|
|
pport=$("$NCP" protocol-nested "$nc" "$thread_name" PROTOCOL.PORT 2>/dev/null | head -1 | sed 's/^{}$//')
|
|
phost=$("$NCP" protocol-nested "$nc" "$thread_name" PROTOCOL.HOST 2>/dev/null | head -1 | sed 's/^{}$//')
|
|
isserver=$("$NCP" protocol-nested "$nc" "$thread_name" PROTOCOL.ISSERVER 2>/dev/null | head -1)
|
|
obib=$("$NCP" protocol-field "$nc" "$thread_name" OBWORKASIB 2>/dev/null | head -1)
|
|
outonly=$("$NCP" protocol-field "$nc" "$thread_name" OUTBOUNDONLY 2>/dev/null | head -1)
|
|
direction="?"
|
|
[ "$isserver" = "1" ] && direction="inbound-tcp"
|
|
[ "$obib" = "1" ] && [ "$direction" = "?" ] && direction="inbound-icl"
|
|
[ "$outonly" = "1" ] && [ "$direction" = "?" ] && direction="outbound"
|
|
emit "$site" "$thread_name" "${pport:-—}" "${phost:-—}" "${pname:-?}" "$direction" "$nc" "$line"
|
|
done < <(grep -nE $GREP_FLAGS "^protocol[[:space:]]+[A-Za-z0-9_]*${QUERY}[A-Za-z0-9_]*[[:space:]]*\{" "$nc" 2>/dev/null)
|
|
fi
|
|
;;
|
|
|
|
port)
|
|
# Find protocols whose inner PROTOCOL.PORT equals QUERY.
|
|
"$NCP" list-protocols "$nc" 2>/dev/null | while IFS= read -r tname; do
|
|
p=$("$NCP" protocol-nested "$nc" "$tname" PROTOCOL.PORT 2>/dev/null | head -1)
|
|
if [ "$p" = "$QUERY" ]; then
|
|
pname=$("$NCP" protocol-field "$nc" "$tname" PROCESSNAME 2>/dev/null | head -1)
|
|
phost=$("$NCP" protocol-nested "$nc" "$tname" PROTOCOL.HOST 2>/dev/null | head -1 | sed 's/^{}$//')
|
|
line=$("$NCP" protocol-line "$nc" "$tname" 2>/dev/null)
|
|
isserver=$("$NCP" protocol-nested "$nc" "$tname" PROTOCOL.ISSERVER 2>/dev/null | head -1)
|
|
obib=$("$NCP" protocol-field "$nc" "$tname" OBWORKASIB 2>/dev/null | head -1)
|
|
outonly=$("$NCP" protocol-field "$nc" "$tname" OUTBOUNDONLY 2>/dev/null | head -1)
|
|
direction="?"
|
|
[ "$isserver" = "1" ] && direction="inbound-tcp"
|
|
[ "$obib" = "1" ] && [ "$direction" = "?" ] && direction="inbound-icl"
|
|
[ "$outonly" = "1" ] && [ "$direction" = "?" ] && direction="outbound"
|
|
emit "$site" "$tname" "$p" "${phost:-—}" "${pname:-?}" "$direction" "$nc" "${line:-?}"
|
|
fi
|
|
done
|
|
;;
|
|
|
|
host)
|
|
"$NCP" list-protocols "$nc" 2>/dev/null | while IFS= read -r tname; do
|
|
h=$("$NCP" protocol-nested "$nc" "$tname" PROTOCOL.HOST 2>/dev/null | head -1 | sed 's/^{}$//')
|
|
if [ "$CASE_SENSITIVE" = "1" ]; then
|
|
[[ "$h" == *"$QUERY"* ]] || continue
|
|
else
|
|
shopt -s nocasematch 2>/dev/null
|
|
[[ "$h" == *"$QUERY"* ]] || { shopt -u nocasematch 2>/dev/null; continue; }
|
|
shopt -u nocasematch 2>/dev/null
|
|
fi
|
|
[ -z "$h" ] && continue
|
|
pname=$("$NCP" protocol-field "$nc" "$tname" PROCESSNAME 2>/dev/null | head -1)
|
|
p=$("$NCP" protocol-nested "$nc" "$tname" PROTOCOL.PORT 2>/dev/null | head -1 | sed 's/^{}$//')
|
|
line=$("$NCP" protocol-line "$nc" "$tname" 2>/dev/null)
|
|
emit "$site" "$tname" "${p:-—}" "$h" "${pname:-?}" "?" "$nc" "${line:-?}"
|
|
done
|
|
;;
|
|
|
|
process)
|
|
"$NCP" list-protocols "$nc" 2>/dev/null | while IFS= read -r tname; do
|
|
pname=$("$NCP" protocol-field "$nc" "$tname" PROCESSNAME 2>/dev/null | head -1)
|
|
if [ "$CASE_SENSITIVE" = "1" ]; then
|
|
[[ "$pname" == *"$QUERY"* ]] || continue
|
|
else
|
|
shopt -s nocasematch 2>/dev/null
|
|
[[ "$pname" == *"$QUERY"* ]] || { shopt -u nocasematch 2>/dev/null; continue; }
|
|
shopt -u nocasematch 2>/dev/null
|
|
fi
|
|
p=$("$NCP" protocol-nested "$nc" "$tname" PROTOCOL.PORT 2>/dev/null | head -1 | sed 's/^{}$//')
|
|
h=$("$NCP" protocol-nested "$nc" "$tname" PROTOCOL.HOST 2>/dev/null | head -1 | sed 's/^{}$//')
|
|
line=$("$NCP" protocol-line "$nc" "$tname" 2>/dev/null)
|
|
emit "$site" "$tname" "${p:-—}" "${h:-—}" "$pname" "?" "$nc" "${line:-?}"
|
|
done
|
|
;;
|
|
|
|
xlate|tclproc)
|
|
# threads that reference a given xlate or tclproc
|
|
need_pattern="$QUERY"
|
|
"$NCP" list-protocols "$nc" 2>/dev/null | while IFS= read -r tname; do
|
|
if [ "$MODE" = "xlate" ]; then
|
|
hits=$("$NCP" xlate-refs "$nc" "$tname" 2>/dev/null | grep -F -- "$need_pattern" || true)
|
|
else
|
|
hits=$("$NCP" tclproc-refs "$nc" "$tname" 2>/dev/null | grep -F -- "$need_pattern" || true)
|
|
fi
|
|
[ -z "$hits" ] && continue
|
|
pname=$("$NCP" protocol-field "$nc" "$tname" PROCESSNAME 2>/dev/null | head -1)
|
|
line=$("$NCP" protocol-line "$nc" "$tname" 2>/dev/null)
|
|
emit "$site" "$tname" "—" "—" "${pname:-?}" "?" "$nc" "${line:-?}"
|
|
done
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Format output
|
|
case "$FORMAT" in
|
|
tsv)
|
|
printf "site\tthread\tport\thost\tprocess\tdirection\tfile\tline\n"
|
|
cat "$RESULTS"
|
|
;;
|
|
table)
|
|
{
|
|
printf "site\tthread\tport\thost\tprocess\tdirection\tline\n"
|
|
awk -F'\t' '{printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $8}' "$RESULTS"
|
|
} | awk -F'\t' '
|
|
{ for (i=1;i<=NF;i++){ if (length($i)>w[i]) w[i]=length($i); cell[NR,i]=$i }; rows=NR; cols=NF }
|
|
END {
|
|
for (r=1; r<=rows; r++) {
|
|
for (c=1; c<=cols; c++) printf "%-*s ", w[c], cell[r,c]
|
|
printf "\n"
|
|
if (r==1) { for (c=1; c<=cols; c++) for (k=0;k<w[c];k++) printf "-"; for (c=1; c<=cols; c++) printf " "; printf "\n" }
|
|
}
|
|
}'
|
|
;;
|
|
jsonl)
|
|
awk -F'\t' '
|
|
function esc(s) { gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s); return s }
|
|
{ printf "{\"site\":\"%s\",\"thread\":\"%s\",\"port\":\"%s\",\"host\":\"%s\",\"process\":\"%s\",\"direction\":\"%s\",\"file\":\"%s\",\"line\":\"%s\"}\n",
|
|
esc($1),esc($2),esc($3),esc($4),esc($5),esc($6),esc($7),esc($8) }
|
|
' "$RESULTS"
|
|
;;
|
|
esac
|
|
|
|
# Emit count to stderr
|
|
# v0.7.5: tr -cd '0-9' instead of tr -d ' ' — Cygwin wc.exe CR-taint defense
|
|
# (printf '%d' "5\r" fails with "invalid number").
|
|
n=$(wc -l < "$RESULTS" | tr -cd '0-9')
|
|
[ "$FORMAT" = "table" ] && printf '\n%d match(es)\n' "${n:-0}" >&2
|
|
# Always exit 0 on a successful search (even zero matches). Without this, the
|
|
# trailing `[ "$FORMAT" = "table" ] && ...` test leaves the script exit code at
|
|
# 1 for tsv/jsonl (the && short-circuits), mis-signaling failure to callers.
|
|
exit 0
|