Multi-select and Advanced Selection
Multi-select turns fzf from a "pick one" tool into a "pick many" tool. Combined with reload and transform, it becomes a full interactive workflow engine.
Basic Multi-select
# Enable with -m or --multi
ls | fzf -m
# Limit selections
ls | fzf --multi=5 # allow max 5 selections
# With visible counter
ls | fzf -m --info=inline
Keys in multi-select mode:
| Key | Action |
|---|---|
Tab | Toggle current item |
Shift+Tab | Toggle and move up |
Ctrl+A | Select all |
Ctrl+D | Deselect all |
Enter | Output all marked items |
Using Multi-select Output
# Open multiple files in vim (all in buffers)
vim $(find . -type f | fzf -m)
# Delete multiple files
find . -name "*.bak" | fzf -m | xargs rm -v
# Open all selected in separate terminal windows
find . -type f -name "*.conf" | fzf -m | while IFS= read -r file; do
# Process each selected file
echo "Processing: $file"
done
# Batch process with xargs (-0 for null-delimiter safety)
find . -name "*.py" | fzf -m --print0 | xargs -0 python -m py_compile
--reload — Dynamic List Refresh
reload triggers a new source command while staying inside fzf. The list updates in place:
# Switch between listing files and directories
find . -type f | fzf \
--bind 'ctrl-f:reload(find . -type f)' \
--bind 'ctrl-d:reload(find . -type d)' \
--bind 'ctrl-a:reload(find .)' \
--header 'CTRL-F: files CTRL-D: dirs CTRL-A: all'
Reload Based on Query
# Live grep mode: reload with ripgrep as you type
: | fzf \
--ansi \
--disabled \
--bind 'start:reload(echo "Start typing to grep...")' \
--bind 'change:reload(rg --color=always --line-number --no-heading {q} || echo "No results")' \
--delimiter=: \
--preview 'bat --color=always --highlight-line={2} {1}' \
--preview-window 'right:60%:+{2}/2' \
--header 'Type to grep. Ctrl+/ for preview.'
--disabled disables fzf's own filtering so rg handles it.
--transform — Dynamic Option Change
transform executes a shell command and interprets its stdout as fzf options to apply:
# Toggle between file and directory view with different headers
find . | fzf \
--bind 'ctrl-t:transform(
if [[ ! $FZF_PROMPT =~ Dir ]]; then
echo "change-prompt(📁 Dir> )+reload(find . -type d)"
else
echo "change-prompt(📄 File> )+reload(find . -type f)"
fi
)' \
--prompt '📄 File> '
FZF_PROMPT and FZF_QUERY in Transforms
Inside --bind actions and --transform, these variables are available:
| Variable | Value |
|---|---|
$FZF_QUERY | Current query string |
$FZF_PROMPT | Current prompt text |
$FZF_ACTION | Current key/event name |
$FZF_KEY | Raw key pressed |
$FZF_SELECT_COUNT | Number of marked items |
$FZF_MATCH_COUNT | Number of matching items |
$FZF_TOTAL_COUNT | Total number of items |
$FZF_NTH | Current --nth value |
# Show selection count in border label
ls | fzf \
-m \
--border=rounded \
--border-label=" 0 selected" \
--bind 'focus:transform-border-label( {} )' \
--bind 'load:transform-border-label( 0 of $FZF_TOTAL_COUNT )' \
--bind 'change:transform-border-label( $FZF_SELECT_COUNT of $FZF_MATCH_COUNT selected )'
Interactive Delete with Confirmation
# fzf-powered rm with preview and confirmation
fzf_rm() {
find . \( -name "*.bak" -o -name "*.tmp" \) | fzf \
-m \
--preview 'file {}; wc -l {}' \
--header 'Tab: mark Enter: DELETE marked files' \
--bind 'enter:execute(echo "Deleting:"; echo {} | xargs ls -lh | head -5; echo "---"; read -p "Confirm? [y/N] " a; [[ $a == y ]] && echo {} | xargs rm -v)' \
--bind 'ctrl-a:select-all'
}