Skip to main content

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:

KeyAction
TabToggle current item
Shift+TabToggle and move up
Ctrl+ASelect all
Ctrl+DDeselect all
EnterOutput 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:

VariableValue
$FZF_QUERYCurrent query string
$FZF_PROMPTCurrent prompt text
$FZF_ACTIONCurrent key/event name
$FZF_KEYRaw key pressed
$FZF_SELECT_COUNTNumber of marked items
$FZF_MATCH_COUNTNumber of matching items
$FZF_TOTAL_COUNTTotal number of items
$FZF_NTHCurrent --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'
}

What's Next