Mastering multi-select mode to pick many items at once, securing input and output with null delimiters, transforming item fields, and rendering ANSI colors from input inside the fzf interface.

In episode 5 you learned to select and exit fzf — including tasting multi-select with Tab and --multi. This time we finish it. This episode covers multi-select, transformations, and ANSI, three abilities that turn fzf from an ordinary list into a real list-processing tool.
One thing we will stress from the start: fzf is just a filter. It takes a text list, narrows it down fuzzily, then prints the selection. All of fzf's power lives in how we control the input and output on both sides of that filter. This episode gives you full control over both. A quick example: seq 20 | fzf --multi already selects many items with just two flags.
fzf's default mode behaves like a cashier at a store: you hand over one item, it processes it, done. Multi-select mode turns it into a shopping cart — you mark many items, then the whole selection comes out at once on a single Enter.
seq 20 | fzf --multiOnce --multi is on, Tab and Shift+Tab mark and unmark the active item — remember the marker from episode 4, that's the indicator that appears next to selected items. Every time you press Enter, fzf prints all selected items, one per line.
Two --multi behaviors that often surprise beginners:
--multi can be limited to a maximum count: --multi=5 only allows up to 5 selected items. Useful for "pick up to 3 files then execute" workflows.select-all and deselect-all actions can be bound to keys via --bind — for example --bind 'ctrl-a:select-all' — so you can mark the entire filtered result without pressing Tab dozens of times. Custom keybindings are dissected in their own episode.Caution
Without --multi, Tab does nothing. This is intentional behavior: the only way to mark multiple items is to enable the mode first. If Tab feels "dead", check whether --multi is set.
Multi-select output is one line per item. That sounds simple, but it hides a classic trap: if you pass the result to xargs without special handling, spaces inside file names will split one name into many arguments.
fzf --multi | xargs rmA file named laporan 2026.pdf will be split into laporan and 2026.pdf — two different arguments. Fortunately fzf provides two special flags for this problem, and that's the next topic.
--read0 and --print0By default, the boundary between one item and the next in fzf is the newline character. The problem: file names on Linux are allowed to contain newlines, tabs, or spaces — combinations that can make your pipeline parse incorrectly. The solution is the null delimiter (\0): a character that technically cannot appear inside a file name, so it is safe as a separator.
| Flag | Function |
|---|---|
--read0 | Read input separated by nulls, not newlines |
--print0 | Print output separated by nulls, not newlines |
This pair works together with null-aware tools, most commonly find -print0 and xargs -0:
find . -type f -print0 | fzf --read0 --print0 | xargs -0 -r rmLet's dissect the line above: find -print0 emits paths with a null separator; --read0 tells fzf the items are null-separated; --print0 keeps fzf's output in null format; xargs -0 accepts that format; and -r prevents rm from running with no input. With this pattern, laporan 2026.pdf stays one intact argument.
Tip
Rule of thumb: as soon as one end of the pipeline uses nulls, every end must use nulls. Matching delimiters on only part of the chain will produce output that either splits apart or fuses together.
Not all fzf input looks like a list of file names. Often you filter column-structured output — the results of ls -l, /etc/passwd, logs, or CSV. For such lists, fzf provides three flags that work together: --delimiter, --nth, and --with-nth.
Imagine each line as a business card: there's a name field, a title field, and an email field. --delimiter defines the separator between fields (default is space and tab), --nth defines which fields take part in the search, and --with-nth defines which fields are displayed. This separates "what you search for" from "what you see".
--delimiter and --nth: Narrowing the Search ScopeBy default, fzf searches the entire line. With --nth, you restrict the search to specific fields — while the full line is still displayed:
cat /etc/passwd | fzf --delimiter=: --nth=1In /etc/passwd, lines look like root:x:0:0:root:/root:/bin/bash. With --delimiter=: and --nth=1, the query admin only matches the first field — you won't get false results from a home path containing the same word.
--with-nth: Changing the Display--with-nth transforms how lines are presented. The simplest syntax is a list of field indexes, with .. as a range and -1 as the last field:
cat /etc/passwd | fzf --delimiter=: --with-nth=1,6The result is that each line only shows field 1 (user) and field 6 (home), separated by the delimiter. For more control, --with-nth accepts templates with curly braces, including {n} for the line's sequence number (zero-based):
echo foo,bar,baz | fzf --delimiter=, --with-nth='{n},{1},{3},{2},{1..2}'Important
Two important consequences: because fzf searches the --with-nth-transformed fields, you cannot search fields hidden by --with-nth — unless you also list them in --nth. Also, --with-nth makes fzf keep the original lines in memory, so use it only when the transformation is really needed.
--accept-nth: Transform Only When Accepting--with-nth changes what's on screen. But there is another case: you want to display the full line (or informative fields) for browsing, yet output only specific fields on Enter. That's where --accept-nth comes in:
echo foo:bar:baz | fzf --delimiter=: --accept-nth=2The list shows foo:bar:baz, but what's printed on Enter is bar. This --with-nth + --accept-nth combination is common for tabular data: show wide context, emit a value that a script can use directly. --accept-nth also accepts curly-brace templates like --with-nth.
Here is a summary of the field index patterns that apply to --nth, --with-nth, and --accept-nth:
| Expression | Meaning |
|---|---|
1 | First field |
2 | Second field |
-1 | Last field |
-2 | Second to last field |
3..5 | Fields 3 through 5 |
2.. | Fields 2 through the end |
..-3 | First field through third from the end |
.. | All fields |
Colored output — from ls --color, git status, rg, to bat — contains ANSI escape codes behind the scenes. By default fzf ignores these codes and shows plain text. If you want the colors to appear, enable --ansi:
ls --color=always | fzf --ansi--color=always forces ls to emit color codes even when its output isn't a terminal (piped to fzf). Without --ansi, those colors are discarded; with --ansi, fzf parses the codes and re-renders them with its own color palette.
Important
Many tools only emit color when the output is considered a terminal (TTY). Piped to fzf, they stop coloring. The solution is always adding the "always" flag — --color=always for ls and rg, --color=always for bat — so the escape codes keep flowing into fzf.
With --ansi active, --highlight-line highlights the entire active line instead of only the part that matches the query. This helps a lot when reading long colored lines — for example the output of git status:
git status --porcelain | fzf --ansi --highlight-lineWith this combination, the item currently highlighted stands out immediately without losing the original colors of the input.
Episode 6 closes the "list processing" phase: you can now select many items with --multi (plus select-all/deselect-all), secure pipelines against spaces and newlines with --read0/--print0, slice structured lines with --delimiter, --nth, --with-nth, and --accept-nth, and preserve the input's original colors with --ansi and --highlight-line.
The key takeaways:
--multi turns fzf into a shopping cart; Tab marks, Enter collects.--read0/--print0 + find -print0 + xargs -0 make pipelines immune to spaces.--delimiter slices lines into fields; --nth sets what's searched; --with-nth sets what's seen; --accept-nth sets what's printed.--ansi brings input colors to life; --highlight-line marks the active line.But all of this still runs inside fzf. In episode 7, we will pull fzf out of a standalone command line and embed it directly into your shell — CTRL-T to select files, CTRL-R to search history, and ALT-C to change directories. See you in episode 7!