Ctrl-R is a pipeline¶
Level: 301 · you press Ctrl-R with fzf installed, and want to know what list you are searching
One line: In bash, fzf's Ctrl-R runs fc -lnr -2147483648 | perl -n -l0 -e '…' | fzf --read0 --scheme=history -n2..,... fc lists the history newest first. The perl one-liner puts a number in front of each entry, drops the older copies of a repeated command, and ends every entry with a NUL, so a command that spans several lines stays one item. fzf reads those items and breaks ties newest first. Every stage can be run by hand, on /bin/bash 3.2 as well as bash 5.2. zsh builds the same list from its $history array, and fish from builtin history -z.
Measured¶
The widget comes from fzf itself. The first command prints the perl branch of __fzf_history__ from fzf --bash, and the script cuts the perl program out of that text instead of copying it. Every later command runs in a fresh interactive bash. Its history file is a copy of demo/history.txt, and a two-line for loop has been typed into it first. The command itself is typed after a space, and HISTCONTROL=ignorespace keeps it out of the history it is listing. Tabs and NULs are printed as \t and \0.
Verified output of fzfhist_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ fzf --bash | sed -n '/^if command -v perl/,/^else/p'
if command -v perl > /dev/null; then
__fzf_history__() {
local output script
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$(
set +o pipefail
builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
if [[ -z $READLINE_POINT ]]; then
echo "$READLINE_LINE"
else
READLINE_POINT=0x7fffffff
fi
}
else # awk - fallback for POSIX systems
$ fzf --bash | grep -n -F -e 'BASH_VERSINFO' -e '"\C-r"'
131:if ((BASH_VERSINFO[0] < 4)); then
144: bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
145: bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
146: bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
161: bind -m emacs-standard -x '"\C-r": __fzf_history__'
162: bind -m vi-command -x '"\C-r": __fzf_history__'
163: bind -m vi-insert -x '"\C-r": __fzf_history__'
$ cat history.txt
make test
vim notes.txt
git status
make test
git push
git status
$ history
1 make test
2 vim notes.txt
3 git status
4 make test
5 git push
6 git status
7 for f in *.txt
do wc -l < "$f"
done > /dev/null
$ fc -lnr -2147483648
\t for f in *.txt
do wc -l < "$f"
done > /dev/null
\t git status
\t git push
\t make test
\t git status
\t vim notes.txt
\t make test
# the same, in a bash started without -O lithist
$ fc -lnr -2147483648
\t for f in *.txt; do wc -l < "$f"; done > /dev/null
\t git status
\t git push
\t make test
\t git status
\t vim notes.txt
\t make test
$ fc -lnr -2147483648 | last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script"
7\tfor f in *.txt
\tdo wc -l < "$f"
\tdone > /dev/null\0
6\tgit status\0
5\tgit push\0
4\tmake test\0
2\tvim notes.txt\0
1\tmake test
\t\0
$ fc -lnr -2147483648 | last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" | fzf --read0 --print0 --scheme=history -n2..,.. --filter git
6\tgit status\0
5\tgit push\0
$ fc -lnr -2147483648 | last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" | fzf --read0 --print0 -n2..,.. --filter git
5\tgit push\0
6\tgit status\0
$ fc -lnr -2147483648 | last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" | fzf --read0 --print0 --scheme=history -n2..,.. --filter make
4\tmake test\0
1\tmake test
\t\0
$ fc -lnr -2147483648 | last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" | fzf --read0 --print0 --scheme=history -n2..,.. --filter '^git'
6\tgit status\0
5\tgit push\0
$ fc -lnr -2147483648 | last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" | fzf --read0 --print0 --scheme=history -n.. --filter '^git'; echo "status $?"
status 1
$ fc -lnr -2147483648 | last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" | fzf --read0 --scheme=history -n2..,.. --filter wc
7\tfor f in *.txt
\tdo wc -l < "$f"
\tdone > /dev/null
$ output=$(fc -lnr -2147483648 | last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" | fzf --read0 --scheme=history -n2..,.. --filter wc); perl -pe 's/^\d*\t//' <<< "$output"
for f in *.txt
do wc -l < "$f"
done > /dev/null
fc -lnr -2147483648lists everything, newest first.-llists,-nleaves out the numbers, and-rreverses the order. The first event,-2147483648, lies far before the oldest, so the list starts at the oldest. Each entry begins with a tab and a space. The loop kept its three lines because that bash was started with-O lithist. Without it, the same loop came back as one line joined with;.- The perl stage numbers, de-duplicates and terminates.
getcswallows the tab in front of the first entry.$/ = "\n\t"makes a newline followed by a tab the end of a record, and-lchomps that separator off. The loop's second and third lines start without a tab, so the loop stays one record.s/\n/\n\t/gmindents those lines with a tab, and-l0prints a NUL after each record. The number ishistory 1plus one, minus the record's position, so it counts down from 7 and agrees withhistory.!$seen{$_}++prints a command only the first time perl sees it. Reading newest first, that is the newest copy, so event 3, the oldergit status, is gone. - The oldest entry keeps its newline, and slips past the de-duplication. The last record has no newline and tab after it to chomp, so
1\tmake testends in\n\t. It is then a different string frommake test, the de-duplication lets it through, and--filter makefinds both 4 and 1. --read0makes the loop one item.--filter wcprinted the loop's three lines as one match.--print0is there only to show where each item ends; the widget reads fzf's output without it. The last command is the widget's final step.perl -pe 's/^\d*\t//'removes the number from the first line and the tab from the others, and what is left is the loop as it was typed.--scheme=historybreaks ties newest first. Bothgitentries start withgit. With the history scheme, the newer6 git statuscame first. With the default scheme, the shorter5 git pushdid. The manual says the history scheme sets--tiebreak=index, and this input is newest first.-n2..,..is two field ranges: from the second field on, and all fields.^gitmatched with it. With-n..alone, the same^gitmatched nothing, because every item starts with its number.
On a Mac¶
/bin/bash 3.2 runs every stage, fc -lnr -2147483648 included, and printed the same key as bash 5.2, so the pipeline is not what differs. The binding is. fzf --bash checks BASH_VERSINFO[0] < 4 (line 131 above). Below bash 4, Ctrl-R is a readline macro that types `__fzf_history__` into the command line. From bash 4 it is bind -x, which calls the function directly. The function printed at the top serves both. It sets READLINE_LINE, and echoes it when READLINE_POINT is empty, which is the output the macro's backticks paste.
In zsh and fish¶
zsh does not call fc here. The zsh/parameter module's $history maps each event number to its command. The widget prints them as number, tab, command, NUL, and perl keeps the first copy of each command. This script builds the history in memory with fc -R and print -s:
Verified output of fzfhist_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ fzf --zsh | sed -n '/commands,history/,/FZF_DEFAULT_OPTS_FILE/p'
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
$ fc -rl 1
8 the line being typed
7 for f in *.txt\ndo wc -l < "$f"\ndone
6 git status
5 git push
4 make test
3 git status
2 vim notes.txt
1 make test
$ printf "%s\t%s\000" "${(kv)history[@]}" | show
7\tfor f in *.txt
do wc -l < "$f"
done\0
6\tgit status\0
5\tgit push\0
4\tmake test\0
3\tgit status\0
2\tvim notes.txt\0
1\tmake test\0
$ printf "%s\t%s\000" "${(kv)history[@]}" | perl -0 -ne "$dedup" | show
7\tfor f in *.txt
\tdo wc -l < "$f"
\tdone\0
6\tgit status\0
5\tgit push\0
4\tmake test\0
2\tvim notes.txt\0
$ printf "%s\t%s\000" "${(kv)history[@]}" | perl -0 -ne "$dedup" | fzf --read0 --print0 --scheme=history -n2..,.. --filter make | show
4\tmake test\0
$ print -l true 'print -r -- ${(k)history}' | zsh -f -i 2>/dev/null
1
$history came out newest first, and the loop kept its real newlines. fc -rl 1 shows the one event $history left out: event 8, the stand-in for the line being typed. In an interactive zsh, the running command is the event left out. The last command printed 1 for a shell whose history held true and the print line that asked. zsh's perl splits on NULs, so no record ends in a stray newline, and the older duplicates are all dropped: --filter make found only 4 make test.
fish keeps no duplicates to begin with:
Verified output of fzfhist_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ fzf --fish | sed -n '/if type -q perl/,/^ else/p'
if type -q perl
set -a FZF_DEFAULT_OPTS '--tac'
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
else
$ builtin history
for f in *.txt
wc -l < $f
end
git status
git push
make test
vim notes.txt
$ builtin history -z --reverse | show
vim notes.txt\0
make test\0
git push\0
git status\0
for f in *.txt
wc -l < $f
end\0
$ eval $FZF_DEFAULT_COMMAND | show
1\tvim notes.txt\0
2\tmake test\0
3\tgit push\0
4\tgit status\0
5\tfor f in *.txt
\twc -l < $f
\tend\0
$ eval $FZF_DEFAULT_COMMAND | fzf --read0 --print0 --tac --scheme=history --nth=2..,.. --filter git | show
4\tgit status\0
3\tgit push\0
$ eval $FZF_DEFAULT_COMMAND | fzf --read0 --print0 --tac --scheme=history --nth=2..,.. --accept-nth=2.. --filter wc | show
5\tfor f in *.txt
\twc -l < $f
\tend\0
make test and git status were each appended twice, and builtin history lists each once, in the position of its newest copy. -z --reverse prints the list oldest first with a NUL after each entry. The widget's perl numbers the entries with $., so the numbers count entries from the oldest (1 to 5 here) rather than naming events, and it indents continuation lines. --tac then turns the list newest first. The widget also passes --accept-nth=2.., so that picking an entry drops the number, but --filter ignored it: the number is still there. The script re-runs itself as fish --private, so the appended entries never reach a history file.
| bash | zsh | fish | |
|---|---|---|---|
| where the list comes from | fc -lnr -2147483648 |
$history, from zsh/parameter |
builtin history -z --reverse |
| repeats removed by | perl; the oldest entry can slip through | perl, on NUL-separated records | fish itself |
| the number | the event number, counted down from history 1 |
the event number | a count from the oldest |
| newest first by | fc -r |
the order of $history |
--tac |
| a multi-line command | one item with -O lithist, one joined line without |
one item | one item |
If you are coming from another library¶
- Perl. Records are not always lines ↗ covers
$/,-0andchomp. The widget sets$/to a newline and a tab, andchompremoves exactly$/. That is why the oldest entry keeps its newline. - Encodings. The NUL byte ↗ explains why the zero byte is the separator you can trust in a list of arbitrary text, such as commands that contain newlines.
- Rust. Fuzzy finding ↗ covers turning the binding on and pressing Ctrl-R, and what fish's own history pager already offers on that key.
See also¶
- History — the list this pipeline reads: how bash, zsh and fish keep it, and when they write it
- Fuzzy means in order —
--scheme=historyagainst the default scheme on other input - The extended search syntax — the
^in^git - fzf is a filter —
--filterand its exit status - fzf 0.67.0,
shell/key-bindings.bash↗ — the widget, with its zsh and fish siblings in the same folder - bash manual, Bash History Builtins ↗ —
fcandhistory - zsh manual, Zsh Modules ↗ —
zsh/parameterand$history - fish,
history↗ —-z,--reverseandappend