I'm looking for a nuanced history behavior I haven't seen anywhere.
- During a terminal session, I want every command, regardless of exit status, to be appended to the in-memory history.
- 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, replacingIDENTIFIER
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, doinghistory -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 myHISTCONTROL
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.
grep foo file
will exit with1
(failure) if there is nofoo
in the file. But the command ran successfully. Would that be a failed command for you? Conversely,find . -name foo
will exit with0
(success) even if there is no file namedfoo
. Is that a failed command? Or what if you run thefalse
builtin which always exits with failure. Should that count as a failed command even though its purpose is to fail?grep foo file
even if it didn't find results, because it ran. I would also want thatfind
to be preserved, because it ran.false
specifically seems like a real edge case, and I'd be fine resolving that in either way. The post I link to uses the test$exit_status -eq 127
, which is "command not found." Beyond that, it's my understanding that commands are free to use non-zero exit codes as they see fit, so perhaps the goal is to avoid preserving typos / syntax errors / non-real commands.