Skip to content

The Bleep 4.0.3

Latest

Choose a tag to compare

@stamparm stamparm released this 21 Aug 09:23
· 2 commits to master since this release

Reading what your failed command printed sometimes means running it again, and
this release is mostly about that decision being sound. Three independent
reviews took the tool apart; everything below came out of them, out of the
suggestion corpus, or out of sitting down and typing typos into a real shell.

Upgrade if you use the automatic replay — that is, if you have ever seen
"has to run again to be read" and let it go ahead without being asked.

Your previous command runs again less often, and never on a bad proof

bleep skips asking when running your command again cannot have an effect. Four
ways it could reach that conclusion wrongly:

  • Exit status 127 meant "nothing happened". It does not. make, npm run
    and sh -c all report a child's missing command that way, after the rest of
    the work has already happened — so a failed build could be re-run unasked.
  • An empty PATH entry means the current directory. Skipping it made a
    local ./deploy look absent, so it was re-run without asking.
  • bash -c imports exported shell functions, and sources $BASH_ENV before
    the command it was given. A function that failed ran twice; a command that did
    not exist at all still had an effect.
  • An assignment in front of the command was dropped from the proof.
    PATH=/tmp/mine git satus had the dispatcher asking /usr/bin/git about
    satus and using the answer to authorise re-running a different git.

Each of those was reproduced end to end before it was fixed, and each has a test
that fails if it comes back.

This means more prompts than 4.0.2, not fewer. A command with a leading
assignment (LC_ALL=C ls), or given as a path (/bin/ls, ./deploy), now asks
before it is re-run. The alternative was keeping a list of environment variables
believed harmless, and this is the wrong project for a list like that.

Also: xxd is no longer treated as read-only (it takes an output file, and
xxd -r patches one in place), and git commit that failed is no longer
answered with git reset HEAD~ — every failed commit is a commit that has not
happened, so what that offered to throw away was the one before it.

Two ways it could run code as you

  • The cache directory fell back to a predictable name in a shared /tmp when
    there was no home directory to use, created with whatever the umask allowed.
    The compiled rule pack lives there and is executed on the next correction, so
    whoever created that directory first ran code as you. It is 0700 now, and
    refused outright if it belongs to somebody else.
  • The first-run tracker was a predictable /tmp name opened for writing, so a
    symlink planted there was followed.

--debug no longer prints the values of your env setting, which is where
people keep tokens — and the issue template now says to read what you paste.

It is right more often

Four rules were dead against what their tool actually printsgit add,
git stash pp, git rebase with a leftover merge directory, and hostscli
each for however many releases ago the wording moved. The framework catches what
a rule raises, so a dead rule looks exactly like a rule with nothing to say.
That class now has two gates: every rule is thrown malformed output and held to
not raising, and --doctor reports any that do.

Suggestions that were confidently wrong:

  • ls-la answered lsblk, and gitstatus answered aa-status, with ls -la
    and git status sitting one row further down the list.
  • sudo apt-get updte on a machine without sudo answered su do apt-get updte.
  • systemctl statu ssh answered sysctl statu ssh.
  • ln -s /etc/hostname /tmp/link, where the link already existed, offered a
    symlink on top of /etc/hostname.
  • mkdir -p x && rmdir y answered mkdir -p -p x.
  • diff --colour=alwys a b answered diff --color a b, silently dropping the
    value.

And three new rules that each cover an ecosystem rather than a tool:

  • argparse_invalid_choice — Python's standard library parser, so pytest,
    mypy, pre-commit, tox and coverage come with it. Thanks to
    @TrixSec (#5).
  • commander_suggestion — commander.js, so prettier, eslint, prisma,
    nest and turbo come with it. Thanks to
    @TrixSec (#4).
  • git_unknown_subcommandgit remote ad, git worktree lst,
    git notes ad. git prints its usage rather than a suggestion for these, and
    nothing was reading it.
  • invalid_argument_for_optionls --sort=nmae, and every other GNU tool
    that prints the value it refused and the ones it accepts.

The measured suggestion quality is now 85/85 against The Fuck 3.32's
55/85, on a corpus of real captured output. That number is reproducible for the
first time: it used to consult Debian's command-not-found database, so it was
measuring the machine.

Faster where it matters

An unknown command is the commonest way a command fails, and it scanned every
name on PATH twice. 187ms to 93ms across five typos. A multi-megabyte shell
history is read from the end rather than in full, and a replayed command's output
is capped rather than held whole.

Also

Rules can no longer start a program without a timeout — twenty of them could,
and a rule that never returns is bleep frozen at your prompt. Every GitHub
Action in the release workflow is pinned to a commit. ctrl+c at the prompt no
longer raises, on Windows or on POSIX. ctrl+p and ctrl+n go the way every
shell's history goes.

Full detail: CHANGELOG.md