9

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.

5
  • 8
    How do you define a successful command? For example, grep foo file will exit with 1 (failure) if there is no foo in the file. But the command ran successfully. Would that be a failed command for you? Conversely, find . -name foo will exit with 0 (success) even if there is no file named foo. Is that a failed command? Or what if you run the false builtin which always exits with failure. Should that count as a failed command even though its purpose is to fail?
    – terdon
    Commented Oct 13, 2018 at 11:38
  • I hadn't counted on that being as thorny as you suggest. I would want to preserve grep foo file even if it didn't find results, because it ran. I would also want that find 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.
    – Tom
    Commented Oct 13, 2018 at 16:50
  • 1
    When exactly do you want these incorrect commands to vanish from history? After you finally fix them after some number of attempts, so that you can use history for fixing? Or immediately after you try to run such a command?
    – muru
    Commented Oct 15, 2018 at 4:38
  • I want them to disappear when the terminal session is terminated. And anyway, I don't think there's any kind of good value for "after X attempts." From what I can glean, each session keeps its new history entries in RAM until the session is closed, at which point its history is written to disk. I want bad commands to stay in RAM along with good commands, but to not survive the transition to permanent storage.
    – Tom
    Commented Oct 15, 2018 at 18:15
  • Actually, I'm not certain about my speculation about RAM vs disk. It's possible that what I've been seeing is that concurrent terminal sessions simply don't update from shared history, not that shared history isn't being written to on a per-command basis. In either case, I think it's not only acceptable but desirable for a session to clean up its own bad commands when the session ends and not before.
    – Tom
    Commented Oct 15, 2018 at 18:17

2 Answers 2

1

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.

0

Add this to you Bash profile file:

nohist() { history -d $HISTCMD; }; trap nohist ERR

EDIT: this is better as the error line can be retrieved easily:

ce() { eval "$BASH_COMMAND;  e='$BASH_COMMAND'"; false; }
if [[ $- == *i* ]]; then trap ce ERR; fi # capture error command
alias e='$e' # type e and then expand with ctrl-alt-e

Also, this will capture every output for reuse (but it has an issue with echo and hist

save_output() { 
   exec 1>&3 
   { [ -f /tmp/current ] && mv /tmp/current /tmp/last; }
   exec > >(tee /tmp/current)
}

exec 3>&1
trap save_output DEBUG

https://stackoverflow.com/a/48398357/4240654

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.