fzf Cheatsheet

Cheatsheet

fzf Cheatsheet

last updated 2026-10-26Daniel Corneschi25 min read

fzf is an interactive filter for the command line. It reads a list of lines (files, processes, branches, history), lets you narrow it by typing, and prints the line you choose. It works with any command that prints lines, so it is the glue behind many small shell helpers.

The commands and functions below ran with fzf 0.74.5 on macOS, under bash 3.2.57 (the macOS /bin/bash, the oldest bash they have to work with) and zsh 5.9, with bat 0.26.1.

1. Install

# Debian / Ubuntu
sudo apt install fzf

# RHEL family, Fedora (the package is in the Fedora repositories, on RHEL it may need EPEL)
sudo dnf install fzf

# macOS (Homebrew)
brew install fzf

# Any Linux or macOS, from source
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install

fzf --version

Distribution packages can be old. The --bash and --zsh shell integration below needs fzf 0.48.0 or newer.

fzf on RHEL and Ubuntu: versions and files

OSWhere fzf comes fromVersionfzf --bash works?Key bindings fileCompletion
RHEL 7Not in the CentOS 7 repositories or the EPEL 7 archive. Use the git installeranywith the installer’s files~/.fzf/shell/key-bindings.bash~/.fzf/shell/completion.bash
RHEL 8, 9, 10Not in BaseOS, AppStream or CRB. EPEL (sudo dnf install epel-release, then sudo dnf install fzf)0.58.0yes/usr/share/fzf/shell/key-bindings.bash/etc/bash_completion.d/fzf
Ubuntu 22.04universe0.29.0no/usr/share/doc/fzf/examples/key-bindings.bash/usr/share/bash-completion/completions/fzf
Ubuntu 24.04universe0.44.1no/usr/share/doc/fzf/examples/key-bindings.bash/usr/share/bash-completion/completions/fzf
Ubuntu 26.04universe0.67.0yes/usr/share/doc/fzf/examples/key-bindings.bash/usr/share/doc/fzf/examples/completion.bash

Ubuntu 25.04 and 25.10 both have 0.60.3. The ~/.fzf.bash file of the legacy line in section 2 is created only by the git installer (~/.fzf/install, unless you pass --bin). A package install never creates it, so that line does nothing after apt install or dnf install.

bat: versions and command names

The fp preview uses bat when it is installed. It is not in the base RHEL repositories of any version; the package is bat.

OSbat
RHEL 7not found
RHEL 8 (EPEL)0.24.0, command bat
RHEL 9 (EPEL)0.26.1, command bat
RHEL 10 (EPEL)0.26.1, command bat
Ubuntu 22.040.19.0, command batcat
Ubuntu 24.040.24.0, command batcat
Ubuntu 26.040.25.0, command batcat

On Debian and Ubuntu the command is batcat, because the name bat was already taken by another package. The preview of fp in section 6 tries both names, so it works on Ubuntu without a symlink.

The RHEL rows come from the EPEL repository metadata (x86_64); “not in the base repositories” comes from the BaseOS, AppStream and CRB (PowerTools on 8) repositories of Rocky Linux 8, 9 and 10 and the CentOS 7 vault, which carry the same packages as RHEL. The Ubuntu rows come from the Ubuntu archive (Launchpad) and the file lists of the .deb packages. Versions move, so check them again before you depend on one.

2. Shell Integration and Key Bindings

# ~/.bashrc  (fzf 0.48.0 or newer)
eval "$(fzf --bash)"

# ~/.zshrc   (fzf 0.48.0 or newer)
source <(fzf --zsh)

# ~/.config/fish/config.fish
fzf --fish | source

# Older fzf (below 0.48.0), installed with the git installer (it writes ~/.fzf.bash)
[ -f ~/.fzf.bash ] && source ~/.fzf.bash

# Older fzf installed from a package: source the file that the package ships (see section 1)
[ -f /usr/share/fzf/shell/key-bindings.bash ] && source /usr/share/fzf/shell/key-bindings.bash                 # RHEL 8, 9, 10 (EPEL)
[ -f /usr/share/doc/fzf/examples/key-bindings.bash ] && source /usr/share/doc/fzf/examples/key-bindings.bash   # Ubuntu 22.04, 24.04

Reload the shell (source ~/.bashrc). Three key bindings are added:

KeysAction
CTRL-RFuzzy-search the command history and insert the result
CTRL-TInsert a file or directory path into the command line
ALT-CPick a subdirectory and cd into it

On macOS, ALT-C types a special character unless the terminal treats Option as Meta (in iTerm2: Profiles, Keys, “Left Option key: Esc+”; in Terminal.app: “Use Option as Meta key”).

Fuzzy Completion with **

The same shell integration adds completion: type ** where an argument goes and press TAB, and fzf picks it.

TypedPicks
vim **<TAB>Files under the current directory (works after any command)
vim ../**<TAB>Files under the parent directory
cd **<TAB>Directories
kill -9 **<TAB>A process; inserts its PID
ssh **<TAB>A host from ~/.ssh/config, ~/.ssh/known_hosts, /etc/ssh/ssh_config or /etc/hosts

To use another trigger than **, set it in your shell config, for example export FZF_COMPLETION_TRIGGER='~~'.

3. Search Syntax (Typed in the Query Box)

TypedMeaning
git pushBoth terms match, each fuzzy (AND)
'wildExact match, no fuzzy matching
'wild'Exact match on word boundaries: wild and a wild b, not wildcard
^coreStarts with core
.md$Ends with .md
!testDoes not contain test
!^testDoes not start with test
^src | ^appStarts with src or app (OR, spaces around the bar)

Case is smart by default: a query in lowercase ignores case, and one uppercase letter makes it case-sensitive (core finds Core.md and core.md, Core only Core.md). -i always ignores case, +i never does. With -e every term is exact, and ' then makes a term fuzzy.

Keys Inside fzf

KeysAction
CTRL-J, CTRL-K, Down, UpMove down and up
CTRL-N, CTRL-PMove to the next or previous match (with --history, browse the query history instead)
ENTERAccept the highlighted line (all marked lines with -m)
ESC, CTRL-C, CTRL-G, CTRL-QQuit without selecting
TAB, Shift-TABMark or unmark a line and move (with -m)
CTRL-W, CTRL-UDelete the word before the cursor, or the whole query
Shift-Up, Shift-DownScroll the preview

4. Options

OptionEffect
--height 40%Use the bottom 40% of the screen instead of the whole terminal
--layout=reverseInput line on top, candidates below
--borderDraw a border
-m, --multiMulti-select: mark rows with TAB, the selection prints one per line
--preview 'cmd {}'Show a preview window. {} is the highlighted line, {1} its first field
-q, --queryInitial query
-f, --filterNon-interactive: print the matches for the query and exit (good for scripts and tests)
--tacReverse the input order
--no-sortKeep the input order instead of sorting by match score
-1, --select-1If only one line matches, select it without asking
-0, --exit-0Exit if nothing matches
--ansiProcess ANSI color codes in the input (needed for git log --color=always)
-e, --exactExact matching for every term (' makes a term fuzzy)
-i, +iAlways ignore case, or always match case (default: smart case)
--header '<text>'A fixed line of text above the list
--preview-window right,50%Position and size of the preview; up, down, left, sizes in % or lines
--preview-window hiddenStart with the preview hidden (show it with a toggle-preview binding)
-d, --delimiter :Split lines into fields at : (default: whitespace)
-n, --nth 2Search only field 2
--with-nth 2..Show only fields 2 and later (the output is still the whole line)
--print0End each output line with NUL instead of a newline, for xargs -0

Placeholders

Used in --preview and in --bind commands. fzf puts each value in single quotes.

PlaceholderValue
{}The highlighted line
{1}, {2}Field 1, field 2 (see --delimiter)
{+}Every marked line (the highlighted one if none is marked)
{q}The current query

Key Bindings with –bind

fzf --preview 'cat {}' --bind 'ctrl-/:toggle-preview'   # CTRL-/ shows and hides the preview
fzf --bind 'enter:become(vim {})'                        # ENTER replaces fzf with vim on the chosen file
fzf --bind 'ctrl-o:execute(less {})'                     # CTRL-O opens less, then returns to fzf
fzf --bind 'ctrl-r:reload(ls)'                           # CTRL-R replaces the list with new output

become ends fzf and runs the command in its place, so nothing is printed. execute runs it and comes back to the list.

5. Environment Variables

# ~/.bashrc
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
export FZF_DEFAULT_COMMAND="find . -type f -not -path '*/.git/*'"
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"      # CTRL-T does not read FZF_DEFAULT_COMMAND
VariableEffect
FZF_DEFAULT_OPTSOptions added to every fzf run
FZF_DEFAULT_COMMANDThe command that produces the list for a plain fzf (no pipe). rg --files is another common choice
FZF_CTRL_T_COMMANDThe command that produces the list for CTRL-T. It does not fall back to FZF_DEFAULT_COMMAND
FZF_ALT_C_COMMANDThe same for ALT-C (directories)

When FZF_CTRL_T_COMMAND and FZF_ALT_C_COMMAND are not set, CTRL-T and ALT-C use fzf’s built-in walker (files and directories for CTRL-T, directories for ALT-C), so setting only FZF_DEFAULT_COMMAND changes a plain fzf and nothing else. FZF_DEFAULT_COMMAND must be a real binary. An alias does not work there, and if the binary is missing the list is empty.

Themes

fzf --color=light            # built-in schemes: dark (default), light, base16 (the terminal's 16 colors), bw
fzf --style=full:rounded     # style presets (fzf 0.58+): default, minimal, full[:border style]
fzf --color=dark,hl:#ff5f87,pointer:#ff5f87   # a base scheme with single elements changed

The main elements are fg, bg, hl (matched letters), fg+, bg+ and hl+ (the current line), prompt, pointer, marker, info, border and header; man fzf lists all of them under COLOR NAMES.

Themes from the editor color schemes, added to your own options instead of replacing them:

# Dracula (github.com/dracula/fzf)
export FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --color=fg:#f8f8f2,bg:#282a36,hl:#bd93f9 --color=fg+:#f8f8f2,bg+:#44475a,hl+:#bd93f9 --color=info:#ffb86c,prompt:#50fa7b,pointer:#ff79c6 --color=marker:#ff79c6,spinner:#ffb86c,header:#6272a4"

# Catppuccin Mocha (github.com/catppuccin/fzf, also Latte, Frappé and Macchiato)
export FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --color=bg+:#313244,bg:#1E1E2E,spinner:#F5E0DC,hl:#F38BA8 --color=fg:#CDD6F4,header:#F38BA8,info:#CBA6F7,pointer:#F5E0DC --color=marker:#B4BEFE,fg+:#CDD6F4,prompt:#CBA6F7,hl+:#F38BA8 --color=selected-bg:#45475A --color=border:#6C7086,label:#CDD6F4"

The projects’ own snippets assign FZF_DEFAULT_OPTS='...' directly, which drops the options you set above (--height, --layout, --border); "$FZF_DEFAULT_OPTS ..." keeps them. Tokyo Night has its file in extras/fzf/ of github.com/folke/tokyonight.nvim.

6. Aliases and Functions

Every alias and function below has its own block, so you can copy only the ones you want into ~/.bashrc or ~/.zshrc. To take all of them at once, use All in One Block at the end of this section. All of them work in bash and zsh, and each one works on its own, except where the text says it needs another block. The key bindings come from section 2 and the default options from section 5.

Overview

NameKindWhat it does
fmaliasfzf --multi, multi-select
fpaliasFile picker with a bat or batcat preview (the first 50 lines if neither exists)
fglaliasBrowse commits, preview the diff of the highlighted one
fhelpaliasList all the aliases and functions below in fzf; ENTER prints the chosen name
fe [query]functionOpen a file in $EDITOR, jump straight to it if only one matches
fcd [dir]functioncd into a subdirectory, skipping .git
fgbfunctionCheck out a local or remote branch, strips remotes/origin/
fgafunctionStage files picked from the modified and untracked ones (TAB for several), safe for names with spaces
fgc [message]functionStage everything (modified, deleted, new and untracked files) and commit it, after a yes/no check
fgsfunctionBrowse stashes with a diff preview, then show the chosen one
fawsfunctionPick an AWS CLI profile and export AWS_PROFILE for the current shell
fkcfunctionSwitch the current kubectl context
fsshfunctionPick a host from ~/.ssh/config (wildcard entries skipped) and connect
fportfunctionList listening TCP ports and print the chosen line (the PID is the second column)
fsys [action]functionPick a systemd service: show its status, follow its logs (fsys logs) or run an action (fsys restart)
fjl [priority]functionSearch this boot’s journal at a priority or worse (default warning), newest first
fenvfunctionPick an environment variable, preview and print its value
fman [query]functionPick a man page with a preview of it, then open it
_fdcfunctionHelper for the three Docker functions: pick running containers, print their names
fdlfunctionFollow the logs of a container (needs _fdc)
fdxfunctionOpen a shell in a container, bash if it exists, else sh (needs _fdc)
fdsfunctionStop containers, TAB for several (needs _fdc)

Aliases

fm: multi-select

alias fm='fzf --multi'                                   # TAB marks rows, one per line on output

fp: file picker with preview

The preview uses bat, or batcat on Debian and Ubuntu, and the first 50 lines when neither is installed.

# fp: file picker with preview (bat, batcat, or the first 50 lines)
alias fp="fzf --preview 'if command -v bat >/dev/null; then bat --color=always --style=numbers {}; elif command -v batcat >/dev/null; then batcat --color=always --style=numbers {}; else head -n 50 {}; fi'"

fgl: browse commits

alias fgl="git log --oneline --color=always | fzf --ansi --preview 'git show --color=always {1}'"   # browse commits

fhelp: list these aliases and functions

# fhelp: list the aliases and functions of this cheatsheet, ENTER prints the chosen name
alias fhelp='printf "%s\n" \
  "fm               alias      fzf --multi, multi-select" \
  "fp               alias      file picker with a bat or batcat preview" \
  "fgl              alias      browse commits with a diff preview" \
  "fe [query]       function   open a file in EDITOR" \
  "fcd [dir]        function   cd into a subdirectory" \
  "fgb              function   check out a local or remote branch" \
  "fga              function   stage picked files" \
  "fgc [message]    function   stage everything and commit, after a yes/no check" \
  "fgs              function   browse stashes" \
  "faws             function   pick an AWS profile and export AWS_PROFILE" \
  "fkc              function   switch the kubectl context" \
  "fssh             function   connect to a host from ~/.ssh/config" \
  "fport            function   list listening TCP ports" \
  "fsys [action]    function   systemd service: status, logs, restart, ..." \
  "fjl [priority]   function   search the journal of this boot" \
  "fenv             function   look up an environment variable" \
  "fman [query]     function   open a man page" \
  "fdl              function   follow the logs of a container" \
  "fdx              function   open a shell in a container" \
  "fds              function   stop containers" \
  | fzf --no-sort --header "fzf aliases and functions" | awk "{print \$1}"'

The list is written into the alias, because the shell keeps neither the comments nor which functions came from here (declare -F also shows fzf’s own internal functions and anything else you defined). When you add or remove a function, change its line here too.

Functions

fe: open a file in $EDITOR

# fe [query]: open the chosen file in $EDITOR
fe() { local f; f=$(fzf --query "$1" --select-1 --exit-0) && [ -n "$f" ] && ${EDITOR:-vim} "$f"; }

fcd: cd into a subdirectory

# fcd [dir]: cd into a subdirectory
fcd() { local d; d=$(command find "${1:-.}" -type d -not -path '*/.git/*' 2>/dev/null | fzf) && [ -n "$d" ] && cd "$d"; }

fgb: check out a branch

# fgb: check out a branch (local or remote)
fgb() {
  local b
  b=$(git branch --all | grep -v HEAD | sed 's#^[* ]*##; s#^remotes/[^/]*/##' | sort -u | fzf) && [ -n "$b" ] && git checkout "$b"
}

fgb removes the remotes/<name>/ prefix, because git checkout origin/x would give a detached HEAD, while git checkout x creates a tracking branch.

fga: stage picked files

# fga: stage files picked from the modified and untracked ones (TAB for several)
fga() {
  local sel f files=()
  sel=$(git ls-files --modified --others --exclude-standard | sort -u | fzf -m) || return
  while IFS= read -r f; do [ -n "$f" ] && files+=("$f"); done <<< "$sel"
  [ "${#files[@]}" -gt 0 ] && git add -- "${files[@]}" && git status --short
}

fgc: stage everything and commit

# fgc [message]: stage ALL changes (modified, deleted, new and untracked) and commit them, after a yes/no check
fgc() {
  local msg="$*" answer
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "not in a git repo" >&2; return 1; }
  [ -n "$(git status --porcelain)" ] || { echo "nothing to commit"; return 0; }
  git status --short
  if [ -z "$msg" ]; then printf 'Commit message: '; IFS= read -r msg; fi
  [ -n "$msg" ] || { echo "empty message, nothing done" >&2; return 1; }
  answer=$(printf 'yes\nno\n' | fzf --height 20% --header "Stage ALL changes above and commit \"$msg\"?")
  [ "$answer" = yes ] && git add -A && git commit -m "$msg"
}

fgc stages everything the repository does not ignore. It runs git add -A, which covers the whole working tree even when you run it from a subdirectory, new and untracked files included. Read the status list before you answer yes: a .env file, a key, a build folder or a large file that is not in .gitignore goes into the commit. Answering no (or leaving fzf with ESC) changes nothing, and an empty message stops it. For a selective commit use fga and then git commit.

fgs: browse stashes

# fgs: pick a stash and show it (use "git stash apply <name>" to restore)
fgs() {
  local s
  s=$(git stash list | fzf --preview 'git stash show -p --color=always $(echo {} | cut -d: -f1)' | cut -d: -f1) \
    && [ -n "$s" ] && git stash show -p --color=always "$s"
}

faws: pick an AWS profile

# faws: pick an AWS CLI profile and export it for this shell
faws() {
  local p
  p=$(aws configure list-profiles | fzf --header 'AWS_PROFILE') && [ -n "$p" ] && export AWS_PROFILE="$p" && echo "AWS_PROFILE=$p"
}

faws only exports the profile name. For an SSO profile you still log in with aws sso login --profile "$AWS_PROFILE".

fkc: switch the kubectl context

# fkc: switch the current kubectl context (this edits your kubeconfig for every terminal)
fkc() { local c; c=$(kubectl config get-contexts -o name | fzf --header 'kubectl context') && [ -n "$c" ] && kubectl config use-context "$c"; }

kubectl config use-context edits the kubeconfig, so the new context applies to every terminal and every later command. If the current context can be a production cluster, pick the context for one command instead: kubectl --context "$(kubectl config get-contexts -o name | fzf)" get pods.

fssh: connect to a host from ~/.ssh/config

# fssh: pick a host from ~/.ssh/config and connect
fssh() {
  local h
  h=$(awk 'tolower($1)=="host" { for (i=2; i<=NF; i++) if ($i !~ /[*?!]/) print $i }' ~/.ssh/config 2>/dev/null | sort -u | fzf --header 'ssh host') \
    && [ -n "$h" ] && ssh "$h"
}

fport: list listening ports

# fport: list listening TCP ports and print the chosen line (PID is column 2)
fport() { lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | sed 1d | fzf --header 'COMMAND PID ... NAME'; }

fport only prints a line. To free a port, find the PID with fport, then kill <pid>.

fsys: manage a systemd service

# fsys [action]: pick a systemd service; status by default, "logs" follows its journal,
# any other systemctl action (start, stop, restart, enable, disable, ...) runs with sudo
fsys() {
  local act=${1:-status} u
  u=$( { systemctl list-units --type=service --all --no-legend --plain | awk '$2 != "not-found" {print $1}'
         systemctl list-unit-files --type=service --no-legend | awk '{print $1}'; } \
       | grep -v '@\.service$' | sort -u \
       | fzf --header "systemctl $act" --preview 'SYSTEMD_COLORS=1 systemctl status --no-pager {}') || return
  [ -n "$u" ] || return
  case $act in
    status) systemctl status --no-pager "$u" ;;
    logs)   journalctl -u "$u" -f -n 100 ;;
    *)      if [ "$(id -u)" -eq 0 ]; then systemctl "$act" "$u"; else sudo systemctl "$act" "$u"; fi ;;
  esac
}
RunDoes
fsyssystemctl status of the picked service
fsys logsFollow its journal (journalctl -u <unit> -f, the last 100 lines first); CTRL-C stops
fsys restartsudo systemctl restart it; also start, stop, enable, disable or any other systemctl action

The list holds the loaded services and the installed ones that are not loaded (so a disabled service can be started too), with a live systemctl status preview. It leaves out units that no longer exist (not-found) and templates such as getty@.service, which can’t be started without an instance name. As root it runs the action without sudo. fsys needs systemd, so it works on Linux, not on macOS; it was tested on Debian 12 with systemd 252.

fjl: search the journal

# fjl [priority]: search this boot's journal at <priority> or worse (default warning), newest first
fjl() { journalctl -b -p "${1:-warning}" -o short-iso --no-pager | fzf -m --tac --no-sort --header "journal, ${1:-warning} and worse"; }

fjl lists this boot’s journal entries at the given priority or worse, newest first: fjl for warnings and errors, fjl err for errors only. Type a unit name or a word to narrow the list; TAB marks several lines and ENTER prints them. The priorities are emerg, alert, crit, err, warning, notice, info and debug. Reading the whole journal needs root or membership in the systemd-journal (or adm) group; otherwise you see only your own entries. Like fsys, it needs systemd.

fenv: look up an environment variable

# fenv: pick an exported environment variable and print its value
fenv() {
  local v
  v=$(env | sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' | sort -u | fzf --preview 'printenv {}') && [ -n "$v" ] && printenv "$v"
}

fenv lists exported variables only: a shell variable without export is not in the environment, so it does not appear.

fman: open a man page

# fman [query]: pick a man page from "man -k" with a preview, then open it
fman() {
  local sel name sec
  sel=$(man -k . 2>/dev/null | fzf --query "$1" \
          --preview 'man "$(echo {} | sed -E "s/^[^(]*\(([^)]*)\).*/\1/")" "$(echo {} | sed -E "s/^ *([^ ,(]+).*/\1/")" 2>/dev/null') || return
  [ -n "$sel" ] || return
  name=$(echo "$sel" | sed -E 's/^ *([^ ,(]+).*/\1/')
  sec=$(echo "$sel" | sed -E 's/^[^(]*\(([^)]*)\).*/\1/')
  man "$sec" "$name"
}

fman reads the list from man -k ., so it shows nothing until the man page index exists. Build it with sudo mandb on Linux or sudo /usr/libexec/makewhatis on macOS. Minimal installs and containers often have no man pages at all (Ubuntu’s minimized images restore them with unminimize).

_fdc: pick containers (helper)

The three Docker functions below need this helper.

# _fdc: pick running containers and print their names (helper for fdl, fdx, fds)
_fdc() { docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' | fzf "$@" | cut -f1; }

fdl: follow container logs

# fdl: follow the logs of a container
fdl() { local c; c=$(_fdc) && [ -n "$c" ] && docker logs -f --tail 100 "$c"; }

fdx: open a shell in a container

# fdx: open a shell in a container, bash if it exists, else sh
fdx() { local c; c=$(_fdc) && [ -n "$c" ] && docker exec -it "$c" sh -c 'command -v bash >/dev/null && exec bash || exec sh'; }

fds: stop containers

# fds: stop containers (TAB for several)
fds() { local sel c cs=(); sel=$(_fdc -m) || return; while IFS= read -r c; do [ -n "$c" ] && cs+=("$c"); done <<< "$sel"; [ "${#cs[@]}" -gt 0 ] && docker stop "${cs[@]}"; }

fdx and fds act on running containers only (docker ps). fds stops what you mark, so look at the marked rows before pressing ENTER.

All in One Block

The same aliases and functions as above, in one block to paste into ~/.bashrc or ~/.zshrc in a single step.

# ---- fzf aliases and functions, works in bash and zsh ----

# ---- aliases ----
alias fm='fzf --multi'                                   # TAB marks rows, one per line on output
# fp: file picker with preview (bat, batcat, or the first 50 lines)
alias fp="fzf --preview 'if command -v bat >/dev/null; then bat --color=always --style=numbers {}; elif command -v batcat >/dev/null; then batcat --color=always --style=numbers {}; else head -n 50 {}; fi'"
alias fgl="git log --oneline --color=always | fzf --ansi --preview 'git show --color=always {1}'"   # browse commits
# fhelp: list the aliases and functions of this cheatsheet, ENTER prints the chosen name
alias fhelp='printf "%s\n" \
  "fm               alias      fzf --multi, multi-select" \
  "fp               alias      file picker with a bat or batcat preview" \
  "fgl              alias      browse commits with a diff preview" \
  "fe [query]       function   open a file in EDITOR" \
  "fcd [dir]        function   cd into a subdirectory" \
  "fgb              function   check out a local or remote branch" \
  "fga              function   stage picked files" \
  "fgc [message]    function   stage everything and commit, after a yes/no check" \
  "fgs              function   browse stashes" \
  "faws             function   pick an AWS profile and export AWS_PROFILE" \
  "fkc              function   switch the kubectl context" \
  "fssh             function   connect to a host from ~/.ssh/config" \
  "fport            function   list listening TCP ports" \
  "fsys [action]    function   systemd service: status, logs, restart, ..." \
  "fjl [priority]   function   search the journal of this boot" \
  "fenv             function   look up an environment variable" \
  "fman [query]     function   open a man page" \
  "fdl              function   follow the logs of a container" \
  "fdx              function   open a shell in a container" \
  "fds              function   stop containers" \
  | fzf --no-sort --header "fzf aliases and functions" | awk "{print \$1}"'

# ---- functions ----
# fe [query]: open the chosen file in $EDITOR
fe() { local f; f=$(fzf --query "$1" --select-1 --exit-0) && [ -n "$f" ] && ${EDITOR:-vim} "$f"; }

# fcd [dir]: cd into a subdirectory
fcd() { local d; d=$(command find "${1:-.}" -type d -not -path '*/.git/*' 2>/dev/null | fzf) && [ -n "$d" ] && cd "$d"; }

# fgb: check out a branch (local or remote)
fgb() {
  local b
  b=$(git branch --all | grep -v HEAD | sed 's#^[* ]*##; s#^remotes/[^/]*/##' | sort -u | fzf) && [ -n "$b" ] && git checkout "$b"
}

# fga: stage files picked from the modified and untracked ones (TAB for several)
fga() {
  local sel f files=()
  sel=$(git ls-files --modified --others --exclude-standard | sort -u | fzf -m) || return
  while IFS= read -r f; do [ -n "$f" ] && files+=("$f"); done <<< "$sel"
  [ "${#files[@]}" -gt 0 ] && git add -- "${files[@]}" && git status --short
}

# fgc [message]: stage ALL changes (modified, deleted, new and untracked) and commit them, after a yes/no check
fgc() {
  local msg="$*" answer
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "not in a git repo" >&2; return 1; }
  [ -n "$(git status --porcelain)" ] || { echo "nothing to commit"; return 0; }
  git status --short
  if [ -z "$msg" ]; then printf 'Commit message: '; IFS= read -r msg; fi
  [ -n "$msg" ] || { echo "empty message, nothing done" >&2; return 1; }
  answer=$(printf 'yes\nno\n' | fzf --height 20% --header "Stage ALL changes above and commit \"$msg\"?")
  [ "$answer" = yes ] && git add -A && git commit -m "$msg"
}

# fgs: pick a stash and show it (use "git stash apply <name>" to restore)
fgs() {
  local s
  s=$(git stash list | fzf --preview 'git stash show -p --color=always $(echo {} | cut -d: -f1)' | cut -d: -f1) \
    && [ -n "$s" ] && git stash show -p --color=always "$s"
}

# faws: pick an AWS CLI profile and export it for this shell
faws() {
  local p
  p=$(aws configure list-profiles | fzf --header 'AWS_PROFILE') && [ -n "$p" ] && export AWS_PROFILE="$p" && echo "AWS_PROFILE=$p"
}

# fkc: switch the current kubectl context (this edits your kubeconfig for every terminal)
fkc() { local c; c=$(kubectl config get-contexts -o name | fzf --header 'kubectl context') && [ -n "$c" ] && kubectl config use-context "$c"; }

# fssh: pick a host from ~/.ssh/config and connect
fssh() {
  local h
  h=$(awk 'tolower($1)=="host" { for (i=2; i<=NF; i++) if ($i !~ /[*?!]/) print $i }' ~/.ssh/config 2>/dev/null | sort -u | fzf --header 'ssh host') \
    && [ -n "$h" ] && ssh "$h"
}

# fport: list listening TCP ports and print the chosen line (PID is column 2)
fport() { lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | sed 1d | fzf --header 'COMMAND PID ... NAME'; }

# fsys [action]: pick a systemd service; status by default, "logs" follows its journal,
# any other systemctl action (start, stop, restart, enable, disable, ...) runs with sudo
fsys() {
  local act=${1:-status} u
  u=$( { systemctl list-units --type=service --all --no-legend --plain | awk '$2 != "not-found" {print $1}'
         systemctl list-unit-files --type=service --no-legend | awk '{print $1}'; } \
       | grep -v '@\.service$' | sort -u \
       | fzf --header "systemctl $act" --preview 'SYSTEMD_COLORS=1 systemctl status --no-pager {}') || return
  [ -n "$u" ] || return
  case $act in
    status) systemctl status --no-pager "$u" ;;
    logs)   journalctl -u "$u" -f -n 100 ;;
    *)      if [ "$(id -u)" -eq 0 ]; then systemctl "$act" "$u"; else sudo systemctl "$act" "$u"; fi ;;
  esac
}

# fjl [priority]: search this boot's journal at <priority> or worse (default warning), newest first
fjl() { journalctl -b -p "${1:-warning}" -o short-iso --no-pager | fzf -m --tac --no-sort --header "journal, ${1:-warning} and worse"; }

# fenv: pick an exported environment variable and print its value
fenv() {
  local v
  v=$(env | sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' | sort -u | fzf --preview 'printenv {}') && [ -n "$v" ] && printenv "$v"
}

# fman [query]: pick a man page from "man -k" with a preview, then open it
fman() {
  local sel name sec
  sel=$(man -k . 2>/dev/null | fzf --query "$1" \
          --preview 'man "$(echo {} | sed -E "s/^[^(]*\(([^)]*)\).*/\1/")" "$(echo {} | sed -E "s/^ *([^ ,(]+).*/\1/")" 2>/dev/null') || return
  [ -n "$sel" ] || return
  name=$(echo "$sel" | sed -E 's/^ *([^ ,(]+).*/\1/')
  sec=$(echo "$sel" | sed -E 's/^[^(]*\(([^)]*)\).*/\1/')
  man "$sec" "$name"
}

# _fdc: pick running containers and print their names (helper for fdl, fdx, fds)
_fdc() { docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' | fzf "$@" | cut -f1; }

# fdl: follow the logs of a container
fdl() { local c; c=$(_fdc) && [ -n "$c" ] && docker logs -f --tail 100 "$c"; }

# fdx: open a shell in a container, bash if it exists, else sh
fdx() { local c; c=$(_fdc) && [ -n "$c" ] && docker exec -it "$c" sh -c 'command -v bash >/dev/null && exec bash || exec sh'; }

# fds: stop containers (TAB for several)
fds() { local sel c cs=(); sel=$(_fdc -m) || return; while IFS= read -r c; do [ -n "$c" ] && cs+=("$c"); done <<< "$sel"; [ "${#cs[@]}" -gt 0 ] && docker stop "${cs[@]}"; }

How the Functions Are Written

  • They do nothing on ESC. fe, fcd and fgb check that fzf returned something, so they never run a command on an empty selection.
  • Selections are read with a here-string. fga and fds first store fzf’s output (sel=$(fzf -m)) and then read it with <<< "$sel". The shorter done < <(fzf -m) works in bash but not in zsh: there fzf never appears, so nothing gets staged or stopped.
  • Your aliases leak into functions. Bash expands aliases when a function is defined, so an alias such as alias find='find . -name' (common in ~/.bash_aliases) changes the find call inside fcd: it stops working. That is why fcd uses command find. If you add your own functions, put command in front of any command that you have aliased.
  • In zsh, $EDITOR is not split into words. fe runs ${EDITOR:-vim} ..., which works in bash for EDITOR='code --wait' but fails in zsh with fe: command not found: code --wait. In zsh use an editor that is a single word, or a small wrapper script.

7. Gotchas

  • fzf reads from a pipe when there is one, and runs FZF_DEFAULT_COMMAND (or a built-in find) only when there is none. In scripts without a terminal, pipe the list in yourself.
  • Ubuntu and Debian rename bat to batcat. The preview in fp handles both names. Commands that you type yourself, such as fzf --preview 'bat {}', need the real name or a symlink (ln -s "$(command -v batcat)" ~/.local/bin/bat).
  • A plain fzf lists files, not directories. Use find . -type d | fzf or ALT-C. (CTRL-T lists files and directories.)
  • {} in a preview is quoted for the shell by fzf, but a line that contains single quotes can still confuse a complex preview command. Prefer {1}, {2} fields for tabular input.
  • Old fzf versions (below 0.48.0) have no --bash and --zsh. Use the installer script, which writes ~/.fzf.bash.