History expansion¶
Level: 101 · you have typed sudo !! and want to know what else ! can do
One line: In an interactive bash or zsh, !!, !$, !-3:2 and ^old^new are rewritten from the history before the line runs, and the shell prints the rewritten line first. A ! inside double quotes is rewritten too, which is where event not found comes from, and bash 3.2 and bash 5.2 disagree about "wow!". A script has no history expansion, and fish has none anywhere: to fish, !! is a command it cannot find.
Measured¶
The session is an interactive bash with its stderr joined to its stdout, because stderr is where bash shows its work. Each typed line is echoed by the line editor, bash then prints the expanded line, and the command's own output comes last. PS1 and PS2 are empty so that no prompt gets in the way, and grep -v drops the job-control warning at the start and the exit at the end.
Verified output of histbang_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ PS1= PS2= HISTFILE=$PWD/hist bash --norc -i <<'EOF' 2>&1 | grep -v -e 'job control' -e 'process group' -e '^exit$'
echo alpha beta gamma
echo !$
!!
echo !-3:2 !-3:$
^beta^delta
!echo:p
history -p '!!' '!-2:1'
EOF
echo alpha beta gamma
alpha beta gamma
echo !$
echo gamma
gamma
!!
echo gamma
gamma
echo !-3:2 !-3:$
echo beta gamma
beta gamma
^beta^delta
echo delta gamma
delta gamma
!echo:p
echo delta gamma
history -p '!!' '!-2:1'
echo delta gamma
delta
$ bash -c 'echo "in a script: wow!ok !!"'
in a script: wow!ok !!
$ bash -c 'set -o history; history -s "echo last words"; history -p "!!" "!$"'
echo last words
words
!$is the last word of the previous command. bash printedecho gammabefore running it.!!is the whole previous command, which is nowecho gamma. The history holds the expanded line, not the!$that was typed.!-3:2and!-3:$count back three commands and take word 2 and the last word. Words are counted from 0, the command name, so word 2 ofecho alpha beta gammaisbeta.^beta^deltarepeats the previous command with its firstbetareplaced.:pprints and does not run.!echo:pfound the latest command starting withechoand printed it, and nodelta gammaoutput follows.history -pexpands its arguments and prints them, without running anything.- A script has none of this.
bash -cprintedwow!ok !!untouched. History expansion is on only in an interactive shell. A script that wants it can still callhistory -p, once it has a history to expand from:set -o historyturns one on, andhistory -sadds a line to it.
On a Mac¶
The key above is shared: bash 3.2 and bash 5.2 expanded every word the same way. Where they part is a ! inside double quotes, and this key is split on purpose:
Verified output of histbang_quotes_sh.sh on Linux — regenerated by tools/run_examples.py, never hand-typed.
$ PS1= PS2= HISTFILE=$PWD/hist bash --norc -i <<'EOF' 2>&1 | grep -v -e 'job control' -e 'process group' -e '^exit$'
echo "wow!ok"
echo "wow!"
echo 'wow!ok'
echo wow\!ok
set +H
echo "wow!ok"
EOF
echo "wow!ok"
bash: !ok: event not found
echo "wow!"
wow!
echo 'wow!ok'
wow!ok
echo wow\!ok
wow!ok
set +H
echo "wow!ok"
wow!ok
Verified output of histbang_quotes_sh.sh on macOS — regenerated by tools/run_examples.py, never hand-typed.
$ PS1= PS2= HISTFILE=$PWD/hist bash --norc -i <<'EOF' 2>&1 | grep -v -e 'job control' -e 'process group' -e '^exit$'
echo "wow!ok"
echo "wow!"
echo 'wow!ok'
echo wow\!ok
set +H
echo "wow!ok"
EOF
echo "wow!ok"
bash: !ok": event not found
echo "wow!"
bash: !": event not found
echo 'wow!ok'
wow!ok
echo wow\!ok
wow!ok
set +H
echo "wow!ok"
wow!ok
- Both expand
!inside double quotes.echo "wow!ok"failed on both, because!okasks for the latest command starting withokand there is none. A line whose expansion fails is not run at all: nowow!okfollows the error. - bash 3.2 reads the closing quote as part of the event:
!ok": event not found. It even fails onecho "wow!", with!": event not found. Bash 5.2 ends the word at the closing quote, reports!ok, and leaves a!just before"alone, so"wow!"printswow!. A string ending in!that works in a Linux terminal fails in/bin/bashon a Mac. - Single quotes and a backslash protect
!in both, andset +Hturns expansion off for the rest of the session.
In zsh and fish¶
zsh has the same !-words, on by default in an interactive shell (the BANG_HIST option). The first typed line, setopt verbose, makes zsh echo each later line as it reads it, and zsh prints the expanded line after it:
Verified output of histbang_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ PS1= PS2= zsh -f -i +o promptsp +o promptcr <<'EOF' 2>&1
setopt verbose
echo alpha beta gamma
echo !$
!!
echo !-3:2 !-3:$
^beta^delta
!echo:p
echo "wow!ok"
echo 'wow!ok'
setopt nobanghist
echo "wow!ok"
EOF
echo alpha beta gamma
alpha beta gamma
echo !$
echo gamma
gamma
!!
echo gamma
gamma
echo !-3:2 !-3:$
echo beta gamma
beta gamma
^beta^delta
echo delta gamma
delta gamma
!echo:p
echo delta gamma
echo "wow!ok"
zsh: event not found: ok
echo 'wow!ok'
wow!ok
setopt nobanghist
echo "wow!ok"
wow!ok
$ zsh -f -c 'echo "in a script: wow!ok !!"'
in a script: wow!ok !!
Every designator expanded as it did in bash. The error has zsh's wording, zsh: event not found: ok, and single quotes protect ! as in bash. setopt nobanghist is zsh's set +H. A zsh script, like a bash one, leaves ! alone.
fish has no history expansion. A line fish cannot parse fails before it runs, so each of these runs in a child fish:
Verified output of histbang_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ !!
fish: Unknown command: !!
fish:
!!
^^
(exit status 127)
$ echo !$
fish: Expected a variable name after this $.
echo !$
^
(exit status 127)
$ echo "wow!ok" !! !-2
wow!ok !! !-2
$ python3 $at_a_terminal 'function last_history_item; echo $history[1]; end' "abbr --add !! --position anywhere --function last_history_item" "echo ran >> log" "!!"
$ cat log
ran
ran
$ string replace -r "when: \d+" "when: <epoch>" < ~/.local/share/fish/fish_history
- cmd: function last_history_item; echo $history[1]; end
when: <epoch>
- cmd: abbr --add !! --position anywhere --function last_history_item
when: <epoch>
- cmd: echo ran >> log
when: <epoch>
!!is a command name fish looked for and did not find, with exit status 127.!$is a syntax error, because$has to be followed by a variable name.- As an argument,
!means nothing:echo "wow!ok" !! !-2printed all three words as typed. - The replacement fish's documentation suggests is an abbreviation:
abbr --add !! --position anywhere --function last_history_item, wherelast_history_itemprints$history[1], the most recent command. An abbreviation expands only in the line editor, so this was typed at a terminal byfish_at_a_terminal.py.logholdsrantwice: the typed!!becameecho ran >> logand ran again. The history file holds that command once and no!!at all. The abbreviation expanded before the line was saved, and a command repeated straight after itself is saved once, as What gets remembered showed.
This fish key is shared too, so on a Mac, fish 4.3.2 behaves exactly this way.
| bash | zsh | fish | |
|---|---|---|---|
!!, !$, ^old^new at the prompt |
expanded, and the expansion printed | expanded, and the expansion printed | not special; an abbreviation can stand in for !! |
| in a script | not expanded | not expanded | not special |
! inside double quotes |
expanded; 3.2 and 5.2 differ at the closing quote | expanded | not special |
| turn it off | set +H |
setopt nobanghist |
nothing to turn off |
| show without running | !…:p, history -p |
!…:p |
— |
If you are coming from another library¶
- Rust. Fuzzy finding ↗ sets up fzf's Ctrl-R, the other way to reach an old command: search for it instead of counting back to it. In this library, fzf has its own chapter.
See also¶
- What gets remembered — what
!!has to choose from - bash manual, History Expansion ↗
- zsh manual, History Expansion ↗
- fish manual,
abbr↗ and fish for bash users ↗