Skip to main content

Pipeline Patterns and Script Design

The Two Patterns of fzf Scripting

Pattern 1: Inline Pipe

# Source → fzf → Consumer in one line
docker ps --format "{{.Names}}" | fzf | xargs docker exec -it -e TERM=xterm bash

Simple, readable. Ideal for one-off commands and aliases.

Pattern 2: Capture + Branch

# Capture selection, check exit code, branch
selected=$(ls | fzf)
case $? in
0) nvim "$selected" ;; # success
1) echo "No match" ;; # no results
130) echo "Cancelled" ;; # Esc / CTRL-C
esac

More control. Ideal for shell functions.

Chaining Multiple fzf Pickers

Two-step selector: namespace → pod
# Pick Kubernetes namespace, then pick pod in that namespace
k8s_exec() {
local ns pod
ns=$(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}' \
| tr ' ' '\n' | fzf --prompt 'Namespace> ' --height=40%)
[[ -z "$ns" ]] && return

pod=$(kubectl get pods -n "$ns" --no-headers \
| awk '{print $1, $3}' \
| fzf --prompt "Pod ($ns)> " --height=40% \
| awk '{print $1}')
[[ -z "$pod" ]] && return

kubectl exec -it -n "$ns" "$pod" -- bash
}

Non-interactive Batch Mode (--filter)

Use --filter to run fzf as a fuzzy grep without opening the TUI:

# Find all files matching "config" fuzzily (no TUI)
ls | fzf --filter="config"

# Use in scripts where there's no TTY
find . -type f | fzf --filter="test" | head -5

# Pipe into further processing
ls /etc | fzf --filter="nginx" | xargs cat

Handling No Selection Gracefully

pick_file() {
local file
file=$(find . -type f | fzf) || {
# fzf exits non-zero on cancel or no-match
echo "No file selected." >&2
return 1
}
echo "$file"
}

# Usage
if file=$(pick_file); then
nvim "$file"
fi

fzf Without a TTY (Script Context)

fzf requires a TTY for the interactive TUI. In non-interactive environments, handle gracefully:

pick_or_default() {
local default="$1"
shift
if [ -t 1 ]; then
# TTY available: use interactive fzf
printf '%s\n' "$@" | fzf --prompt 'Select> '
else
# No TTY: return default
echo "$default"
fi
}

Building a CLI Tool with fzf

fman-all: a complete man page browser
#!/usr/bin/env bash
# fman-all: fuzzy man page browser with sections
set -euo pipefail

PROMPT="📖 Man Pages> "
PREVIEW='man $(echo {} | awk "{print \$1}") 2>/dev/null | head -60 || echo "No preview"'

main() {
local selection key
local out
out=$(man -k . 2>/dev/null | sort -u | fzf \
--ansi \
--expect=ctrl-c \
--preview "$PREVIEW" \
--preview-window 'right:60%:wrap' \
--prompt "$PROMPT" \
--header $'Enter: open Ctrl+C: quit\nSearch: name or description' \
--bind 'ctrl-/:toggle-preview')

key=$(head -1 <<< "$out")
selection=$(tail -1 <<< "$out")

[[ "$key" == "ctrl-c" || -z "$selection" ]] && exit 0

local page section
page=$(echo "$selection" | awk '{print $1}')
section=$(echo "$selection" | grep -oP '\(\K[^\)]+')

man "${section:+-s $section}" "$page"
}

main "$@"

Environment Variable Pattern

Override fzf options per function
# Each function can override defaults cleanly
fzf_with_opts() {
FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $1" fzf "${@:2}"
}

# Usage
ls | fzf_with_opts "--height=20 --prompt='File> '"

What's Next