Skip to content

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.
  • ignorespace leaves out a line that starts with a space.
  • ignoredups leaves out a line identical to the one just before it. The second echo b went. The second echo a stayed, because echo b came in between.
  • ignoreboth is both of those.
  • erasedups removes every earlier copy before adding a line, so the order changes. echo a now comes last, where it was typed the second time.
  • Left out is not left unrun. echo secret printed secret, and the list holds only history.
  • HISTIGNORE is a colon-separated list of patterns, and each pattern must match the whole line. & matches the previous line, so it drops the repeated echo a. ls dropped ls but not ls -d /. cd * dropped cd /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.
  • histignorespace is bash's ignorespace.
  • histignoredups is bash's ignoredups: consecutive copies only.
  • histignorealldups is bash's erasedups: the file came out in the same order, echo a last.
  • zshaddhistory is the hook zsh calls with each line before saving it. A non-zero return keeps the line out, so cd /tmp is 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 ignorespace does in bash, with nothing to turn on.
  • A command repeated straight after itself is saved once. The second echo b is 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_history decides alone. The function above returns non-zero for any command starting cd, and cd /tmp is missing. But echo spaced was saved, and so was the helper's own exit, 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 after cmd:: across repeated runs on macOS, fish stored echo spaced sometimes 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