Files
fd/src/main.rs
T
SAY-5 ed47664191 fix: flag any pattern containing a path separator, not just ones that name a directory
Without --full-path, fd matches patterns against file names, so any pattern
containing a path separator can never match. The previous implementation
in main.rs fired the path-separator diagnostic only when the pattern also
named an existing directory on disk, so the most common Linux/macOS typo
- pasting a full path as the pattern - silently returned zero matches.

Restructure ensure_search_pattern_is_not_a_path so that any '/' in the
pattern triggers the diagnostic unconditionally, and keep the existing
'directory must exist on disk' guard only for the native '\' separator
on Windows, which is also the regex escape character. The Windows check
is short-circuited via '||' so the is_dir syscall never runs when the
pattern already contains '/'. The #[cfg_attr(not(windows), allow(unused_mut))]
attribute keeps the mut binding warning-free on non-Windows targets.

Update the error message to drop the now-ambiguous parenthetical that
showed the platform-specific separator character, and widen the
integration test assertions to cover the full first line so the
assert_failure_with_error helper (which trims lines on both sides) cannot
early-accept on a partial prefix.

Closes #1873

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
2026-04-20 18:18:21 -07:00

537 lines
23 KiB
Rust

mod cli;
mod config;
mod dir_entry;
mod error;
mod exec;
mod exit_codes;
mod filesystem;
mod filetypes;
mod filter;
mod fmt;
mod hyperlink;
mod output;
mod regex_helper;
mod walk;
use std::env;
use std::io::IsTerminal;
use std::path::Path;
use std::sync::Arc;
use anyhow::{Context, Result, anyhow, bail};
use clap::{CommandFactory, Parser};
use globset::GlobBuilder;
use lscolors::LsColors;
use regex::bytes::{Regex, RegexBuilder, RegexSetBuilder};
use crate::cli::{ColorWhen, HyperlinkWhen, Opts};
use crate::config::Config;
use crate::exec::CommandSet;
use crate::exit_codes::ExitCode;
use crate::filetypes::FileTypes;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::TimeFilter;
use crate::regex_helper::{pattern_has_uppercase_char, pattern_matches_strings_with_leading_dot};
// We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/481
// FIXME: re-enable jemalloc on macOS, see comment in Cargo.toml file for more infos
// This has to be kept in sync with the Cargo.toml file section that declares a
// dependency on tikv-jemallocator.
#[cfg(all(
not(windows),
not(target_os = "android"),
not(target_os = "macos"),
not(target_os = "freebsd"),
not(target_os = "openbsd"),
not(target_os = "illumos"),
not(all(target_env = "musl", target_pointer_width = "32")),
not(target_arch = "riscv64"),
feature = "use-jemalloc"
))]
#[global_allocator]
static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
// vivid --color-mode 8-bit generate molokai
const DEFAULT_LS_COLORS: &str = "
ow=0:or=0;38;5;16;48;5;203:no=0:ex=1;38;5;203:cd=0;38;5;203;48;5;236:mi=0;38;5;16;48;5;203:*~=0;38;5;243:st=0:pi=0;38;5;16;48;5;81:fi=0:di=0;38;5;81:so=0;38;5;16;48;5;203:bd=0;38;5;81;48;5;236:tw=0:ln=0;38;5;203:*.m=0;38;5;48:*.o=0;38;5;243:*.z=4;38;5;203:*.a=1;38;5;203:*.r=0;38;5;48:*.c=0;38;5;48:*.d=0;38;5;48:*.t=0;38;5;48:*.h=0;38;5;48:*.p=0;38;5;48:*.cc=0;38;5;48:*.ll=0;38;5;48:*.jl=0;38;5;48:*css=0;38;5;48:*.md=0;38;5;185:*.gz=4;38;5;203:*.nb=0;38;5;48:*.mn=0;38;5;48:*.go=0;38;5;48:*.xz=4;38;5;203:*.so=1;38;5;203:*.rb=0;38;5;48:*.pm=0;38;5;48:*.bc=0;38;5;243:*.py=0;38;5;48:*.as=0;38;5;48:*.pl=0;38;5;48:*.rs=0;38;5;48:*.sh=0;38;5;48:*.7z=4;38;5;203:*.ps=0;38;5;186:*.cs=0;38;5;48:*.el=0;38;5;48:*.rm=0;38;5;208:*.hs=0;38;5;48:*.td=0;38;5;48:*.ui=0;38;5;149:*.ex=0;38;5;48:*.js=0;38;5;48:*.cp=0;38;5;48:*.cr=0;38;5;48:*.la=0;38;5;243:*.kt=0;38;5;48:*.ml=0;38;5;48:*.vb=0;38;5;48:*.gv=0;38;5;48:*.lo=0;38;5;243:*.hi=0;38;5;243:*.ts=0;38;5;48:*.ko=1;38;5;203:*.hh=0;38;5;48:*.pp=0;38;5;48:*.di=0;38;5;48:*.bz=4;38;5;203:*.fs=0;38;5;48:*.png=0;38;5;208:*.zsh=0;38;5;48:*.mpg=0;38;5;208:*.pid=0;38;5;243:*.xmp=0;38;5;149:*.iso=4;38;5;203:*.m4v=0;38;5;208:*.dot=0;38;5;48:*.ods=0;38;5;186:*.inc=0;38;5;48:*.sxw=0;38;5;186:*.aif=0;38;5;208:*.git=0;38;5;243:*.gvy=0;38;5;48:*.tbz=4;38;5;203:*.log=0;38;5;243:*.txt=0;38;5;185:*.ico=0;38;5;208:*.csx=0;38;5;48:*.vob=0;38;5;208:*.pgm=0;38;5;208:*.pps=0;38;5;186:*.ics=0;38;5;186:*.img=4;38;5;203:*.fon=0;38;5;208:*.hpp=0;38;5;48:*.bsh=0;38;5;48:*.sql=0;38;5;48:*TODO=1:*.php=0;38;5;48:*.pkg=4;38;5;203:*.ps1=0;38;5;48:*.csv=0;38;5;185:*.ilg=0;38;5;243:*.ini=0;38;5;149:*.pyc=0;38;5;243:*.psd=0;38;5;208:*.htc=0;38;5;48:*.swp=0;38;5;243:*.mli=0;38;5;48:*hgrc=0;38;5;149:*.bst=0;38;5;149:*.ipp=0;38;5;48:*.fsi=0;38;5;48:*.tcl=0;38;5;48:*.exs=0;38;5;48:*.out=0;38;5;243:*.jar=4;38;5;203:*.xls=0;38;5;186:*.ppm=0;38;5;208:*.apk=4;38;5;203:*.aux=0;38;5;243:*.rpm=4;38;5;203:*.dll=1;38;5;203:*.eps=0;38;5;208:*.exe=1;38;5;203:*.doc=0;38;5;186:*.wma=0;38;5;208:*.deb=4;38;5;203:*.pod=0;38;5;48:*.ind=0;38;5;243:*.nix=0;38;5;149:*.lua=0;38;5;48:*.epp=0;38;5;48:*.dpr=0;38;5;48:*.htm=0;38;5;185:*.ogg=0;38;5;208:*.bin=4;38;5;203:*.otf=0;38;5;208:*.yml=0;38;5;149:*.pro=0;38;5;149:*.cxx=0;38;5;48:*.tex=0;38;5;48:*.fnt=0;38;5;208:*.erl=0;38;5;48:*.sty=0;38;5;243:*.bag=4;38;5;203:*.rst=0;38;5;185:*.pdf=0;38;5;186:*.pbm=0;38;5;208:*.xcf=0;38;5;208:*.clj=0;38;5;48:*.gif=0;38;5;208:*.rar=4;38;5;203:*.elm=0;38;5;48:*.bib=0;38;5;149:*.tsx=0;38;5;48:*.dmg=4;38;5;203:*.tmp=0;38;5;243:*.bcf=0;38;5;243:*.mkv=0;38;5;208:*.svg=0;38;5;208:*.cpp=0;38;5;48:*.vim=0;38;5;48:*.bmp=0;38;5;208:*.ltx=0;38;5;48:*.fls=0;38;5;243:*.flv=0;38;5;208:*.wav=0;38;5;208:*.m4a=0;38;5;208:*.mid=0;38;5;208:*.hxx=0;38;5;48:*.pas=0;38;5;48:*.wmv=0;38;5;208:*.tif=0;38;5;208:*.kex=0;38;5;186:*.mp4=0;38;5;208:*.bak=0;38;5;243:*.xlr=0;38;5;186:*.dox=0;38;5;149:*.swf=0;38;5;208:*.tar=4;38;5;203:*.tgz=4;38;5;203:*.cfg=0;38;5;149:*.xml=0;
38;5;185:*.jpg=0;38;5;208:*.mir=0;38;5;48:*.sxi=0;38;5;186:*.bz2=4;38;5;203:*.odt=0;38;5;186:*.mov=0;38;5;208:*.toc=0;38;5;243:*.bat=1;38;5;203:*.asa=0;38;5;48:*.awk=0;38;5;48:*.sbt=0;38;5;48:*.vcd=4;38;5;203:*.kts=0;38;5;48:*.arj=4;38;5;203:*.blg=0;38;5;243:*.c++=0;38;5;48:*.odp=0;38;5;186:*.bbl=0;38;5;243:*.idx=0;38;5;243:*.com=1;38;5;203:*.mp3=0;38;5;208:*.avi=0;38;5;208:*.def=0;38;5;48:*.cgi=0;38;5;48:*.zip=4;38;5;203:*.ttf=0;38;5;208:*.ppt=0;38;5;186:*.tml=0;38;5;149:*.fsx=0;38;5;48:*.h++=0;38;5;48:*.rtf=0;38;5;186:*.inl=0;38;5;48:*.yaml=0;38;5;149:*.html=0;38;5;185:*.mpeg=0;38;5;208:*.java=0;38;5;48:*.hgrc=0;38;5;149:*.orig=0;38;5;243:*.conf=0;38;5;149:*.dart=0;38;5;48:*.psm1=0;38;5;48:*.rlib=0;38;5;243:*.fish=0;38;5;48:*.bash=0;38;5;48:*.make=0;38;5;149:*.docx=0;38;5;186:*.json=0;38;5;149:*.psd1=0;38;5;48:*.lisp=0;38;5;48:*.tbz2=4;38;5;203:*.diff=0;38;5;48:*.epub=0;38;5;186:*.xlsx=0;38;5;186:*.pptx=0;38;5;186:*.toml=0;38;5;149:*.h264=0;38;5;208:*.purs=0;38;5;48:*.flac=0;38;5;208:*.tiff=0;38;5;208:*.jpeg=0;38;5;208:*.lock=0;38;5;243:*.less=0;38;5;48:*.dyn_o=0;38;5;243:*.scala=0;38;5;48:*.mdown=0;38;5;185:*.shtml=0;38;5;185:*.class=0;38;5;243:*.cache=0;38;5;243:*.cmake=0;38;5;149:*passwd=0;38;5;149:*.swift=0;38;5;48:*shadow=0;38;5;149:*.xhtml=0;38;5;185:*.patch=0;38;5;48:*.cabal=0;38;5;48:*README=0;38;5;16;48;5;186:*.toast=4;38;5;203:*.ipynb=0;38;5;48:*COPYING=0;38;5;249:*.gradle=0;38;5;48:*.matlab=0;38;5;48:*.config=0;38;5;149:*LICENSE=0;38;5;249:*.dyn_hi=0;38;5;243:*.flake8=0;38;5;149:*.groovy=0;38;5;48:*INSTALL=0;38;5;16;48;5;186:*TODO.md=1:*.ignore=0;38;5;149:*Doxyfile=0;38;5;149:*TODO.txt=1:*setup.py=0;38;5;149:*Makefile=0;38;5;149:*.gemspec=0;38;5;149:*.desktop=0;38;5;149:*.rgignore=0;38;5;149:*.markdown=0;38;5;185:*COPYRIGHT=0;38;5;249:*configure=0;38;5;149:*.DS_Store=0;38;5;243:*.kdevelop=0;38;5;149:*.fdignore=0;38;5;149:*README.md=0;38;5;16;48;5;186:*.cmake.in=0;38;5;149:*SConscript=0;38;5;149:*CODEOWNERS=0;38;5;149:*.localized=0;38;5;243:*.gitignore=0;38;5;149:*Dockerfile=0;38;5;149:*.gitconfig=0;38;5;149:*INSTALL.md=0;38;5;16;48;5;186:*README.txt=0;38;5;16;48;5;186:*SConstruct=0;38;5;149:*.scons_opt=0;38;5;243:*.travis.yml=0;38;5;186:*.gitmodules=0;38;5;149:*.synctex.gz=0;38;5;243:*LICENSE-MIT=0;38;5;249:*MANIFEST.in=0;38;5;149:*Makefile.in=0;38;5;243:*Makefile.am=0;38;5;149:*INSTALL.txt=0;38;5;16;48;5;186:*configure.ac=0;38;5;149:*.applescript=0;38;5;48:*appveyor.yml=0;38;5;186:*.fdb_latexmk=0;38;5;243:*CONTRIBUTORS=0;38;5;16;48;5;186:*.clang-format=0;38;5;149:*LICENSE-APACHE=0;38;5;249:*CMakeLists.txt=0;38;5;149:*CMakeCache.txt=0;38;5;243:*.gitattributes=0;38;5;149:*CONTRIBUTORS.md=0;38;5;16;48;5;186:*.sconsign.dblite=0;38;5;243:*requirements.txt=0;38;5;149:*CONTRIBUTORS.txt=0;38;5;16;48;5;186:*package-lock.json=0;38;5;243:*.CFUserTextEncoding=0;38;5;243
";
fn main() {
let result = run();
match result {
Ok(exit_code) => {
exit_code.exit();
}
Err(err) => {
eprintln!("[fd error]: {err:#}");
ExitCode::GeneralError.exit();
}
}
}
fn run() -> Result<ExitCode> {
let opts = Opts::parse();
#[cfg(feature = "completions")]
if let Some(shell) = opts.gen_completions()? {
return print_completions(shell);
}
set_working_dir(&opts)?;
let search_paths = opts.search_paths()?;
if search_paths.is_empty() {
bail!("No valid search paths given.");
}
ensure_search_pattern_is_not_a_path(&opts)?;
let pattern = &opts.pattern;
let exprs = &opts.exprs;
let empty = Vec::new();
let pattern_regexps = exprs
.as_ref()
.unwrap_or(&empty)
.iter()
.chain([pattern])
.map(|pat| build_pattern_regex(pat, &opts))
.collect::<Result<Vec<String>>>()?;
let config = construct_config(opts, &pattern_regexps)?;
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regexps)?;
let regexps = pattern_regexps
.into_iter()
.map(|pat| build_regex(pat, &config))
.collect::<Result<Vec<Regex>>>()?;
walk::scan(&search_paths, regexps, config)
}
#[cfg(feature = "completions")]
#[cold]
fn print_completions(shell: clap_complete::Shell) -> Result<ExitCode> {
// The program name is the first argument.
let first_arg = env::args().next();
let program_name = first_arg
.as_ref()
.map(Path::new)
.and_then(|path| path.file_stem())
.and_then(|file| file.to_str())
.unwrap_or("fd");
let mut cmd = Opts::command();
cmd.build();
clap_complete::generate(shell, &mut cmd, program_name, &mut std::io::stdout());
Ok(ExitCode::Success)
}
fn set_working_dir(opts: &Opts) -> Result<()> {
if let Some(ref base_directory) = opts.base_directory {
if !filesystem::is_existing_directory(base_directory) {
return Err(anyhow!(
"The '--base-directory' path '{}' is not a directory.",
base_directory.to_string_lossy()
));
}
env::set_current_dir(base_directory).with_context(|| {
format!(
"Could not set '{}' as the current working directory",
base_directory.to_string_lossy()
)
})?;
}
Ok(())
}
/// Detect if the user accidentally supplied a path instead of a search pattern.
///
/// Without `--full-path`, fd matches patterns against file names, so any pattern
/// containing a path separator can never match. Two cases are worth a friendly
/// error rather than silent "no results":
///
/// 1. The pattern contains '/'. '/' is always a path separator (including on
/// Windows) and has no regex meaning, so flagging it is safe and catches the
/// common Linux/macOS mistake of pasting a full path as the pattern.
/// 2. On Windows only, the pattern contains the native `\` separator *and*
/// names an existing directory on disk. We can't treat `\` as a pure
/// path-separator signal there because it is also the regex escape char,
/// so valid regex patterns like `\Ac` or `\d+` must still run. Requiring
/// that the pattern resolves to a real directory avoids those false
/// positives while preserving the legacy diagnostic for operators who
/// literally typed a directory path.
///
/// See https://github.com/sharkdp/fd/issues/1873.
fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
if opts.full_path {
return Ok(());
}
// Start with the cheap check: '/' is always a path separator, including on
// Windows, and has no regex meaning, so flagging it is safe and catches the
// Linux/macOS mistake of pasting a full path as the pattern.
#[cfg_attr(not(windows), allow(unused_mut))]
let mut should_warn = opts.pattern.contains('/');
// On Windows we additionally accept the native `\` separator, but only when
// the pattern actually resolves to an existing directory - `\` is also the
// regex escape char there, so valid patterns like `\Ac` or `\d+` must still
// run. The is_dir syscall is only needed when `should_warn` is still false,
// so short-circuit via `||` to avoid the stat call on the happy path.
#[cfg(windows)]
{
should_warn = should_warn
|| (opts.pattern.contains(std::path::MAIN_SEPARATOR)
&& Path::new(&opts.pattern).is_dir());
}
if should_warn {
Err(anyhow!(
"The search pattern '{pattern}' contains a path-separation character \
and will not lead to any search results.\n\n\
If you want to search for all files inside the '{pattern}' directory, use a match-all pattern:\n\n \
fd . '{pattern}'\n\n\
Instead, if you want your pattern to match the full file path, use:\n\n \
fd --full-path '{pattern}'",
pattern = &opts.pattern,
))
} else {
Ok(())
}
}
fn build_pattern_regex(pattern: &str, opts: &Opts) -> Result<String> {
Ok(if opts.glob && !pattern.is_empty() {
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
glob.regex().to_owned()
} else if opts.fixed_strings {
// Treat pattern as literal string if '--fixed-strings' is used
regex::escape(pattern)
} else {
String::from(pattern)
})
}
fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> {
match (cfg!(windows), path_separator) {
(true, Some(sep)) if sep.len() > 1 => Err(anyhow!(
"A path separator must be exactly one byte, but \
the given separator is {} bytes: '{}'.\n\
In some shells on Windows, '/' is automatically \
expanded. Try to use '//' instead.",
sep.len(),
sep
)),
_ => Ok(()),
}
}
fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config> {
// The search will be case-sensitive if the command line flag is set or
// if any of the patterns has an uppercase character (smart case).
let case_sensitive = !opts.ignore_case
&& (opts.case_sensitive
|| pattern_regexps
.iter()
.any(|pat| pattern_has_uppercase_char(pat)));
let path_separator = opts
.path_separator
.take()
.or_else(filesystem::default_path_separator);
let actual_path_separator = path_separator
.clone()
.unwrap_or_else(|| std::path::MAIN_SEPARATOR.to_string());
check_path_separator_length(path_separator.as_deref())?;
let size_limits = std::mem::take(&mut opts.size);
let time_constraints = extract_time_constraints(&opts)?;
#[cfg(unix)]
let owner_constraint: Option<OwnerFilter> = opts.owner.and_then(OwnerFilter::filter_ignore);
#[cfg(windows)]
let ansi_colors_support =
nu_ansi_term::enable_ansi_support().is_ok() || std::env::var_os("TERM").is_some();
#[cfg(not(windows))]
let ansi_colors_support = true;
let interactive_terminal = std::io::stdout().is_terminal();
let colored_output = match opts.color {
ColorWhen::Always => true,
ColorWhen::Never => false,
ColorWhen::Auto => {
let no_color = env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty());
ansi_colors_support && !no_color && interactive_terminal
}
};
let ls_colors = if colored_output {
Some(LsColors::from_env().unwrap_or_else(|| LsColors::from_string(DEFAULT_LS_COLORS)))
} else {
None
};
let hyperlink = match opts.hyperlink {
HyperlinkWhen::Always => true,
HyperlinkWhen::Never => false,
HyperlinkWhen::Auto => colored_output,
};
let command = extract_command(&mut opts, colored_output)?;
let has_command = command.is_some();
let full_path_base = if opts.full_path {
Some(env::current_dir().context(
"Could not determine current directory. \
This is required for --full-path.",
)?)
} else {
None
};
Ok(Config {
case_sensitive,
full_path_base,
ignore_hidden: !(opts.hidden || opts.rg_alias_ignore()),
read_fdignore: !(opts.no_ignore || opts.rg_alias_ignore()),
read_vcsignore: !(opts.no_ignore || opts.rg_alias_ignore() || opts.no_ignore_vcs),
require_git_to_read_vcsignore: !opts.no_require_git,
read_parent_ignore: !opts.no_ignore_parent,
read_global_ignore: !(opts.no_ignore
|| opts.rg_alias_ignore()
|| opts.no_global_ignore_file),
follow_links: opts.follow,
one_file_system: opts.one_file_system,
null_separator: opts.null_separator,
quiet: opts.quiet,
max_depth: opts.max_depth(),
min_depth: opts.min_depth(),
prune: opts.prune,
threads: opts.threads().get(),
max_buffer_time: opts.max_buffer_time,
ls_colors,
hyperlink,
interactive_terminal,
file_types: opts.filetype.as_ref().map(|values| {
use crate::cli::FileType::*;
let mut file_types = FileTypes::default();
for value in values {
match value {
File => file_types.files = true,
Directory => file_types.directories = true,
Symlink => file_types.symlinks = true,
Executable => {
file_types.executables_only = true;
file_types.files = true;
}
Empty => file_types.empty_only = true,
BlockDevice => file_types.block_devices = true,
CharDevice => file_types.char_devices = true,
Socket => file_types.sockets = true,
Pipe => file_types.pipes = true,
}
}
// If only 'empty' was specified, search for both files and directories:
if file_types.empty_only && !(file_types.files || file_types.directories) {
file_types.files = true;
file_types.directories = true;
}
file_types
}),
extensions: opts
.extensions
.as_ref()
.map(|exts| {
let patterns = exts
.iter()
.map(|e| e.trim_start_matches('.'))
.map(|e| format!(r".\.{}$", regex::escape(e)));
RegexSetBuilder::new(patterns)
.case_insensitive(true)
.build()
})
.transpose()?,
format: opts
.format
.as_deref()
.map(crate::fmt::FormatTemplate::parse),
command: command.map(Arc::new),
batch_size: opts.batch_size,
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
ignore_files: std::mem::take(&mut opts.ignore_file),
size_constraints: size_limits,
time_constraints,
#[cfg(unix)]
owner_constraint,
show_filesystem_errors: opts.show_errors,
path_separator,
actual_path_separator,
max_results: opts.max_results(),
strip_cwd_prefix: opts.strip_cwd_prefix(|| !(opts.null_separator || has_command)),
ignore_contain: opts.ignore_contain,
})
}
fn extract_command(opts: &mut Opts, colored_output: bool) -> Result<Option<CommandSet>> {
opts.exec
.command
.take()
.map(Ok)
.or_else(|| {
if !opts.list_details {
return None;
}
let res = determine_ls_command(colored_output)
.map(|cmd| CommandSet::new_batch([cmd]).unwrap());
Some(res)
})
.transpose()
}
fn determine_ls_command(colored_output: bool) -> Result<Vec<&'static str>> {
#[allow(unused)]
let gnu_ls = |command_name| {
let color_arg = if colored_output {
"--color=always"
} else {
"--color=never"
};
// Note: we use short options here (instead of --long-options) to support more
// platforms (like BusyBox).
vec![
command_name,
"-l", // long listing format
"-h", // human readable file sizes
"-d", // list directories themselves, not their contents
color_arg,
]
};
let cmd: Vec<&str> = if cfg!(unix) {
if !cfg!(any(
target_os = "macos",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)) {
// Assume ls is GNU ls
gnu_ls("ls")
} else {
// macOS, DragonFlyBSD, FreeBSD
use std::process::{Command, Stdio};
// Use GNU ls, if available (support for --color=auto, better LS_COLORS support)
let gnu_ls_exists = Command::new("gls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("gls")
} else {
let mut cmd = vec![
"ls", // BSD version of ls
"-l", // long listing format
"-h", // '--human-readable' is not available, '-h' is
"-d", // '--directory' is not available, but '-d' is
];
if !cfg!(any(target_os = "netbsd", target_os = "openbsd")) && colored_output {
// -G is not available in NetBSD's and OpenBSD's ls
cmd.push("-G");
}
cmd
}
}
} else if cfg!(windows) {
use std::process::{Command, Stdio};
// Use GNU ls, if available
let gnu_ls_exists = Command::new("ls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("ls")
} else {
return Err(anyhow!(
"'fd --list-details' is not supported on Windows unless GNU 'ls' is installed."
));
}
} else {
return Err(anyhow!(
"'fd --list-details' is not supported on this platform."
));
};
Ok(cmd)
}
fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
let mut time_constraints: Vec<TimeFilter> = Vec::new();
if let Some(ref t) = opts.changed_within {
if let Some(f) = TimeFilter::after(t) {
time_constraints.push(f);
} else {
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
if let Some(ref t) = opts.changed_before {
if let Some(f) = TimeFilter::before(t) {
time_constraints.push(f);
} else {
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
Ok(time_constraints)
}
fn ensure_use_hidden_option_for_leading_dot_pattern(
config: &Config,
pattern_regexps: &[String],
) -> Result<()> {
if cfg!(unix)
&& config.ignore_hidden
&& pattern_regexps
.iter()
.any(|pat| pattern_matches_strings_with_leading_dot(pat))
{
Err(anyhow!(
"The pattern(s) seems to only match files with a leading dot, but hidden files are \
filtered by default. Consider adding -H/--hidden to search hidden files as well \
or adjust your search pattern(s)."
))
} else {
Ok(())
}
}
fn build_regex(pattern_regex: String, config: &Config) -> Result<regex::bytes::Regex> {
RegexBuilder::new(&pattern_regex)
.case_insensitive(!config.case_sensitive)
.dot_matches_new_line(true)
.build()
.map_err(|e| {
anyhow!(
"{}\n\nNote: You can use the '--fixed-strings' option to search for a \
literal string instead of a regular expression. Alternatively, you can \
also use the '--glob' option to match on a glob pattern.",
e
)
})
}