Use clap-derive for option parsing

This makes the definition of arguments to fd a little more ergonomic,
and makes it easier to insure the types for the arguments are consitent.
This commit is contained in:
Thayne McCombs 2022-06-17 02:08:24 -06:00
parent 45d6f55d3a
commit 4e7b403c1f
15 changed files with 999 additions and 1083 deletions

View File

@ -181,6 +181,11 @@ jobs:
command: test
args: --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
- name: Generate completions
id: completions
shell: bash
run: make completions
- name: Create tarball
id: package
shell: bash
@ -193,7 +198,6 @@ jobs:
PKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/package"
ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/"
mkdir -p "${ARCHIVE_DIR}"
mkdir -p "${ARCHIVE_DIR}/autocomplete"
# Binary
cp "${{ steps.strip.outputs.BIN_PATH }}" "$ARCHIVE_DIR"
@ -205,10 +209,7 @@ jobs:
cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR"
# Autocompletion files
cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'${{ env.PROJECT_NAME }}.bash' "$ARCHIVE_DIR/autocomplete/"
cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'${{ env.PROJECT_NAME }}.fish' "$ARCHIVE_DIR/autocomplete/"
cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'_${{ env.PROJECT_NAME }}.ps1' "$ARCHIVE_DIR/autocomplete/"
cp 'contrib/completion/_fd' "$ARCHIVE_DIR/autocomplete/"
cp -r autocomplete "${ARCHIVE_DIR}"
# base compressed package
pushd "${PKG_STAGING}/" >/dev/null
@ -256,9 +257,9 @@ jobs:
gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ env.PROJECT_NAME }}.1"
# Autocompletion files
install -Dm644 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'${{ env.PROJECT_NAME }}.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/${{ env.PROJECT_NAME }}"
install -Dm644 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'${{ env.PROJECT_NAME }}.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/${{ env.PROJECT_NAME }}.fish"
install -Dm644 'contrib/completion/_fd' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ env.PROJECT_NAME }}"
install -Dm644 'autocomplete/fd.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/${{ env.PROJECT_NAME }}"
install -Dm644 'autocomplete/fd.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/${{ env.PROJECT_NAME }}.fish"
install -Dm644 'autocomplete/_fd' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ env.PROJECT_NAME }}"
# README and LICENSE
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
target/
/autocomplete/
**/*.rs.bk

20
Cargo.lock generated
View File

@ -102,6 +102,7 @@ checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"indexmap",
"once_cell",
@ -120,6 +121,19 @@ dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "3.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.2.3"
@ -269,6 +283,12 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
version = "0.1.19"

View File

@ -31,7 +31,6 @@ path = "src/main.rs"
[build-dependencies]
clap = { version = "3.2", features = ["cargo"] }
clap_complete = "3.2"
version_check = "0.9"
[dependencies]
@ -51,10 +50,11 @@ dirs-next = "2.0"
normpath = "0.3.2"
chrono = "0.4"
once_cell = "1.13.1"
clap_complete = {version = "3.2", optional = true}
[dependencies.clap]
version = "3.2"
features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped"]
features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped", "derive"]
[target.'cfg(unix)'.dependencies]
users = "0.11.0"
@ -81,4 +81,6 @@ codegen-units = 1
[features]
use-jemalloc = ["jemallocator"]
default = ["use-jemalloc"]
completions = ["clap_complete"]
base = ["use-jemalloc"]
default = ["use-jemalloc", "completions"]

37
Makefile Normal file
View File

@ -0,0 +1,37 @@
PROFILE=release
EXE=target/$(PROFILE)/fd
prefix=/usr/local
bindir=$(prefix)/bin
datadir=$(prefix)/share
exe_name=fd
$(EXE): Cargo.toml src/**/*.rs
cargo build --profile $(PROFILE)
.PHONY: completions
completions: autocomplete/fd.bash autocomplete/fd.fish autocomplete/fd.ps1 autocomplete/_fd
comp_dir=@mkdir -p autocomplete
autocomplete/fd.bash: $(EXE)
$(comp_dir)
$(EXE) --gen-completions bash > $@
autocomplete/fd.fish: $(EXE)
$(comp_dir)
$(EXE) --gen-completions fish > $@
autocomplete/fd.ps1: $(EXE)
$(comp_dir)
$(EXE) --gen-completions powershell > $@
autocomplete/_fd: contrib/completion/_fd
$(comp_dir)
cp $< $@
install: $(EXE) completions
install -Dm755 $(EXE) $(DESTDIR)$(bindir)/fd
install -Dm644 autocomplete/fd.bash $(DESTDIR)/$(datadir)/bash-completion/completions/$(exe_name)
install -Dm644 autocomplete/fd.fish $(DESTDIR)/$(datadir)/fish/vendor_completions.d/$(exe_name).fish
install -Dm644 autocomplete/_fd $(DESTDIR)/$(datadir)/zsh/site-functions/_$(exe_name)
install -Dm644 doc/fd.1 $(DESTDIR)/$(datadir)/man/man1/$(exe_name).1

View File

@ -612,7 +612,7 @@ chown root:root fd.1.gz
sudo cp fd.1.gz /usr/share/man/man1
sudo cp autocomplete/fd.bash /usr/share/bash-completion/completions/fd
source /usr/share/bash-completion/completions/fd
fd
fd
```
### On macOS
@ -676,7 +676,7 @@ With Rust's package manager [cargo](https://github.com/rust-lang/cargo), you can
```
cargo install fd-find
```
Note that rust version *1.56.0* or later is required.
Note that rust version *1.56.1* or later is required.
`make` is also needed for the build.

View File

@ -1,11 +1,3 @@
use std::fs;
use clap_complete::{generate_to, Shell};
use Shell::*;
//use clap_complete::shells::Shel{Bash, Fish, PowerShell, Elvish};
include!("src/app.rs");
fn main() {
let min_version = "1.56";
@ -17,17 +9,4 @@ fn main() {
std::process::exit(1);
}
}
let var = std::env::var_os("SHELL_COMPLETIONS_DIR").or_else(|| std::env::var_os("OUT_DIR"));
let outdir = match var {
None => return,
Some(outdir) => outdir,
};
fs::create_dir_all(&outdir).unwrap();
let mut app = build_app();
// NOTE: zsh completions are hand written in contrib/completion/_fd
for shell in [Bash, PowerShell, Fish, Elvish] {
generate_to(shell, &mut app, "fd", &outdir).unwrap();
}
}

View File

@ -1,774 +0,0 @@
use clap::{crate_version, AppSettings, Arg, ColorChoice, Command};
pub fn build_app() -> Command<'static> {
let clap_color_choice = if std::env::var_os("NO_COLOR").is_none() {
ColorChoice::Auto
} else {
ColorChoice::Never
};
let mut app = Command::new("fd")
.version(crate_version!())
.color(clap_color_choice)
.setting(AppSettings::DeriveDisplayOrder)
.dont_collapse_args_in_usage(true)
.after_help(
"Note: `fd -h` prints a short and concise overview while `fd --help` gives all \
details.",
)
.arg(
Arg::new("hidden")
.long("hidden")
.short('H')
.overrides_with("hidden")
.help("Search hidden files and directories")
.long_help(
"Include hidden directories and files in the search results (default: \
hidden files and directories are skipped). Files and directories are \
considered to be hidden if their name starts with a `.` sign (dot). \
The flag can be overridden with --no-hidden.",
),
)
.arg(
Arg::new("no-hidden")
.long("no-hidden")
.overrides_with("hidden")
.hide(true)
.long_help(
"Overrides --hidden.",
),
)
.arg(
Arg::new("no-ignore")
.long("no-ignore")
.short('I')
.overrides_with("no-ignore")
.help("Do not respect .(git|fd)ignore files")
.long_help(
"Show search results from files and directories that would otherwise be \
ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file. \
The flag can be overridden with --ignore.",
),
)
.arg(
Arg::new("ignore")
.long("ignore")
.overrides_with("no-ignore")
.hide(true)
.long_help(
"Overrides --no-ignore.",
),
)
.arg(
Arg::new("no-ignore-vcs")
.long("no-ignore-vcs")
.overrides_with("no-ignore-vcs")
.hide_short_help(true)
.help("Do not respect .gitignore files")
.long_help(
"Show search results from files and directories that would otherwise be \
ignored by '.gitignore' files. The flag can be overridden with --ignore-vcs.",
),
)
.arg(
Arg::new("ignore-vcs")
.long("ignore-vcs")
.overrides_with("no-ignore-vcs")
.hide(true)
.long_help(
"Overrides --no-ignore-vcs.",
),
)
.arg(
Arg::new("no-ignore-parent")
.long("no-ignore-parent")
.overrides_with("no-ignore-parent")
.hide_short_help(true)
.help("Do not respect .(git|fd)ignore files in parent directories")
.long_help(
"Show search results from files and directories that would otherwise be \
ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories.",
),
)
.arg(
Arg::new("no-global-ignore-file")
.long("no-global-ignore-file")
.hide(true)
.help("Do not respect the global ignore file")
.long_help("Do not respect the global ignore file."),
)
.arg(
Arg::new("rg-alias-hidden-ignore")
.short('u')
.long("unrestricted")
.overrides_with_all(&["ignore", "no-hidden"])
.multiple_occurrences(true) // Allowed for historical reasons
.hide_short_help(true)
.help("Unrestricted search, alias for '--no-ignore --hidden'")
.long_help(
"Perform an unrestricted search, including ignored and hidden files. This is \
an alias for '--no-ignore --hidden'."
),
)
.arg(
Arg::new("case-sensitive")
.long("case-sensitive")
.short('s')
.overrides_with_all(&["ignore-case", "case-sensitive"])
.help("Case-sensitive search (default: smart case)")
.long_help(
"Perform a case-sensitive search. By default, fd uses case-insensitive \
searches, unless the pattern contains an uppercase character (smart \
case).",
),
)
.arg(
Arg::new("ignore-case")
.long("ignore-case")
.short('i')
.overrides_with_all(&["case-sensitive", "ignore-case"])
.help("Case-insensitive search (default: smart case)")
.long_help(
"Perform a case-insensitive search. By default, fd uses case-insensitive \
searches, unless the pattern contains an uppercase character (smart \
case).",
),
)
.arg(
Arg::new("glob")
.long("glob")
.short('g')
.conflicts_with("fixed-strings")
.overrides_with("glob")
.help("Glob-based search (default: regular expression)")
.long_help("Perform a glob-based search instead of a regular expression search."),
)
.arg(
Arg::new("regex")
.long("regex")
.overrides_with_all(&["glob", "regex"])
.hide_short_help(true)
.help("Regular-expression based search (default)")
.long_help(
"Perform a regular-expression based search (default). This can be used to \
override --glob.",
),
)
.arg(
Arg::new("fixed-strings")
.long("fixed-strings")
.short('F')
.alias("literal")
.overrides_with("fixed-strings")
.hide_short_help(true)
.help("Treat pattern as literal string instead of regex")
.long_help(
"Treat the pattern as a literal string instead of a regular expression. Note \
that this also performs substring comparison. If you want to match on an \
exact filename, consider using '--glob'.",
),
)
.arg(
Arg::new("absolute-path")
.long("absolute-path")
.short('a')
.overrides_with("absolute-path")
.help("Show absolute instead of relative paths")
.long_help(
"Shows the full path starting from the root as opposed to relative paths. \
The flag can be overridden with --relative-path.",
),
)
.arg(
Arg::new("relative-path")
.long("relative-path")
.overrides_with("absolute-path")
.hide(true)
.long_help(
"Overrides --absolute-path.",
),
)
.arg(
Arg::new("list-details")
.long("list-details")
.short('l')
.conflicts_with("absolute-path")
.help("Use a long listing format with file metadata")
.long_help(
"Use a detailed listing format like 'ls -l'. This is basically an alias \
for '--exec-batch ls -l' with some additional 'ls' options. This can be \
used to see more metadata, to show symlink targets and to achieve a \
deterministic sort order.",
),
)
.arg(
Arg::new("follow")
.long("follow")
.short('L')
.alias("dereference")
.overrides_with("follow")
.help("Follow symbolic links")
.long_help(
"By default, fd does not descend into symlinked directories. Using this \
flag, symbolic links are also traversed. \
Flag can be overriden with --no-follow.",
),
)
.arg(
Arg::new("no-follow")
.long("no-follow")
.overrides_with("follow")
.hide(true)
.long_help(
"Overrides --follow.",
),
)
.arg(
Arg::new("full-path")
.long("full-path")
.short('p')
.overrides_with("full-path")
.help("Search full abs. path (default: filename only)")
.long_help(
"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:\n \
fd --glob -p '**/.git/config'",
),
)
.arg(
Arg::new("null_separator")
.long("print0")
.short('0')
.overrides_with("print0")
.conflicts_with("list-details")
.hide_short_help(true)
.help("Separate results by the null character")
.long_help(
"Separate search results by the null character (instead of newlines). \
Useful for piping results to 'xargs'.",
),
)
.arg(
Arg::new("max-depth")
.long("max-depth")
.short('d')
.takes_value(true)
.value_name("depth")
.help("Set maximum search depth (default: none)")
.long_help(
"Limit the directory traversal to a given depth. By default, there is no \
limit on the search depth.",
),
)
// support --maxdepth as well, for compatibility with rg
.arg(
Arg::new("rg-depth")
.long("maxdepth")
.hide(true)
.takes_value(true)
.help("Set maximum search depth (default: none)")
)
.arg(
Arg::new("min-depth")
.long("min-depth")
.takes_value(true)
.value_name("depth")
.hide_short_help(true)
.help("Only show results starting at given depth")
.long_help(
"Only show search results starting at the given depth. \
See also: '--max-depth' and '--exact-depth'",
),
)
.arg(
Arg::new("exact-depth")
.long("exact-depth")
.takes_value(true)
.value_name("depth")
.hide_short_help(true)
.conflicts_with_all(&["max-depth", "min-depth"])
.help("Only show results at exact given depth")
.long_help(
"Only show search results at the exact given depth. This is an alias for \
'--min-depth <depth> --max-depth <depth>'.",
),
)
.arg(
Arg::new("prune")
.long("prune")
.conflicts_with_all(&["size", "exact-depth"])
.hide_short_help(true)
.help("Do not traverse into matching directories")
.long_help("Do not traverse into directories that match the search criteria. If \
you want to exclude specific directories, use the '--exclude=' option.")
)
.arg(
Arg::new("file-type")
.long("type")
.short('t')
.multiple_occurrences(true)
.number_of_values(1)
.takes_value(true)
.value_name("filetype")
.possible_values(&[
"f",
"file",
"d",
"directory",
"l",
"symlink",
"x",
"executable",
"e",
"empty",
"s",
"socket",
"p",
"pipe",
])
.hide_possible_values(true)
.help(
"Filter by type: file (f), directory (d), symlink (l),\nexecutable (x), \
empty (e), socket (s), pipe (p)",
)
.long_help(
"Filter the search by type:\n \
'f' or 'file': regular files\n \
'd' or 'directory': directories\n \
'l' or 'symlink': symbolic links\n \
's' or 'socket': socket\n \
'p' or 'pipe': named pipe (FIFO)\n\n \
'x' or 'executable': executables\n \
'e' or 'empty': empty files or directories\n\n\
This option can be specified more than once to include multiple file types. \
Searching for '--type file --type symlink' will show both regular files as \
well as symlinks. Note that the 'executable' and 'empty' filters work differently: \
'--type executable' implies '--type file' by default. And '--type empty' searches \
for empty files and directories, unless either '--type file' or '--type directory' \
is specified in addition.\n\n\
Examples:\n \
- Only search for files:\n \
fd --type file \n \
fd -tf \n \
- Find both files and symlinks\n \
fd --type file --type symlink \n \
fd -tf -tl \n \
- Find executable files:\n \
fd --type executable\n \
fd -tx\n \
- Find empty files:\n \
fd --type empty --type file\n \
fd -te -tf\n \
- Find empty directories:\n \
fd --type empty --type directory\n \
fd -te -td"
),
)
.arg(
Arg::new("extension")
.long("extension")
.short('e')
.multiple_occurrences(true)
.number_of_values(1)
.takes_value(true)
.value_name("ext")
.help("Filter by file extension")
.long_help(
"(Additionally) filter search results by their file extension. Multiple \
allowable file extensions can be specified.\n\
If you want to search for files without extension, \
you can use the regex '^[^.]+$' as a normal search pattern.",
),
)
.arg(
Arg::new("exec")
.long("exec")
.short('x')
.min_values(1)
.multiple_occurrences(true)
.allow_hyphen_values(true)
.value_terminator(";")
.value_name("cmd")
.conflicts_with("list-details")
.help("Execute a command for each search result")
.long_help(
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
All positional arguments following --exec are considered to be arguments to the command - not to fd. \
It is therefore recommended to place the '-x'/'--exec' option last.\n\
The following placeholders are substituted before the command is executed:\n \
'{}': path (of the current search result)\n \
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension\n\n\
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
Examples:\n\n \
- find all *.zip files and unzip them:\n\n \
fd -e zip -x unzip\n\n \
- find *.h and *.cpp files and run \"clang-format -i ..\" for each of them:\n\n \
fd -e h -e cpp -x clang-format -i\n\n \
- Convert all *.jpg files to *.png files:\n\n \
fd -e jpg -x convert {} {.}.png\
",
),
)
.arg(
Arg::new("exec-batch")
.long("exec-batch")
.short('X')
.min_values(1)
.multiple_occurrences(true)
.allow_hyphen_values(true)
.value_terminator(";")
.value_name("cmd")
.conflicts_with_all(&["exec", "list-details"])
.help("Execute a command with all search results at once")
.long_help(
"Execute the given command once, with all search results as arguments.\n\
One of the following placeholders is substituted before the command is executed:\n \
'{}': path (of all search results)\n \
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension\n\n\
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
Examples:\n\n \
- Find all test_*.py files and open them in your favorite editor:\n\n \
fd -g 'test_*.py' -X vim\n\n \
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
fd -e rs -X wc -l\
"
),
)
.arg(
Arg::new("batch-size")
.long("batch-size")
.takes_value(true)
.value_name("size")
.hide_short_help(true)
.requires("exec-batch")
.help("Max number of arguments to run as a batch with -X")
.long_help(
"Maximum number of arguments to pass to the command given with -X. \
If the number of results is greater than the given size, \
the command given with -X is run again with remaining arguments. \
A batch size of zero means there is no limit (default), but note \
that batching might still happen due to OS restrictions on the \
maximum length of command lines.",
),
)
.arg(
Arg::new("exclude")
.long("exclude")
.short('E')
.takes_value(true)
.value_name("pattern")
.number_of_values(1)
.multiple_occurrences(true)
.help("Exclude entries that match the given glob pattern")
.long_help(
"Exclude files/directories that match the given glob pattern. This \
overrides any other ignore logic. Multiple exclude patterns can be \
specified.\n\n\
Examples:\n \
--exclude '*.pyc'\n \
--exclude node_modules",
),
)
.arg(
Arg::new("ignore-file")
.long("ignore-file")
.takes_value(true)
.value_name("path")
.number_of_values(1)
.multiple_occurrences(true)
.hide_short_help(true)
.help("Add custom ignore-file in '.gitignore' format")
.long_help(
"Add a custom ignore-file in '.gitignore' format. These files have a low \
precedence.",
),
)
.arg(
Arg::new("color")
.long("color")
.short('c')
.takes_value(true)
.value_name("when")
.possible_values(&["never", "auto", "always"])
.hide_possible_values(true)
.help("When to use colors: never, *auto*, always")
.long_help(
"Declare when to use color for the pattern match output:\n \
'auto': show colors if the output goes to an interactive console (default)\n \
'never': do not use colorized output\n \
'always': always use colorized output",
),
)
.arg(
Arg::new("threads")
.long("threads")
.short('j')
.takes_value(true)
.value_name("num")
.hide_short_help(true)
.help("Set number of threads")
.long_help(
"Set number of threads to use for searching & executing (default: number \
of available CPU cores)",
),
)
.arg(
Arg::new("size")
.long("size")
.short('S')
.takes_value(true)
.number_of_values(1)
.allow_hyphen_values(true)
.multiple_occurrences(true)
.help("Limit results based on the size of files")
.long_help(
"Limit results based on the size of files using the format <+-><NUM><UNIT>.\n \
'+': file size must be greater than or equal to this\n \
'-': file size must be less than or equal to this\n\
If neither '+' nor '-' is specified, file size must be exactly equal to this.\n \
'NUM': The numeric size (e.g. 500)\n \
'UNIT': The units for NUM. They are not case-sensitive.\n\
Allowed unit values:\n \
'b': bytes\n \
'k': kilobytes (base ten, 10^3 = 1000 bytes)\n \
'm': megabytes\n \
'g': gigabytes\n \
't': terabytes\n \
'ki': kibibytes (base two, 2^10 = 1024 bytes)\n \
'mi': mebibytes\n \
'gi': gibibytes\n \
'ti': tebibytes",
),
)
.arg(
Arg::new("max-buffer-time")
.long("max-buffer-time")
.takes_value(true)
.hide(true)
.help("Milliseconds to buffer before streaming search results to console")
.long_help(
"Amount of time in milliseconds to buffer, before streaming the search \
results to the console.",
),
)
.arg(
Arg::new("changed-within")
.long("changed-within")
.alias("change-newer-than")
.alias("newer")
.takes_value(true)
.value_name("date|dur")
.number_of_values(1)
.help("Filter by file modification time (newer than)")
.long_help(
"Filter results based on the file modification time. The argument can be provided \
as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min). \
If the time is not specified, it defaults to 00:00:00. \
'--change-newer-than' or '--newer' can be used as aliases.\n\
Examples:\n \
--changed-within 2weeks\n \
--change-newer-than '2018-10-27 10:00:00'\n \
--newer 2018-10-27",
),
)
.arg(
Arg::new("changed-before")
.long("changed-before")
.alias("change-older-than")
.alias("older")
.takes_value(true)
.value_name("date|dur")
.number_of_values(1)
.help("Filter by file modification time (older than)")
.long_help(
"Filter results based on the file modification time. The argument can be provided \
as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min). \
'--change-older-than' or '--older' can be used as aliases.\n\
Examples:\n \
--changed-before '2018-10-27 10:00:00'\n \
--change-older-than 2weeks\n \
--older 2018-10-27",
),
)
.arg(
Arg::new("max-results")
.long("max-results")
.takes_value(true)
.value_name("count")
// We currently do not support --max-results in combination with
// program execution because the results that come up in a --max-results
// search are non-deterministic. Users might think that they can run the
// same search with `--exec rm` attached and get a reliable removal of
// the files they saw in the previous search.
.conflicts_with_all(&["exec", "exec-batch", "list-details"])
.hide_short_help(true)
.help("Limit number of search results")
.long_help("Limit the number of search results to 'count' and quit immediately."),
)
.arg(
Arg::new("max-one-result")
.short('1')
.hide_short_help(true)
.overrides_with("max-results")
.conflicts_with_all(&["exec", "exec-batch", "list-details"])
.help("Limit search to a single result")
.long_help("Limit the search to a single result and quit immediately. \
This is an alias for '--max-results=1'.")
)
.arg(
Arg::new("quiet")
.long("quiet")
.short('q')
.alias("has-results")
.hide_short_help(true)
.conflicts_with_all(&["exec", "exec-batch", "list-details", "max-results"])
.help("Print nothing, exit code 0 if match found, 1 otherwise")
.long_help(
"When the flag is present, the program does not print anything and will \
return with an exit code of 0 if there is at least one match. Otherwise, the \
exit code will be 1. \
'--has-results' can be used as an alias."
)
)
.arg(
Arg::new("show-errors")
.long("show-errors")
.hide_short_help(true)
.overrides_with("show-errors")
.help("Show filesystem errors")
.long_help(
"Enable the display of filesystem errors for situations such as \
insufficient permissions or dead symlinks.",
),
)
.arg(
Arg::new("base-directory")
.long("base-directory")
.takes_value(true)
.value_name("path")
.number_of_values(1)
.allow_invalid_utf8(true)
.hide_short_help(true)
.help("Change current working directory")
.long_help(
"Change the current working directory of fd to the provided path. This \
means that search results will be shown with respect to the given base \
path. Note that relative paths which are passed to fd via the positional \
<path> argument or the '--search-path' option will also be resolved \
relative to this directory.",
),
)
.arg(
Arg::new("pattern")
.allow_invalid_utf8(true)
.help(
"the search pattern (a regular expression, unless '--glob' is used; optional)",
).long_help(
"the search pattern which is either a regular expression (default) or a glob \
pattern (if --glob is used). If no pattern has been specified, every entry \
is considered a match. If your pattern starts with a dash (-), make sure to \
pass '--' first, or it will be considered as a flag (fd -- '-foo').")
)
.arg(
Arg::new("path-separator")
.takes_value(true)
.value_name("separator")
.long("path-separator")
.hide_short_help(true)
.help("Set path separator when printing file paths")
.long_help(
"Set the path separator to use when printing file paths. The default is \
the OS-specific separator ('/' on Unix, '\\' on Windows).",
),
)
.arg(
Arg::new("path")
.multiple_occurrences(true)
.allow_invalid_utf8(true)
.help("the root directory for the filesystem search (optional)")
.long_help(
"The directory where the filesystem search is rooted (optional). If \
omitted, search the current working directory.",
),
)
.arg(
Arg::new("search-path")
.long("search-path")
.takes_value(true)
.conflicts_with("path")
.multiple_occurrences(true)
.hide_short_help(true)
.number_of_values(1)
.allow_invalid_utf8(true)
.help("Provide paths to search as an alternative to the positional <path>")
.long_help(
"Provide paths to search as an alternative to the positional <path> \
argument. Changes the usage to `fd [OPTIONS] --search-path <path> \
--search-path <path2> [<pattern>]`",
),
)
.arg(
Arg::new("strip-cwd-prefix")
.long("strip-cwd-prefix")
.conflicts_with_all(&["path", "search-path"])
.hide_short_help(true)
.help("strip './' prefix from non-tty outputs")
.long_help(
"By default, relative paths are prefixed with './' when the output goes to a non \
interactive terminal (TTY). Use this flag to disable this behaviour."
)
);
if cfg!(unix) {
app = app.arg(
Arg::new("owner")
.long("owner")
.short('o')
.takes_value(true)
.value_name("user:group")
.help("Filter by owning user and/or group")
.long_help(
"Filter files by their user and/or group. \
Format: [(user|uid)][:(group|gid)]. Either side is optional. \
Precede either side with a '!' to exclude files instead.\n\
Examples:\n \
--owner john\n \
--owner :students\n \
--owner '!john:students'",
),
);
}
// Make `--one-file-system` available only on Unix and Windows platforms, as per the
// restrictions on the corresponding option in the `ignore` crate.
// Provide aliases `mount` and `xdev` for people coming from `find`.
if cfg!(any(unix, windows)) {
app = app.arg(
Arg::new("one-file-system")
.long("one-file-system")
.aliases(&["mount", "xdev"])
.hide_short_help(true)
.help("Do not descend into a different file system")
.long_help(
"By default, fd will traverse the file system tree as far as other options \
dictate. With this flag, fd ensures that it does not descend into a \
different file system than the one it started in. Comparable to the -mount \
or -xdev filters of find(1).",
),
);
}
app
}
#[test]
fn verify_app() {
build_app().debug_assert()
}

747
src/cli.rs Normal file
View File

@ -0,0 +1,747 @@
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(feature = "completions")]
use anyhow::anyhow;
use clap::{
builder::RangedU64ValueParser, value_parser, AppSettings, Arg, ArgAction, ArgEnum, ArgGroup,
ArgMatches, Command, ErrorKind, Parser,
};
#[cfg(feature = "completions")]
use clap_complete::Shell;
use normpath::PathExt;
use crate::error::print_error;
use crate::exec::CommandSet;
use crate::filesystem;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::SizeFilter;
// Type for options that don't have any values, but are used to negate
// earlier options
struct Negations;
impl clap::FromArgMatches for Negations {
fn from_arg_matches(_: &ArgMatches) -> clap::Result<Self> {
Ok(Negations)
}
fn update_from_arg_matches(&mut self, _: &ArgMatches) -> clap::Result<()> {
Ok(())
}
}
impl clap::Args for Negations {
fn augment_args(cmd: Command<'_>) -> Command<'_> {
Self::augment_args_for_update(cmd)
}
fn augment_args_for_update(cmd: Command<'_>) -> Command<'_> {
cmd.arg(
Arg::new("no-hidden")
.long("no-hidden")
.overrides_with("hidden")
.hide(true)
.long_help("Overrides --hidden."),
)
.arg(
Arg::new("ignore")
.long("ignore")
.overrides_with("no-ignore")
.hide(true)
.long_help("Overrides --no-ignore."),
)
.arg(
Arg::new("ignore-vcs")
.long("ignore-vcs")
.overrides_with("no-ignore-vcs")
.hide(true)
.long_help("Overrides --no-ignore-vcs."),
)
.arg(
Arg::new("relative-path")
.long("relative-path")
.overrides_with("absolute-path")
.hide(true)
.long_help("Overrides --absolute-path."),
)
.arg(
Arg::new("no-follow")
.long("no-follow")
.overrides_with("follow")
.hide(true)
.long_help("Overrides --follow."),
)
}
}
#[derive(Parser)]
#[clap(
version,
setting(AppSettings::DeriveDisplayOrder),
dont_collapse_args_in_usage = true,
after_help = "Note: `fd -h` prints a short and concise overview while `fd --help` gives all \
details.",
group(ArgGroup::new("execs").args(&["exec", "exec-batch", "list-details"]).conflicts_with_all(&[
"max-results", "has-results", "count"])),
)]
pub struct Opts {
/// Search hidden files and directories
///
/// Include hidden directories and files in the search results (default:
/// hidden files and directories are skipped). Files and directories are considered
/// to be hidden if their name starts with a `.` sign (dot).
/// The flag can be overriden with --no-hidden.
#[clap(long, short = 'H', action, overrides_with = "hidden")]
pub hidden: bool,
/// Do not respect .(git|fd)ignore files
///
/// Show search results from files and directories that would otherwise be
/// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file.
/// The flag can be overridden with --ignore.
#[clap(long, short = 'I', action, overrides_with = "no-ignore")]
pub no_ignore: bool,
/// Do not respect .gitignore files
///
///Show search results from files and directories that would otherwise be
///ignored by '.gitignore' files. The flag can be overridden with --ignore-vcs.
#[clap(long, action, overrides_with = "no-ignore-vcs", hide_short_help = true)]
pub no_ignore_vcs: bool,
/// Do not respect .(git|fd)ignore files in parent directories
///
/// Show search results from files and directories that would otherwise be
/// ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories.
#[clap(
long,
action,
overrides_with = "no-ignore-parent",
hide_short_help = true
)]
pub no_ignore_parent: bool,
/// Do not respect the global ignore file
#[clap(long, action, hide = true)]
pub no_global_ignore_file: bool,
/// Unrestricted search, alias for '--no-ignore --hidden'
///
///Perform an unrestricted search, including ignored and hidden files. This is
///an alias for '--no-ignore --hidden'.
#[clap(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no-hidden"]), action(ArgAction::Count), hide_short_help = true)]
rg_alias_hidden_ignore: u8,
/// Case-sensitive search (default: smart case)
///
///Perform a case-sensitive search. By default, fd uses case-insensitive
///searches, unless the pattern contains an uppercase character (smart case).
#[clap(long, short = 's', action, overrides_with_all(&["ignore-case", "case-sensitive"]))]
pub case_sensitive: bool,
/// Case-insensitive search (default: smart case)
///
/// Perform a case-insensitive search. By default, fd uses case-insensitive searches, unless
/// the pattern contains an uppercase character (smart case).
#[clap(long, short = 'i', action, overrides_with_all(&["case-sensitive", "ignore-case"]))]
pub ignore_case: bool,
/// Glob-based search (default: regular expression)
///
/// Perform a glob-based search instead of a regular expression search.
#[clap(
long,
short = 'g',
action,
conflicts_with("fixed-strings"),
overrides_with("glob")
)]
pub glob: bool,
/// Regular-expression based search (default)
///
///Perform a regular-expression based search (default). This can be used to override --glob.
#[clap(long, action, overrides_with_all(&["glob", "regex"]), hide_short_help = true)]
pub regex: bool,
/// Treat pattern as literal string instead of regex
///
/// Treat the pattern as a literal string instead of a regular expression. Note
/// that this also performs substring comparison. If you want to match on an
/// exact filename, consider using '--glob'.
#[clap(
long,
short = 'F',
alias = "literal",
overrides_with("fixed-strings"),
hide_short_help = true
)]
pub fixed_strings: bool,
/// Show absolute instead of relative paths
///
/// Shows the full path starting with the root as opposed to relative paths.
/// The flag can be overridden with --relative-path.
#[clap(long, short = 'a', action, overrides_with("absolute-path"))]
pub absolute_path: bool,
/// Use a long listing format with file metadata
///
/// Use a detailed listing format like 'ls -l'. This is basically an alias
/// for '--exec-batch ls -l' with some additional 'ls' options. This can be
/// used to see more metadata, to show symlink targets and to achieve a
/// deterministic sort order.
#[clap(long, short = 'l', action, conflicts_with("absolute-path"))]
pub list_details: bool,
/// Follow symbolic links
///
/// By default, fd does not descend into symlinked directories. Using this
/// flag, symbolic links are also traversed.
/// Flag can be overriden with --no-follow.
#[clap(
long,
short = 'L',
alias = "dereference",
action,
overrides_with("follow")
)]
pub follow: bool,
/// Search full abs. path (default: filename only)
///
/// 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:
/// fd --glob -p '**/.git/config'
#[clap(long, short = 'p', action, overrides_with("full-path"))]
pub full_path: bool,
/// Separate results by the null character
///
/// Separate search results by the null character (instead of newlines).
/// Useful for piping results to 'xargs'.
#[clap(
long = "print0",
short = '0',
action,
overrides_with("print0"),
conflicts_with("list-details"),
hide_short_help = true
)]
pub null_separator: bool,
/// Set maximum search depth (default: none)
///
/// Limit the directory traversal to a given depth. By default, there is no
/// limit on the search depth.
#[clap(
long,
short = 'd',
value_name = "depth",
value_parser,
alias("maxdepth")
)]
max_depth: Option<usize>,
/// Only show results starting at given depth
///
/// Only show search results starting at the given depth.
/// See also: '--max-depth' and '--exact-depth'
#[clap(long, value_name = "depth", hide_short_help = true, value_parser)]
min_depth: Option<usize>,
/// Only show results at exact given depth
///
/// Only show search results at the exact given depth. This is an alias for
/// '--min-depth <depth> --max-depth <depth>'.
#[clap(long, value_name = "depth", hide_short_help = true, value_parser, conflicts_with_all(&["max-depth", "min-depth"]))]
exact_depth: Option<usize>,
/// Do not travers into matching directories
///
/// Do not traverse into directories that match the search criteria. If
/// you want to exclude specific directories, use the '--exclude=…' option.
#[clap(long, hide_short_help = true, action, conflicts_with_all(&["size", "exact-depth"]))]
pub prune: bool,
/// Filter by type: file (f), directory (d), symlink (l),\nexecutable (x),
/// empty (e), socket (s), pipe (p))
///
/// Filter the search by type:
///
/// 'f' or 'file': regular files
/// 'd' or 'directory': directories
/// 'l' or 'symlink': symbolic links
/// 's' or 'socket': socket
/// 'p' or 'pipe': named pipe (FIFO)
///
/// 'x' or 'executable': executables
/// 'e' or 'empty': empty files or directories
///
/// This option can be specified more than once to include multiple file types.
/// Searching for '--type file --type symlink' will show both regular files as
/// well as symlinks. Note that the 'executable' and 'empty' filters work differently:
/// '--type executable' implies '--type file' by default. And '--type empty' searches
/// for empty files and directories, unless either '--type file' or '--type directory'
/// is specified in addition.
///
/// Examples:
///
/// - Only search for files:
/// fd --type file …
/// fd -tf …
/// - Find both files and symlinks
/// fd --type file --type symlink …
/// fd -tf -tl …
/// - Find executable files:
/// fd --type executable
/// fd -tx
/// - Find empty files:
/// fd --type empty --type file
/// fd -te -tf
/// - Find empty directories:
/// fd --type empty --type directory
/// fd -te -td"
#[clap(long = "type", short = 't', value_name = "filetype", hide_possible_values = true,
arg_enum, action = ArgAction::Append, number_of_values = 1)]
pub filetype: Option<Vec<FileType>>,
/// Filter by file extension
///
/// (Additionally) filter search results by their file extension. Multiple
/// allowable file extensions can be specified.
///
/// If you want to search for files without extension,
/// you can use the regex '^[^.]+$' as a normal search pattern.
#[clap(long = "extension", short = 'e', value_name = "ext", action = ArgAction::Append, number_of_values = 1)]
pub extensions: Option<Vec<String>>,
#[clap(flatten)]
pub exec: Exec,
/// Max number of arguments to run as a batch with -X
///
/// Maximum number of arguments to pass to the command given with -X.
/// If the number of results is greater than the given size,
/// the command given with -X is run again with remaining arguments.
/// A batch size of zero means there is no limit (default), but note
/// that batching might still happen due to OS restrictions on the
/// maximum length of command lines.
#[clap(
long,
value_name = "size",
hide_short_help = true,
requires("exec-batch"),
value_parser = value_parser!(usize),
default_value_t
)]
pub batch_size: usize,
/// Exclude entries that match the given glob pattern
///
/// "Exclude files/directories that match the given glob pattern. This
/// overrides any other ignore logic. Multiple exclude patterns can be
/// specified.
///
/// Examples:
/// --exclude '*.pyc'
/// --exclude node_modules
#[clap(long, short = 'E', value_name = "pattern", action = ArgAction::Append, number_of_values = 1)]
pub exclude: Vec<String>,
/// Add custom ignore-file in '.gitignore' format
///
/// Add a custom ignore-file in '.gitignore' format. These files have a low
/// precedence.
#[clap(long, value_name = "path", action = ArgAction::Append, number_of_values = 1, hide_short_help = true)]
pub ignore_file: Vec<PathBuf>,
/// When to use colors
#[clap(
long,
short = 'c',
arg_enum,
default_value = "auto",
value_name = "when"
)]
pub color: ColorWhen,
/// Set number of threads
///
/// Set number of threads to use for searching & executing (default: number
/// of available CPU cores)
#[clap(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = RangedU64ValueParser::<usize>::from(1..))]
pub threads: Option<usize>,
/// Limit results based on the size of files
///
/// Limit results based on the size of files using the format <+-><NUM><UNIT>.
/// '+': file size must be greater than or equal to this
/// '-': file size must be less than or equal to this
/// If neither '+' nor '-' is specified, file size must be exactly equal to this.
/// 'NUM': The numeric size (e.g. 500)
/// 'UNIT': The units for NUM. They are not case-sensitive.
/// Allowed unit values:
/// 'b': bytes
/// 'k': kilobytes (base ten, 10^3 = 1000 bytes)
/// 'm': megabytes
/// 'g': gigabytes
/// 't': terabytes
/// 'ki': kibibytes (base two, 2^10 = 1024 bytes)
/// 'mi': mebibytes
/// 'gi': gibibytes
/// 'ti': tebibytes
#[clap(long, short = 'S', number_of_values = 1, value_parser = SizeFilter::from_string, allow_hyphen_values = true, action = ArgAction::Append)]
pub size: Vec<SizeFilter>,
/// Milliseconds to buffer before streaming search results to console
///
/// Amount of time in milliseconds to buffer, before streaming the search
/// results to the console.
#[clap(long, hide = true, action, value_parser = parse_millis)]
pub max_buffer_time: Option<Duration>,
/// Filter by file modification time (newer than)
///
/// Filter results based on the file modification time. The argument can be provided
/// as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min).
/// If the time is not specified, it defaults to 00:00:00.
/// '--change-newer-than' or '--newer' can be used as aliases.
/// Examples:
/// --changed-within 2weeks
/// --change-newer-than '2018-10-27 10:00:00'
/// --newer 2018-10-27
#[clap(
long,
alias("change-newer-than"),
alias("newer"),
value_name = "date|dur",
number_of_values = 1,
action
)]
pub changed_within: Option<String>,
/// Filter by file modification time (older than)
///
/// Filter results based on the file modification time. The argument can be provided
/// as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min).
/// '--change-older-than' or '--older' can be used as aliases.
///
/// Examples:
/// --changed-before '2018-10-27 10:00:00'
/// --change-older-than 2weeks
/// --older 2018-10-27
#[clap(
long,
alias("change-older-than"),
alias("older"),
value_name = "date|dur",
number_of_values = 1,
action
)]
pub changed_before: Option<String>,
/// Limit number of search results
///
/// Limit the number of search results to 'count' and quit immediately.
#[clap(long, value_name = "count", hide_short_help = true, value_parser)]
max_results: Option<usize>,
/// Limit search to a single result
///
/// Limit the search to a single result and quit immediately.
/// This is an alias for '--max-results=1'.
#[clap(
short = '1',
hide_short_help = true,
overrides_with("max-results"),
action
)]
max_one_result: bool,
/// Print nothing, exit code 0 if match found, 1 otherwise
///
/// When the flag is present, the program does not print anything and will
/// return with an exit code of 0 if there is at least one match. Otherwise, the
/// exit code will be 1.
///
/// '--has-results' can be used as an alias.
#[clap(long, short = 'q', alias = "has-results", hide_short_help = true, conflicts_with("max-results"), action)]
pub quiet: bool,
/// Show filesystem errors
///
///Enable the display of filesystem errors for situations such as
///insufficient permissions or dead symlinks.
#[clap(long, hide_short_help = true, overrides_with("show-errors"), action)]
pub show_errors: bool,
/// Change current working directory
///
/// Change the current working directory of fd to the provided path. This
/// means that search results will be shown with respect to the given base
/// path. Note that relative paths which are passed to fd via the positional
/// <path> argument or the '--search-path' option will also be resolved
/// relative to this directory.
#[clap(
long,
value_name = "path",
number_of_values = 1,
action,
hide_short_help = true
)]
pub base_directory: Option<PathBuf>,
/// the search pattern (a regular expression, unless '--glob' is used; optional)
///
/// the search pattern which is either a regular expression (default) or a glob
/// pattern (if --glob is used). If no pattern has been specified, every entry
/// is considered a match. If your pattern starts with a dash (-), make sure to
/// pass '--' first, or it will be considered as a flag (fd -- '-foo').
#[clap(value_parser, default_value = "")]
pub pattern: String,
/// Set path separator when printing file paths
/// Set the path separator to use when printing file paths. The default is
/// the OS-specific separator ('/' on Unix, '\\' on Windows).
#[clap(long, value_name = "separator", hide_short_help = true, action)]
pub path_separator: Option<String>,
/// the root directories for the filesystem search (optional)
///
/// The directories where the filesystem search is rooted (optional).
/// If omitted, search the current working directory.
#[clap(action = ArgAction::Append)]
path: Vec<PathBuf>,
/// Provides paths to search as an alternative to the positional <path>
///
/// Provide paths to search as an alternative to the positional <path>
/// argument. Changes the usage to `fd [OPTIONS] --search-path <path>
/// --search-path <path2> [<pattern>]`
#[clap(long, conflicts_with("path"), action = ArgAction::Append, hide_short_help = true, number_of_values = 1)]
search_path: Vec<PathBuf>,
/// strip './' prefix from non-tty outputs
///
/// By default, relative paths are prefixed with './' when the output goes to a non
/// interactive terminal (TTY). Use this flag to disable this behaviour.
#[clap(long, conflicts_with_all(&["path", "search-path"]), hide_short_help = true, action)]
pub strip_cwd_prefix: bool,
/// Filter by owning user and/or group
///
/// Filter files by their user and/or group.
/// Format: [(user|uid)][:(group|gid)]. Either side is optional.
/// Precede either side with a '!' to exclude files instead.
///
/// Examples:
/// --owner john
/// --owner :students
/// --owner '!john:students'
#[cfg(unix)]
#[clap(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group")]
pub owner: Option<OwnerFilter>,
/// Do not descend into a different file system
///
/// By default, fd will traverse the file system tree as far as other options
/// dictate. With this flag, fd ensures that it does not descend into a
/// different file system than the one it started in. Comparable to the -mount
/// or -xdev filters of find(1).
#[cfg(any(unix, windows))]
#[clap(long, aliases(&["mount", "xdev"]), hide_short_help = true)]
pub one_file_system: bool,
#[cfg(feature = "completions")]
#[clap(long, value_parser = value_parser!(Shell), hide = true, exclusive = true)]
gen_completions: Option<Option<Shell>>,
#[clap(flatten)]
_negations: Negations,
}
impl Opts {
pub fn search_paths(&self) -> anyhow::Result<Vec<PathBuf>> {
// would it make sense to concatenate these?
let paths = if !self.path.is_empty() {
&self.path
} else if !self.search_path.is_empty() {
&self.search_path
} else {
let current_directory = Path::new(".");
ensure_current_directory_exists(current_directory)?;
return Ok(vec![self.normalize_path(current_directory)]);
};
Ok(paths
.iter()
.filter_map(|path| {
if filesystem::is_existing_directory(&path) {
Some(self.normalize_path(path))
} else {
print_error(format!(
"Search path '{}' is not a directory.",
path.to_string_lossy()
));
None
}
})
.collect())
}
fn normalize_path(&self, path: &Path) -> PathBuf {
if self.absolute_path {
filesystem::absolute_path(path.normalize().unwrap().as_path()).unwrap()
} else {
path.to_path_buf()
}
}
pub fn no_search_paths(&self) -> bool {
self.path.is_empty() && self.search_path.is_empty()
}
#[inline]
pub fn rg_alias_ignore(&self) -> bool {
self.rg_alias_hidden_ignore > 0
}
pub fn max_depth(&self) -> Option<usize> {
self.max_depth.or(self.exact_depth)
}
pub fn min_depth(&self) -> Option<usize> {
self.min_depth.or(self.exact_depth)
}
pub fn threads(&self) -> usize {
std::cmp::max(self.threads.unwrap_or_else(num_cpus::get), 1)
}
pub fn max_results(&self) -> Option<usize> {
self.max_results.filter(|&m| m > 0).or_else(|| self.max_one_result.then(|| 1))
}
#[cfg(feature = "completions")]
pub fn gen_completions(&self) -> anyhow::Result<Option<Shell>> {
self.gen_completions
.map(|maybe_shell| match maybe_shell {
Some(sh) => Ok(sh),
None => guess_shell(),
})
.transpose()
}
}
// TODO: windows?
#[cfg(feature = "completions")]
fn guess_shell() -> anyhow::Result<Shell> {
let env_shell = std::env::var_os("SHELL").map(PathBuf::from);
let shell = env_shell.as_ref()
.and_then(|s| s.file_name())
.and_then(|s| s.to_str())
.ok_or_else(|| anyhow!("Unable to get shell from environment"))?;
shell
.parse::<Shell>()
.map_err(|_| anyhow!("Unknown shell {}", shell))
}
#[derive(Copy, Clone, PartialEq, Eq, ArgEnum)]
pub enum FileType {
#[clap(alias = "f")]
File,
#[clap(alias = "d")]
Directory,
#[clap(alias = "l")]
Symlink,
#[clap(alias = "x")]
Executable,
#[clap(alias = "e")]
Empty,
#[clap(alias = "s")]
Socket,
#[clap(alias = "p")]
Pipe,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, ArgEnum)]
pub enum ColorWhen {
/// show colors if the output goes to an interactive console (default)
Auto,
/// always use colorized output
Always,
/// do not use colorized output
Never,
}
// there isn't a derive api for getting grouped values yet,
// so we have to use hand-rolled parsing for exec and exec-batch
pub struct Exec {
pub command: Option<CommandSet>,
}
impl clap::FromArgMatches for Exec {
fn from_arg_matches(matches: &ArgMatches) -> clap::Result<Self> {
let command = matches
.grouped_values_of("exec")
.map(CommandSet::new)
.or_else(|| {
matches
.grouped_values_of("exec-batch")
.map(CommandSet::new_batch)
})
.transpose()
.map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?;
Ok(Exec { command })
}
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> clap::Result<()> {
*self = Self::from_arg_matches(matches)?;
Ok(())
}
}
impl clap::Args for Exec {
fn augment_args(cmd: Command<'_>) -> Command<'_> {
cmd.arg(Arg::new("exec")
.long("exec")
.short('x')
.min_values(1)
.multiple_occurrences(true)
.allow_hyphen_values(true)
.value_terminator(";")
.value_name("cmd")
.conflicts_with("list-details")
.help("Execute a command for each search result")
.long_help(
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
All positional arguments following --exec are considered to be arguments to the command - not to fd. \
It is therefore recommended to place the '-x'/'--exec' option last.\n\
The following placeholders are substituted before the command is executed:\n \
'{}': path (of the current search result)\n \
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension\n\n\
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
Examples:\n\n \
- find all *.zip files and unzip them:\n\n \
fd -e zip -x unzip\n\n \
- find *.h and *.cpp files and run \"clang-format -i ..\" for each of them:\n\n \
fd -e h -e cpp -x clang-format -i\n\n \
- Convert all *.jpg files to *.png files:\n\n \
fd -e jpg -x convert {} {.}.png\
",
),
)
.arg(
Arg::new("exec-batch")
.long("exec-batch")
.short('X')
.min_values(1)
.multiple_occurrences(true)
.allow_hyphen_values(true)
.value_terminator(";")
.value_name("cmd")
.conflicts_with_all(&["exec", "list-details"])
.help("Execute a command with all search results at once")
.long_help(
"Execute the given command once, with all search results as arguments.\n\
One of the following placeholders is substituted before the command is executed:\n \
'{}': path (of all search results)\n \
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension\n\n\
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
Examples:\n\n \
- Find all test_*.py files and open them in your favorite editor:\n\n \
fd -g 'test_*.py' -X vim\n\n \
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
fd -e rs -X wc -l\
"
),
)
}
fn augment_args_for_update(cmd: Command<'_>) -> Command<'_> {
Self::augment_args(cmd)
}
}
fn parse_millis(arg: &str) -> Result<Duration, std::num::ParseIntError> {
Ok(Duration::from_millis(arg.parse()?))
}
fn ensure_current_directory_exists(current_directory: &Path) -> anyhow::Result<()> {
if filesystem::is_existing_directory(current_directory) {
Ok(())
} else {
Err(anyhow!(
"Could not retrieve current directory (has it been deleted?)."
))
}
}

View File

@ -17,6 +17,7 @@ pub fn job(
out_perm: Arc<Mutex<()>>,
show_filesystem_errors: bool,
buffer_output: bool,
path_separator: Option<&str>,
) -> ExitCode {
let mut results: Vec<ExitCode> = Vec::new();
loop {
@ -39,7 +40,12 @@ pub fn job(
// Drop the lock so that other threads can read from the receiver.
drop(lock);
// Generate a command, execute it and store its exit code.
results.push(cmd.execute(dir_entry.path(), Arc::clone(&out_perm), buffer_output))
results.push(cmd.execute(
dir_entry.path(),
path_separator,
Arc::clone(&out_perm),
buffer_output,
))
}
// Returns error in case of any error.
merge_exitcodes(results)
@ -50,6 +56,7 @@ pub fn batch(
cmd: &CommandSet,
show_filesystem_errors: bool,
limit: usize,
path_separator: Option<&str>,
) -> ExitCode {
let paths = rx
.into_iter()
@ -63,5 +70,5 @@ pub fn batch(
}
});
cmd.execute_batch(paths, limit)
cmd.execute_batch(paths, limit, path_separator)
}

View File

@ -35,19 +35,17 @@ pub enum ExecutionMode {
#[derive(Debug, Clone, PartialEq)]
pub struct CommandSet {
mode: ExecutionMode,
path_separator: Option<String>,
commands: Vec<CommandTemplate>,
}
impl CommandSet {
pub fn new<I, S>(input: I, path_separator: Option<String>) -> Result<CommandSet>
pub fn new<I, S>(input: I) -> Result<CommandSet>
where
I: IntoIterator<Item = Vec<S>>,
S: AsRef<str>,
{
Ok(CommandSet {
mode: ExecutionMode::OneByOne,
path_separator,
commands: input
.into_iter()
.map(CommandTemplate::new)
@ -55,14 +53,13 @@ impl CommandSet {
})
}
pub fn new_batch<I, S>(input: I, path_separator: Option<String>) -> Result<CommandSet>
pub fn new_batch<I, S>(input: I) -> Result<CommandSet>
where
I: IntoIterator<Item = Vec<S>>,
S: AsRef<str>,
{
Ok(CommandSet {
mode: ExecutionMode::Batch,
path_separator,
commands: input
.into_iter()
.map(|args| {
@ -83,8 +80,13 @@ impl CommandSet {
self.mode == ExecutionMode::Batch
}
pub fn execute(&self, input: &Path, out_perm: Arc<Mutex<()>>, buffer_output: bool) -> ExitCode {
let path_separator = self.path_separator.as_deref();
pub fn execute(
&self,
input: &Path,
path_separator: Option<&str>,
out_perm: Arc<Mutex<()>>,
buffer_output: bool,
) -> ExitCode {
let commands = self
.commands
.iter()
@ -92,12 +94,10 @@ impl CommandSet {
execute_commands(commands, &out_perm, buffer_output)
}
pub fn execute_batch<I>(&self, paths: I, limit: usize) -> ExitCode
pub fn execute_batch<I>(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode
where
I: Iterator<Item = PathBuf>,
{
let path_separator = self.path_separator.as_deref();
let builders: io::Result<Vec<_>> = self
.commands
.iter()
@ -413,7 +413,7 @@ mod tests {
#[test]
fn tokens_with_placeholder() {
assert_eq!(
CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]], None).unwrap(),
CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
@ -423,7 +423,6 @@ mod tests {
]
}],
mode: ExecutionMode::OneByOne,
path_separator: None,
}
);
}
@ -431,7 +430,7 @@ mod tests {
#[test]
fn tokens_with_no_extension() {
assert_eq!(
CommandSet::new(vec![vec!["echo", "{.}"]], None).unwrap(),
CommandSet::new(vec![vec!["echo", "{.}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
@ -440,7 +439,6 @@ mod tests {
],
}],
mode: ExecutionMode::OneByOne,
path_separator: None,
}
);
}
@ -448,7 +446,7 @@ mod tests {
#[test]
fn tokens_with_basename() {
assert_eq!(
CommandSet::new(vec![vec!["echo", "{/}"]], None).unwrap(),
CommandSet::new(vec![vec!["echo", "{/}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
@ -457,7 +455,6 @@ mod tests {
],
}],
mode: ExecutionMode::OneByOne,
path_separator: None,
}
);
}
@ -465,7 +462,7 @@ mod tests {
#[test]
fn tokens_with_parent() {
assert_eq!(
CommandSet::new(vec![vec!["echo", "{//}"]], None).unwrap(),
CommandSet::new(vec![vec!["echo", "{//}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
@ -474,7 +471,6 @@ mod tests {
],
}],
mode: ExecutionMode::OneByOne,
path_separator: None,
}
);
}
@ -482,7 +478,7 @@ mod tests {
#[test]
fn tokens_with_basename_no_extension() {
assert_eq!(
CommandSet::new(vec![vec!["echo", "{/.}"]], None).unwrap(),
CommandSet::new(vec![vec!["echo", "{/.}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
@ -491,7 +487,6 @@ mod tests {
],
}],
mode: ExecutionMode::OneByOne,
path_separator: None,
}
);
}
@ -499,7 +494,7 @@ mod tests {
#[test]
fn tokens_multiple() {
assert_eq!(
CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]], None).unwrap(),
CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
@ -512,7 +507,6 @@ mod tests {
],
}],
mode: ExecutionMode::OneByOne,
path_separator: None,
}
);
}
@ -520,7 +514,7 @@ mod tests {
#[test]
fn tokens_single_batch() {
assert_eq!(
CommandSet::new_batch(vec![vec!["echo", "{.}"]], None).unwrap(),
CommandSet::new_batch(vec![vec!["echo", "{.}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
@ -529,14 +523,13 @@ mod tests {
],
}],
mode: ExecutionMode::Batch,
path_separator: None,
}
);
}
#[test]
fn tokens_multiple_batch() {
assert!(CommandSet::new_batch(vec![vec!["echo", "{.}", "{}"]], None).is_err());
assert!(CommandSet::new_batch(vec![vec!["echo", "{.}", "{}"]]).is_err());
}
#[test]
@ -546,7 +539,7 @@ mod tests {
#[test]
fn command_set_no_args() {
assert!(CommandSet::new(vec![vec!["echo"], vec![]], None).is_err());
assert!(CommandSet::new(vec![vec!["echo"], vec![]]).is_err());
}
#[test]

View File

@ -1,3 +1,4 @@
use anyhow::anyhow;
use once_cell::sync::Lazy;
use regex::Regex;
@ -24,7 +25,12 @@ const GIBI: u64 = MEBI * 1024;
const TEBI: u64 = GIBI * 1024;
impl SizeFilter {
pub fn from_string(s: &str) -> Option<Self> {
pub fn from_string(s: &str) -> anyhow::Result<Self> {
SizeFilter::parse_opt(s)
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", s))
}
fn parse_opt(s: &str) -> Option<Self> {
if !SIZE_CAPTURES.is_match(s) {
return None;
}
@ -165,7 +171,7 @@ mod tests {
#[test]
fn $name() {
let i = SizeFilter::from_string($value);
assert!(i.is_none());
assert!(i.is_err());
}
)*
};

View File

@ -1,4 +1,4 @@
mod app;
mod cli;
mod config;
mod dir_entry;
mod error;
@ -12,25 +12,25 @@ mod regex_helper;
mod walk;
use std::env;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::Arc;
use std::time;
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use atty::Stream;
use clap::{CommandFactory,Parser};
use globset::GlobBuilder;
use lscolors::LsColors;
use normpath::PathExt;
use regex::bytes::{RegexBuilder, RegexSetBuilder};
use crate::cli::{ColorWhen, Opts};
use crate::config::Config;
use crate::error::print_error;
use crate::exec::CommandSet;
use crate::exit_codes::ExitCode;
use crate::filetypes::FileTypes;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::{SizeFilter, TimeFilter};
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
@ -67,23 +67,42 @@ fn main() {
}
fn run() -> Result<ExitCode> {
let matches = app::build_app().get_matches_from(env::args_os());
let opts = Opts::parse();
set_working_dir(&matches)?;
let search_paths = extract_search_paths(&matches)?;
let pattern = extract_search_pattern(&matches)?;
ensure_search_pattern_is_not_a_path(&matches, pattern)?;
let pattern_regex = build_pattern_regex(&matches, pattern)?;
#[cfg(feature = "completions")]
if let Some(shell) = opts.gen_completions()? {
return print_completions(shell);
}
let config = construct_config(matches, &pattern_regex)?;
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_regex = build_pattern_regex(&opts)?;
let config = construct_config(opts, &pattern_regex)?;
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regex)?;
let re = build_regex(pattern_regex, &config)?;
walk::scan(&search_paths, Arc::new(re), Arc::new(config))
}
fn set_working_dir(matches: &clap::ArgMatches) -> Result<()> {
if let Some(base_directory) = matches.value_of_os("base-directory") {
let base_directory = Path::new(base_directory);
#[cfg(feature = "completions")]
#[cold]
fn print_completions(shell: clap_complete::Shell) -> Result<ExitCode> {
// The program name is the first argument.
let program_name = env::args().next().unwrap_or_else(|| "fd".to_string());
let mut cmd = Opts::command();
cmd.build();
// TODO: fix panic
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.",
@ -100,75 +119,11 @@ fn set_working_dir(matches: &clap::ArgMatches) -> Result<()> {
Ok(())
}
fn ensure_current_directory_exists(current_directory: &Path) -> Result<()> {
if filesystem::is_existing_directory(current_directory) {
Ok(())
} else {
Err(anyhow!(
"Could not retrieve current directory (has it been deleted?)."
))
}
}
fn extract_search_pattern(matches: &clap::ArgMatches) -> Result<&'_ str> {
let pattern = matches
.value_of_os("pattern")
.map(|p| {
p.to_str()
.ok_or_else(|| anyhow!("The search pattern includes invalid UTF-8 sequences."))
})
.transpose()?
.unwrap_or("");
Ok(pattern)
}
fn extract_search_paths(matches: &clap::ArgMatches) -> Result<Vec<PathBuf>> {
let parameter_paths = matches
.values_of_os("path")
.or_else(|| matches.values_of_os("search-path"));
let mut search_paths = match parameter_paths {
Some(paths) => paths
.filter_map(|path| {
let path_buffer = PathBuf::from(path);
if filesystem::is_existing_directory(&path_buffer) {
Some(path_buffer)
} else {
print_error(format!(
"Search path '{}' is not a directory.",
path_buffer.to_string_lossy(),
));
None
}
})
.collect(),
None => {
let current_directory = Path::new(".");
ensure_current_directory_exists(current_directory)?;
vec![current_directory.to_path_buf()]
}
};
if search_paths.is_empty() {
return Err(anyhow!("No valid search paths given."));
}
if matches.is_present("absolute-path") {
update_to_absolute_paths(&mut search_paths);
}
Ok(search_paths)
}
fn update_to_absolute_paths(search_paths: &mut [PathBuf]) {
for buffer in search_paths.iter_mut() {
*buffer = filesystem::absolute_path(buffer.normalize().unwrap().as_path()).unwrap();
}
}
/// Detect if the user accidentally supplied a path instead of a search pattern
fn ensure_search_pattern_is_not_a_path(matches: &clap::ArgMatches, pattern: &str) -> Result<()> {
if !matches.is_present("full-path")
&& pattern.contains(std::path::MAIN_SEPARATOR)
&& Path::new(pattern).is_dir()
fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
if !opts.full_path
&& opts.pattern.contains(std::path::MAIN_SEPARATOR)
&& Path::new(&opts.pattern).is_dir()
{
Err(anyhow!(
"The search pattern '{pattern}' contains a path-separation character ('{sep}') \
@ -177,7 +132,7 @@ fn ensure_search_pattern_is_not_a_path(matches: &clap::ArgMatches, pattern: &str
fd . '{pattern}'\n\n\
Instead, if you want your pattern to match the full file path, use:\n\n \
fd --full-path '{pattern}'",
pattern = pattern,
pattern = &opts.pattern,
sep = std::path::MAIN_SEPARATOR,
))
} else {
@ -185,11 +140,12 @@ fn ensure_search_pattern_is_not_a_path(matches: &clap::ArgMatches, pattern: &str
}
}
fn build_pattern_regex(matches: &clap::ArgMatches, pattern: &str) -> Result<String> {
Ok(if matches.is_present("glob") && !pattern.is_empty() {
fn build_pattern_regex(opts: &Opts) -> Result<String> {
let pattern = &opts.pattern;
Ok(if opts.glob && !pattern.is_empty() {
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
glob.regex().to_owned()
} else if matches.is_present("fixed-strings") {
} else if opts.fixed_strings {
// Treat pattern as literal string if '--fixed-strings' is used
regex::escape(pattern)
} else {
@ -211,28 +167,25 @@ fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> {
}
}
fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Config> {
fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
// The search will be case-sensitive if the command line flag is set or
// if the pattern has an uppercase character (smart case).
let case_sensitive = !matches.is_present("ignore-case")
&& (matches.is_present("case-sensitive") || pattern_has_uppercase_char(pattern_regex));
let case_sensitive =
!opts.ignore_case && (opts.case_sensitive || pattern_has_uppercase_char(pattern_regex));
let path_separator = matches
.value_of("path-separator")
.map_or_else(filesystem::default_path_separator, |s| Some(s.to_owned()));
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 = extract_size_limits(&matches)?;
let time_constraints = extract_time_constraints(&matches)?;
let size_limits = std::mem::replace(&mut opts.size, vec![]);
let time_constraints = extract_time_constraints(&opts)?;
#[cfg(unix)]
let owner_constraint = matches
.value_of("owner")
.map(OwnerFilter::from_string)
.transpose()?
.flatten();
let owner_constraint: Option<OwnerFilter> = opts.owner;
#[cfg(windows)]
let ansi_colors_support =
@ -241,10 +194,12 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
let ansi_colors_support = true;
let interactive_terminal = atty::is(Stream::Stdout);
let colored_output = match matches.value_of("color") {
Some("always") => true,
Some("never") => false,
_ => ansi_colors_support && env::var_os("NO_COLOR").is_none() && interactive_terminal,
let colored_output = match opts.color {
ColorWhen::Always => true,
ColorWhen::Never => false,
ColorWhen::Auto => {
ansi_colors_support && env::var_os("NO_COLOR").is_none() && interactive_terminal
}
};
let ls_colors = if colored_output {
@ -252,80 +207,42 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
} else {
None
};
let command = extract_command(&matches, path_separator.as_deref(), colored_output)?;
let command = extract_command(&mut opts, colored_output)?;
Ok(Config {
case_sensitive,
search_full_path: matches.is_present("full-path"),
ignore_hidden: !(matches.is_present("hidden")
|| matches.is_present("rg-alias-hidden-ignore")),
read_fdignore: !(matches.is_present("no-ignore")
|| matches.is_present("rg-alias-hidden-ignore")),
read_vcsignore: !(matches.is_present("no-ignore")
|| matches.is_present("rg-alias-hidden-ignore")
|| matches.is_present("no-ignore-vcs")),
read_parent_ignore: !matches.is_present("no-ignore-parent"),
read_global_ignore: !(matches.is_present("no-ignore")
|| matches.is_present("rg-alias-hidden-ignore")
|| matches.is_present("no-global-ignore-file")),
follow_links: matches.is_present("follow"),
one_file_system: matches.is_present("one-file-system"),
null_separator: matches.is_present("null_separator"),
quiet: matches.is_present("quiet"),
max_depth: matches
.value_of("max-depth")
.or_else(|| matches.value_of("rg-depth"))
.or_else(|| matches.value_of("exact-depth"))
.map(|n| n.parse::<usize>())
.transpose()
.context("Failed to parse argument to --max-depth/--exact-depth")?,
min_depth: matches
.value_of("min-depth")
.or_else(|| matches.value_of("exact-depth"))
.map(|n| n.parse::<usize>())
.transpose()
.context("Failed to parse argument to --min-depth/--exact-depth")?,
prune: matches.is_present("prune"),
threads: std::cmp::max(
matches
.value_of("threads")
.map(|n| n.parse::<usize>())
.transpose()
.context("Failed to parse number of threads")?
.map(|n| {
if n > 0 {
Ok(n)
} else {
Err(anyhow!("Number of threads must be positive."))
}
})
.transpose()?
.unwrap_or_else(num_cpus::get),
1,
),
max_buffer_time: matches
.value_of("max-buffer-time")
.map(|n| n.parse::<u64>())
.transpose()
.context("Failed to parse max. buffer time argument")?
.map(time::Duration::from_millis),
search_full_path: opts.full_path,
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),
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(),
max_buffer_time: opts.max_buffer_time,
ls_colors,
interactive_terminal,
file_types: matches.values_of("file-type").map(|values| {
file_types: opts.filetype.as_ref().map(|values| {
use crate::cli::FileType::*;
let mut file_types = FileTypes::default();
for value in values {
match value {
"f" | "file" => file_types.files = true,
"d" | "directory" => file_types.directories = true,
"l" | "symlink" => file_types.symlinks = true,
"x" | "executable" => {
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;
}
"e" | "empty" => file_types.empty_only = true,
"s" | "socket" => file_types.sockets = true,
"p" | "pipe" => file_types.pipes = true,
_ => unreachable!(),
Empty => file_types.empty_only = true,
Socket => file_types.sockets = true,
Pipe => file_types.pipes = true,
}
}
@ -337,10 +254,12 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
file_types
}),
extensions: matches
.values_of("extension")
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)
@ -349,75 +268,38 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
})
.transpose()?,
command: command.map(Arc::new),
batch_size: matches
.value_of("batch-size")
.map(|n| n.parse::<usize>())
.transpose()
.context("Failed to parse --batch-size argument")?
.unwrap_or_default(),
exclude_patterns: matches
.values_of("exclude")
.map(|v| v.map(|p| String::from("!") + p).collect())
.unwrap_or_else(Vec::new),
ignore_files: matches
.values_of("ignore-file")
.map(|vs| vs.map(PathBuf::from).collect())
.unwrap_or_else(Vec::new),
batch_size: opts.batch_size,
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
ignore_files: std::mem::replace(&mut opts.ignore_file, vec![]),
size_constraints: size_limits,
time_constraints,
#[cfg(unix)]
owner_constraint,
show_filesystem_errors: matches.is_present("show-errors"),
show_filesystem_errors: opts.show_errors,
path_separator,
actual_path_separator,
max_results: matches
.value_of("max-results")
.map(|n| n.parse::<usize>())
.transpose()
.context("Failed to parse --max-results argument")?
.filter(|&n| n > 0)
.or_else(|| {
if matches.is_present("max-one-result") {
Some(1)
} else {
None
}
}),
strip_cwd_prefix: (!matches.is_present("path")
&& !matches.is_present("search-path")
&& (interactive_terminal || matches.is_present("strip-cwd-prefix"))),
max_results: opts.max_results(),
strip_cwd_prefix: (opts.no_search_paths()
&& (interactive_terminal || opts.strip_cwd_prefix)),
})
}
fn extract_command(
matches: &clap::ArgMatches,
path_separator: Option<&str>,
colored_output: bool,
) -> Result<Option<CommandSet>> {
None.or_else(|| {
matches
.grouped_values_of("exec")
.map(|args| CommandSet::new(args, path_separator.map(str::to_string)))
})
.or_else(|| {
matches
.grouped_values_of("exec-batch")
.map(|args| CommandSet::new_batch(args, path_separator.map(str::to_string)))
})
.or_else(|| {
if !matches.is_present("list-details") {
return None;
}
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 color_arg = format!("--color={:?}", opts.color);
let color = matches.value_of("color").unwrap_or("auto");
let color_arg = format!("--color={}", color);
let res = determine_ls_command(&color_arg, colored_output)
.map(|cmd| CommandSet::new_batch([cmd], path_separator.map(str::to_string)).unwrap());
Some(res)
})
.transpose()
let res = determine_ls_command(&color_arg, colored_output)
.map(|cmd| CommandSet::new_batch([cmd]).unwrap());
Some(res)
})
.transpose()
}
fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&str>> {
@ -499,20 +381,10 @@ fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&st
Ok(cmd)
}
fn extract_size_limits(matches: &clap::ArgMatches) -> Result<Vec<SizeFilter>> {
matches.values_of("size").map_or(Ok(Vec::new()), |vs| {
vs.map(|sf| {
SizeFilter::from_string(sf)
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", sf))
})
.collect::<Result<Vec<_>>>()
})
}
fn extract_time_constraints(matches: &clap::ArgMatches) -> Result<Vec<TimeFilter>> {
fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
let now = time::SystemTime::now();
let mut time_constraints: Vec<TimeFilter> = Vec::new();
if let Some(t) = matches.value_of("changed-within") {
if let Some(ref t) = opts.changed_within {
if let Some(f) = TimeFilter::after(&now, t) {
time_constraints.push(f);
} else {
@ -522,7 +394,7 @@ fn extract_time_constraints(matches: &clap::ArgMatches) -> Result<Vec<TimeFilter
));
}
}
if let Some(t) = matches.value_of("changed-before") {
if let Some(ref t) = opts.changed_before {
if let Some(f) = TimeFilter::before(&now, t) {
time_constraints.push(f);
} else {

View File

@ -349,12 +349,20 @@ fn spawn_receiver(
// This will be set to `Some` if the `--exec` argument was supplied.
if let Some(ref cmd) = config.command {
if cmd.in_batch_mode() {
exec::batch(rx, cmd, show_filesystem_errors, config.batch_size)
exec::batch(
rx,
cmd,
show_filesystem_errors,
config.batch_size,
config.path_separator.as_deref(),
)
} else {
let shared_rx = Arc::new(Mutex::new(rx));
let out_perm = Arc::new(Mutex::new(()));
let path_separator = Arc::new(config.path_separator.clone());
// Each spawned job will store it's thread handle in here.
let mut handles = Vec::with_capacity(threads);
for _ in 0..threads {
@ -362,6 +370,8 @@ fn spawn_receiver(
let cmd = Arc::clone(cmd);
let out_perm = Arc::clone(&out_perm);
let path_separator = path_separator.clone();
// Spawn a job thread that will listen for and execute inputs.
let handle = thread::spawn(move || {
exec::job(
@ -370,6 +380,7 @@ fn spawn_receiver(
out_perm,
show_filesystem_errors,
enable_output_buffering,
path_separator.as_deref(),
)
});
@ -377,6 +388,8 @@ fn spawn_receiver(
handles.push(handle);
}
// TODO: once our MSRV supports scoped threads, it would probablly make sense to
// use that here
// Wait for all threads to exit before exiting the program.
let exit_codes = handles
.into_iter()

View File

@ -1464,7 +1464,13 @@ fn test_exec_batch() {
te.assert_failure_with_error(
&["foo", "--exec-batch", "echo", "{}", "{}"],
"[fd error]: Only one placeholder allowed for batch commands",
"error: Only one placeholder allowed for batch commands\n\
\n\
USAGE:\n\
fd-find [OPTIONS] [--] [PATTERN] [PATH]...\n\
\n\
For more information try --help\n\
",
);
te.assert_failure_with_error(
@ -1479,7 +1485,13 @@ fn test_exec_batch() {
te.assert_failure_with_error(
&["foo", "--exec-batch", "echo {}"],
"[fd error]: First argument of exec-batch is expected to be a fixed executable",
"error: First argument of exec-batch is expected to be a fixed executable\n\
\n\
USAGE:\n\
fd-find [OPTIONS] [--] [PATTERN] [PATH]...\n\
\n\
For more information try --help\n\
",
);
}
}