From df3c32842468800ee3bd0d31d439b413ed495ed4 Mon Sep 17 00:00:00 2001 From: Daveswo <969dwi@gmail.com> Date: Mon, 17 Aug 2026 23:19:07 -0400 Subject: [PATCH] Fix real hardware/busybox breakage: dead compgen dependency, unreliable sourced-path detection, and fork-heavy pipelines in fx sources; verified against busybox ash+awk directly --- contrib/fx/bootstrap.sh | 0 contrib/fx/fx.sh | 28 +++++++++++++++++++--------- contrib/fx/sources.d/20-history.sh | 16 +++++++++++++++- contrib/fx/sources.d/30-selfdiag.sh | 7 ++++++- contrib/fx/sources.d/40-pathfuzzy.sh | 14 +++++++++++--- 5 files changed, 51 insertions(+), 14 deletions(-) mode change 100644 => 100755 contrib/fx/bootstrap.sh mode change 100644 => 100755 contrib/fx/fx.sh diff --git a/contrib/fx/bootstrap.sh b/contrib/fx/bootstrap.sh old mode 100644 new mode 100755 diff --git a/contrib/fx/fx.sh b/contrib/fx/fx.sh old mode 100644 new mode 100755 index 101320d..1887c87 --- a/contrib/fx/fx.sh +++ b/contrib/fx/fx.sh @@ -12,8 +12,15 @@ # fx some-command --that --might --fail # # Env vars: -# FX_SOURCES_DIR override the sources directory (default: sources.d -# next to this script) +# 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 @@ -32,14 +39,17 @@ # 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 +# 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="" @@ -55,7 +65,7 @@ fx() { candidates=$( for src in "$FX_SOURCES_DIR"/*; do [ -x "$src" ] || continue - label=$(basename "$src") + label="${src##*/}" "$src" "$cmd" 2>/dev/null | while IFS= read -r line; do [ -n "$line" ] && printf '[%s] %s\n' "$label" "$line" done diff --git a/contrib/fx/sources.d/20-history.sh b/contrib/fx/sources.d/20-history.sh index fc06ffe..5e9f98e 100755 --- a/contrib/fx/sources.d/20-history.sh +++ b/contrib/fx/sources.d/20-history.sh @@ -3,9 +3,23 @@ # typed and moved on from before (thefuck's history.py idea). Reads # HISTFILE directly since a separate process doesn't inherit the parent # shell's in-memory history. +# +# Single awk pass over the tail of the file: on weak/emulated hardware +# every avoided fork+exec matters (measured: an unoptimized PATH walk in +# 40-pathfuzzy.sh took >30s on this box before being fixed; pipeline +# depth has the same tax, just smaller per-hop) -- a five-process +# tail|grep|grep|awk|tail chain does the same job as one awk script +# reading the file once. cmd="$1" first_word=$(printf '%s' "$cmd" | awk '{print $1}') [ -n "$first_word" ] || exit 0 hf="${HISTFILE:-$HOME/.bash_history}" [ -r "$hf" ] || exit 0 -tail -n 300 "$hf" 2>/dev/null | grep -F -- "$first_word " | grep -vF -- "$cmd" | awk '!seen[$0]++' | tail -5 + +tail -n 300 "$hf" 2>/dev/null | awk -v first="$first_word" -v full="$cmd" ' +index($0, first " ") == 1 && $0 != full && !seen[$0]++ { buf[++n] = $0 } +END { + start = (n > 5) ? n - 4 : 1 + for (i = start; i <= n; i++) print buf[i] +} +' diff --git a/contrib/fx/sources.d/30-selfdiag.sh b/contrib/fx/sources.d/30-selfdiag.sh index 7f081f4..ef0d376 100755 --- a/contrib/fx/sources.d/30-selfdiag.sh +++ b/contrib/fx/sources.d/30-selfdiag.sh @@ -12,6 +12,11 @@ # correctly across branches/arguments in practice. One taught fix this # way covers every future self-diagnosing failure from that program, not # just the one instance that happened to trigger the teaching prompt. +# +# The emitted function runs on every future call to that program for +# the rest of the session, so its own internal extraction is one awk +# pass, not a grep|sed|tail chain -- three fewer forks on every failure +# it ever handles, not just this first one. cmd="$1" prog=$(printf '%s' "$cmd" | awk '{print $1}') [ -n "$prog" ] || exit 0 @@ -19,5 +24,5 @@ prog=$(printf '%s' "$cmd" | awk '{print $1}') printf '%s\n' "$FX_OUTPUT" | grep -qE "^[[:space:]]*${prog}[[:space:]]" || exit 0 cat <&1); ec=\$?; if [ \$ec -ne 0 ]; then sug=\$(printf '%s\\n' "\$out" | grep -E "^[[:space:]]*${prog}[[:space:]]" | sed 's/^[[:space:]]*//' | tail -1); if [ -n "\$sug" ]; then eval "command \$sug"; return \$?; fi; fi; printf '%s\\n' "\$out"; return \$ec; } +${prog}(){ out=\$(command ${prog} "\$@" 2>&1); ec=\$?; if [ \$ec -ne 0 ]; then sug=\$(printf '%s\\n' "\$out" | awk '/^[ \\t]*${prog}[ \\t]/{sub(/^[ \\t]*/,""); s=\$0} END{print s}'); if [ -n "\$sug" ]; then eval "command \$sug"; return \$?; fi; fi; printf '%s\\n' "\$out"; return \$ec; } FIX diff --git a/contrib/fx/sources.d/40-pathfuzzy.sh b/contrib/fx/sources.d/40-pathfuzzy.sh index fdc51cf..41720e2 100755 --- a/contrib/fx/sources.d/40-pathfuzzy.sh +++ b/contrib/fx/sources.d/40-pathfuzzy.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # PATH-fuzzy source: if the first word isn't a known command, alias, or # function, offer a function wrapping the closest-spelled real command # as a candidate -- as a fix you approve through the normal f() flow, @@ -7,6 +7,12 @@ # otherwise), so an alias-based fix silently does nothing when f/fx run # inside a script. # +# Pure POSIX: walks $PATH directly instead of `compgen -c`, which is a +# bash-only builtin and does not exist under busybox ash -- this source +# used to be dead weight on exactly the minimal/embedded shells this +# project targets. Tested against busybox ash + busybox awk directly, +# not just bash + gawk. +# # Uses Damerau-Levenshtein-lite (edit distance + adjacent transposition) # so the canonical "gti" -> "git" typo scores 1, not 2 -- plain Hamming # or substitution-only distance ties it with unrelated 3-letter commands @@ -15,9 +21,11 @@ cmd="$1" first_word=$(printf '%s' "$cmd" | awk '{print $1}') [ -n "$first_word" ] || exit 0 command -v "$first_word" >/dev/null 2>&1 && exit 0 -type "$first_word" >/dev/null 2>&1 && exit 0 -compgen -c 2>/dev/null | sort -u | awk -v target="$first_word" ' +IFS=: +for d in $PATH; do + ls "$d" 2>/dev/null +done | sort -u | awk -v target="$first_word" ' function min3(a, b, c) { return (a < b ? (a < c ? a : c) : (b < c ? b : c)) } function distance(a, b, n, m, i, j, ca, cb, cost, tmp) { n = length(a); m = length(b)