sharkdp/fd
Patterns and conventions
Recurring patterns to keep in mind when reading or writing fd code.
Module layout
The codebase is flat. src/ has one file per concern, with a few small subdirectories (exec/, filter/, fmt/). There is no lib.rs exposing a public API: fd is built only as a binary. Internal modules are referenced through crate::module_name and pub is used liberally because everything is in the same crate.
Configuration is built once, then read
Opts (src/cli.rs) is the raw clap-parsed input. Config (src/config.rs) is the canonicalised, immutable bag of settings the rest of the program reads. The conversion happens in construct_config in src/main.rs, with all the policy decisions concentrated there:
- Smart-case decision (
pattern_has_uppercase_char). - Color decision (terminal detection ×
NO_COLOR×--color). LsColorsresolution (env var, falling back to the bundledDEFAULT_LS_COLORS).CommandSetconstruction from--exec/--exec-batch/--list-details.
After walk::scan is called, Config is read-only. New flags should follow this pattern: add to Opts, derive in construct_config, expose as a Config field.
Lazy metadata
DirEntry (src/dir_entry.rs) wraps the underlying entry but does not call metadata() eagerly. Both metadata and style are stored in OnceCell<Option<…>>. Filters call entry.metadata() only when they actually need it (e.g., size and time filters), and the result is cached for any subsequent caller (the printer also reads style).
The pattern is "pay for what you use, exactly once":
pub fn metadata(&self) -> Option<&Metadata> {
self.metadata
.get_or_init(|| match &self.inner {
DirEntryInner::Normal(e) => e.metadata().ok(),
DirEntryInner::BrokenSymlink(path) => path.symlink_metadata().ok(),
})
.as_ref()
}Backpressure via batched channel sends
Senders accumulate results into a Batch (Arc<Mutex<Option<Vec<WorkerResult>>>>) and only push the first item of a new batch onto the channel. Subsequent items append into the same Vec, which the receiver eventually consumes:
fn send(&mut self, item: WorkerResult) -> Result<(), SendError<()>> {
let mut batch = self.batch.lock();
if self.needs_flush(batch.as_ref()) {
drop(batch);
self.batch = Batch::new();
batch = self.batch.lock();
}
let items = batch.as_mut().unwrap();
items.push(item);
if items.len() == 1 {
self.tx.send(self.batch.clone()).map_err(|_| SendError(()))?;
}
Ok(())
}This minimises lock contention and channel sends without forcing the senders to block on a slow receiver.
Smart switchover from buffering to streaming
Anywhere fd has to choose between "I want sorted output" and "I want results immediately", the same pattern appears: buffer with a hard limit and a deadline, then switch to streaming. ReceiverBuffer is the canonical example (src/walk.rs). It also explains why fd results are mostly sorted for fast searches but appear in walker order on huge trees.
cfg-gated platform code
Cross-platform code is gated with #[cfg(unix)]/#[cfg(windows)] rather than runtime checks:
src/filesystem.rshas parallel#[cfg(unix)]and#[cfg(windows)]implementations ofis_block_device,is_char_device,is_socket,is_pipe, andosstr_to_bytes.src/output.rshas separateprint_entry_uncolorizeddefinitions to write raw bytes on Unix.src/filter/owner.rsis entirely Unix-only.src/main.rsuses a long target-cfg list to gatetikv-jemallocator.
When adding new functionality, mirror this style. Avoid runtime cfg!() checks inside hot loops.
Error handling
- Top-level errors propagate as
anyhow::Result<...>.run()returnsResult<ExitCode>andmainformats with{:#}to print the entire chain. - Filesystem errors during traversal are swallowed into
WorkerResult::Error(ignore::Error)and only surfaced via--show-errors(gated byConfig::show_filesystem_errors). - User-facing diagnostics use
print_error(src/error.rs), which prefixes[fd error]:. - Recoverable parse failures (e.g., a malformed user-supplied size constraint) bail out early with
anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", s)style messages.
Reuse the FormatTemplate
--exec/--exec-batch and --format share the same template engine (src/fmt/mod.rs). Anything you build that needs to interpolate paths should reuse this engine; do not introduce a parallel placeholder system.
Coding style
rustfmtis enforced by CI (.github/workflows/CICD.yml'sensure_cargo_fmtjob).rustfmt.tomlis intentionally tiny — the project does not customise much beyond the defaults.clippyruns with--all-features -- -Dwarnings. New lints must be addressed, not allowed, unless you have a strong reason.- Edition 2024 is used (
Cargo.toml). Take advantage of let-chains and the newtry_blocksemantics; recent commits already do. - Function-level doc comments are used on most public APIs. Comments tend to explain "why", not "what".
Naming conventions
- Types use
UpperCamelCase:Opts,Config,DirEntry,WorkerResult,CommandSet,FormatTemplate,SizeFilter,TimeFilter,OwnerFilter. - Modules are
snake_case:walk,filter,fmt,exec,regex_helper,dir_entry,exit_codes,filesystem. - Boolean fields are positively named:
case_sensitive,null_separator,read_fdignore,read_vcsignore. The CLI exposes--no-fooflags andOptshas hiddenbar: ()overrides forclap'soverrides_with.
Testing convention
Each non-trivial parser or helper has its own #[cfg(test)] mod tests block. Behavioural tests live in tests/tests.rs and use the shared TestEnv harness. See testing.
Public-API surface
There is none. fd is a binary, not a library, so feel free to refactor internal modules freely. The only stable surface is the CLI itself, the documented exit codes (src/exit_codes.rs), and the file formats fd reads (.fdignore, .ignore, .gitignore, the global ignore file).
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.