1use std::num::NonZeroUsize;
2use std::path::{Path, PathBuf};
3use std::time::Duration;
4
5use anyhow::anyhow;
6use clap::{
7 error::ErrorKind, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, Parser,
8 ValueEnum,
9};
10#[cfg(feature = "completions")]
11use clap_complete::Shell;
12use normpath::PathExt;
13
14use crate::error::print_error;
15use crate::exec::CommandSet;
16use crate::filesystem;
17#[cfg(unix)]
18use crate::filter::OwnerFilter;
19use crate::filter::SizeFilter;
20
21#[derive(Parser)]
22#[command(
23 name = "fd",
24 version,
25 about = "A program to find entries in your filesystem",
26 after_long_help = "Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues",
27 max_term_width = 98,
28 args_override_self = true,
29 group(ArgGroup::new("execs").args(&["exec", "exec_batch", "list_details"]).conflicts_with_all(&[
30 "max_results", "quiet", "max_one_result"])),
31)]
32pub struct Opts {
33 #[arg(
40 long,
41 short = 'H',
42 help = "Search hidden files and directories",
43 long_help
44 )]
45 pub hidden: bool,
46
47 #[arg(long, overrides_with = "hidden", hide = true, action = ArgAction::SetTrue)]
49 no_hidden: (),
50
51 #[arg(
55 long,
56 short = 'I',
57 help = "Do not respect .(git|fd)ignore files",
58 long_help
59 )]
60 pub no_ignore: bool,
61
62 #[arg(long, overrides_with = "no_ignore", hide = true, action = ArgAction::SetTrue)]
64 ignore: (),
65
66 #[arg(
70 long,
71 hide_short_help = true,
72 help = "Do not respect .gitignore files",
73 long_help
74 )]
75 pub no_ignore_vcs: bool,
76
77 #[arg(long, overrides_with = "no_ignore_vcs", hide = true, action = ArgAction::SetTrue)]
79 ignore_vcs: (),
80
81 #[arg(
91 long,
92 overrides_with = "require_git",
93 hide_short_help = true,
94 long_help
96 )]
97 pub no_require_git: bool,
98
99 #[arg(long, overrides_with = "no_require_git", hide = true, action = ArgAction::SetTrue)]
101 require_git: (),
102
103 #[arg(
106 long,
107 hide_short_help = true,
108 help = "Do not respect .(git|fd)ignore files in parent directories",
109 long_help
110 )]
111 pub no_ignore_parent: bool,
112
113 #[arg(long, hide = true)]
115 pub no_global_ignore_file: bool,
116
117 #[arg(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no_hidden"]), action(ArgAction::Count), hide_short_help = true,
120 help = "Unrestricted search, alias for '--no-ignore --hidden'",
121 long_help,
122 )]
123 rg_alias_hidden_ignore: u8,
124
125 #[arg(
127 long,
128 short = 's',
129 overrides_with("ignore_case"),
130 long_help = "Perform a case-sensitive search. By default, fd uses case-insensitive \
131 searches, unless the pattern contains an uppercase character (smart \
132 case)."
133 )]
134 pub case_sensitive: bool,
135
136 #[arg(
140 long,
141 short = 'i',
142 overrides_with("case_sensitive"),
143 help = "Case-insensitive search (default: smart case)",
144 long_help
145 )]
146 pub ignore_case: bool,
147
148 #[arg(
150 long,
151 short = 'g',
152 conflicts_with("fixed_strings"),
153 help = "Glob-based search (default: regular expression)",
154 long_help
155 )]
156 pub glob: bool,
157
158 #[arg(
161 long,
162 overrides_with("glob"),
163 hide_short_help = true,
164 help = "Regular-expression based search (default)",
165 long_help
166 )]
167 pub regex: bool,
168
169 #[arg(
173 long,
174 short = 'F',
175 alias = "literal",
176 hide_short_help = true,
177 help = "Treat pattern as literal string stead of regex",
178 long_help
179 )]
180 pub fixed_strings: bool,
181
182 #[arg(
186 long = "and",
187 value_name = "pattern",
188 help = "Additional search patterns that need to be matched",
189 long_help,
190 hide_short_help = true,
191 allow_hyphen_values = true
192 )]
193 pub exprs: Option<Vec<String>>,
194
195 #[arg(
198 long,
199 short = 'a',
200 help = "Show absolute instead of relative paths",
201 long_help
202 )]
203 pub absolute_path: bool,
204
205 #[arg(long, overrides_with = "absolute_path", hide = true, action = ArgAction::SetTrue)]
207 relative_path: (),
208
209 #[arg(
214 long,
215 short = 'l',
216 conflicts_with("absolute_path"),
217 help = "Use a long listing format with file metadata",
218 long_help
219 )]
220 pub list_details: bool,
221
222 #[arg(
224 long,
225 short = 'L',
226 alias = "dereference",
227 long_help = "By default, fd does not descend into symlinked directories. Using this \
228 flag, symbolic links are also traversed. \
229 Flag can be overridden with --no-follow."
230 )]
231 pub follow: bool,
232
233 #[arg(long, overrides_with = "follow", hide = true, action = ArgAction::SetTrue)]
235 no_follow: (),
236
237 #[arg(
240 long,
241 short = 'p',
242 help = "Search full abs. path (default: filename only)",
243 long_help,
244 verbatim_doc_comment
245 )]
246 pub full_path: bool,
247
248 #[arg(
251 long = "print0",
252 short = '0',
253 conflicts_with("list_details"),
254 hide_short_help = true,
255 help = "Separate search results by the null character",
256 long_help
257 )]
258 pub null_separator: bool,
259
260 #[arg(
263 long,
264 short = 'd',
265 value_name = "depth",
266 alias("maxdepth"),
267 help = "Set maximum search depth (default: none)",
268 long_help
269 )]
270 max_depth: Option<usize>,
271
272 #[arg(
275 long,
276 value_name = "depth",
277 hide_short_help = true,
278 help = "Only show search results starting at the given depth.",
279 long_help
280 )]
281 min_depth: Option<usize>,
282
283 #[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]),
286 help = "Only show search results at the exact given depth",
287 long_help,
288 )]
289 exact_depth: Option<usize>,
290
291 #[arg(
299 long,
300 short = 'E',
301 value_name = "pattern",
302 help = "Exclude entries that match the given glob pattern",
303 long_help
304 )]
305 pub exclude: Vec<String>,
306
307 #[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]),
310 long_help,
311 )]
312 pub prune: bool,
313
314 #[arg(
349 long = "type",
350 short = 't',
351 value_name = "filetype",
352 hide_possible_values = true,
353 value_enum,
354 help = "Filter by type: file (f), directory (d/dir), symlink (l), \
355 executable (x), empty (e), socket (s), pipe (p), \
356 char-device (c), block-device (b)",
357 long_help
358 )]
359 pub filetype: Option<Vec<FileType>>,
360
361 #[arg(
367 long = "extension",
368 short = 'e',
369 value_name = "ext",
370 help = "Filter by file extension",
371 long_help
372 )]
373 pub extensions: Option<Vec<String>>,
374
375 #[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment, value_name = "size",
393 help = "Limit results based on the size of files",
394 long_help,
395 verbatim_doc_comment,
396 )]
397 pub size: Vec<SizeFilter>,
398
399 #[arg(
411 long,
412 alias("change-newer-than"),
413 alias("newer"),
414 alias("changed-after"),
415 value_name = "date|dur",
416 help = "Filter by file modification time (newer than)",
417 long_help
418 )]
419 pub changed_within: Option<String>,
420
421 #[arg(
431 long,
432 alias("change-older-than"),
433 alias("older"),
434 value_name = "date|dur",
435 help = "Filter by file modification time (older than)",
436 long_help
437 )]
438 pub changed_before: Option<String>,
439
440 #[cfg(unix)]
449 #[arg(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group",
450 help = "Filter by owning user and/or group",
451 long_help,
452 )]
453 pub owner: Option<OwnerFilter>,
454
455 #[arg(
462 long,
463 value_name = "fmt",
464 help = "Print results according to template",
465 conflicts_with = "list_details"
466 )]
467 pub format: Option<String>,
468
469 #[command(flatten)]
470 pub exec: Exec,
471
472 #[arg(
479 long,
480 value_name = "size",
481 hide_short_help = true,
482 requires("exec_batch"),
483 value_parser = value_parser!(usize),
484 default_value_t,
485 help = "Max number of arguments to run as a batch size with -X",
486 long_help,
487 )]
488 pub batch_size: usize,
489
490 #[arg(
492 long,
493 value_name = "path",
494 hide_short_help = true,
495 help = "Add a custom ignore-file in '.gitignore' format",
496 long_help
497 )]
498 pub ignore_file: Vec<PathBuf>,
499
500 #[arg(
502 long,
503 short = 'c',
504 value_enum,
505 default_value_t = ColorWhen::Auto,
506 value_name = "when",
507 help = "When to use colors",
508 long_help,
509 )]
510 pub color: ColorWhen,
511
512 #[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = str::parse::<NonZeroUsize>)]
515 pub threads: Option<NonZeroUsize>,
516
517 #[arg(long, hide = true, value_parser = parse_millis)]
522 pub max_buffer_time: Option<Duration>,
523
524 #[arg(
526 long,
527 value_name = "count",
528 hide_short_help = true,
529 overrides_with("max_one_result"),
530 help = "Limit the number of search results",
531 long_help
532 )]
533 max_results: Option<usize>,
534
535 #[arg(
538 short = '1',
539 hide_short_help = true,
540 overrides_with("max_results"),
541 help = "Limit search to a single result",
542 long_help
543 )]
544 max_one_result: bool,
545
546 #[arg(
551 long,
552 short = 'q',
553 alias = "has-results",
554 hide_short_help = true,
555 conflicts_with("max_results"),
556 help = "Print nothing, exit code 0 if match found, 1 otherwise",
557 long_help
558 )]
559 pub quiet: bool,
560
561 #[arg(
564 long,
565 hide_short_help = true,
566 help = "Show filesystem errors",
567 long_help
568 )]
569 pub show_errors: bool,
570
571 #[arg(
577 long,
578 value_name = "path",
579 hide_short_help = true,
580 help = "Change current working directory",
581 long_help
582 )]
583 pub base_directory: Option<PathBuf>,
584
585 #[arg(
590 default_value = "",
591 hide_default_value = true,
592 value_name = "pattern",
593 help = "the search pattern (a regular expression, unless '--glob' is used; optional)",
594 long_help
595 )]
596 pub pattern: String,
597
598 #[arg(
601 long,
602 value_name = "separator",
603 hide_short_help = true,
604 help = "Set path separator when printing file paths",
605 long_help
606 )]
607 pub path_separator: Option<String>,
608
609 #[arg(action = ArgAction::Append,
612 value_name = "path",
613 help = "the root directories for the filesystem search (optional)",
614 long_help,
615 )]
616 pub path: Vec<PathBuf>,
617
618 #[arg(
622 long,
623 conflicts_with("path"),
624 value_name = "search-path",
625 hide_short_help = true,
626 help = "Provides paths to search as an alternative to the positional <path> argument",
627 long_help
628 )]
629 search_path: Vec<PathBuf>,
630
631 #[arg(long, conflicts_with_all(&["path", "search_path"]), value_name = "when", hide_short_help = true, require_equals = true, long_help)]
637 strip_cwd_prefix: Option<Option<StripCwdWhen>>,
638
639 #[arg(
640 long,
641 hide_short_help = true,
642 conflicts_with("quiet"),
643 help = "Show progress indicator",
644 long_help
645 )]
646 pub show_progress: bool,
647
648 #[arg(long, value_parser = parse_millis, default_value = "250", hide = true)]
649 pub show_progress_refresh_rate: Option<Duration>,
650
651 #[arg(long, default_value = "512", hide = true)]
652 pub local_cache_counter_threshold: Option<usize>,
653
654 #[arg(long, value_parser = parse_millis, default_value = "3000", hide = true)]
655 pub global_counter_duration_when_startup: Option<Duration>,
656
657 #[cfg(any(unix, windows))]
662 #[arg(long, aliases(&["mount", "xdev"]), hide_short_help = true, long_help)]
663 pub one_file_system: bool,
664
665 #[cfg(feature = "completions")]
666 #[arg(long, hide = true, exclusive = true)]
667 gen_completions: Option<Option<Shell>>,
668}
669
670impl Opts {
671 pub fn search_paths(&self) -> anyhow::Result<Vec<PathBuf>> {
672 let paths = if !self.path.is_empty() {
674 &self.path
675 } else if !self.search_path.is_empty() {
676 &self.search_path
677 } else {
678 let current_directory = Path::new("./");
679 ensure_current_directory_exists(current_directory)?;
680 return Ok(vec![self.normalize_path(current_directory)]);
681 };
682 Ok(paths
683 .iter()
684 .filter_map(|path| {
685 if filesystem::is_existing_directory(path) {
686 Some(self.normalize_path(path))
687 } else {
688 print_error(format!(
689 "Search path '{}' is not a directory.",
690 path.to_string_lossy()
691 ));
692 None
693 }
694 })
695 .collect())
696 }
697
698 fn normalize_path(&self, path: &Path) -> PathBuf {
699 if self.absolute_path {
700 filesystem::absolute_path(path.normalize().unwrap().as_path()).unwrap()
701 } else if path == Path::new(".") {
702 PathBuf::from("./")
704 } else {
705 path.to_path_buf()
706 }
707 }
708
709 pub fn no_search_paths(&self) -> bool {
710 self.path.is_empty() && self.search_path.is_empty()
711 }
712
713 #[inline]
714 pub fn rg_alias_ignore(&self) -> bool {
715 self.rg_alias_hidden_ignore > 0
716 }
717
718 pub fn max_depth(&self) -> Option<usize> {
719 self.max_depth.or(self.exact_depth)
720 }
721
722 pub fn min_depth(&self) -> Option<usize> {
723 self.min_depth.or(self.exact_depth)
724 }
725
726 pub fn threads(&self) -> NonZeroUsize {
727 self.threads.unwrap_or_else(default_num_threads)
728 }
729
730 pub fn max_results(&self) -> Option<usize> {
731 self.max_results
732 .filter(|&m| m > 0)
733 .or_else(|| self.max_one_result.then_some(1))
734 }
735
736 pub fn strip_cwd_prefix<P: FnOnce() -> bool>(&self, auto_pred: P) -> bool {
737 use self::StripCwdWhen::*;
738 self.no_search_paths()
739 && match self.strip_cwd_prefix.map_or(Auto, |o| o.unwrap_or(Always)) {
740 Auto => auto_pred(),
741 Always => true,
742 Never => false,
743 }
744 }
745
746 #[cfg(feature = "completions")]
747 pub fn gen_completions(&self) -> anyhow::Result<Option<Shell>> {
748 self.gen_completions
749 .map(|maybe_shell| match maybe_shell {
750 Some(sh) => Ok(sh),
751 None => {
752 Shell::from_env().ok_or_else(|| anyhow!("Unable to get shell from environment"))
753 }
754 })
755 .transpose()
756 }
757}
758
759fn default_num_threads() -> NonZeroUsize {
761 let fallback = NonZeroUsize::MIN;
764 let limit = NonZeroUsize::new(64).unwrap();
767
768 std::thread::available_parallelism()
769 .unwrap_or(fallback)
770 .min(limit)
771}
772
773#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
774pub enum FileType {
775 #[value(alias = "f")]
776 File,
777 #[value(alias = "d", alias = "dir")]
778 Directory,
779 #[value(alias = "l")]
780 Symlink,
781 #[value(alias = "b")]
782 BlockDevice,
783 #[value(alias = "c")]
784 CharDevice,
785 #[value(alias = "x")]
787 Executable,
788 #[value(alias = "e")]
789 Empty,
790 #[value(alias = "s")]
791 Socket,
792 #[value(alias = "p")]
793 Pipe,
794}
795
796#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
797pub enum ColorWhen {
798 Auto,
800 Always,
802 Never,
804}
805
806#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
807pub enum StripCwdWhen {
808 Auto,
810 Always,
812 Never,
814}
815
816pub struct Exec {
819 pub command: Option<CommandSet>,
820}
821
822impl clap::FromArgMatches for Exec {
823 fn from_arg_matches(matches: &ArgMatches) -> clap::error::Result<Self> {
824 let command = matches
825 .get_occurrences::<String>("exec")
826 .map(CommandSet::new)
827 .or_else(|| {
828 matches
829 .get_occurrences::<String>("exec_batch")
830 .map(CommandSet::new_batch)
831 })
832 .transpose()
833 .map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?;
834 Ok(Exec { command })
835 }
836
837 fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> clap::error::Result<()> {
838 *self = Self::from_arg_matches(matches)?;
839 Ok(())
840 }
841}
842
843impl clap::Args for Exec {
844 fn augment_args(cmd: Command) -> Command {
845 cmd.arg(Arg::new("exec")
846 .action(ArgAction::Append)
847 .long("exec")
848 .short('x')
849 .num_args(1..)
850 .allow_hyphen_values(true)
851 .value_terminator(";")
852 .value_name("cmd")
853 .conflicts_with("list_details")
854 .help("Execute a command for each search result")
855 .long_help(
856 "Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
857 There is no guarantee of the order commands are executed in, and the order should not be depended upon. \
858 All positional arguments following --exec are considered to be arguments to the command - not to fd. \
859 It is therefore recommended to place the '-x'/'--exec' option last.\n\
860 The following placeholders are substituted before the command is executed:\n \
861 '{}': path (of the current search result)\n \
862 '{/}': basename\n \
863 '{//}': parent directory\n \
864 '{.}': path without file extension\n \
865 '{/.}': basename without file extension\n \
866 '{{': literal '{' (for escaping)\n \
867 '}}': literal '}' (for escaping)\n\n\
868 If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
869 Examples:\n\n \
870 - find all *.zip files and unzip them:\n\n \
871 fd -e zip -x unzip\n\n \
872 - find *.h and *.cpp files and run \"clang-format -i ..\" for each of them:\n\n \
873 fd -e h -e cpp -x clang-format -i\n\n \
874 - Convert all *.jpg files to *.png files:\n\n \
875 fd -e jpg -x convert {} {.}.png\
876 ",
877 ),
878 )
879 .arg(
880 Arg::new("exec_batch")
881 .action(ArgAction::Append)
882 .long("exec-batch")
883 .short('X')
884 .num_args(1..)
885 .allow_hyphen_values(true)
886 .value_terminator(";")
887 .value_name("cmd")
888 .conflicts_with_all(["exec", "list_details"])
889 .help("Execute a command with all search results at once")
890 .long_help(
891 "Execute the given command once, with all search results as arguments.\n\
892 The order of the arguments is non-deterministic, and should not be relied upon.\n\
893 One of the following placeholders is substituted before the command is executed:\n \
894 '{}': path (of all search results)\n \
895 '{/}': basename\n \
896 '{//}': parent directory\n \
897 '{.}': path without file extension\n \
898 '{/.}': basename without file extension\n \
899 '{{': literal '{' (for escaping)\n \
900 '}}': literal '}' (for escaping)\n\n\
901 If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
902 Examples:\n\n \
903 - Find all test_*.py files and open them in your favorite editor:\n\n \
904 fd -g 'test_*.py' -X vim\n\n \
905 - Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
906 fd -e rs -X wc -l\
907 "
908 ),
909 )
910 }
911
912 fn augment_args_for_update(cmd: Command) -> Command {
913 Self::augment_args(cmd)
914 }
915}
916
917fn parse_millis(arg: &str) -> Result<Duration, std::num::ParseIntError> {
918 Ok(Duration::from_millis(arg.parse()?))
919}
920
921fn ensure_current_directory_exists(current_directory: &Path) -> anyhow::Result<()> {
922 if filesystem::is_existing_directory(current_directory) {
923 Ok(())
924 } else {
925 Err(anyhow!(
926 "Could not retrieve current directory (has it been deleted?)."
927 ))
928 }
929}