cloverleaf-larry/lib/nc-find.sh
Bryan Johnson 9dd5821436 v0.7.5: OAuth CR-taint fix + mouse opt-in + CR-safety sweep
- 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>
2026-05-27 19:17:48 -07:00

236 lines
11 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)
thread_name=$(printf '%s' "$raw" | sed -n 's/^[0-9]*:protocol[[:space:]]\+\([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