fd_lib/
cli.rs

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    /// Include hidden directories and files in the search results (default:
34    /// hidden files and directories are skipped). Files and directories are
35    /// considered to be hidden if their name starts with a `.` sign (dot).
36    /// Any files or directories that are ignored due to the rules described by
37    /// --no-ignore are still ignored unless otherwise specified.
38    /// The flag can be overridden with --no-hidden.
39    #[arg(
40        long,
41        short = 'H',
42        help = "Search hidden files and directories",
43        long_help
44    )]
45    pub hidden: bool,
46
47    /// Overrides --hidden
48    #[arg(long, overrides_with = "hidden", hide = true, action = ArgAction::SetTrue)]
49    no_hidden: (),
50
51    /// Show search results from files and directories that would otherwise be
52    /// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file,
53    /// The flag can be overridden with --ignore.
54    #[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    /// Overrides --no-ignore
63    #[arg(long, overrides_with = "no_ignore", hide = true, action = ArgAction::SetTrue)]
64    ignore: (),
65
66    ///Show search results from files and directories that
67    ///would otherwise be ignored by '.gitignore' files.
68    ///The flag can be overridden with --ignore-vcs.
69    #[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    /// Overrides --no-ignore-vcs
78    #[arg(long, overrides_with = "no_ignore_vcs", hide = true, action = ArgAction::SetTrue)]
79    ignore_vcs: (),
80
81    /// Do not require a git repository to respect gitignores.
82    /// By default, fd will only respect global gitignore rules, .gitignore rules,
83    /// and local exclude rules if fd detects that you are searching inside a
84    /// git repository. This flag allows you to relax this restriction such that
85    /// fd will respect all git related ignore rules regardless of whether you're
86    /// searching in a git repository or not.
87    ///
88    ///
89    /// This flag can be disabled with --require-git.
90    #[arg(
91        long,
92        overrides_with = "require_git",
93        hide_short_help = true,
94        // same description as ripgrep's flag: ripgrep/crates/core/app.rs
95        long_help
96    )]
97    pub no_require_git: bool,
98
99    /// Overrides --no-require-git
100    #[arg(long, overrides_with = "no_require_git", hide = true, action = ArgAction::SetTrue)]
101    require_git: (),
102
103    /// Show search results from files and directories that would otherwise be
104    /// ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories.
105    #[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    /// Do not respect the global ignore file
114    #[arg(long, hide = true)]
115    pub no_global_ignore_file: bool,
116
117    /// Perform an unrestricted search, including ignored and hidden files. This is
118    /// an alias for '--no-ignore --hidden'.
119    #[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    /// Case-sensitive search (default: smart case)
126    #[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    /// Perform a case-insensitive search. By default, fd uses case-insensitive
137    /// searches, unless the pattern contains an uppercase character (smart
138    /// case).
139    #[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    /// Perform a glob-based search instead of a regular expression search.
149    #[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    /// Perform a regular-expression based search (default). This can be used to
159    /// override --glob.
160    #[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    /// Treat the pattern as a literal string instead of a regular expression. Note
170    /// that this also performs substring comparison. If you want to match on an
171    /// exact filename, consider using '--glob'.
172    #[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    /// Add additional required search patterns, all of which must be matched. Multiple
183    /// additional patterns can be specified. The patterns are regular
184    /// expressions, unless '--glob' or '--fixed-strings' is used.
185    #[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    /// Shows the full path starting from the root as opposed to relative paths.
196    /// The flag can be overridden with --relative-path.
197    #[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    /// Overrides --absolute-path
206    #[arg(long, overrides_with = "absolute_path", hide = true, action = ArgAction::SetTrue)]
207    relative_path: (),
208
209    /// Use a detailed listing format like 'ls -l'. This is basically an alias
210    /// for '--exec-batch ls -l' with some additional 'ls' options. This can be
211    /// used to see more metadata, to show symlink targets and to achieve a
212    /// deterministic sort order.
213    #[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    /// Follow symbolic links
223    #[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    /// Overrides --follow
234    #[arg(long, overrides_with = "follow", hide = true, action = ArgAction::SetTrue)]
235    no_follow: (),
236
237    /// By default, the search pattern is only matched against the filename (or directory name). Using this flag, the pattern is matched against the full (absolute) path. Example:
238    ///   fd --glob -p '**/.git/config'
239    #[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    /// Separate search results by the null character (instead of newlines).
249    /// Useful for piping results to 'xargs'.
250    #[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    /// Limit the directory traversal to a given depth. By default, there is no
261    /// limit on the search depth.
262    #[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    /// Only show search results starting at the given depth.
273    /// See also: '--max-depth' and '--exact-depth'
274    #[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    /// Only show search results at the exact given depth. This is an alias for
284    /// '--min-depth <depth> --max-depth <depth>'.
285    #[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    /// Exclude files/directories that match the given glob pattern. This
292    /// overrides any other ignore logic. Multiple exclude patterns can be
293    /// specified.
294    ///
295    /// Examples:
296    /// {n}  --exclude '*.pyc'
297    /// {n}  --exclude node_modules
298    #[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    /// Do not traverse into directories that match the search criteria. If
308    /// you want to exclude specific directories, use the '--exclude=…' option.
309    #[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]),
310        long_help,
311        )]
312    pub prune: bool,
313
314    /// Filter the search by type:
315    /// {n}  'f' or 'file':         regular files
316    /// {n}  'd' or 'dir' or 'directory':    directories
317    /// {n}  'l' or 'symlink':      symbolic links
318    /// {n}  's' or 'socket':       socket
319    /// {n}  'p' or 'pipe':         named pipe (FIFO)
320    /// {n}  'b' or 'block-device': block device
321    /// {n}  'c' or 'char-device':  character device
322    /// {n}{n}  'x' or 'executable':   executables
323    /// {n}  'e' or 'empty':        empty files or directories
324    ///
325    /// This option can be specified more than once to include multiple file types.
326    /// Searching for '--type file --type symlink' will show both regular files as
327    /// well as symlinks. Note that the 'executable' and 'empty' filters work differently:
328    /// '--type executable' implies '--type file' by default. And '--type empty' searches
329    /// for empty files and directories, unless either '--type file' or '--type directory'
330    /// is specified in addition.
331    ///
332    /// Examples:
333    /// {n}  - Only search for files:
334    /// {n}      fd --type file …
335    /// {n}      fd -tf …
336    /// {n}  - Find both files and symlinks
337    /// {n}      fd --type file --type symlink …
338    /// {n}      fd -tf -tl …
339    /// {n}  - Find executable files:
340    /// {n}      fd --type executable
341    /// {n}      fd -tx
342    /// {n}  - Find empty files:
343    /// {n}      fd --type empty --type file
344    /// {n}      fd -te -tf
345    /// {n}  - Find empty directories:
346    /// {n}      fd --type empty --type directory
347    /// {n}      fd -te -td
348    #[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    /// (Additionally) filter search results by their file extension. Multiple
362    /// allowable file extensions can be specified.
363    ///
364    /// If you want to search for files without extension,
365    /// you can use the regex '^[^.]+$' as a normal search pattern.
366    #[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    /// Limit results based on the size of files using the format <+-><NUM><UNIT>.
376    ///    '+': file size must be greater than or equal to this
377    ///    '-': file size must be less than or equal to this
378    ///
379    /// If neither '+' nor '-' is specified, file size must be exactly equal to this.
380    ///    'NUM':  The numeric size (e.g. 500)
381    ///    'UNIT': The units for NUM. They are not case-sensitive.
382    /// Allowed unit values:
383    ///     'b':  bytes
384    ///     'k':  kilobytes (base ten, 10^3 = 1000 bytes)
385    ///     'm':  megabytes
386    ///     'g':  gigabytes
387    ///     't':  terabytes
388    ///     'ki': kibibytes (base two, 2^10 = 1024 bytes)
389    ///     'mi': mebibytes
390    ///     'gi': gibibytes
391    ///     'ti': tebibytes
392    #[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    /// Filter results based on the file modification time. Files with modification times
400    /// greater than the argument are returned. The argument can be provided
401    /// as a specific point in time (YYYY-MM-DD HH:MM:SS or @timestamp) or as a duration (10h, 1d, 35min).
402    /// If the time is not specified, it defaults to 00:00:00.
403    /// '--change-newer-than', '--newer', or '--changed-after' can be used as aliases.
404    ///
405    /// Examples:
406    /// {n}    --changed-within 2weeks
407    /// {n}    --change-newer-than '2018-10-27 10:00:00'
408    /// {n}    --newer 2018-10-27
409    /// {n}    --changed-after 1day
410    #[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    /// Filter results based on the file modification time. Files with modification times
422    /// less than the argument are returned. The argument can be provided
423    /// as a specific point in time (YYYY-MM-DD HH:MM:SS or @timestamp) or as a duration (10h, 1d, 35min).
424    /// '--change-older-than' or '--older' can be used as aliases.
425    ///
426    /// Examples:
427    /// {n}    --changed-before '2018-10-27 10:00:00'
428    /// {n}    --change-older-than 2weeks
429    /// {n}    --older 2018-10-27
430    #[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    /// Filter files by their user and/or group.
441    /// Format: [(user|uid)][:(group|gid)]. Either side is optional.
442    /// Precede either side with a '!' to exclude files instead.
443    ///
444    /// Examples:
445    /// {n}    --owner john
446    /// {n}    --owner :students
447    /// {n}    --owner '!john:students'
448    #[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    /// Instead of printing the file normally, print the format string with the following placeholders replaced:
456    ///   '{}': path (of the current search result)
457    ///   '{/}': basename
458    ///   '{//}': parent directory
459    ///   '{.}': path without file extension
460    ///   '{/.}': basename without file extension
461    #[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    /// Maximum number of arguments to pass to the command given with -X.
473    /// If the number of results is greater than the given size,
474    /// the command given with -X is run again with remaining arguments.
475    /// A batch size of zero means there is no limit (default), but note
476    /// that batching might still happen due to OS restrictions on the
477    /// maximum length of command lines.
478    #[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    /// Add a custom ignore-file in '.gitignore' format. These files have a low precedence.
491    #[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    /// Declare when to use color for the pattern match output
501    #[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    /// Set number of threads to use for searching & executing (default: number
513    /// of available CPU cores)
514    #[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = str::parse::<NonZeroUsize>)]
515    pub threads: Option<NonZeroUsize>,
516
517    /// Milliseconds to buffer before streaming search results to console
518    ///
519    /// Amount of time in milliseconds to buffer, before streaming the search
520    /// results to the console.
521    #[arg(long, hide = true, value_parser = parse_millis)]
522    pub max_buffer_time: Option<Duration>,
523
524    ///Limit the number of search results to 'count' and quit immediately.
525    #[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    /// Limit the search to a single result and quit immediately.
536    /// This is an alias for '--max-results=1'.
537    #[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    /// When the flag is present, the program does not print anything and will
547    /// return with an exit code of 0 if there is at least one match. Otherwise, the
548    /// exit code will be 1.
549    /// '--has-results' can be used as an alias.
550    #[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    /// Enable the display of filesystem errors for situations such as
562    /// insufficient permissions or dead symlinks.
563    #[arg(
564        long,
565        hide_short_help = true,
566        help = "Show filesystem errors",
567        long_help
568    )]
569    pub show_errors: bool,
570
571    /// Change the current working directory of fd to the provided path. This
572    /// means that search results will be shown with respect to the given base
573    /// path. Note that relative paths which are passed to fd via the positional
574    /// <path> argument or the '--search-path' option will also be resolved
575    /// relative to this directory.
576    #[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    /// the search pattern which is either a regular expression (default) or a glob
586    /// pattern (if --glob is used). If no pattern has been specified, every entry
587    /// is considered a match. If your pattern starts with a dash (-), make sure to
588    /// pass '--' first, or it will be considered as a flag (fd -- '-foo').
589    #[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    /// Set the path separator to use when printing file paths. The default is
599    /// the OS-specific separator ('/' on Unix, '\' on Windows).
600    #[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    /// The directory where the filesystem search is rooted (optional). If
610    /// omitted, search the current working directory.
611    #[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    /// Provide paths to search as an alternative to the positional <path>
619    /// argument. Changes the usage to `fd [OPTIONS] --search-path <path>
620    /// --search-path <path2> [<pattern>]`
621    #[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    /// By default, relative paths are prefixed with './' when -x/--exec,
632    /// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
633    /// path starting with '-' being treated as a command line option. Use
634    /// this flag to change this behavior. If this flag is used without a value,
635    /// it is equivalent to passing "always".
636    #[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    /// By default, fd will traverse the file system tree as far as other options
658    /// dictate. With this flag, fd ensures that it does not descend into a
659    /// different file system than the one it started in. Comparable to the -mount
660    /// or -xdev filters of find(1).
661    #[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        // would it make sense to concatenate these?
673        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            // Change "." to "./" as a workaround for https://github.com/BurntSushi/ripgrep/pull/2711
703            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
759/// Get the default number of threads to use, if not explicitly specified.
760fn default_num_threads() -> NonZeroUsize {
761    // If we can't get the amount of parallelism for some reason, then
762    // default to a single thread, because that is safe.
763    let fallback = NonZeroUsize::MIN;
764    // To limit startup overhead on massively parallel machines, don't use more
765    // than 64 threads.
766    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    /// A file which is executable by the current effective user
786    #[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    /// show colors if the output goes to an interactive console (default)
799    Auto,
800    /// always use colorized output
801    Always,
802    /// do not use colorized output
803    Never,
804}
805
806#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
807pub enum StripCwdWhen {
808    /// Use the default behavior
809    Auto,
810    /// Always strip the ./ at the beginning of paths
811    Always,
812    /// Never strip the ./
813    Never,
814}
815
816// there isn't a derive api for getting grouped values yet,
817// so we have to use hand-rolled parsing for exec and exec-batch
818pub 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}