Files
self-healing-autopoietic-shell/contrib/fx/fx.sh
T

94 lines
3.8 KiB
Bash
Raw Normal View History

#!/bin/sh
# fx: automates f()'s "enter a fix" prompt by gathering candidate fixes
# from a directory of pluggable sources and letting you pick one (or
# auto-picking the top one) before piping it into f's stdin.
#
# Zero changes to f.sh: this only decides what gets typed at the
# "[LEARN] ... Enter a fix" prompt, using the stdin f() already reads.
#
# Usage:
# . src/f.sh
# . contrib/fx/fx.sh
# fx some-command --that --might --fail
#
# Env vars:
# FX_SOURCES_DIR the sources directory. Defaults to ./contrib/fx/sources.d
# (i.e. source this file from the repo root). There is no
# portable way for a *sourced* POSIX shell file to learn
# its own path -- $0 is the enclosing shell's name, and
# bash's BASH_SOURCE has no equivalent in dash/ash, so
# this does not try to be clever about it. Anything
# other than "source from repo root" (a different cwd,
# a temp dir a la bootstrap.sh) must set this explicitly
# *before* sourcing this file.
# FX_AUTO if set, auto-pick the top candidate instead of
# showing a picker
# FX_NO_PROBE if set, skip fx's own diagnostic run of the command
# (cheaper, but the 30-selfdiag.sh source gets no
# output to read and will have no opinion)
#
# Source contract (see sources.d/*.sh):
# - any executable file in $FX_SOURCES_DIR
# - invoked as: sourcefile "$CMD"
# - may read $FX_OUTPUT (captured stdout+stderr of one real attempt,
# empty if FX_NO_PROBE was set)
# - prints zero or more candidate fix lines to stdout, one per line
# - non-zero exit or no output = "no opinion", silently skipped
# - filename prefix (NN-name) sets display/consideration order
#
# Adding a source: drop a new executable file in sources.d/ that follows
# the contract above. Nothing else to register or edit.
# Resolved to an absolute path *now*, at source time -- a relative
# default would silently re-resolve against whatever cwd happens to be
# active later when fx() is actually called, which is almost never the
# repo root in practice.
FX_SOURCES_DIR="${FX_SOURCES_DIR:-$(pwd)/contrib/fx/sources.d}"
fx() {
if [ ! -d "$FX_SOURCES_DIR" ]; then
echo "fx: FX_SOURCES_DIR '$FX_SOURCES_DIR' not found -- no sources will fire." >&2
echo "fx: set FX_SOURCES_DIR before sourcing fx.sh if not running from the repo root." >&2
fi
cmd="$*"
FX_OUTPUT=""
if [ -z "$FX_NO_PROBE" ]; then
FX_OUTPUT=$("$@" 2>&1)
if [ $? -eq 0 ]; then
printf '%s\n' "$FX_OUTPUT"
return 0
fi
fi
export FX_OUTPUT
candidates=$(
for src in "$FX_SOURCES_DIR"/*; do
[ -x "$src" ] || continue
label="${src##*/}"
"$src" "$cmd" 2>/dev/null | while IFS= read -r line; do
[ -n "$line" ] && printf '[%s] %s\n' "$label" "$line"
done
done | awk -F'] ' '!seen[$2]++'
)
chosen=""
if [ -n "$candidates" ]; then
if [ -n "$FX_AUTO" ]; then
chosen=$(printf '%s\n' "$candidates" | head -1 | sed 's/^\[[^]]*\] //')
elif command -v fzf >/dev/null 2>&1; then
chosen=$(printf '%s\n' "$candidates" | fzf --prompt="fix for: $cmd > " --height=40% | sed 's/^\[[^]]*\] //')
else
i=0
printf '%s\n' "$candidates" | while IFS= read -r line; do
i=$((i+1)); printf '%d) %s\n' "$i" "$line"
done
printf 'pick a number (blank to type your own): '
read -r n
[ -n "$n" ] && chosen=$(printf '%s\n' "$candidates" | sed -n "${n}p" | sed 's/^\[[^]]*\] //')
fi
fi
printf '%s\n' "$chosen" | f "$@"
}