sharkdp/fd
Walker
Active contributors: sharkdp, tmccombs, tavianator
Purpose
The walker is fd's hot path. It enumerates filesystem entries from one or more root paths, applies the configured filters, batches surviving results, and either streams them to stdout or hands them to the command-execution subsystem. It also owns the Ctrl-C handler and the buffer-then-stream policy that keeps short searches sorted and long searches responsive.
Directory layout
src/
├── walk.rs # WorkerState, ReceiverBuffer, Batch/BatchSender,
│ # build_walker, build_overrides, spawn_senders, scan
└── dir_entry.rs # DirEntry wrapper used throughoutThe walker delegates the actual recursion to ignore::WalkParallel from the ignore crate.
Key abstractions
| Type | File | Description |
|---|---|---|
WorkerState |
src/walk.rs |
Holds patterns, Config, and the two Arc<AtomicBool> flags. The methods on it (scan, build_walker, spawn_senders, receive) are the entire walker. |
Batch |
src/walk.rs |
Arc<Mutex<Option<Vec<WorkerResult>>>>. Multiple senders append into the same Vec between channel sends. |
BatchSender |
src/walk.rs |
Per-thread wrapper around the Sender and current Batch. Decides when to flush. |
WorkerResult |
src/walk.rs |
Entry(DirEntry) or Error(ignore::Error). |
ReceiverBuffer |
src/walk.rs |
The state machine that buffers up to 1000 results or 100 ms, sorts, then streams. |
ReceiverMode |
src/walk.rs |
Two states: Buffering and Streaming. |
DirEntry |
src/dir_entry.rs |
Either a Normal(ignore::DirEntry) or BrokenSymlink(PathBuf). Lazy metadata + style. |
Constants of note:
MAX_BUFFER_LENGTH = 1000— switch to streaming after this many buffered entries.DEFAULT_MAX_BUFFER_TIME = 100ms— also switches to streaming after this much wall time.- Channel capacity =
2 * config.threads, set inWorkerState::scan. - Batch limit =
0x100(256), or1when running per-result--execwith multiple threads (so thatBatches spread across exec workers).
How it works
sequenceDiagram
participant Main as walk::scan
participant Walker as ignore::WalkParallel
participant Sender as Sender thread (per cpu)
participant Channel as crossbeam-channel<br/>(bounded 2*threads)
participant Receiver as ReceiverBuffer
Main->>Main: build WalkParallel via build_walker()<br/>install ctrlc handler
Main->>Walker: walker.run(|| sender_closure)
par For each thread
Walker->>Sender: traversed entry
Sender->>Sender: skip root (depth==0),<br/>skip if quit_flag set,<br/>apply filter chain,<br/>style if printing
Sender->>Channel: BatchSender.send(WorkerResult)
end
Receiver->>Channel: recv_deadline(deadline) /<br/>recv() in streaming mode
Channel-->>Receiver: Batch
Receiver->>Receiver: if Buffering and<br/>len > MAX_BUFFER_LENGTH<br/>→ stream(): sort, switch to Streaming
Receiver->>Receiver: if Streaming → print directly
Receiver-->>Main: ExitCode after disconnect / max_results / SIGINTSetup
WorkerState::scan does five things:
- Calls
build_walker(paths)to produce theignore::WalkParallel. - If colored output is on, registers a
ctrlchandler. First Ctrl-C setsquit_flag; the second one callsExitCode::KilledBySigint.exit()immediately. - Creates a bounded
crossbeam-channelwith capacity2 * threads. - Inside
thread::scope, spawns the receiver and runsspawn_senderson the calling thread. - After the senders finish and the receiver joins, returns
KilledBySigintif the interrupt flag was set, or the exit code from the receiver.
build_walker
WorkerState::build_walker constructs an ignore::WalkBuilder configured to:
hidden(config.ignore_hidden),- respect
.ignore, parent-directory ignores, and VCS ignores per the user's flags, - require git for VCS handling unless
--no-require-gitwas passed, add_custom_ignore_filename(".fdignore")when fdignore is enabled,- read the global ignore file from
${etcetera::config_dir()}/fd/ignoreif present, - add any
--ignore-filepaths, - register
--excludepatterns viaOverrideBuilder, follow_links(config.follow_links)andsame_file_system(config.one_file_system),- honour
--max-depth, - run with
config.threadsthreads.
Additional search paths (paths[1..]) are added with builder.add(path); the first path is the builder's primary root.
spawn_senders filter chain
Inside the closure passed to WalkParallel::run, every entry passes through this chain:
- Quit check. If
quit_flagis set, returnWalkState::Quit. ignore_contain. If the entry is a directory and contains any of the configured marker files, skip the whole subtree (WalkState::Skip).- Skip the root.
e.depth() == 0→WalkState::Continuewithout emitting. - Broken-symlink rescue. If
ignorereportsIo(NotFound)for a path thatsymlink_metadatasays is a symlink, wrap it asDirEntry::broken_symlink(path)instead of dropping it. - Other walk errors. Either dropped silently or forwarded as
WorkerResult::Errordepending on flow. min_depth. Below the threshold →WalkState::Continuewithout emitting.- Pattern match. All conjuncted regexes (the main pattern + every
--and) must match. The match input is the basename, or the full absolute path when--full-pathis set (search_str_for_entry). extensionsRegexSet. Match against the basename.FileTypes::should_ignore. Reject anything outside the requested types, plus theexecutables_onlyandempty_onlygates.- Owner constraint (Unix only). Rejected if metadata cannot be read.
- Size constraints. Skip non-files. For files, every
SizeFiltermust accept the metadata length. - Time constraints. Every
TimeFiltermust accept the modified time. - Style precomputation. If we are printing (no command set) and have
LsColors, compute and cache the entry'sStylehere so the receiver does not have to. - Send.
BatchSender::send(WorkerResult::Entry(entry)). If the channel is closed →WalkState::Quit. - Prune. With
--prune, returnWalkState::Skipso a matching directory is not descended into.
ReceiverBuffer
The receiver runs in Buffering mode initially:
- Calls
rx.recv_deadline(deadline)so it stops blocking when the buffer-time deadline is hit. - Each
Entrygoes into a localbuffer. Ifbuffer.len() > MAX_BUFFER_LENGTH, it switches toStreamingmode by sorting and flushing the buffer. - Each
Erroris forwarded to stderr if--show-errors. - After
max_resultsentries →stop(). - In
Streamingmode, every entry is written directly viaoutput::print_entry. After draining a batch, it flushes if the channel is empty so users see progress. - On a
RecvTimeoutError::Timeout, switches to streaming. - On
Disconnected, callsstop()to print any remaining buffered entries.
Quiet mode and exit codes
When Config::quiet is true, the very first Entry returns Err(ExitCode::HasResults(true)), short-circuiting the walk. After the receiver is done, stop() returns HasResults(num_results > 0). ExitCode::HasResults(true) becomes process exit code 0; false becomes 1. This is how fd -q works as a "does it exist" predicate.
--exec integration
When Config::command is set, WorkerState::receive does not run the ReceiverBuffer. Instead:
- In batch mode, it calls
exec::batch(rx.into_iter().flatten(), cmd, config). - In one-by-one mode, it spawns
config.threadsworker threads inside athread::scope, each runningexec::job(rx.into_iter().flatten(), cmd, config). TheExitCodes are merged.
See command-execution for the consumer side.
Concurrency safety
quit_flagandinterrupt_flagareArc<AtomicBool>shared between the senders, the receiver, and the Ctrl-C handler.- The
Batchmutex is short-lived: eachsendonly holds it long enough to push a single entry. TheIntoIteratorimpl onBatchtake()s the innerVecat the receiver, which is fine because by then no senders are touching it. - The bounded channel provides backpressure: senders block when they get ahead of the receiver, which keeps memory usage proportional to the receiver's processing speed.
Entry points for modification
- To change the buffer policy, edit
MAX_BUFFER_LENGTHandDEFAULT_MAX_BUFFER_TIME, or change the logic inReceiverBuffer::recv/poll. - To add a new walker-time filter, drop another check into the closure inside
WorkerState::spawn_senders. Keep the cheapest checks first; expensive ones (size, time, owner) requiremetadata()and should stay later in the chain. - To change how broken symlinks are reported, edit the
Err(ignore::Error::WithPath { ... })arm inspawn_senders. - To support a new walk option, prefer extending
ignore::WalkBuilderconfiguration inbuild_walkerover rolling a custom mechanism.
Related pages
- Filters — the per-entry checks invoked here.
- Ignore rules — what
WalkBuilderis configured with. - Command execution — the alternate consumer of
WorkerResult. - Output — what
ReceiverBuffer::printultimately calls.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.