#!/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 override the sources directory (default: sources.d # next to this script) # 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. # $0 is the enclosing shell's name when this file is *sourced*, not this # file's own path -- ${BASH_SOURCE[0]} is reliable under bash; for other # POSIX shells, set FX_SOURCES_DIR explicitly before sourcing this file. _fx_self="${BASH_SOURCE:-$0}" FX_SOURCES_DIR="${FX_SOURCES_DIR:-$(CDPATH= cd -- "$(dirname -- "$_fx_self")" 2>/dev/null && pwd)/sources.d}" unset _fx_self fx() { 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=$(basename "$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 "$@" }