Two terminals, one history file¶
Level: 201 · you keep two terminal windows open and have lost commands from one of them
One line: Two bash windows on one ~/.bash_history both keep their commands, added in the order the windows close, until one window has run more commands than HISTSIZE. Then the last window to close writes its own list over the file. histappend stops that, and PROMPT_COMMAND='history -a' writes each command at the next prompt. zsh appends in the same way unless APPEND_HISTORY is off, and fish writes every command to the file as it runs.
Measured¶
A and B are two real interactive bashes open at the same time on one HISTFILE, the way two terminal windows share ~/.bash_history. The script types into each through a fifo and reads back what each prints. It never sleeps: PROMPT_COMMAND=echo makes a shell print an empty line whenever it is ready for its next command, and the script waits for that line before typing anything else. So the events happen in the order they are printed, on every run. A shell exits when the script closes its input, as at Ctrl-D. Each scenario starts from a file holding one line, echo old.
Verified output of histtwo_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
## Nothing set. B exits first, then A
A$ echo from-A
from-A
B$ echo from-B
from-B
(B exits)
(A exits)
$ cat hist
echo old
echo from-B
echo from-A
## HISTSIZE=3 HISTFILESIZE=100, and A runs four commands
A$ echo a1
a1
A$ echo a2
a2
A$ echo a3
a3
A$ echo a4
a4
B$ echo from-B
from-B
(B exits)
(A exits)
$ cat hist
echo a2
echo a3
echo a4
## The same, with histappend (bash -O histappend)
A$ echo a1
a1
A$ echo a2
a2
A$ echo a3
a3
A$ echo a4
a4
B$ echo from-B
from-B
(B exits)
(A exits)
$ cat hist
echo old
echo from-B
echo a2
echo a3
echo a4
## PROMPT_COMMAND='history -a; echo'
A$ echo from-A
from-A
B$ echo from-B
from-B
A$ echo again-A
again-A
(both still open)
$ cat hist
echo old
echo from-A
echo from-B
echo again-A
B$ history -n; history
1 echo old
2 echo from-B
3 history -n; history
4 echo from-B
5 echo again-A
(B exits)
(A exits)
- With nothing set, nothing was lost. Both commands are in the file, B's first because B exited first. Each shell appended the commands it had run to what was already there. The saying that "the last window to close wins" is not what happened here.
- It is what happens once a window outgrows
HISTSIZE. WithHISTSIZE=3, A ran four commands, more than its list can hold. At exit it wrote its list over the file, soecho oldand B'secho from-Bare gone. So is A's ownecho a1, which had already dropped out of A's list. This is the rule from The history is a list in memory, andHISTFILESIZE=100did not prevent it. histappendmakes the exit an append, always.echo oldandecho from-Bsurvived.echo a1is still missing, because a shell can only append what its list still holds.PROMPT_COMMAND='history -a'writes as you go. (The script's version adds; echofor its own ready signal.) With both shells still open, the file already held every command in the order they were typed, whichever window typed them.history -nis less reliable than it looks. It reads "the lines not already read" into B's list. B got its ownecho from-Ba second time and never got A'secho from-A. That fits bash counting by line number. B had read one line at startup and appended one, so it treated the first two lines as read.echo from-Ahad become line 2 and was skipped, and reading started at line 3,echo from-B.
On a Mac¶
The key is shared: /bin/bash 3.2 on the Mac printed the same bytes as bash 5.2 on Ubuntu. The one caveat is from the previous lesson: in bash 3.2, history -a, and so PROMPT_COMMAND='history -a', wrote nothing in a session that had started with no history file or an empty one.
Terminal.app changes the picture, for zsh and for bash. None of the examples run inside Terminal.app, so what follows is read from the two files it relies on, /etc/zshrc_Apple_Terminal and /etc/bashrc_Apple_Terminal on macOS 26. They are not measured output. macOS's /etc/zshrc and /etc/bashrc load them when TERM_PROGRAM is Apple_Terminal. There is no such file for any other terminal on this Mac, and none on Linux.
- Terminal gives each window a
TERM_SESSION_ID. A zsh in that window saves the window's new commands to~/.zsh_sessions/<id>.historynew, and at exit appends them both to the sharedHISTFILEand to a per-window<id>.history. That per-window file is what a window restored by Terminal's Resume reads back, so a reopened window gets its own history rather than everyone's. bash does the same under~/.bash_sessions. - So in Terminal.app, two windows' histories are appended to the shared file at exit, never written over it, and the file's comments recommend raising
HISTSIZEandSAVEHISTbecause of it. - The zsh version turns itself off when
INC_APPEND_HISTORY,INC_APPEND_HISTORY_TIMEorSHARE_HISTORYis set, orSHELL_SESSION_HISTORY=0.SHELL_SESSIONS_DISABLE=1turns off the whole session mechanism. The bash version turns itself off whenhistappendis set orHISTTIMEFORMATis set, and entirely when the file~/.bash_sessions_disableexists. - The
Saving session...and...saving history...truncating history files...lines a Terminal window prints as it closes come from these scripts.
In zsh and fish¶
zsh appends at exit by default, has an option that makes the last exit win, and two options that write as you go. A and B here are zsh -d -i with a scratch .zshrc that sets HISTFILE and SAVEHIST and defines precmd() { echo }, the zsh spelling of the empty-line ready signal:
Verified output of histtwo_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
## Nothing set. B exits first, then A
A% echo from-A
from-A
B% echo from-B
from-B
(B exits)
(A exits)
$ cat hist
echo old
echo from-B
echo from-A
## unsetopt appendhistory (zsh +o appendhistory)
A% echo from-A
from-A
B% echo from-B
from-B
(B exits)
(A exits)
$ cat hist
echo old
echo from-A
## setopt incappendhistory (zsh -o incappendhistory)
A% echo from-A
from-A
B% echo from-B
from-B
A% echo again-A
again-A
(both still open)
$ cat hist
echo old
echo from-A
echo from-B
echo again-A
A% fc -ln 1
echo old
echo from-A
echo again-A
(B exits)
(A exits)
$ cat hist
echo old
echo from-A
echo from-B
echo again-A
fc -ln 1
## setopt sharehistory (zsh -o sharehistory)
A% echo from-A
from-A
B% echo from-B
from-B
A% echo again-A
again-A
(both still open)
$ cat hist
echo old
: <epoch>:0;echo from-A
: <epoch>:0;echo from-B
: <epoch>:0;echo again-A
A% fc -ln 1
echo old
echo from-A
echo from-B
echo again-A
(B exits)
(A exits)
$ cat hist
echo old
echo from-A
echo from-B
echo again-A
fc -ln 1
- Nothing set: the same as bash. Both commands survive, in the order the shells exited.
APPEND_HISTORYis on by default. unsetopt appendhistory: the last shell to exit wins. A wrote its list over the file, and B'secho from-Bis gone.setopt incappendhistory: each command reaches the file as it is typed, so with both shells open the file holds all three, in typed order. A's own list,fc -ln 1, still has only A's commands.setopt sharehistory: the file is written as you go, and A reads it back. A'sfc -ln 1includes B'secho from-B. While both shells run, the lines it writes carry the: <epoch>:0;prefix of zsh's extended format, which Three history file formats takes apart.
fish writes each command into the file as it runs. A here is an interactive fish at a terminal, typed into by fish_at_a_terminal.py. Halfway through, A's second command starts B, another interactive fish at another terminal, through the same helper:
Verified output of histtwo_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ python3 fish_at_a_terminal.py "echo from-A" "python3 fish_at_a_terminal.py 'echo from-B'" "echo again-A"
$ string replace -r "when: \d+" "when: <epoch>" < ~/.local/share/fish/fish_history
- cmd: echo from-A
when: <epoch>
- cmd: python3 fish_at_a_terminal.py 'echo from-B'
when: <epoch>
paths:
- fish_at_a_terminal.py
- cmd: echo from-B
when: <epoch>
- cmd: echo again-A
when: <epoch>
The file holds all four commands in the order they ran: A's, then the command that started B, then B's, then A's again. There is no exit step at which one fish could write over another. What a running fish shows of another session's commands is a separate question. fish's history merge ↗ is the command for reading them into a running session, and this page does not measure it. The key is shared, so this is also what fish 4.3.2 does on a Mac.
If you are coming from another library¶
- Rust. Fuzzy finding ↗ puts fzf on Ctrl-R, which searches whatever this page has left in the file. In this library, fzf has its own chapter.
See also¶
- The history is a list in memory —
HISTSIZE,HISTFILESIZEand the exit-time write - What gets remembered — including Ubuntu's default
histappend - bash manual, The Shopt Builtin ↗ —
histappend - bash manual, Bash Variables ↗ —
PROMPT_COMMAND,HISTSIZE,HISTFILESIZE - zsh manual, Options: History ↗ —
APPEND_HISTORY,INC_APPEND_HISTORY,SHARE_HISTORY - fish manual,
history↗