119

First of all, this is not a duplicate of any existing threads on SE. I have read these two threads (1st, 2nd) on better bash history, but none of the answers work - - I am on Fedora 15 by the way.

I added the following to the .bashrc file in the user directory (/home/aahan/), and it doesn't work. Anyone has a clue?

HISTCONTROL=ignoredups:erasedups  # no duplicate entries
HISTSIZE=1000                     # custom history size
HISTFILESIZE=100000                 # custom history file size
shopt -s histappend                      # append to history, don't overwrite it
PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"  # Save and reload the history after each command finishes

Okay this is what I want with the bash history (priority):

  • don't store duplicates, erase any existing ones
  • immediately share history with all open terminals
  • always append history, not overwrite it
  • store multi-line commands as a single command (which is off by default)
  • what's the default History size and history file size?
its_me
  • 13,959
  • 1
    bump! anyone? Nothing works. Could this be a problem with Fedora 15 (with Gnome 3 and running on a Windows host virtual machine)? – its_me Aug 07 '11 at 17:53
  • Please don't "bump" posts here. If they aren't getting answered you need to ask more clearly, include the right bits of context clues, or something. If you need attention on your posts you can issue bounties that help get more people paying them more attention. – Caleb Aug 08 '11 at 20:07
  • 1
    Are you sure you are even using bash? (echo $SHELL). Do the settings work if you run them manually from your open shell? Obviously since they do work for so many others the settings are right, you are just implementing them wrong. And no Fedora15/Gnome3/being a virtual machine have little to do with the actual function of bash. – Caleb Aug 08 '11 at 20:08
  • @Caleb, first sorry about bumping the post. Also, I tried my best to be very clear. No, I haven't issued them in the shell. I just copy-pasted them into .bashrc file. Is that wrong? Can you add an "answer" to this post with the actual shell commands? (please bear with my noob-ity.) – its_me Aug 08 '11 at 20:25
  • 1
    Everything you write in scripts or .bashrc ARE actual shell commands. Scripts are just series of shell commands. Also the edit you recently made removing the export bit was a bad idea, that should be kept. – Caleb Aug 08 '11 at 20:47
  • So for example, is this export HISTCONTROL=ignoredups:erasedups a command that should be implemented in shell? If so, I think I tried that multiple times. No changes have taken place in the .bashrc file. (isn't that where I should be looking?) – its_me Aug 08 '11 at 20:52
  • Is using zsh an option? Many bash problems can be solved by switching to zsh. History management is one of the areas where zsh is smarter. – Gilles 'SO- stop being evil' Aug 10 '11 at 00:01
  • @Gilles is zsh just like the default terminal (shell)? I mean the commands and stuff? (Checking it out already!) – its_me Aug 10 '11 at 00:06

11 Answers11

160

This is actually a really interesting behavior and I confess I have greatly underestimated the question at the beginning. But first the facts:

1. What works

The functionality can be achieved in several ways, though each works a bit differently. Note that, in each case, to have the history "transferred" to another terminal (updated), one has to press Enter in the terminal, where he/she wants to retrieve the history.

  • option 1:

     shopt -s histappend
     HISTCONTROL=ignoredups
     PROMPT_COMMAND="history -a; history -n; $PROMPT_COMMAND"
    

This has two drawbacks:

  1. At login (opening a terminal), the last command from the history file is read twice into the current terminal's history buffer;
  2. The buffers of different terminals do not stay in sync with the history file.
  • option 2:

     HISTCONTROL=ignoredups
     PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"
    

(Yes, no need for shopt -s histappend and yes, it has to be history -c in the middle of PROMPT_COMMAND) This version has also two important drawbacks:

  1. The history file has to be initialized. It has to contain at least one non-empty line (can be anything).
  2. The history command can give false output - see below.

[Edit] "And the winner is..."

  • option 3:

     HISTCONTROL=ignoredups:erasedups
     shopt -s histappend
     PROMPT_COMMAND="history -n; history -w; history -c; history -r; $PROMPT_COMMAND"
    

This is as far as it gets. It is the only option to have both erasedups and common history working simultaneously. This is probably the final solution to all your problems, Aahan.


2. Why does option 2 not seem to work (or: what really doesn't work as expected)?

As I mentioned, each of the above solutions works differently. But the most misleading interpretation of how the settings work comes from analysing the output of history command. In many cases, the command can give false output. Why? Because it is executed before the sequence of other history commands contained in the PROMPT_COMMAND! However, when using the second or third option, one can monitor the changes of .bash_history contents (using watch -n1 "tail -n20 .bash_history" for example) and see what the real history is.

3. Why option 3 is so complicated?

It all lies in the way erasedups works. As the bash manual states, "(...) erasedups causes all previous lines matching the current line to be removed from the history list before that line is saved". So this is really what the OP wanted (and not just, as I previously thought, to have no duplicates appearing in sequence). Here's why each of the history -. commands either has to or can not be in the PROMPT_COMMAND:

  • history -n has to be there before history -w to read from .bash_history the commands saved from any other terminal,

  • history -w has to be there in order to save the new history to the file after bash has checked if the command was a duplicate,

  • history -a must not be placed there instead of history -w, because it will add to the file any new command, regardless of whether it was checked as a duplicate.

  • history -c is also needed because it prevents trashing the history buffer after each command,

  • and finally, history -r is needed to restore the history buffer from file, thus finally making the history shared across terminal sessions.

Be aware that this solution will mess up the history order by putting all history from other terminals in front of the newest command entered in the current terminal. It also does not delete duplicate lines already in the history file unless you enter that command again.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
  • Actually, the history sharing among terminals is working flawlessly for me. But I can't prevent history duplication. That's the only issue ATM. – its_me Aug 09 '11 at 23:27
  • 3
    +1 for "But the most misleading interpretation of how the settings work comes from analysing the output of history command." I think this is the core of the OP's issue. Excellent deduction. – jasonwryan Aug 09 '11 at 23:29
  • Have you read the whole explanation and tested by tail -f .bash_history? This is much more complicated than it seems. See also my comment under your own answer. – rozcietrzewiacz Aug 09 '11 at 23:29
  • yes, I tried $ tail -f .bash_history and it listed the commands history (but without IDs - - i.e., unlike $ history). Like I said, I am (still) unable to prevent duplication. Does your above answer in a way imply that preventing history duplication is not possible? You gotta see this as well. – its_me Aug 09 '11 at 23:42
  • 2
    I finally saw your point. By 'duplicates' you meant something else than myself. I only focused at sequences of the same command. Updated my answer - see "option 3". Also, for your case, to test how the history works you should actually use watch "tail -n 20 .bash_history" instead of the tail -f .bash_history. – rozcietrzewiacz Aug 10 '11 at 07:43
  • You might also find the following question and it's answer is to be very helpful: http://stackoverflow.com/questions/338285/prevent-duplicates-from-being-saved-in-bash-history#answer-7449399 – trusktr Oct 12 '12 at 20:59
  • 4
    Option 3 just wiped out all of my 100,000 lines of history :( (bash 3.2.25(1)-release) – Felipe Alvarez Jun 10 '14 at 06:51
  • 4
    history -c is also needed because it prevents trashing the history buffer after each command Why does this trashing occurs? – Piotr Dobrogost Feb 29 '16 at 20:42
  • Also note that you may want to check that the value of $PROMPT_COMMAND doesn't already contain history commands so you don't end up reading and writing it dozens of times per <CR> at the prompt. – dragon788 Jun 20 '17 at 21:56
  • 9
    Your solution 3 actually does not work. It's extremely buggy and dependent on the order that users hit enter in different terminals. It will often result in lost commands, especially when switching back and forth between terminals. Source: tried it out. – 6cef Sep 12 '18 at 21:07
  • 6
    In my case I do NOT want to immediately share history, just converge on close while avoiding history loss (such as on termination without clean exit). I settled on adding history -a to the prompt for memory safety, and putting option 3's sequence in a trap function (trap deduphistory EXIT). – HonoredMule Oct 08 '18 at 18:30
  • Additionally, this sequence in the function will remove reliance on adequately sized HISTSIZE: cat ~/.bash_history | nl | sort -k 2 | tac | uniq -f 1 | sort -n | cut -f 2 > ~/.bash_history_new; mv ~/.bash_history_new ~/.bash_history. (In this case history commands are not used at all.) – HonoredMule Oct 08 '18 at 18:33
  • Option 2 seems to work just fine. – Ethereal Mar 06 '19 at 19:02
  • @rozcietrzewiacz, if I don't care at all about duplicated entries in my history file, would you say this answer to another question is correct in its recomendation? Essentially, the advice given uses history -a; instead of history -n; history -w;. – Marc.2377 Jun 17 '19 at 02:10
  • @HonoredMule Would you care putting the "trap" idea into an answer? – quazgar Dec 04 '19 at 07:52
  • Done (plus a few more characters). – HonoredMule Dec 09 '19 at 07:48
  • 2
    The above comment about using tgrap dedupe_history EXIT is the same solution I settled on. Seems less wasteful and susceptible to races than reading/writing my 10,000 line history files on every prompt. Has been working well for me for years now. My own dedupe function reads tac < ~/.bash_history | awk '!a[$0]++' | tac >/tmp/deduped && mv -f /tmp/deduped ~/.bash_history (Otherwise this answer is BRILLIANT though, and enhanced my understanding no end!) – Jonathan Hartley Jun 26 '20 at 17:09
  • 2
    @Piotr Dobrogost When you enter a command, it is appended to the history list and that list is written to the file. Enter a different command, and any new line added to the file will be appended to the history list with history -n, in this case the first command we entered. So you would have a duplicate of that first command in the history list since history -n doesn't remove duplicates. history -c; history -r syncs the list with the file, making everything in the file 'read', and preventing history -n from putting it into the list. –  Sep 02 '20 at 00:27
  • I will say two things about the solution: 1. The history of a session is only updated after you have input a command. 2. Your history file size can't be larger than your history size since the file will always be written with what is in the list. –  Sep 02 '20 at 00:46
  • 1
    Actually, one last thing: history -w will not clear duplicates before writing to the file. The check for duplicates happens on a command when you type it. And it is only checked against commands in the list, not the file. You can test this by adding duplicate lines at the end of your .bash_history with a text editor, then opening bash and running history -c; history -r; history -w; history. So history -a just adds to the file any new thing you've written regardless of whether it was a duplicate. My sane solution. –  Sep 02 '20 at 07:18
  • 1
    @ToniJarjour your "sane solution" is now 404. I guess that's why they try to get peeps to create the content in stack exchange. Would have liked to see your solution. – NeilG Jul 05 '22 at 02:38
  • 1
    @NeilG I have an answer on this question, just updated it. –  Jul 05 '22 at 23:15
  • Thanks @ToniJarjour I'm trying it now. – NeilG Jul 09 '22 at 10:00
10

In your prompt command, you are using the -c switch. From man bash:

-c   Clear the history list by deleting all the entries

To share your history with all open terminals, you can use -n:

-n   Read the history lines not already read from the history file into the current history list. These are lines appended to the history file since the beginning of the current bash session.

The default size is also in the manual:

HISTSIZE The number of commands to remember in the command history (see HISTORY below). The default value is 500.

To save multi-line commands:

The cmdhist shell option, if enabled, causes the shell to attempt to save each line of a multi-line command in the same history entry, adding semicolons where necessary to preserve syntactic correctness. The lithist shell option causes the shell to save the command with embedded newlines instead of semicolons.

Also, you shouldn't preface HIST* commands with export — they are bash-only variables not environmental variables: HISTCONTROL=ignoredups:erasedups is sufficient.

jasonwryan
  • 73,126
  • So, this export PROMPT_COMMAND="history -a; history -n; history -r; $PROMPT_COMMAND" is correct? Also, do you know how I can make the history file store multi-line commands as a single command (which is off by default)?? – its_me Aug 07 '11 at 07:52
  • 1
    Yes. As per the man page quoted above, shopt -s cmdhist will save multi-lines. – jasonwryan Aug 07 '11 at 08:12
  • okay, I tried 'em all. Looks like HISTCONTROL=ignoredups:erasedups is not working. I also tried having just that in the .bashrc file (among custom functions). Any idea what could be wrong? I am using Fedora 15 on a virtual machine - - Windows 7 host. – its_me Aug 07 '11 at 08:15
  • Make sure there is nothing in the other startup files, like /etc/bashrc, .bash_profile etc that is overriding it. – jasonwryan Aug 07 '11 at 08:26
  • I dont see any issues with the .bash_profile file. As for the content in /etc/bashrc I put it here, please take a look - http://pastebin.com/Uae6sE6s – its_me Aug 07 '11 at 08:33
  • EDIT: Also, Is all this in C language? How are you able to interpret these files? – its_me Aug 07 '11 at 08:39
  • Do you have a .bash_history? Is it writeable? Otherwise, I'd go back to basics and remove everything and add the commands one at a time and then test. – jasonwryan Aug 07 '11 at 08:42
  • Yes. That is how I am testing. I cleared .bash_history file and then tested. What happens is, it works when I open the terminal for the first time and issue commands. Once I close the terminal and open it once again, the commands are duplicated. Looks like they are being considered as different terminals. – its_me Aug 07 '11 at 08:46
  • More generally, you don't need to export any variables you set in ~/.bashrc unless you need to make them visible to programs other than bash, since sub-shells will execute ~/.bashrc and perform the same setup for themselves. – Chris Page Jun 05 '15 at 13:08
10

Using a combination of @rozcietrzewiacz's option 3 with an exit trap will enable terminals that maintain their own independent history sessions which converge on close. It even seems to work well with multiple sessions across different machines sharing a remote home directory.

export HISTSIZE=5000
export HISTFILESIZE=5000
export HISTCONTROL=ignorespace:erasedups
shopt -s histappend
function historymerge {
    history -n; history -w; history -c; history -r;
}
trap historymerge EXIT
PROMPT_COMMAND="history -a; $PROMPT_COMMAND"
  • The historymerge function loads out-of-session lines from the history file, combines them with session history, writes out a new history file with deduplication (thus squashing any previously appended duplicate lines), and reloads history.

  • Keeping history -a in the prompt minimizes potential for losing history, as it updates the history file without deduplication on every command.

  • Finally, the trap triggers historymerge on session close for an up-to-date clean history file with the most recently closed session's commands bubbled to the end of the file (I think).

With this, each terminal session will have its own independent history starting from launch. I find that more useful as I tend to have different terminals open for different tasks (thus am wanting to repeat different commands in each). It's also mercifully simpler than making sense of multiple terminals constantly trying to re-sync their ordered history in distributed fashion (though you can still do that deliberately using the historymerge function with select sessions or all of them).

Note that you'll want your size limits to be large enough to hold the desired history length plus the volume of non-deduplicated lines which may be added by all concurrently active sessions. 5000 is sufficient for me largely thanks to the extensive use of HISTIGNORE to filter out spammy low-value commands.

HonoredMule
  • 273
  • 2
  • 8
  • 1
    Your historymerge does not dedup history for me. At least not the old one. – Suor May 07 '21 at 05:59
  • If only using historymerge with a trap, then a useful HISTSIZE hack would be to export HISTSIZE=$(wc -l "$HISTFILE") – Samveen Nov 27 '22 at 13:00
8

This is what I came up with and I am happy with it so far…

alias hfix='history -n && history | sort -k2 -k1nr | uniq -f1 | sort -n | cut -c8- > ~/.tmp$$ && history -c && history -r ~/.tmp$$ && history -w && rm ~/.tmp$$'  
HISTCONTROL=ignorespace  
shopt -s histappend  
shopt -s extglob  
HISTSIZE=1000  
HISTFILESIZE=2000  
export HISTIGNORE="!(+(*\ *))"  
PROMPT_COMMAND="hfix; $PROMPT_COMMAND" 

NOTES:

  • Yes, it is complicated... but, it removes all duplicates and yet preserves chronology within each terminal!
  • My HISTIGNORE ignores all commands that don't have arguments. This may not be desirable by some folks and can be left out.
phk
  • 5,953
  • 7
  • 42
  • 71
  • Wouldn't the HISTIGNORE pattern work just as well without the +() expression? It was really weird to see a regex that ENDS with a 'one or more of X' - that is only useful when you want to continue the expression 'and then Y'. – TamaMcGlinn Dec 24 '19 at 10:05
  • 1
    Of note: It doesn't take into account nor is compatible with HISTTIMEFORMAT – Petru Zaharia Mar 04 '20 at 07:39
2

In your .bashrc first add:

HISTCONTROL='erasedups'

Then add the following history cleaning helper function (it removes duplicates):

clean_bash_history_file() {
  bash_history_file=$(mktemp "$USER"_bash_historyXXXXXX)
  awk 'NR == FNR { a[$0]++; next; }; ++b[$0] == a[$0]' \
      "$HOME/.bash_history" "$HOME/.bash_history" > "$bash_history_file"
  mv "$bash_history_file" "$HOME/.bash_history"
  unset bash_history_file
}

Then make the following history update function:

update_history() {   
    history -a
    clean_bash_history_file
    history -c
    history -r
}

Finally, add

PROMPT_COMMAND=update_history

This will keep the correct history order, and it will remove all duplicates, guaranteed.

It also allows HISTFILESIZE to be larger than HISTSIZE. Just make sure shopt -s histappend is also in your .bashrc

  • I like this one @ToniJarjour , I'm not exactly sure why but it seems more "sane" as you say. It's pretty clear what it's doing. I'm just trying to update it to handle different bash history's in different locations. I'm trying to implement a different bash history for each of my Tmux sessions. I source an "activate" script in the local environment every time I open a shell so I can easily export a different HISTFILE location in the local environment every time. – NeilG Jul 09 '22 at 10:00
  • To work with different history files defined by $HISTFILE I think this update to the second line of the history clean function will do it. This should also be an improvement for those keeping their bash history somewhere other than the default:

    awk 'NR == FNR { a[$1]++; next; }; ++b[$0] == a[$0]' "$HISTFILE" "$HISTFILE" > "$bash_history_file"

    – NeilG Jul 09 '22 at 10:34
  • Nope. Screws up the bash history, I think because you may have not considered time stamps, but after removing time stamps only records "exit" in the history. Not working. – NeilG Jul 09 '22 at 11:24
  • 1
    Ok, I fixed it by changing your awk line to: tail -r $HISTFILE | cat -n | sort -k2 -k1n | uniq -f1 | sort -nk1,1 | cut -f2- | tail -r > $bash_history_file, where $HISTFILE can be used instead of $HOME/.bash_history. I got the line to eliminate duplicates while retaining order from https://unix.stackexchange.com/questions/194780/remove-duplicate-lines-while-keeping-the-order-of-the-lines and just reversed it fore and aft in order to preserve the most recent occurrence of duplicates instead of the first occurrence. Now works for me. – NeilG Jul 09 '22 at 12:36
  • Bash doesnt have tail -r, but tac works. tac $HISTFILE | cat -n | sort -k2 -k1n | uniq -f1 | sort -nk1,1 | cut -f2- | tac – alchemy Oct 12 '22 at 22:58
1

Use this instead:

HISTCONTROL=ignoreboth
Michael Mrozek
  • 93,103
  • 40
  • 240
  • 233
verndog
  • 19
  • 1
1

It does not work, because you forget about:

 -n   read all history lines not already read from the history file
      and append them to the history list

But it seems history -n is just buggy when export HISTCONTROL=ignoreboth:erasedups is in effect.

Lets experiment:

$ PROMPT_COMMAND=
$ export HISTCONTROL=ignoreboth:erasedups
$ export HISTFILE=~/.bash_myhistory
$ HISTIGNORE='history:history -w'
$ history -c
$ history -w

Here we turn on dups erasing, switch history to custom file, clear the history. After all commands complete we have empty history file and one command at current history.

$ history
$ cat ~/.bash_myhistory
$ history
$ 1  [2019-06-17 14:57:19] cat ~/.bash_myhistory

Open second terminal and run those six command too. After that:

$ echo "X"
$ echo "Y"
$ history -w
$ history
  1  [2019-06-17 15:00:21] echo "X"
  2  [2019-06-17 15:00:23] echo "Y"

Now your current history has two commands and history file has:

#1560772821
echo "X"
#1560772823
echo "Y"

Back to first terminal:

$ history -n
$ history
1  [2019-06-17 14:57:19] cat ~/.bash_myhistory 
2  [2019-06-17 15:03:12] history -n

Huh... none of echo commands are read. Switch to second terminal again and:

$ echo "Z"
$ history -w

Now the history file is:

#1560772821
echo "X"
#1560772823
echo "Y"
#1560773057
echo "Z"

Switch to first terminal again:

$ history -n
$ history
  1  [2019-06-17 14:57:19] cat ~/.bash_myhistory 
  2  [2019-06-17 15:03:12] history -n
echo "Z"

You can see that echo "Z" command is merged to history -n.

Another bug is because of commands are read from the history by command number and not by command time, I think. I expect others echo commands appeared at the history

1

I went with a combination of answers here:

  • I want my history merged at the end, so I used the trap function.
  • I also want my history files separated by hosts so I changed HISTFILE to point to a directory and named files depending on hostname.
  • I added ignorespace option so I can type clear text passwords and not have them show up on the history, e.g.: $ ./.secretfunction -user myuser -password mypassword will not be saved because it starts with space.
  • I added history* and exit to HISTIGNORE just to keep those commands including anything past history like history | grep awesomecommand out of there since I normally wouldnt want it. You can add others like: logout, fg, bg, etc.
  • I then put it all in my ~/.bash_aliases file so it's not conflicting on upgrades as it has happened sometimes.
HISTCONTROL=ignoreboth:erasedups:ignorespace
HISTIGNORE="history*:exit"
HISTFILE=~/.bash_history/.$(hostname)_history
shopt -s histappend
function historymerge {
    history -n; history -w; history -c; history -r;
}
trap historymerge EXIT
PROMPT_COMMAND="history -a; $PROMPT_COMMAND"

Grep your ~/ for PROMPT_COMMAND to make sure you arent already getting it modified with history -a. I also added the TIME/DATE format HISTTIMEFORMAT="%x %r %Z " I liked but didnt include in the above block since that is locale sensitive.

Marlon
  • 131
1

I found a lot of solutions either do not handle history timestamps, multi-line history entries, or just leave the history a mess, without any nice ordering.

My solution was to first turn on $HISTTIMEFORMAT (only needs to be set, can be the emtry string if you don't like seeing the timestamps). And then merge the on-disk ".bash_history" with the in-memory shell 'history'. Preserving timestamp ordering, and command order within those timestamps.

Optionally removing unique commands (even if multi-line), and/or removing (cleaning out) simple and/or sensitive commands, according to defined perl RE's. Adjust to suit!

This is the result... https://antofthy.gitlab.io/software/history_merge.bash.txt

I call this either on demand via and alias ('hc' for history clean) and from the ".bash_logout" profile on shell exit.

Adjust as you like to suit your need.

Enjoy.

anthony
  • 610
1

Here's another combination of the previous answers.

In ~/.bashrc:

shopt -s histappend
HISTSIZE=10000
HISTFILESIZE=20000
HISTCONTROL="ignoreboth:erasedups"
HISTIGNORE="history:exit"
function historyclean {
    if [[ -e "$HISTFILE" ]]; then
        exec {history_lock}<"$HISTFILE" && flock -x $history_lock
        history -a
        tac "$HISTFILE" | awk '!x[$0]++' | tac > "$HISTFILE.tmp$$"
        mv -f "$HISTFILE.tmp$$" "$HISTFILE"
        history -c
        history -r
        flock -u $history_lock && unset history_lock
    fi  
}
function historymerge {
    history -n; history -w; history -c; history -r; 
}
trap historymerge EXIT

PROMPT_COMMAND="historyclean;$PROMPT_COMMAND"

Compared to previous answers:

  • historymerge() does not remove or prevent duplicates; but it's useful with trap not to loose history
  • use of /tmp is not secure

Reused ideas: lock file, trap on exit, remove duplicates, merge order. Credits to rozcietrzewiacz, honoredmule, anthony and tony-j.

0

erasedups does not trim (like chomp in some languages) the leading and trailing spaces. This is a bug. Also erasedups does not delete all previous entries.

  • You can set HISTIGNORE to ignore specific commands or when they match certain regexes, HISTIGNORE=" *:ls:ll:cd:cd -:history:history -*:man:man *:pwd:exit:date:* --help:" and if you use ignorespace you won't have any proceeding whitespace saved on commands anyways. – dragon788 Jul 18 '20 at 20:06