What gets remembered¶
Level: 101 · you have typed a password on a command line and want it out of the history
One line: bash saves every line you type unless HISTCONTROL or HISTIGNORE says otherwise, and zsh does unless an option or a zshaddhistory function does. fish on its own leaves out a line that starts with a space and a command repeated straight after itself. Define fish_should_add_to_history, and that function decides alone: the leading space stops working.
Measured¶
The same five lines go into a fresh interactive bash for each HISTCONTROL setting, and the history file is printed after the shell exits. As in the other bash examples: --norc, a scratch HISTFILE, and 2>/dev/null for the prompts and the job-control warning. >/dev/null hides what the typed echo commands print.
Verified output of histkeep_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ cat typed
echo a
echo secret
echo b
echo b
echo a
$ HISTCONTROL= HISTFILE=$PWD/hist bash --norc -i <typed >/dev/null 2>/dev/null; cat hist
echo a
echo secret
echo b
echo b
echo a
$ HISTCONTROL=ignorespace HISTFILE=$PWD/hist bash --norc -i <typed >/dev/null 2>/dev/null; cat hist
echo a
echo b
echo b
echo a
$ HISTCONTROL=ignoredups HISTFILE=$PWD/hist bash --norc -i <typed >/dev/null 2>/dev/null; cat hist
echo a
echo secret
echo b
echo a
$ HISTCONTROL=ignoreboth HISTFILE=$PWD/hist bash --norc -i <typed >/dev/null 2>/dev/null; cat hist
echo a
echo b
echo a
$ HISTCONTROL=erasedups HISTFILE=$PWD/hist bash --norc -i <typed >/dev/null 2>/dev/null; cat hist
echo secret
echo b
echo a
$ HISTCONTROL=ignorespace HISTFILE=$PWD/hist bash --norc -i 2>/dev/null <<'EOF'
echo secret
history
EOF
secret
1 history
$ cat typed
cd /tmp
echo a
echo a
echo secret
ls
ls -d /
echo b
$ HISTIGNORE='&:ls:cd *:[ ]*' HISTFILE=$PWD/hist bash --norc -i <typed >/dev/null 2>/dev/null; cat hist
echo a
ls -d /
echo b
- Unset, bash keeps every line, the leading space included.
ignorespaceleaves out a line that starts with a space.ignoredupsleaves out a line identical to the one just before it. The secondecho bwent. The secondecho astayed, becauseecho bcame in between.ignorebothis both of those.erasedupsremoves every earlier copy before adding a line, so the order changes.echo anow comes last, where it was typed the second time.- Left out is not left unrun.
echo secretprintedsecret, and the list holds onlyhistory. HISTIGNOREis a colon-separated list of patterns, and each pattern must match the whole line.&matches the previous line, so it drops the repeatedecho a.lsdroppedlsbut notls -d /.cd *droppedcd /tmp, and[ ]*dropped the line that starts with a space.
On a Mac¶
The bash, zsh and fish keys on this page are all shared: macOS printed exactly what Ubuntu printed. The difference is in what a new account starts with, and this key is split on purpose:
Verified output of histkeep_skel_sh.sh on Linux — regenerated by tools/run_examples.py, never hand-typed.
$ grep -n -e HIST -e histappend /etc/skel/.bashrc; echo "exit status $?"
13:HISTCONTROL=ignoreboth
16:shopt -s histappend
18:# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
19:HISTSIZE=1000
20:HISTFILESIZE=2000
exit status 0
Verified output of histkeep_skel_sh.sh on macOS — regenerated by tools/run_examples.py, never hand-typed.
$ grep -n -e HIST -e histappend /etc/skel/.bashrc; echo "exit status $?"
grep: /etc/skel/.bashrc: No such file or directory
exit status 2
On Ubuntu, every new account's ~/.bashrc is a copy of /etc/skel/.bashrc, and that file turns on HISTCONTROL=ignoreboth and histappend and sets HISTSIZE=1000 and HISTFILESIZE=2000. A Mac has no /etc/skel. Someone who moves from an Ubuntu account to bash on a Mac therefore loses, without having changed anything: the leading space that kept a line out of the history, the duplicates they never saw, histappend (see Two terminals, one history file), and the longer history. bash's own defaults, measured in The history is a list in memory, are 500 entries and 500 lines, against Ubuntu's 1000 and 2000.
In zsh and fish¶
zsh spells the same choices as options. The five lines go into zsh -d -i with a scratch ZDOTDIR, once per option:
Verified output of histkeep_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ cat typed
echo a
echo secret
echo b
echo b
echo a
$ zsh -d -i <typed >/dev/null 2>/dev/null; cat hist
echo a
echo secret
echo b
echo b
echo a
$ zsh -d -i -o histignorespace <typed >/dev/null 2>/dev/null; cat hist
echo a
echo b
echo b
echo a
$ zsh -d -i -o histignoredups <typed >/dev/null 2>/dev/null; cat hist
echo a
echo secret
echo b
echo a
$ zsh -d -i -o histignorealldups <typed >/dev/null 2>/dev/null; cat hist
echo secret
echo b
echo a
$ cat typed
zshaddhistory() { [[ $1 != "cd "* ]] }
cd /tmp
echo kept
$ zsh -d -i <typed >/dev/null 2>/dev/null; cat hist
zshaddhistory() { [[ $1 != "cd "* ]] }
echo kept
- Nothing set: every line.
histignorespaceis bash'signorespace.histignoredupsis bash'signoredups: consecutive copies only.histignorealldupsis bash'serasedups: the file came out in the same order,echo alast.zshaddhistoryis the hook zsh calls with each line before saving it. A non-zero return keeps the line out, socd /tmpis missing. The line that defined the function was saved, because the function did not exist yet when that line was added.
fish has one rule you can see without configuring anything, and a function that replaces it. The lines are typed at a terminal by fish_at_a_terminal.py, because fish saves nothing that did not come from one:
Verified output of histkeep_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ python3 $at_a_terminal "echo a" " echo secret" "echo b" "echo b" "echo c"
$ string replace -r "when: \d+" "when: <epoch>" < ~/.local/share/fish/fish_history
- cmd: echo a
when: <epoch>
- cmd: echo b
when: <epoch>
- cmd: echo c
when: <epoch>
$ python3 $at_a_terminal 'function fish_should_add_to_history; string match -qv -- "cd *" $argv; end' "cd /tmp" "echo kept" " echo spaced"
$ string replace -r "when: \d+" "when: <epoch>" < ~/.local/share/fish/fish_history | string replace -r -- "^- cmd: +" "- cmd: "
- cmd: function fish_should_add_to_history; string match -qv -- "cd *" $argv; end
when: <epoch>
- cmd: echo kept
when: <epoch>
- cmd: echo spaced
when: <epoch>
- cmd: exit
when: <epoch>
- A leading space keeps a command out, as
ignorespacedoes in bash, with nothing to turn on. - A command repeated straight after itself is saved once. The second
echo bis not in the file. Nothing is typed twice with another command in between: when that was tried, fish sometimes removed the older copy from the file before exiting and sometimes did not. fish_should_add_to_historydecides alone. The function above returns non-zero for any command startingcd, andcd /tmpis missing. Butecho spacedwas saved, and so was the helper's ownexit, which it types with a leading space precisely to keep it out. With the function defined, a leading space kept nothing out. A function that should honour the space has to test for it itself. (The second listing squeezes the spaces aftercmd:: across repeated runs on macOS, fish storedecho spacedsometimes with its leading space and sometimes without. That the line was saved did not vary.)
So for a fish user on a Mac: the leading space works out of the box, on macOS as on Ubuntu, and stops working the moment you write a fish_should_add_to_history that does not check for it.
| to leave out | bash | zsh | fish |
|---|---|---|---|
| a line starting with a space | HISTCONTROL=ignorespace |
setopt histignorespace |
built in, until fish_should_add_to_history is defined |
| a repeat of the line just before | HISTCONTROL=ignoredups |
setopt histignoredups |
built in |
| every earlier copy | HISTCONTROL=erasedups |
setopt histignorealldups |
varied between runs; not shown |
| lines matching a pattern | HISTIGNORE='cd *:ls' |
zshaddhistory() { … } |
fish_should_add_to_history |
If you are coming from another library¶
- Rust. Fuzzy finding ↗ searches the history with fzf's Ctrl-R. Whatever this page kept out of the file, that search cannot find. In this library, fzf has its own chapter.
See also¶
- The history is a list in memory
- History expansion —
!!reuses what was remembered - bash manual, Bash Variables ↗ —
HISTCONTROL,HISTIGNORE - zsh manual, Options: History ↗ and Hook Functions ↗ —
zshaddhistory - fish manual, Interactive use ↗ — the history and its search