Bash – how to avoid writing failed bash commands to bash_history

bashcommand history

I'm looking for a nuanced history behavior I haven't seen anywhere.

  1. During a terminal session, I want every command, regardless of exit status, to be appended to the in-memory history.
  2. Upon exiting the session, when the in-memory history is written to disk at ~/.bash_history, I want to omit commands that failed.

Hopefully the reasoning is clear: within a session, one of the most common uses of history is to re-attempt the last (failed) command, either with a minor edit, or after changing some environmental fact in the hopes of getting better results.

But, at the same time, I don't want my long-term history to be littered with bad commands.

I've seen this post, which shows how to (1) determine the exit status of the last command, and (2) how to remove history entries by index.

And I've seen this answer, which shows how to use trap to execute commands when a session ends.

(I've also studied this mega-answer, which seems to cover a lot of quirks having to do with history manipulation. Although I'm not sure how relevant it is.)

I have a rough idea of how I might accomplish my goal, but I'm not very experienced with bash programming, and I'm having trouble cashing it out. My plan:

  • I create a new file, $TEMP/.bash_history_failures_IDENTIFIER for each terminal session, replacing IDENTIFIER with some kind of session-specific value (PID?) that guarantees multiple sessions don't clobber each other
  • using a combination of number=$(history 1) and [ $exit_status -eq 127 ], I write the index of each bad command to the current session's failures file
  • using the trap hook to execute code on exit, I invoke a function that iterates through the indexes in this session's failures file, doing history -d $number on each entry
  • finally, I let the remains of in-memory history (which should be all and only successful commands) be written to ~/.bash_history in accordance with whatever are my HISTCONTROL settings

Is what I'm proposing possible? What am I overlooking? Will I run into problems deleting items from history in ascending order? What's a good, stable way of creating a unique file for each session?


What do I consider a "failed" command?

I don't want long-term history to contain user-errors:

  • commands that don't exist
  • syntax errors

I want to keep commands that are correct, even if they were unable to execute for some other reason (e.g. inadequate permissions, non-existent input paths).

The two examples that come to mind are that I constantly forget where to insert the semicolons in one-liners like this:

for pkg in $(ls ./packages); do cp ./.eslintrc.yml ./packages/$pkg/; done

I don't want history to contain the entries where I put the semis in the wrong place.

The other case is that it usually takes me several attempts to come up with the correct options for cut, and most of my mistakes are syntax errors in specifying the arguments to it.

I'd love it if my long-term bash history could omit the many failed attempts at getting working commands right. I'm not concerned about history containing e.g. searches that happen to produce no results.


Edit to add:

The hstr CLI tool for managing history likes to add some stuff to one's shell profile:

$ hstr --show-configuration

# HSTR configuration - add this to ~/.bashrc
alias hh=hstr                    # hh to be alias for hstr
export HSTR_CONFIG=hicolor       # get more colors
shopt -s histappend              # append new history items to .bash_history
export HISTCONTROL=ignorespace   # leading space hides commands from history
export HISTFILESIZE=10000        # increase history file size (default is 500)
export HISTSIZE=${HISTFILESIZE}  # increase history size (default is 500)
# ensure synchronization between bash memory and history file
export PROMPT_COMMAND="history -a; history -n; ${PROMPT_COMMAND}"
# if this is interactive shell, then bind hstr to Ctrl-r (for Vi mode check doc)
if [[ $- =~ .*i.* ]]; then bind '"\C-r": "\C-a hstr -- \C-j"'; fi
# if this is interactive shell, then bind 'kill last command' to Ctrl-x k
if [[ $- =~ .*i.* ]]; then bind '"\C-xk": "\C-a hstr -k \C-j"'; fi

I have a hunch a solution can be cannibalized from hstr's codebase.

Best Answer

Try this:

export PROMPT_COMMAND='LAST_COMMAND_EXIT=$? && history -a && test 127 -eq $LAST_COMMAND_EXIT && head -n -2 $HISTFILE >${HISTFILE}_temp && mv ${HISTFILE}_temp $HISTFILE'

Simply deletes the last two line (timestamp and command) of histfile if the exit status is 127. Works well for me, if you don't have timestamp in history you should use head -n -1 instead of -2

Of course you should add this line to your .bashrc to make it persistent.

Related Question