diff --git a/functions/cleanup.fish b/functions/cleanup.fish index eaa30db..b908ee7 100644 --- a/functions/cleanup.fish +++ b/functions/cleanup.fish @@ -14,6 +14,8 @@ # EXAMPLE # cleanup function cleanup --description 'Log orphans to ~/.removed_orphans and remove them' + __fish_help_header (status current-function) $argv; and return 0 + set -l orphans (pacman -Qtdq) if test -n "$orphans" echo "📝 Logging orphans to ~/.removed_orphans..." diff --git a/functions/fzf-update.fish b/functions/fzf-update.fish index 1a403ed..5297dc8 100644 --- a/functions/fzf-update.fish +++ b/functions/fzf-update.fish @@ -14,6 +14,8 @@ # EXAMPLE # fzf-update function fzf-update --description 'Install or upgrade fzf from git HEAD' + __fish_help_header (status current-function) $argv; and return 0 + if test -d ~/.fzf echo "Updating fzf..." git -C ~/.fzf pull --ff-only diff --git a/functions/limine-edit.fish b/functions/limine-edit.fish index f22a652..9f3b502 100644 --- a/functions/limine-edit.fish +++ b/functions/limine-edit.fish @@ -16,6 +16,8 @@ # EXAMPLE # limine-edit function limine-edit --description 'Safely edit and re-verify Limine configuration' + __fish_help_header (status current-function) $argv; and return 0 + # 1. Open the config with sudoedit sudoedit /boot/limine.conf diff --git a/functions/lock.fish b/functions/lock.fish index c1e61bc..302d29d 100644 --- a/functions/lock.fish +++ b/functions/lock.fish @@ -13,5 +13,7 @@ # EXAMPLE # lock function lock --wraps='loginctl' --description 'alias lock=loginctl' + __fish_help_header (status current-function) $argv; and return 0 + loginctl lock-session end diff --git a/functions/screensleep.fish b/functions/screensleep.fish index 452ff9e..0bb4442 100644 --- a/functions/screensleep.fish +++ b/functions/screensleep.fish @@ -14,6 +14,8 @@ # EXAMPLE # screensleep function screensleep --description 'Turn off the display using KDE PowerDevil' + __fish_help_header (status current-function) $argv; and return 0 + # Optional: 1-second delay to ensure no keystrokes wake it immediately sleep 1 busctl --user call \ diff --git a/functions/sudo-toggle.fish b/functions/sudo-toggle.fish index e46547f..c218e98 100644 --- a/functions/sudo-toggle.fish +++ b/functions/sudo-toggle.fish @@ -19,6 +19,8 @@ # EXAMPLE # sudo-toggle function sudo-toggle --description 'Toggle sudo password requirement on/off' + __fish_help_header (status current-function) $argv; and return 0 + # Check the file size using sudo stat to see if our bypass rule is active set -l file_size (sudo stat -c %s /etc/sudoers.d/nofail-toggle 2>/dev/null) diff --git a/functions/tmux-clean.fish b/functions/tmux-clean.fish index cf775c4..c31765e 100644 --- a/functions/tmux-clean.fish +++ b/functions/tmux-clean.fish @@ -14,6 +14,8 @@ # EXAMPLE # tmux-clean function tmux-clean --description 'Kill all tmux sessions except the current one' + __fish_help_header (status current-function) $argv; and return 0 + # Get a list of all session names that are NOT currently attached set sessions (tmux list-sessions -F '#{session_name} #{session_attached}' | string match -rv ' 1$' | string split -f1 ' ') diff --git a/functions/upgrade.fish b/functions/upgrade.fish index 68702bb..20d2fc9 100644 --- a/functions/upgrade.fish +++ b/functions/upgrade.fish @@ -21,6 +21,8 @@ # EXAMPLE # upgrade function upgrade --description 'Full system upgrade via paru or yay' + __fish_help_header (status current-function) $argv; and return 0 + # Opinionated guard (C4): integrations disabled if not __fish_config_op_enabled (status current-function) set -l c_err (set_color red) diff --git a/tests/functional.fish b/tests/functional.fish index 2f356e4..f027194 100644 --- a/tests/functional.fish +++ b/tests/functional.fish @@ -237,6 +237,58 @@ function test_help_renderer_degrades_safely test $failed -eq 0 end +function test_help_never_executes_destructive_path + # These eight ignore $argv entirely, so before the header-driven help + # landed, `upgrade --help` ran `paru -Syu --noconfirm`. The check has + # to prove --help does NOT reach the destructive path *without* ever + # running it: every external binary the eight can reach is shadowed by + # a recording stub on PATH, and the recorder must stay empty. + # + # WARNING: a silent pass here means a MISSING STUB, not success. If a + # function shows neither an EXECUTED line nor its own help, its + # command is absent from the stub list below -- add it. A test that + # cannot fail proves nothing about a body that runs sudo pacman -Rns. + set -l root (realpath (dirname (status filename))/..) + set -l tmp (mktemp -d) + mkdir -p $tmp/bin + set -l log $tmp/invoked.log + touch $log + + for b in sudo pacman paru yay loginctl busctl tmux systemd-inhibit \ + sudoedit limine-enroll-config limine-mkinitcpio sbctl git fzf steam + printf '#!/bin/sh\necho "$(basename "$0") $*" >> %s\n' $log >$tmp/bin/$b + chmod +x $tmp/bin/$b + end + + set -l failed 0 + for fn in cleanup fzf-update limine-edit lock screensleep sudo-toggle \ + tmux-clean upgrade + set -l out (env TERM=dumb PATH="$tmp/bin:$PATH" HOME=$tmp \ + fish --no-config -c \ + "set -g fish_function_path $root/functions $fish_function_path + $fn --help" 2>/dev/null) + set -l code $status + + if test $code -ne 0 + echo " $fn --help exited $code, expected 0" + set failed 1 + end + if not contains -- $fn $out + echo " $fn --help did not print its own help" + set failed 1 + end + set -l ran (string trim -- (command cat $log)) + if test -n "$ran" + echo " $fn --help EXECUTED: $ran" + set failed 1 + end + echo -n "" >$log + end + + rm -rf $tmp + test $failed -eq 0 +end + function functional_test_main set -l names (functions -a | string match 'test_*' | sort) set -l failed 0