From 6655356ce3f1da56fdb31b49275e34e21e7a69a5 Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Fri, 17 Jun 2022 02:06:02 -0600 Subject: [PATCH 01/25] Upgrade clap to 3.2 --- Cargo.lock | 43 +++++++++++++++++++++++++------------------ Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03a483e..f86d116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,15 +96,15 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.18" +version = "3.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" dependencies = [ "atty", "bitflags", "clap_lex", "indexmap", - "lazy_static", + "once_cell", "strsim", "termcolor", "terminal_size", @@ -113,18 +113,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "3.1.4" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da92e6facd8d73c22745a5d3cbb59bdf8e46e3235c923e516527d8e81eec14a4" +checksum = "0f6ebaab5f25e4f0312dfa07cb30a755204b96e6531457c2cfdecfdf5f2adf40" dependencies = [ "clap", ] [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" dependencies = [ "os_str_bytes", ] @@ -241,13 +241,13 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -606,9 +606,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", @@ -706,19 +706,20 @@ dependencies = [ [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "users" @@ -749,9 +750,15 @@ dependencies = [ [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" diff --git a/Cargo.toml b/Cargo.toml index 1d4e4f4..994be07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ name = "fd" path = "src/main.rs" [build-dependencies] -clap = { version = "3.1", features = ["cargo"] } +clap = { version = "3.2", features = ["cargo"] } clap_complete = "3.1" version_check = "0.9" From 45d6f55d3aecdeb01ee2317423ae9459c771bca7 Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Mon, 18 Jul 2022 00:03:13 -0600 Subject: [PATCH 02/25] Bump MSRV to 1.56.1 Because that is what is needed by clap 2 --- .github/workflows/CICD.yml | 2 +- Cargo.lock | 34 +++++++++++++++++----------------- Cargo.toml | 4 ++-- clippy.toml | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index a0803ab..6ba1509 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1,7 +1,7 @@ name: CICD env: - MIN_SUPPORTED_RUST_VERSION: "1.56.0" + MIN_SUPPORTED_RUST_VERSION: "1.56.1" CICD_INTERMEDIATES_DIR: "_cicd-intermediates" on: diff --git a/Cargo.lock b/Cargo.lock index f86d116..130ba23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,30 +113,30 @@ dependencies = [ [[package]] name = "clap_complete" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f6ebaab5f25e4f0312dfa07cb30a755204b96e6531457c2cfdecfdf5f2adf40" +checksum = "c09fbb00fb6e20e92f785598ecbf0c118f269737490c57af28b1ed07f392be16" dependencies = [ "clap", ] [[package]] name = "clap_lex" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" +checksum = "87eba3c8c7f42ef17f6c659fc7416d0f4758cd3e58861ee63c5fa4a4dde649e4" dependencies = [ "os_str_bytes", ] [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" dependencies = [ "cfg-if", - "lazy_static", + "once_cell", ] [[package]] @@ -265,9 +265,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" [[package]] name = "hermit-abi" @@ -304,9 +304,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", @@ -490,18 +490,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ "proc-macro2", ] @@ -606,9 +606,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.96" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 994be07..c78ee50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ path = "src/main.rs" [build-dependencies] clap = { version = "3.2", features = ["cargo"] } -clap_complete = "3.1" +clap_complete = "3.2" version_check = "0.9" [dependencies] @@ -53,7 +53,7 @@ chrono = "0.4" once_cell = "1.13.1" [dependencies.clap] -version = "3.1" +version = "3.2" features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped"] [target.'cfg(unix)'.dependencies] diff --git a/clippy.toml b/clippy.toml index 0d369b5..56ce04e 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.56.0" +msrv = "1.56.1" From 4e7b403c1fea376f1b5be8cb25e10688a4b120f5 Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Fri, 17 Jun 2022 02:08:24 -0600 Subject: [PATCH 03/25] 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. --- .github/workflows/CICD.yml | 17 +- .gitignore | 1 + Cargo.lock | 20 + Cargo.toml | 8 +- Makefile | 37 ++ README.md | 4 +- build.rs | 21 - src/app.rs | 774 ------------------------------------- src/cli.rs | 747 +++++++++++++++++++++++++++++++++++ src/exec/job.rs | 11 +- src/exec/mod.rs | 45 +-- src/filter/size.rs | 10 +- src/main.rs | 356 ++++++----------- src/walk.rs | 15 +- tests/tests.rs | 16 +- 15 files changed, 999 insertions(+), 1083 deletions(-) create mode 100644 Makefile delete mode 100644 src/app.rs create mode 100644 src/cli.rs diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 6ba1509..c2e76be 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -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" diff --git a/.gitignore b/.gitignore index 324c57f..fddac43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ +/autocomplete/ **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index 130ba23..bd5b6ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index c78ee50..42ea78e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6ca17ba --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 7af5da6..f630dfa 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/build.rs b/build.rs index d78641a..fc54326 100644 --- a/build.rs +++ b/build.rs @@ -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(); - } } diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index a9eba45..0000000 --- a/src/app.rs +++ /dev/null @@ -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 --max-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 <+->.\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 \ - 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 ") - .long_help( - "Provide paths to search as an alternative to the positional \ - argument. Changes the usage to `fd [OPTIONS] --search-path \ - --search-path []`", - ), - ) - .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() -} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..3d1c804 --- /dev/null +++ b/src/cli.rs @@ -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 { + 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, + /// 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, + /// Only show results at exact given depth + /// + /// Only show search results at the exact given depth. This is an alias for + /// '--min-depth --max-depth '. + #[clap(long, value_name = "depth", hide_short_help = true, value_parser, conflicts_with_all(&["max-depth", "min-depth"]))] + exact_depth: Option, + /// 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>, + /// 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>, + + #[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, + /// 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, + /// 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::::from(1..))] + pub threads: Option, + /// Limit results based on the size of files + /// + /// Limit results based on the size of files using the format <+->. + /// '+': 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, + /// 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, + /// 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, + /// 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, + /// 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, + /// 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 + /// 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, + /// 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, + /// 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, + /// Provides paths to search as an alternative to the positional + /// + /// Provide paths to search as an alternative to the positional + /// argument. Changes the usage to `fd [OPTIONS] --search-path + /// --search-path []` + #[clap(long, conflicts_with("path"), action = ArgAction::Append, hide_short_help = true, number_of_values = 1)] + search_path: Vec, + /// 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, + /// 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>, + + #[clap(flatten)] + _negations: Negations, +} + +impl Opts { + pub fn search_paths(&self) -> anyhow::Result> { + // 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 { + self.max_depth.or(self.exact_depth) + } + + pub fn min_depth(&self) -> Option { + 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 { + 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> { + 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 { + 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::() + .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, +} + +impl clap::FromArgMatches for Exec { + fn from_arg_matches(matches: &ArgMatches) -> clap::Result { + 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 { + 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?)." + )) + } +} + diff --git a/src/exec/job.rs b/src/exec/job.rs index 9b95ac2..c4d93ee 100644 --- a/src/exec/job.rs +++ b/src/exec/job.rs @@ -17,6 +17,7 @@ pub fn job( out_perm: Arc>, show_filesystem_errors: bool, buffer_output: bool, + path_separator: Option<&str>, ) -> ExitCode { let mut results: Vec = 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) } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index fc26da2..21ebfea 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -35,19 +35,17 @@ pub enum ExecutionMode { #[derive(Debug, Clone, PartialEq)] pub struct CommandSet { mode: ExecutionMode, - path_separator: Option, commands: Vec, } impl CommandSet { - pub fn new(input: I, path_separator: Option) -> Result + pub fn new(input: I) -> Result where I: IntoIterator>, S: AsRef, { Ok(CommandSet { mode: ExecutionMode::OneByOne, - path_separator, commands: input .into_iter() .map(CommandTemplate::new) @@ -55,14 +53,13 @@ impl CommandSet { }) } - pub fn new_batch(input: I, path_separator: Option) -> Result + pub fn new_batch(input: I) -> Result where I: IntoIterator>, S: AsRef, { 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>, 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>, + 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(&self, paths: I, limit: usize) -> ExitCode + pub fn execute_batch(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode where I: Iterator, { - let path_separator = self.path_separator.as_deref(); - let builders: io::Result> = 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] diff --git a/src/filter/size.rs b/src/filter/size.rs index 2af20bc..a3d7c41 100644 --- a/src/filter/size.rs +++ b/src/filter/size.rs @@ -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 { + pub fn from_string(s: &str) -> anyhow::Result { + SizeFilter::parse_opt(s) + .ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", s)) + } + + fn parse_opt(s: &str) -> Option { 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()); } )* }; diff --git a/src/main.rs b/src/main.rs index 99aee3e..a0908dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { - 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 { + // 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> { - 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 { - Ok(if matches.is_present("glob") && !pattern.is_empty() { +fn build_pattern_regex(opts: &Opts) -> Result { + 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 { +fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result { // 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 = opts.owner; #[cfg(windows)] let ansi_colors_support = @@ -241,10 +194,12 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result 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()) - .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::()) - .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::()) - .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::()) - .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 Result()) - .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::()) - .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> { - 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> { + 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> { @@ -499,20 +381,10 @@ fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result Result> { - 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::>>() - }) -} - -fn extract_time_constraints(matches: &clap::ArgMatches) -> Result> { +fn extract_time_constraints(opts: &Opts) -> Result> { let now = time::SystemTime::now(); let mut time_constraints: Vec = 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 Date: Mon, 18 Jul 2022 02:19:11 -0600 Subject: [PATCH 04/25] Improve help output after switch to clap-derive Make it more like it used to be. --- src/cli.rs | 99 +++++++++++++++++++++++++++++++---------------------- src/main.rs | 2 +- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 3d1c804..1a10844 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,8 +4,8 @@ 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, + builder::RangedU64ValueParser, value_parser, AppSettings, Arg, ArgAction, ArgGroup, ArgMatches, + Command, ErrorKind, Parser, ValueEnum, }; #[cfg(feature = "completions")] use clap_complete::Shell; @@ -92,7 +92,7 @@ pub struct Opts { /// 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. + /// The flag can be overridden with --no-hidden. #[clap(long, short = 'H', action, overrides_with = "hidden")] pub hidden: bool, /// Do not respect .(git|fd)ignore files @@ -187,7 +187,7 @@ pub struct Opts { /// /// By default, fd does not descend into symlinked directories. Using this /// flag, symbolic links are also traversed. - /// Flag can be overriden with --no-follow. + /// Flag can be overridden with --no-follow. #[clap( long, short = 'L', @@ -200,9 +200,16 @@ pub struct Opts { /// /// 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: + /// (absolute) path. + /// Example: /// fd --glob -p '**/.git/config' - #[clap(long, short = 'p', action, overrides_with("full-path"))] + #[clap( + long, + short = 'p', + action, + overrides_with("full-path"), + verbatim_doc_comment + )] pub full_path: bool, /// Separate results by the null character /// @@ -247,8 +254,8 @@ pub struct Opts { /// 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 by type: file (f), directory (d), symlink (l), + /// executable (x), empty (e), socket (s), pipe (p) /// /// Filter the search by type: /// @@ -286,13 +293,12 @@ pub struct Opts { /// 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)] + arg_enum, action = ArgAction::Append, number_of_values = 1, verbatim_doc_comment)] pub filetype: Option>, /// 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)] @@ -320,14 +326,13 @@ pub struct Opts { 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. + /// 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)] + /// Examples: + /// --exclude '*.pyc' + /// --exclude node_modules + #[clap(long, short = 'E', value_name = "pattern", action = ArgAction::Append, number_of_values = 1, verbatim_doc_comment)] pub exclude: Vec, /// Add custom ignore-file in '.gitignore' format /// @@ -336,12 +341,18 @@ pub struct Opts { #[clap(long, value_name = "path", action = ArgAction::Append, number_of_values = 1, hide_short_help = true)] pub ignore_file: Vec, /// When to use colors + /// + /// 'auto': show colors if the output goes to an interactive console (default) + /// 'never': do not use colorized output + /// 'always': always use colorized output #[clap( long, short = 'c', arg_enum, default_value = "auto", - value_name = "when" + value_name = "when", + hide_possible_values = true, + verbatim_doc_comment )] pub color: ColorWhen, /// Set number of threads @@ -368,7 +379,7 @@ pub struct Opts { /// '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)] + #[clap(long, short = 'S', number_of_values = 1, value_parser = SizeFilter::from_string, allow_hyphen_values = true, action = ArgAction::Append, verbatim_doc_comment)] pub size: Vec, /// Milliseconds to buffer before streaming search results to console /// @@ -378,8 +389,8 @@ pub struct Opts { pub max_buffer_time: Option, /// 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). + /// 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: @@ -392,13 +403,14 @@ pub struct Opts { alias("newer"), value_name = "date|dur", number_of_values = 1, + verbatim_doc_comment, action )] pub changed_within: Option, /// 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). + /// 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: @@ -411,6 +423,7 @@ pub struct Opts { alias("older"), value_name = "date|dur", number_of_values = 1, + verbatim_doc_comment, action )] pub changed_before: Option, @@ -419,9 +432,8 @@ pub struct Opts { /// 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, - /// Limit search to a single result + /// Limit search to a single result and quit immediately /// - /// Limit the search to a single result and quit immediately. /// This is an alias for '--max-results=1'. #[clap( short = '1', @@ -437,7 +449,14 @@ pub struct Opts { /// 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)] + #[clap( + long, + short = 'q', + alias = "has-results", + hide_short_help = true, + conflicts_with("max-results"), + action + )] pub quiet: bool, /// Show filesystem errors /// @@ -466,7 +485,7 @@ pub struct Opts { /// 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 = "")] + #[clap(value_parser, default_value = "", hide_default_value = true)] pub pattern: String, /// Set path separator when printing file paths /// Set the path separator to use when printing file paths. The default is @@ -475,15 +494,13 @@ pub struct Opts { pub path_separator: Option, /// the root directories for the filesystem search (optional) /// - /// The directories where the filesystem search is rooted (optional). + /// The directories where the filesystem search is rooted. /// If omitted, search the current working directory. #[clap(action = ArgAction::Append)] path: Vec, - /// Provides paths to search as an alternative to the positional + /// Provides paths to search as an alternative to the positional argument /// - /// Provide paths to search as an alternative to the positional - /// argument. Changes the usage to `fd [OPTIONS] --search-path - /// --search-path []` + /// Changes the usage to `fd [OPTIONS] --search-path --search-path []` #[clap(long, conflicts_with("path"), action = ArgAction::Append, hide_short_help = true, number_of_values = 1)] search_path: Vec, /// strip './' prefix from non-tty outputs @@ -494,16 +511,15 @@ pub struct Opts { 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. + /// 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")] + #[clap(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group", verbatim_doc_comment)] pub owner: Option, /// Do not descend into a different file system /// @@ -581,7 +597,9 @@ impl Opts { } pub fn max_results(&self) -> Option { - self.max_results.filter(|&m| m > 0).or_else(|| self.max_one_result.then(|| 1)) + self.max_results + .filter(|&m| m > 0) + .or_else(|| self.max_one_result.then(|| 1)) } #[cfg(feature = "completions")] @@ -599,7 +617,8 @@ impl Opts { #[cfg(feature = "completions")] fn guess_shell() -> anyhow::Result { let env_shell = std::env::var_os("SHELL").map(PathBuf::from); - let shell = env_shell.as_ref() + 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"))?; @@ -608,7 +627,7 @@ fn guess_shell() -> anyhow::Result { .map_err(|_| anyhow!("Unknown shell {}", shell)) } -#[derive(Copy, Clone, PartialEq, Eq, ArgEnum)] +#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] pub enum FileType { #[clap(alias = "f")] File, @@ -626,7 +645,7 @@ pub enum FileType { Pipe, } -#[derive(Copy, Clone, PartialEq, Eq, Debug, ArgEnum)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)] pub enum ColorWhen { /// show colors if the output goes to an interactive console (default) Auto, diff --git a/src/main.rs b/src/main.rs index a0908dd..d721ceb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ use std::time; use anyhow::{anyhow, bail, Context, Result}; use atty::Stream; -use clap::{CommandFactory,Parser}; +use clap::{CommandFactory, Parser}; use globset::GlobBuilder; use lscolors::LsColors; use regex::bytes::{RegexBuilder, RegexSetBuilder}; From 066ce41299adb081b4fce99004efa64907794b4c Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Mon, 25 Jul 2022 22:23:47 -0600 Subject: [PATCH 05/25] Fix case of color options to ls --- src/cli.rs | 11 +++++++++++ src/main.rs | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 1a10844..cb127e3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -655,6 +655,17 @@ pub enum ColorWhen { Never, } +impl ColorWhen { + pub fn as_str(&self) -> &'static str { + use ColorWhen::*; + match *self { + Auto => "auto", + Always => "always", + Never => "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 { diff --git a/src/main.rs b/src/main.rs index d721ceb..7857949 100644 --- a/src/main.rs +++ b/src/main.rs @@ -293,7 +293,7 @@ fn extract_command(opts: &mut Opts, colored_output: bool) -> Result Date: Mon, 25 Jul 2022 22:27:26 -0600 Subject: [PATCH 06/25] Fix clippy warnings --- src/cli.rs | 2 +- src/main.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index cb127e3..fdd6140 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -554,7 +554,7 @@ impl Opts { Ok(paths .iter() .filter_map(|path| { - if filesystem::is_existing_directory(&path) { + if filesystem::is_existing_directory(path) { Some(self.normalize_path(path)) } else { print_error(format!( diff --git a/src/main.rs b/src/main.rs index 7857949..86c4ef5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -182,7 +182,7 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result { .unwrap_or_else(|| std::path::MAIN_SEPARATOR.to_string()); check_path_separator_length(path_separator.as_deref())?; - let size_limits = std::mem::replace(&mut opts.size, vec![]); + let size_limits = std::mem::take(&mut opts.size); let time_constraints = extract_time_constraints(&opts)?; #[cfg(unix)] let owner_constraint: Option = opts.owner; @@ -270,7 +270,7 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result { command: command.map(Arc::new), batch_size: opts.batch_size, exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(), - ignore_files: std::mem::replace(&mut opts.ignore_file, vec![]), + ignore_files: std::mem::take(&mut opts.ignore_file), size_constraints: size_limits, time_constraints, #[cfg(unix)] From ff7336b202100f3773e1f7aa1ba38b657cfad89e Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Thu, 8 Sep 2022 01:18:04 -0600 Subject: [PATCH 07/25] clap derive suggestions from epage --- src/cli.rs | 91 ++++++++++++++++++------------------------------------ 1 file changed, 30 insertions(+), 61 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index fdd6140..5eada79 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -93,31 +93,26 @@ pub struct Opts { /// 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. - #[clap(long, short = 'H', action, overrides_with = "hidden")] + #[clap(long, short = 'H', action)] 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")] + #[clap(long, short = 'I', action)] 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)] + #[clap(long, action, 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 - )] + #[clap(long, action, hide_short_help = true)] pub no_ignore_parent: bool, /// Do not respect the global ignore file #[clap(long, action, hide = true)] @@ -132,48 +127,36 @@ pub struct Opts { /// ///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"]))] + #[clap(long, short = 's', action, overrides_with("ignore-case"))] 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"]))] + #[clap(long, short = 'i', action, overrides_with("case-sensitive"))] 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") - )] + #[clap(long, short = 'g', action, conflicts_with("fixed-strings"))] 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)] + #[clap(long, action, overrides_with("glob"), 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 - )] + #[clap(long, short = 'F', alias = "literal", 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"))] + #[clap(long, short = 'a', action)] pub absolute_path: bool, /// Use a long listing format with file metadata /// @@ -188,13 +171,7 @@ pub struct Opts { /// By default, fd does not descend into symlinked directories. Using this /// flag, symbolic links are also traversed. /// Flag can be overridden with --no-follow. - #[clap( - long, - short = 'L', - alias = "dereference", - action, - overrides_with("follow") - )] + #[clap(long, short = 'L', alias = "dereference", action)] pub follow: bool, /// Search full abs. path (default: filename only) /// @@ -203,13 +180,7 @@ pub struct Opts { /// (absolute) path. /// Example: /// fd --glob -p '**/.git/config' - #[clap( - long, - short = 'p', - action, - overrides_with("full-path"), - verbatim_doc_comment - )] + #[clap(long, short = 'p', action, verbatim_doc_comment)] pub full_path: bool, /// Separate results by the null character /// @@ -219,7 +190,6 @@ pub struct Opts { long = "print0", short = '0', action, - overrides_with("print0"), conflicts_with("list-details"), hide_short_help = true )] @@ -292,8 +262,15 @@ pub struct Opts { /// - 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, verbatim_doc_comment)] + #[clap( + long = "type", + short = 't', + action, + value_name = "filetype", + hide_possible_values = true, + value_enum, + verbatim_doc_comment + )] pub filetype: Option>, /// Filter by file extension /// @@ -301,7 +278,7 @@ pub struct Opts { /// 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)] + #[clap(long = "extension", short = 'e', value_name = "ext", action)] pub extensions: Option>, #[clap(flatten)] @@ -332,13 +309,13 @@ pub struct Opts { /// Examples: /// --exclude '*.pyc' /// --exclude node_modules - #[clap(long, short = 'E', value_name = "pattern", action = ArgAction::Append, number_of_values = 1, verbatim_doc_comment)] + #[clap(long, short = 'E', value_name = "pattern", verbatim_doc_comment)] pub exclude: Vec, /// 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)] + #[clap(long, value_name = "path", action, hide_short_help = true)] pub ignore_file: Vec, /// When to use colors /// @@ -348,8 +325,8 @@ pub struct Opts { #[clap( long, short = 'c', - arg_enum, - default_value = "auto", + value_enum, + default_value_t = ColorWhen::Auto, value_name = "when", hide_possible_values = true, verbatim_doc_comment @@ -379,7 +356,7 @@ pub struct Opts { /// '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, verbatim_doc_comment)] + #[clap(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, action, verbatim_doc_comment)] pub size: Vec, /// Milliseconds to buffer before streaming search results to console /// @@ -402,7 +379,6 @@ pub struct Opts { alias("change-newer-than"), alias("newer"), value_name = "date|dur", - number_of_values = 1, verbatim_doc_comment, action )] @@ -422,7 +398,6 @@ pub struct Opts { alias("change-older-than"), alias("older"), value_name = "date|dur", - number_of_values = 1, verbatim_doc_comment, action )] @@ -462,7 +437,7 @@ pub struct Opts { /// ///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)] + #[clap(long, hide_short_help = true, action)] pub show_errors: bool, /// Change current working directory /// @@ -471,13 +446,7 @@ pub struct Opts { /// path. Note that relative paths which are passed to fd via the positional /// 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 - )] + #[clap(long, value_name = "path", action, hide_short_help = true)] pub base_directory: Option, /// the search pattern (a regular expression, unless '--glob' is used; optional) /// @@ -501,7 +470,7 @@ pub struct Opts { /// Provides paths to search as an alternative to the positional argument /// /// Changes the usage to `fd [OPTIONS] --search-path --search-path []` - #[clap(long, conflicts_with("path"), action = ArgAction::Append, hide_short_help = true, number_of_values = 1)] + #[clap(long, conflicts_with("path"), action, hide_short_help = true)] search_path: Vec, /// strip './' prefix from non-tty outputs /// From a50e417c676dc867d6288aa15ef0385c7aa9cb5c Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Fri, 9 Sep 2022 00:09:26 -0600 Subject: [PATCH 08/25] Add action clap attribute to fixed_strings --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 5eada79..992c707 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -150,7 +150,7 @@ pub struct Opts { /// 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", hide_short_help = true)] + #[clap(long, short = 'F', action, alias = "literal", hide_short_help = true)] pub fixed_strings: bool, /// Show absolute instead of relative paths /// From f4c34b81c71b8ab8185c089c8112670f849b1dd9 Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Sat, 8 Oct 2022 23:51:45 -0600 Subject: [PATCH 09/25] Upgrade to clap 4.0 --- Cargo.lock | 96 ++++++++++++++++++++++++++++++-------------------- Cargo.toml | 5 ++- src/cli.rs | 88 +++++++++++++++++++++++---------------------- tests/tests.rs | 10 +++--- 4 files changed, 110 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd5b6ad..57ce4d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,36 +96,34 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.20" +version = "4.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" +checksum = "3b1a0a4208c6c483b952ad35c6eed505fc13b46f08f631b81e828084a9318d74" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", - "indexmap", "once_cell", "strsim", "termcolor", "terminal_size", - "textwrap", ] [[package]] name = "clap_complete" -version = "3.2.2" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c09fbb00fb6e20e92f785598ecbf0c118f269737490c57af28b1ed07f392be16" +checksum = "11cba7abac9b56dfe2f035098cdb3a43946f276e6db83b72c4e692343f9aab9a" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "3.2.18" +version = "4.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +checksum = "db342ce9fda24fb191e2ed4e102055a4d381c1086a06630174cd8da8d5d917ce" dependencies = [ "heck", "proc-macro-error", @@ -136,9 +134,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87eba3c8c7f42ef17f6c659fc7416d0f4758cd3e58861ee63c5fa4a4dde649e4" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" dependencies = [ "os_str_bytes", ] @@ -190,6 +188,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fd-find" version = "8.4.0" @@ -277,12 +296,6 @@ dependencies = [ "regex", ] -[[package]] -name = "hashbrown" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" - [[package]] name = "heck" version = "0.4.0" @@ -323,14 +336,10 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "1.9.1" +name = "io-lifetimes" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" -dependencies = [ - "autocfg", - "hashbrown", -] +checksum = "1ea37f355c05dde75b84bba2d767906ad522e97cd9e2eef2be7a4ab7fb442c06" [[package]] name = "jemalloc-sys" @@ -365,6 +374,12 @@ version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +[[package]] +name = "linux-raw-sys" +version = "0.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" + [[package]] name = "log" version = "0.4.17" @@ -510,9 +525,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -609,6 +624,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustix" +version = "0.35.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c825b8aa8010eb9ee99b75f05e10180b9278d161583034d7574c9d617aeada" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "same-file" version = "1.0.6" @@ -656,12 +685,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.1.17" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +checksum = "8440c860cf79def6164e4a0a983bcc2305d82419177a0e0c71930d049e3ac5a1" dependencies = [ - "libc", - "winapi", + "rustix", + "windows-sys", ] [[package]] @@ -686,15 +715,6 @@ dependencies = [ "syn", ] -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" -dependencies = [ - "terminal_size", -] - [[package]] name = "thiserror" version = "1.0.31" diff --git a/Cargo.toml b/Cargo.toml index 42ea78e..09b953d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ name = "fd" path = "src/main.rs" [build-dependencies] -clap = { version = "3.2", features = ["cargo"] } version_check = "0.9" [dependencies] @@ -50,10 +49,10 @@ dirs-next = "2.0" normpath = "0.3.2" chrono = "0.4" once_cell = "1.13.1" -clap_complete = {version = "3.2", optional = true} +clap_complete = {version = "4.0", optional = true} [dependencies.clap] -version = "3.2" +version = "4.0" features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped", "derive"] [target.'cfg(unix)'.dependencies] diff --git a/src/cli.rs b/src/cli.rs index 992c707..88d323c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,8 +4,8 @@ use std::time::Duration; #[cfg(feature = "completions")] use anyhow::anyhow; use clap::{ - builder::RangedU64ValueParser, value_parser, AppSettings, Arg, ArgAction, ArgGroup, ArgMatches, - Command, ErrorKind, Parser, ValueEnum, + builder::RangedU64ValueParser, error::ErrorKind, value_parser, Arg, ArgAction, ArgGroup, + ArgMatches, Command, Parser, ValueEnum, }; #[cfg(feature = "completions")] use clap_complete::Shell; @@ -23,23 +23,24 @@ use crate::filter::SizeFilter; struct Negations; impl clap::FromArgMatches for Negations { - fn from_arg_matches(_: &ArgMatches) -> clap::Result { + fn from_arg_matches(_: &ArgMatches) -> clap::error::Result { Ok(Negations) } - fn update_from_arg_matches(&mut self, _: &ArgMatches) -> clap::Result<()> { + fn update_from_arg_matches(&mut self, _: &ArgMatches) -> clap::error::Result<()> { Ok(()) } } impl clap::Args for Negations { - fn augment_args(cmd: Command<'_>) -> Command<'_> { + fn augment_args(cmd: Command) -> Command { Self::augment_args_for_update(cmd) } - fn augment_args_for_update(cmd: Command<'_>) -> Command<'_> { + fn augment_args_for_update(cmd: Command) -> Command { cmd.arg( - Arg::new("no-hidden") + Arg::new("no_hidden") + .action(ArgAction::Count) .long("no-hidden") .overrides_with("hidden") .hide(true) @@ -47,27 +48,31 @@ impl clap::Args for Negations { ) .arg( Arg::new("ignore") + .action(ArgAction::Count) .long("ignore") - .overrides_with("no-ignore") + .overrides_with("no_ignore") .hide(true) .long_help("Overrides --no-ignore."), ) .arg( - Arg::new("ignore-vcs") + Arg::new("ignore_vcs") + .action(ArgAction::Count) .long("ignore-vcs") - .overrides_with("no-ignore-vcs") + .overrides_with("no_ignore_vcs") .hide(true) .long_help("Overrides --no-ignore-vcs."), ) .arg( - Arg::new("relative-path") + Arg::new("relative_path") + .action(ArgAction::Count) .long("relative-path") - .overrides_with("absolute-path") + .overrides_with("absolute_path") .hide(true) .long_help("Overrides --absolute-path."), ) .arg( - Arg::new("no-follow") + Arg::new("no_follow") + .action(ArgAction::Count) .long("no-follow") .overrides_with("follow") .hide(true) @@ -79,12 +84,12 @@ impl clap::Args for Negations { #[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"])), + args_override_self = true, + 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 @@ -121,24 +126,24 @@ pub struct Opts { /// ///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)] + #[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("ignore-case"))] + #[clap(long, short = 's', action, overrides_with("ignore_case"))] 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("case-sensitive"))] + #[clap(long, short = 'i', action, overrides_with("case_sensitive"))] 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"))] + #[clap(long, short = 'g', action, conflicts_with("fixed_strings"))] pub glob: bool, /// Regular-expression based search (default) /// @@ -164,7 +169,7 @@ pub struct Opts { /// 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"))] + #[clap(long, short = 'l', action, conflicts_with("absolute_path"))] pub list_details: bool, /// Follow symbolic links /// @@ -190,7 +195,7 @@ pub struct Opts { long = "print0", short = '0', action, - conflicts_with("list-details"), + conflicts_with("list_details"), hide_short_help = true )] pub null_separator: bool, @@ -216,13 +221,13 @@ pub struct Opts { /// /// Only show search results at the exact given depth. This is an alias for /// '--min-depth --max-depth '. - #[clap(long, value_name = "depth", hide_short_help = true, value_parser, conflicts_with_all(&["max-depth", "min-depth"]))] + #[clap(long, value_name = "depth", hide_short_help = true, value_parser, conflicts_with_all(&["max_depth", "min_depth"]))] exact_depth: Option, /// 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"]))] + #[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), /// executable (x), empty (e), socket (s), pipe (p) @@ -296,7 +301,7 @@ pub struct Opts { long, value_name = "size", hide_short_help = true, - requires("exec-batch"), + requires("exec_batch"), value_parser = value_parser!(usize), default_value_t )] @@ -413,7 +418,7 @@ pub struct Opts { #[clap( short = '1', hide_short_help = true, - overrides_with("max-results"), + overrides_with("max_results"), action )] max_one_result: bool, @@ -429,7 +434,7 @@ pub struct Opts { short = 'q', alias = "has-results", hide_short_help = true, - conflicts_with("max-results"), + conflicts_with("max_results"), action )] pub quiet: bool, @@ -476,7 +481,7 @@ pub struct Opts { /// /// 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)] + #[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 /// @@ -501,7 +506,7 @@ pub struct Opts { pub one_file_system: bool, #[cfg(feature = "completions")] - #[clap(long, value_parser = value_parser!(Shell), hide = true, exclusive = true)] + #[clap(long, action, hide = true, exclusive = true)] gen_completions: Option>, #[clap(flatten)] @@ -642,13 +647,13 @@ pub struct Exec { } impl clap::FromArgMatches for Exec { - fn from_arg_matches(matches: &ArgMatches) -> clap::Result { + fn from_arg_matches(matches: &ArgMatches) -> clap::error::Result { let command = matches .grouped_values_of("exec") .map(CommandSet::new) .or_else(|| { matches - .grouped_values_of("exec-batch") + .grouped_values_of("exec_batch") .map(CommandSet::new_batch) }) .transpose() @@ -656,23 +661,23 @@ impl clap::FromArgMatches for Exec { Ok(Exec { command }) } - fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> clap::Result<()> { + fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> clap::error::Result<()> { *self = Self::from_arg_matches(matches)?; Ok(()) } } impl clap::Args for Exec { - fn augment_args(cmd: Command<'_>) -> Command<'_> { + fn augment_args(cmd: Command) -> Command { cmd.arg(Arg::new("exec") + .action(ArgAction::Append) .long("exec") .short('x') - .min_values(1) - .multiple_occurrences(true) + .num_args(1..) .allow_hyphen_values(true) .value_terminator(";") .value_name("cmd") - .conflicts_with("list-details") + .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). \ @@ -696,15 +701,15 @@ impl clap::Args for Exec { ), ) .arg( - Arg::new("exec-batch") + Arg::new("exec_batch") + .action(ArgAction::Append) .long("exec-batch") .short('X') - .min_values(1) - .multiple_occurrences(true) + .num_args(1..) .allow_hyphen_values(true) .value_terminator(";") .value_name("cmd") - .conflicts_with_all(&["exec", "list-details"]) + .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\ @@ -725,7 +730,7 @@ impl clap::Args for Exec { ) } - fn augment_args_for_update(cmd: Command<'_>) -> Command<'_> { + fn augment_args_for_update(cmd: Command) -> Command { Self::augment_args(cmd) } } @@ -743,4 +748,3 @@ fn ensure_current_directory_exists(current_directory: &Path) -> anyhow::Result<( )) } } - diff --git a/tests/tests.rs b/tests/tests.rs index a55bf95..ab49543 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1466,10 +1466,9 @@ fn test_exec_batch() { &["foo", "--exec-batch", "echo", "{}", "{}"], "error: Only one placeholder allowed for batch commands\n\ \n\ - USAGE:\n\ - fd-find [OPTIONS] [--] [PATTERN] [PATH]...\n\ + Usage: fd-find [OPTIONS] [PATTERN] [PATH]...\n\ \n\ - For more information try --help\n\ + For more information try '--help'\n\ ", ); @@ -1487,10 +1486,9 @@ fn test_exec_batch() { &["foo", "--exec-batch", "echo {}"], "error: First argument of exec-batch is expected to be a fixed executable\n\ \n\ - USAGE:\n\ - fd-find [OPTIONS] [--] [PATTERN] [PATH]...\n\ + Usage: fd-find [OPTIONS] [PATTERN] [PATH]...\n\ \n\ - For more information try --help\n\ + For more information try '--help'\n\ ", ); } From 5b9e302e9b51eb156851f93e3c47a35dff51430b Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Sun, 9 Oct 2022 00:02:28 -0600 Subject: [PATCH 10/25] Support powershell when guessing shell for completions --- src/cli.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 88d323c..2af1fa3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -591,14 +591,21 @@ impl Opts { #[cfg(feature = "completions")] fn guess_shell() -> anyhow::Result { let env_shell = std::env::var_os("SHELL").map(PathBuf::from); - let shell = env_shell + if let Some(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::() - .map_err(|_| anyhow!("Unknown shell {}", shell)) + { + shell + .parse::() + .map_err(|_| anyhow!("Unknown shell {}", shell)) + } else { + // Assume powershell on windows + #[cfg(windows)] + return Ok(Shell::Powershell); + #[cfg(not(windows))] + return Err(anyhow!("Unable to get shell from environment")); + } } #[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] From 86c33492a7d56991b8d96cc66521252acec8cf3d Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Sun, 9 Oct 2022 01:05:27 -0600 Subject: [PATCH 11/25] Fix name of powershell in guess_shell --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 0194518..ffea92d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -605,7 +605,7 @@ fn guess_shell() -> anyhow::Result { } else { // Assume powershell on windows #[cfg(windows)] - return Ok(Shell::Powershell); + return Ok(Shell::PowerShell); #[cfg(not(windows))] return Err(anyhow!("Unable to get shell from environment")); } From aca64c09f85fcb1beac8bdea5e3e8cb1293de44b Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Sun, 9 Oct 2022 01:25:03 -0600 Subject: [PATCH 12/25] Actually test if exec or exec-batch is used By the time we check if we should strip the cwd, we've already moved the command out of the options, so store if we got that out earlier. --- src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 1288423..9888b02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -208,6 +208,7 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result { None }; let command = extract_command(&mut opts, colored_output)?; + let has_command = command.is_some(); Ok(Config { case_sensitive, @@ -280,7 +281,7 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result { actual_path_separator, max_results: opts.max_results(), strip_cwd_prefix: (opts.no_search_paths() - && (opts.strip_cwd_prefix || !(opts.null_separator || opts.exec.command.is_some()))), + && (opts.strip_cwd_prefix || !(opts.null_separator || has_command))), }) } From 10ecb64ff3b410152b44ed70f173a836af2b68dc Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Mon, 10 Oct 2022 23:58:33 -0600 Subject: [PATCH 13/25] Fix typo in test --- tests/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests.rs b/tests/tests.rs index 9d4ee5a..a99e975 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1501,7 +1501,7 @@ fn test_exec_batch() { &["foo", "--exec-batch", "echo {}"], "error: First argument of exec-batch is expected to be a fixed executable\n\ \n\ - Usage: fd-find [OPTIONS] [PATTERN] [PATH]...\n\ + Usage: fd-find [OPTIONS] [PATTERN] [PATH]...\n\ \n\ For more information try '--help'\n\ ", From bf1a6f66801d5cbca7eb2b7af7e035619208228d Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Tue, 11 Oct 2022 00:35:21 -0600 Subject: [PATCH 14/25] Update clap depenency version --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68211a7..a010b82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,9 +113,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.11" +version = "4.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed45cc2c62a3eff523e718d8576ba762c83a3146151093283ac62ae11933a73" +checksum = "385007cbbed899260395a4107435fead4cad80684461b3cc78238bdcb0bad58f" dependencies = [ "atty", "bitflags", diff --git a/Cargo.toml b/Cargo.toml index 18f2baa..e23fc49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ once_cell = "1.15.0" clap_complete = {version = "4.0", optional = true} [dependencies.clap] -version = "4.0" +version = "4.0.12" features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped", "derive"] [target.'cfg(unix)'.dependencies] From c0b14705cd4247dfd8ebdd4dba137a8cc8ae2cc2 Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Tue, 11 Oct 2022 00:39:57 -0600 Subject: [PATCH 15/25] Update msrv to 1.60 Because that is what clap requires --- .github/workflows/CICD.yml | 2 +- build.rs | 2 +- clippy.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 89f652b..5d5c363 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1,7 +1,7 @@ name: CICD env: - MIN_SUPPORTED_RUST_VERSION: "1.57.0" + MIN_SUPPORTED_RUST_VERSION: "1.60.0" CICD_INTERMEDIATES_DIR: "_cicd-intermediates" on: diff --git a/build.rs b/build.rs index ab1219b..224f2d3 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,5 @@ fn main() { - let min_version = "1.57"; + let min_version = "1.60"; match version_check::is_min_version(min_version) { Some(true) => {} diff --git a/clippy.toml b/clippy.toml index 23b32c1..16caf02 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.57.0" +msrv = "1.60.0" From c2115884de298b6c89a75d3be330672a71b2a9f0 Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Tue, 11 Oct 2022 22:27:57 -0600 Subject: [PATCH 16/25] Fix deprecations from clap 4.0 --- src/cli.rs | 129 +++++++++++++++++++++++------------------------------ 1 file changed, 56 insertions(+), 73 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index ffea92d..cf91609 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -82,9 +82,8 @@ impl clap::Args for Negations { } #[derive(Parser)] -#[clap( +#[command( version, - dont_collapse_args_in_usage = true, after_help = "Note: `fd -h` prints a short and concise overview while `fd --help` gives all \ details.", after_long_help = "Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues", @@ -99,70 +98,70 @@ pub struct Opts { /// 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. - #[clap(long, short = 'H', action)] + #[arg(long, short = 'H')] 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)] + #[arg(long, short = 'I')] 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, hide_short_help = true)] + #[arg(long, 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, hide_short_help = true)] + #[arg(long, hide_short_help = true)] pub no_ignore_parent: bool, /// Do not respect the global ignore file - #[clap(long, action, hide = true)] + #[arg(long, 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)] + #[arg(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("ignore_case"))] + #[arg(long, short = 's', overrides_with("ignore_case"))] 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("case_sensitive"))] + #[arg(long, short = 'i', overrides_with("case_sensitive"))] 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"))] + #[arg(long, short = 'g', conflicts_with("fixed_strings"))] 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("glob"), hide_short_help = true)] + #[arg(long, overrides_with("glob"), 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', action, alias = "literal", hide_short_help = true)] + #[arg(long, short = 'F', alias = "literal", 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)] + #[arg(long, short = 'a')] pub absolute_path: bool, /// Use a long listing format with file metadata /// @@ -170,14 +169,14 @@ pub struct Opts { /// 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"))] + #[arg(long, short = 'l', 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 overridden with --no-follow. - #[clap(long, short = 'L', alias = "dereference", action)] + #[arg(long, short = 'L', alias = "dereference")] pub follow: bool, /// Search full abs. path (default: filename only) /// @@ -186,16 +185,15 @@ pub struct Opts { /// (absolute) path. /// Example: /// fd --glob -p '**/.git/config' - #[clap(long, short = 'p', action, verbatim_doc_comment)] + #[arg(long, short = 'p', verbatim_doc_comment)] 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( + #[arg( long = "print0", short = '0', - action, conflicts_with("list_details"), hide_short_help = true )] @@ -204,31 +202,25 @@ pub struct Opts { /// /// 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") - )] + #[arg(long, short = 'd', value_name = "depth", alias("maxdepth"))] max_depth: Option, /// 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)] + #[arg(long, value_name = "depth", hide_short_help = true)] min_depth: Option, /// Only show results at exact given depth /// /// Only show search results at the exact given depth. This is an alias for /// '--min-depth --max-depth '. - #[clap(long, value_name = "depth", hide_short_help = true, value_parser, conflicts_with_all(&["max_depth", "min_depth"]))] + #[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]))] exact_depth: Option, /// 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"]))] + #[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]))] pub prune: bool, /// Filter by type: file (f), directory (d), symlink (l), /// executable (x), empty (e), socket (s), pipe (p) @@ -268,10 +260,9 @@ pub struct Opts { /// - Find empty directories: /// fd --type empty --type directory /// fd -te -td" - #[clap( + #[arg( long = "type", short = 't', - action, value_name = "filetype", hide_possible_values = true, value_enum, @@ -284,10 +275,10 @@ pub struct Opts { /// 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)] + #[arg(long = "extension", short = 'e', value_name = "ext")] pub extensions: Option>, - #[clap(flatten)] + #[command(flatten)] pub exec: Exec, /// Max number of arguments to run as a batch with -X @@ -298,7 +289,7 @@ pub struct Opts { /// 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( + #[arg( long, value_name = "size", hide_short_help = true, @@ -315,20 +306,20 @@ pub struct Opts { /// Examples: /// --exclude '*.pyc' /// --exclude node_modules - #[clap(long, short = 'E', value_name = "pattern", verbatim_doc_comment)] + #[arg(long, short = 'E', value_name = "pattern", verbatim_doc_comment)] pub exclude: Vec, /// 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, hide_short_help = true)] + #[arg(long, value_name = "path", hide_short_help = true)] pub ignore_file: Vec, /// When to use colors /// /// 'auto': show colors if the output goes to an interactive console (default) /// 'never': do not use colorized output /// 'always': always use colorized output - #[clap( + #[arg( long, short = 'c', value_enum, @@ -342,7 +333,7 @@ pub struct Opts { /// /// 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::::from(1..))] + #[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = RangedU64ValueParser::::from(1..))] pub threads: Option, /// Limit results based on the size of files /// @@ -362,13 +353,13 @@ pub struct Opts { /// 'mi': mebibytes /// 'gi': gibibytes /// 'ti': tebibytes - #[clap(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, action, verbatim_doc_comment)] + #[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment)] pub size: Vec, /// 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)] + #[arg(long, hide = true, value_parser = parse_millis)] pub max_buffer_time: Option, /// Filter by file modification time (newer than) /// @@ -380,13 +371,12 @@ pub struct Opts { /// --changed-within 2weeks /// --change-newer-than '2018-10-27 10:00:00' /// --newer 2018-10-27 - #[clap( + #[arg( long, alias("change-newer-than"), alias("newer"), value_name = "date|dur", - verbatim_doc_comment, - action + verbatim_doc_comment )] pub changed_within: Option, /// Filter by file modification time (older than) @@ -399,29 +389,23 @@ pub struct Opts { /// --changed-before '2018-10-27 10:00:00' /// --change-older-than 2weeks /// --older 2018-10-27 - #[clap( + #[arg( long, alias("change-older-than"), alias("older"), value_name = "date|dur", - verbatim_doc_comment, - action + verbatim_doc_comment )] pub changed_before: Option, /// 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)] + #[arg(long, value_name = "count", hide_short_help = true)] max_results: Option, /// Limit 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 - )] + #[arg(short = '1', hide_short_help = true, overrides_with("max_results"))] max_one_result: bool, /// Print nothing, exit code 0 if match found, 1 otherwise /// @@ -430,20 +414,19 @@ pub struct Opts { /// exit code will be 1. /// /// '--has-results' can be used as an alias. - #[clap( + #[arg( long, short = 'q', alias = "has-results", hide_short_help = true, - conflicts_with("max_results"), - action + conflicts_with("max_results") )] 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, action)] + #[arg(long, hide_short_help = true)] pub show_errors: bool, /// Change current working directory /// @@ -452,7 +435,7 @@ pub struct Opts { /// path. Note that relative paths which are passed to fd via the positional /// argument or the '--search-path' option will also be resolved /// relative to this directory. - #[clap(long, value_name = "path", action, hide_short_help = true)] + #[arg(long, value_name = "path", hide_short_help = true)] pub base_directory: Option, /// the search pattern (a regular expression, unless '--glob' is used; optional) /// @@ -460,23 +443,23 @@ pub struct Opts { /// 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 = "", hide_default_value = true)] + #[arg(default_value = "", hide_default_value = true)] 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)] + #[arg(long, value_name = "separator", hide_short_help = true)] pub path_separator: Option, /// the root directories for the filesystem search (optional) /// /// The directories where the filesystem search is rooted. /// If omitted, search the current working directory. - #[clap(action = ArgAction::Append)] + #[arg(action = ArgAction::Append)] path: Vec, /// Provides paths to search as an alternative to the positional argument /// /// Changes the usage to `fd [OPTIONS] --search-path --search-path []` - #[clap(long, conflicts_with("path"), action, hide_short_help = true)] + #[arg(long, conflicts_with("path"), hide_short_help = true)] search_path: Vec, /// strip './' prefix from -0/--print-0 output /// @@ -484,7 +467,7 @@ pub struct Opts { /// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a /// path starting with '-' being treated as a command line option. Use /// this flag to disable this behaviour. - #[clap(long, conflicts_with_all(&["path", "search_path"]), hide_short_help = true, action)] + #[arg(long, conflicts_with_all(&["path", "search_path"]), hide_short_help = true)] pub strip_cwd_prefix: bool, /// Filter by owning user and/or group /// @@ -496,7 +479,7 @@ pub struct Opts { /// --owner :students /// --owner '!john:students' #[cfg(unix)] - #[clap(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group", verbatim_doc_comment)] + #[arg(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group", verbatim_doc_comment)] pub owner: Option, /// Do not descend into a different file system /// @@ -505,11 +488,11 @@ pub struct Opts { /// 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)] + #[arg(long, aliases(&["mount", "xdev"]), hide_short_help = true)] pub one_file_system: bool, #[cfg(feature = "completions")] - #[clap(long, action, hide = true, exclusive = true)] + #[arg(long, hide = true, exclusive = true)] gen_completions: Option>, #[clap(flatten)] @@ -613,19 +596,19 @@ fn guess_shell() -> anyhow::Result { #[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] pub enum FileType { - #[clap(alias = "f")] + #[value(alias = "f")] File, - #[clap(alias = "d")] + #[value(alias = "d")] Directory, - #[clap(alias = "l")] + #[value(alias = "l")] Symlink, - #[clap(alias = "x")] + #[value(alias = "x")] Executable, - #[clap(alias = "e")] + #[value(alias = "e")] Empty, - #[clap(alias = "s")] + #[value(alias = "s")] Socket, - #[clap(alias = "p")] + #[value(alias = "p")] Pipe, } From b6f0088b68dfc385e57691a33ad6e71aacce91dc Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Tue, 11 Oct 2022 23:19:17 -0600 Subject: [PATCH 17/25] Change thread option to take u32 instead of usize To simplify the option parsing. --- src/cli.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index cf91609..fe55392 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,7 +4,7 @@ use std::time::Duration; #[cfg(feature = "completions")] use anyhow::anyhow; use clap::{ - builder::RangedU64ValueParser, error::ErrorKind, value_parser, Arg, ArgAction, ArgGroup, + error::ErrorKind, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, Parser, ValueEnum, }; #[cfg(feature = "completions")] @@ -333,8 +333,8 @@ pub struct Opts { /// /// Set number of threads to use for searching & executing (default: number /// of available CPU cores) - #[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = RangedU64ValueParser::::from(1..))] - pub threads: Option, + #[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = 1..)] + pub threads: Option, /// Limit results based on the size of files /// /// Limit results based on the size of files using the format <+->. @@ -553,7 +553,12 @@ impl Opts { } pub fn threads(&self) -> usize { - std::cmp::max(self.threads.unwrap_or_else(num_cpus::get), 1) + // This will panic if the number of threads passed in is more than usize::MAX in an environment + // where usize is less than 32 bits (for example 16-bit architectures). It's pretty + // unlikely fd will be running in such an environment, and even more unlikely someone would + // be trying to use that many threads on such an environment, so I think panicing is an + // appropriate way to handle that. + std::cmp::max(self.threads.map_or_else(num_cpus::get, |n| n.try_into().expect("too many threads")), 1) } pub fn max_results(&self) -> Option { From aec125637b10797d81314053665c47f3381d9a70 Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Fri, 14 Oct 2022 23:14:03 -0600 Subject: [PATCH 18/25] Fix format --- src/cli.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index fe55392..4a9c66d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,8 +4,8 @@ use std::time::Duration; #[cfg(feature = "completions")] use anyhow::anyhow; use clap::{ - error::ErrorKind, value_parser, Arg, ArgAction, ArgGroup, - ArgMatches, Command, Parser, ValueEnum, + error::ErrorKind, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, Parser, + ValueEnum, }; #[cfg(feature = "completions")] use clap_complete::Shell; @@ -558,7 +558,11 @@ impl Opts { // unlikely fd will be running in such an environment, and even more unlikely someone would // be trying to use that many threads on such an environment, so I think panicing is an // appropriate way to handle that. - std::cmp::max(self.threads.map_or_else(num_cpus::get, |n| n.try_into().expect("too many threads")), 1) + std::cmp::max( + self.threads + .map_or_else(num_cpus::get, |n| n.try_into().expect("too many threads")), + 1, + ) } pub fn max_results(&self) -> Option { From a3622ba294e2554e32ef0deac0d0a74f627b582f Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Wed, 19 Oct 2022 23:43:45 -0600 Subject: [PATCH 19/25] Remove TODO comment In favor of tracking scoped threads in #1141 --- src/walk.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/walk.rs b/src/walk.rs index 1577fa0..0148a8b 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -367,9 +367,6 @@ 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() .map(|handle| handle.join().unwrap()) From 3782278f029bd919cf72feaa9d063e64375e3d52 Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Thu, 20 Oct 2022 00:34:52 -0600 Subject: [PATCH 20/25] Improve help output --- src/cli.rs | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 4a9c66d..f975ec7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -122,8 +122,6 @@ pub struct Opts { /// Do not respect the global ignore file #[arg(long, 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'. #[arg(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no_hidden"]), action(ArgAction::Count), hide_short_help = true)] @@ -145,13 +143,9 @@ pub struct Opts { /// Perform a glob-based search instead of a regular expression search. #[arg(long, short = 'g', conflicts_with("fixed_strings"))] pub glob: bool, - /// Regular-expression based search (default) - /// - ///Perform a regular-expression based search (default). This can be used to override --glob. + /// Perform a regular-expression based search (default). This can be used to override --glob. #[arg(long, overrides_with("glob"), 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'. @@ -187,8 +181,6 @@ pub struct Opts { /// fd --glob -p '**/.git/config' #[arg(long, short = 'p', verbatim_doc_comment)] 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'. #[arg( @@ -204,20 +196,14 @@ pub struct Opts { /// limit on the search depth. #[arg(long, short = 'd', value_name = "depth", alias("maxdepth"))] max_depth: Option, - /// Only show results starting at given depth - /// /// Only show search results starting at the given depth. /// See also: '--max-depth' and '--exact-depth' #[arg(long, value_name = "depth", hide_short_help = true)] min_depth: Option, - /// Only show results at exact given depth - /// /// Only show search results at the exact given depth. This is an alias for /// '--min-depth --max-depth '. #[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]))] exact_depth: Option, - /// 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. #[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]))] @@ -281,8 +267,6 @@ pub struct Opts { #[command(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. @@ -308,29 +292,20 @@ pub struct Opts { /// --exclude node_modules #[arg(long, short = 'E', value_name = "pattern", verbatim_doc_comment)] pub exclude: Vec, - /// Add custom ignore-file in '.gitignore' format - /// /// Add a custom ignore-file in '.gitignore' format. These files have a low /// precedence. #[arg(long, value_name = "path", hide_short_help = true)] pub ignore_file: Vec, /// When to use colors - /// - /// 'auto': show colors if the output goes to an interactive console (default) - /// 'never': do not use colorized output - /// 'always': always use colorized output #[arg( long, short = 'c', value_enum, default_value_t = ColorWhen::Auto, value_name = "when", - hide_possible_values = true, verbatim_doc_comment )] pub color: ColorWhen, - /// Set number of threads - /// /// Set number of threads to use for searching & executing (default: number /// of available CPU cores) #[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = 1..)] @@ -353,7 +328,7 @@ pub struct Opts { /// 'mi': mebibytes /// 'gi': gibibytes /// 'ti': tebibytes - #[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment)] + #[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment, value_name = "size")] pub size: Vec, /// Milliseconds to buffer before streaming search results to console /// @@ -428,8 +403,6 @@ pub struct Opts { ///insufficient permissions or dead symlinks. #[arg(long, hide_short_help = true)] 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 @@ -471,7 +444,7 @@ pub struct Opts { 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)]. + /// Format: [(user|uid)][:(group|gid)]. /// Either side is optional. Precede either side with a '!' to exclude files instead. /// /// Examples: From 36ee44a3f6ca6cbe06229ed7a133a501fb28d8ab Mon Sep 17 00:00:00 2001 From: Thayne McCombs Date: Tue, 1 Nov 2022 02:30:22 -0600 Subject: [PATCH 21/25] Make help output more like what it was before use clap_derive In particular, we specifically use `long_help` instead of doc comments because using doc comments will always trim the "." off the end of the first paragraph, and will include the short help as the first paragraph of the full help. --- src/cli.rs | 553 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 317 insertions(+), 236 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index f975ec7..4507613 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -84,8 +84,6 @@ impl clap::Args for Negations { #[derive(Parser)] #[command( version, - after_help = "Note: `fd -h` prints a short and concise overview while `fd --help` gives all \ - details.", after_long_help = "Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues", args_override_self = true, group(ArgGroup::new("execs").args(&["exec", "exec_batch", "list_details"]).conflicts_with_all(&[ @@ -93,208 +91,269 @@ impl clap::Args for Negations { )] 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 overridden with --no-hidden. - #[arg(long, short = 'H')] + #[arg( + long, + short = 'H', + 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." + )] 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. - #[arg(long, short = 'I')] + #[arg( + long, + short = 'I', + 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." + )] 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. - #[arg(long, hide_short_help = true)] + #[arg( + long, + hide_short_help = true, + 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." + )] 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. - #[arg(long, hide_short_help = true)] + #[arg( + long, + hide_short_help = true, + long_help = "Show search results from files and directories that would otherwise be \ + ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories." + )] pub no_ignore_parent: bool, /// Do not respect the global ignore file #[arg(long, hide = true)] pub no_global_ignore_file: bool, - ///Perform an unrestricted search, including ignored and hidden files. This is - ///an alias for '--no-ignore --hidden'. - #[arg(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no_hidden"]), action(ArgAction::Count), hide_short_help = true)] + /// Unrestricted search, alias for '--no-ignore --hidden' + #[arg(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no_hidden"]), action(ArgAction::Count), hide_short_help = true, + long_help = "Perform an unrestricted search, including ignored and hidden files. This is \ + an alias for '--no-ignore --hidden'." + )] 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). - #[arg(long, short = 's', overrides_with("ignore_case"))] + #[arg( + long, + short = 's', + overrides_with("ignore_case"), + long_help = "Perform a case-sensitive search. By default, fd uses case-insensitive \ + searches, unless the pattern contains an uppercase character (smart \ + case)." + )] 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). - #[arg(long, short = 'i', overrides_with("case_sensitive"))] + #[arg( + long, + short = 'i', + overrides_with("case_sensitive"), + long_help = "Perform a case-insensitive search. By default, fd uses case-insensitive \ + searches, unless the pattern contains an uppercase character (smart \ + case)." + )] pub ignore_case: bool, /// Glob-based search (default: regular expression) - /// - /// Perform a glob-based search instead of a regular expression search. - #[arg(long, short = 'g', conflicts_with("fixed_strings"))] + #[arg( + long, + short = 'g', + conflicts_with("fixed_strings"), + long_help = "Perform a glob-based search instead of a regular expression search." + )] pub glob: bool, - /// Perform a regular-expression based search (default). This can be used to override --glob. - #[arg(long, overrides_with("glob"), hide_short_help = true)] + /// Regular-expression based search (default) + #[arg( + long, + overrides_with("glob"), + hide_short_help = true, + long_help = "Perform a regular-expression based search (default). This can be used to \ + override --glob." + )] pub regex: bool, - /// 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(long, short = 'F', alias = "literal", hide_short_help = true)] + /// Treat pattern as literal string stead of regex + #[arg( + long, + short = 'F', + alias = "literal", + hide_short_help = true, + 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'." + )] 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. - #[arg(long, short = 'a')] + #[arg( + long, + short = 'a', + long_help = "Shows the full path starting from the root as opposed to relative paths. \ + The flag can be overridden with --relative-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. - #[arg(long, short = 'l', conflicts_with("absolute_path"))] + #[arg( + long, + short = 'l', + conflicts_with("absolute_path"), + 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." + )] 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 overridden with --no-follow. - #[arg(long, short = 'L', alias = "dereference")] + #[arg( + long, + short = 'L', + alias = "dereference", + 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." + )] 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' - #[arg(long, short = 'p', verbatim_doc_comment)] + #[arg( + long, + short = 'p', + 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'" + )] pub full_path: bool, - /// Separate search results by the null character (instead of newlines). - /// Useful for piping results to 'xargs'. + /// Separate search results by the null character #[arg( long = "print0", short = '0', conflicts_with("list_details"), - hide_short_help = true + hide_short_help = true, + long_help = "Separate search results by the null character (instead of newlines). \ + Useful for piping results to 'xargs'." )] 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. - #[arg(long, short = 'd', value_name = "depth", alias("maxdepth"))] + #[arg( + long, + short = 'd', + value_name = "depth", + alias("maxdepth"), + long_help = "Limit the directory traversal to a given depth. By default, there is no \ + limit on the search depth." + )] max_depth: Option, /// Only show search results starting at the given depth. - /// See also: '--max-depth' and '--exact-depth' - #[arg(long, value_name = "depth", hide_short_help = true)] + #[arg( + long, + value_name = "depth", + hide_short_help = true, + long_help = "Only show search results starting at the given depth. \ + See also: '--max-depth' and '--exact-depth'" + )] min_depth: Option, - /// Only show search results at the exact given depth. This is an alias for - /// '--min-depth --max-depth '. - #[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]))] + /// Only show search results at the exact given depth + #[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]), + long_help = "Only show search results at the exact given depth. This is an alias for \ + '--min-depth --max-depth '.", + )] exact_depth: Option, /// Do not traverse into directories that match the search criteria. If /// you want to exclude specific directories, use the '--exclude=…' option. - #[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]))] + #[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]), + long_help = "Do not traverse into directories that match the search criteria. If \ + you want to exclude specific directories, use the '--exclude=…' option.", + )] pub prune: bool, /// Filter by type: file (f), directory (d), symlink (l), /// executable (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" #[arg( long = "type", short = 't', value_name = "filetype", hide_possible_values = true, value_enum, - verbatim_doc_comment + 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" )] pub filetype: Option>, /// 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. - #[arg(long = "extension", short = 'e', value_name = "ext")] + #[arg( + long = "extension", + short = 'e', + value_name = "ext", + 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." + )] pub extensions: Option>, #[command(flatten)] pub exec: Exec, - /// 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. + /// Max number of arguments to run as a batch size with -X #[arg( long, value_name = "size", hide_short_help = true, requires("exec_batch"), value_parser = value_parser!(usize), - default_value_t + default_value_t, + 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.", )] 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 - #[arg(long, short = 'E', value_name = "pattern", verbatim_doc_comment)] + #[arg( + long, + short = 'E', + value_name = "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" + )] pub exclude: Vec, - /// Add a custom ignore-file in '.gitignore' format. These files have a low - /// precedence. - #[arg(long, value_name = "path", hide_short_help = true)] + /// Add a custom ignore-file in '.gitignore' format + #[arg( + long, + value_name = "path", + hide_short_help = true, + long_help = "Add a custom ignore-file in '.gitignore' format. These files have a low precedence" + )] pub ignore_file: Vec, /// When to use colors #[arg( @@ -303,7 +362,7 @@ pub struct Opts { value_enum, default_value_t = ColorWhen::Auto, value_name = "when", - verbatim_doc_comment + long_help = "Declare when to use color for the pattern match output", )] pub color: ColorWhen, /// Set number of threads to use for searching & executing (default: number @@ -311,24 +370,24 @@ pub struct Opts { #[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = 1..)] pub threads: Option, /// Limit results based on the size of files - /// - /// Limit results based on the size of files using the format <+->. - /// '+': 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 - #[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment, value_name = "size")] + #[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment, value_name = "size", + long_help = "Limit results based on the size of files using the format <+->.\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", + )] pub size: Vec, /// Milliseconds to buffer before streaming search results to console /// @@ -337,131 +396,153 @@ pub struct Opts { #[arg(long, hide = true, value_parser = parse_millis)] pub max_buffer_time: Option, /// Filter by file modification time (newer than) - /// - /// 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 #[arg( long, alias("change-newer-than"), alias("newer"), value_name = "date|dur", - verbatim_doc_comment + 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" )] pub changed_within: Option, /// Filter by file modification time (older than) - /// - /// 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 #[arg( long, alias("change-older-than"), alias("older"), value_name = "date|dur", - verbatim_doc_comment + 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" )] pub changed_before: Option, /// Limit number of search results - /// - /// Limit the number of search results to 'count' and quit immediately. - #[arg(long, value_name = "count", hide_short_help = true)] + #[arg( + long, + value_name = "count", + hide_short_help = true, + long_help = "Limit the number of search results to 'count' and quit immediately." + )] max_results: Option, - /// Limit search to a single result and quit immediately - /// - /// This is an alias for '--max-results=1'. - #[arg(short = '1', hide_short_help = true, overrides_with("max_results"))] + /// Limit search to a single result + #[arg( + short = '1', + hide_short_help = true, + overrides_with("max_results"), + long_help = "Limit the search to a single result and quit immediately. \ + This is an alias for '--max-results=1'." + )] 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. #[arg( long, short = 'q', alias = "has-results", hide_short_help = true, - conflicts_with("max_results") + conflicts_with("max_results"), + 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." )] pub quiet: bool, /// Show filesystem errors - /// - ///Enable the display of filesystem errors for situations such as - ///insufficient permissions or dead symlinks. - #[arg(long, hide_short_help = true)] + #[arg( + long, + hide_short_help = true, + long_help = "Enable the display of filesystem errors for situations such as \ + insufficient permissions or dead symlinks." + )] pub show_errors: bool, - /// 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 - /// argument or the '--search-path' option will also be resolved - /// relative to this directory. - #[arg(long, value_name = "path", hide_short_help = true)] + /// Change current working directory + #[arg( + long, + value_name = "path", + hide_short_help = true, + 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 \ + argument or the '--search-path' option will also be resolved \ + relative to this directory." + )] pub base_directory: Option, /// 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'). - #[arg(default_value = "", hide_default_value = true)] + #[arg( + default_value = "", + hide_default_value = true, + value_name = "pattern", + 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')." + )] 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). - #[arg(long, value_name = "separator", hide_short_help = true)] + #[arg( + long, + value_name = "separator", + hide_short_help = true, + long_help = "Set the path separator to use when printing file paths. The default is \ + the OS-specific separator ('/' on Unix, '\\' on Windows)." + )] pub path_separator: Option, /// the root directories for the filesystem search (optional) - /// - /// The directories where the filesystem search is rooted. - /// If omitted, search the current working directory. - #[arg(action = ArgAction::Append)] + #[arg(action = ArgAction::Append, + value_name = "path", + long_help = "The directory where the filesystem search is rooted (optional). If \ + omitted, search the current working directory.", + )] path: Vec, /// Provides paths to search as an alternative to the positional argument - /// - /// Changes the usage to `fd [OPTIONS] --search-path --search-path []` - #[arg(long, conflicts_with("path"), hide_short_help = true)] + #[arg( + long, + conflicts_with("path"), + value_name = "search-path", + hide_short_help = true, + long_help = "Provide paths to search as an alternative to the positional \ + argument. Changes the usage to `fd [OPTIONS] --search-path \ + --search-path []`" + )] search_path: Vec, - /// strip './' prefix from -0/--print-0 output - /// /// By default, relative paths are prefixed with './' when -x/--exec, /// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a /// path starting with '-' being treated as a command line option. Use /// this flag to disable this behaviour. - #[arg(long, conflicts_with_all(&["path", "search_path"]), hide_short_help = true)] + #[arg(long, conflicts_with_all(&["path", "search_path"]), hide_short_help = true, + long_help = "By default, relative paths are prefixed with './' when -x/--exec, \ + -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a \ + path starting with '-' being treated as a command line option. Use \ + this flag to disable this behaviour.", + )] pub strip_cwd_prefix: bool, /// Filter by owning 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)] - #[arg(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group", verbatim_doc_comment)] + #[arg(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user: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'" + )] pub owner: Option, - /// 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))] - #[arg(long, aliases(&["mount", "xdev"]), hide_short_help = true)] + #[arg(long, aliases(&["mount", "xdev"]), hide_short_help = true, + 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).")] pub one_file_system: bool, #[cfg(feature = "completions")] @@ -609,8 +690,8 @@ impl ColorWhen { use ColorWhen::*; match *self { Auto => "auto", - Always => "always", Never => "never", + Always => "always", } } } From 3f72ef4cdd0ac3588c244fc9ea0cf08f1bc7a4ce Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 1 Nov 2022 20:07:09 +0100 Subject: [PATCH 22/25] Remove outdated TODO comment --- src/cli.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 4507613..2428852 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -636,7 +636,6 @@ impl Opts { } } -// TODO: windows? #[cfg(feature = "completions")] fn guess_shell() -> anyhow::Result { let env_shell = std::env::var_os("SHELL").map(PathBuf::from); From ebd48d406e182ad8e3f1b41756170f9ba0f01cc1 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 1 Nov 2022 20:12:56 +0100 Subject: [PATCH 23/25] Fix unit tests --- tests/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index a99e975..e07dc23 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1481,7 +1481,7 @@ fn test_exec_batch() { &["foo", "--exec-batch", "echo", "{}", "{}"], "error: Only one placeholder allowed for batch commands\n\ \n\ - Usage: fd-find [OPTIONS] [PATTERN] [PATH]...\n\ + Usage: fd-find [OPTIONS] [pattern] [path]...\n\ \n\ For more information try '--help'\n\ ", @@ -1501,7 +1501,7 @@ fn test_exec_batch() { &["foo", "--exec-batch", "echo {}"], "error: First argument of exec-batch is expected to be a fixed executable\n\ \n\ - Usage: fd-find [OPTIONS] [PATTERN] [PATH]...\n\ + Usage: fd-find [OPTIONS] [pattern] [path]...\n\ \n\ For more information try '--help'\n\ ", From 55029e889e4771c1fc7a5f8f9e0aa73247e2cdbd Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 1 Nov 2022 20:13:32 +0100 Subject: [PATCH 24/25] Minor: add missing full stop --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 2428852..4c9c274 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -352,7 +352,7 @@ pub struct Opts { long, value_name = "path", hide_short_help = true, - long_help = "Add a custom ignore-file in '.gitignore' format. These files have a low precedence" + long_help = "Add a custom ignore-file in '.gitignore' format. These files have a low precedence." )] pub ignore_file: Vec, /// When to use colors From 5771e74b95abc03f53c8df9ea94241fc8a096896 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 1 Nov 2022 20:18:17 +0100 Subject: [PATCH 25/25] Add newlines between options --- src/cli.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index 4c9c274..fa69fba 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -100,6 +100,7 @@ pub struct Opts { The flag can be overridden with --no-hidden." )] pub hidden: bool, + /// Do not respect .(git|fd)ignore files #[arg( long, @@ -109,6 +110,7 @@ pub struct Opts { The flag can be overridden with --ignore." )] pub no_ignore: bool, + /// Do not respect .gitignore files #[arg( long, @@ -117,6 +119,7 @@ pub struct Opts { ignored by '.gitignore' files. The flag can be overridden with --ignore-vcs." )] pub no_ignore_vcs: bool, + /// Do not respect .(git|fd)ignore files in parent directories #[arg( long, @@ -125,15 +128,18 @@ pub struct Opts { ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories." )] pub no_ignore_parent: bool, + /// Do not respect the global ignore file #[arg(long, hide = true)] pub no_global_ignore_file: bool, + /// Unrestricted search, alias for '--no-ignore --hidden' #[arg(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no_hidden"]), action(ArgAction::Count), hide_short_help = true, long_help = "Perform an unrestricted search, including ignored and hidden files. This is \ an alias for '--no-ignore --hidden'." )] rg_alias_hidden_ignore: u8, + /// Case-sensitive search (default: smart case) #[arg( long, @@ -144,6 +150,7 @@ pub struct Opts { case)." )] pub case_sensitive: bool, + /// Case-insensitive search (default: smart case) #[arg( long, @@ -154,6 +161,7 @@ pub struct Opts { case)." )] pub ignore_case: bool, + /// Glob-based search (default: regular expression) #[arg( long, @@ -162,6 +170,7 @@ pub struct Opts { long_help = "Perform a glob-based search instead of a regular expression search." )] pub glob: bool, + /// Regular-expression based search (default) #[arg( long, @@ -171,6 +180,7 @@ pub struct Opts { override --glob." )] pub regex: bool, + /// Treat pattern as literal string stead of regex #[arg( long, @@ -182,6 +192,7 @@ pub struct Opts { exact filename, consider using '--glob'." )] pub fixed_strings: bool, + /// Show absolute instead of relative paths #[arg( long, @@ -190,6 +201,7 @@ pub struct Opts { The flag can be overridden with --relative-path." )] pub absolute_path: bool, + /// Use a long listing format with file metadata #[arg( long, @@ -201,6 +213,7 @@ pub struct Opts { deterministic sort order." )] pub list_details: bool, + /// Follow symbolic links #[arg( long, @@ -211,6 +224,7 @@ pub struct Opts { Flag can be overriden with --no-follow." )] pub follow: bool, + /// Search full abs. path (default: filename only) #[arg( long, @@ -221,6 +235,7 @@ pub struct Opts { fd --glob -p '**/.git/config'" )] pub full_path: bool, + /// Separate search results by the null character #[arg( long = "print0", @@ -231,6 +246,7 @@ pub struct Opts { Useful for piping results to 'xargs'." )] pub null_separator: bool, + /// Set maximum search depth (default: none) #[arg( long, @@ -241,6 +257,7 @@ pub struct Opts { limit on the search depth." )] max_depth: Option, + /// Only show search results starting at the given depth. #[arg( long, @@ -250,12 +267,14 @@ pub struct Opts { See also: '--max-depth' and '--exact-depth'" )] min_depth: Option, + /// Only show search results at the exact given depth #[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]), long_help = "Only show search results at the exact given depth. This is an alias for \ '--min-depth --max-depth '.", )] exact_depth: Option, + /// Do not traverse into directories that match the search criteria. If /// you want to exclude specific directories, use the '--exclude=…' option. #[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]), @@ -263,6 +282,7 @@ pub struct Opts { you want to exclude specific directories, use the '--exclude=…' option.", )] pub prune: bool, + /// Filter by type: file (f), directory (d), symlink (l), /// executable (x), empty (e), socket (s), pipe (p) #[arg( @@ -303,6 +323,7 @@ pub struct Opts { fd -te -td" )] pub filetype: Option>, + /// Filter by file extension #[arg( long = "extension", @@ -334,6 +355,7 @@ pub struct Opts { maximum length of command lines.", )] pub batch_size: usize, + /// Exclude entries that match the given glob pattern #[arg( long, @@ -347,6 +369,7 @@ pub struct Opts { --exclude node_modules" )] pub exclude: Vec, + /// Add a custom ignore-file in '.gitignore' format #[arg( long, @@ -355,6 +378,7 @@ pub struct Opts { long_help = "Add a custom ignore-file in '.gitignore' format. These files have a low precedence." )] pub ignore_file: Vec, + /// When to use colors #[arg( long, @@ -365,10 +389,12 @@ pub struct Opts { long_help = "Declare when to use color for the pattern match output", )] pub color: ColorWhen, + /// Set number of threads to use for searching & executing (default: number /// of available CPU cores) #[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = 1..)] pub threads: Option, + /// Limit results based on the size of files #[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment, value_name = "size", long_help = "Limit results based on the size of files using the format <+->.\n \ @@ -389,12 +415,14 @@ pub struct Opts { 'ti': tebibytes", )] pub size: Vec, + /// Milliseconds to buffer before streaming search results to console /// /// Amount of time in milliseconds to buffer, before streaming the search /// results to the console. #[arg(long, hide = true, value_parser = parse_millis)] pub max_buffer_time: Option, + /// Filter by file modification time (newer than) #[arg( long, @@ -411,6 +439,7 @@ pub struct Opts { --newer 2018-10-27" )] pub changed_within: Option, + /// Filter by file modification time (older than) #[arg( long, @@ -426,6 +455,7 @@ pub struct Opts { --older 2018-10-27" )] pub changed_before: Option, + /// Limit number of search results #[arg( long, @@ -434,6 +464,7 @@ pub struct Opts { long_help = "Limit the number of search results to 'count' and quit immediately." )] max_results: Option, + /// Limit search to a single result #[arg( short = '1', @@ -443,6 +474,7 @@ pub struct Opts { This is an alias for '--max-results=1'." )] max_one_result: bool, + /// Print nothing, exit code 0 if match found, 1 otherwise #[arg( long, @@ -456,6 +488,7 @@ pub struct Opts { '--has-results' can be used as an alias." )] pub quiet: bool, + /// Show filesystem errors #[arg( long, @@ -464,6 +497,7 @@ pub struct Opts { insufficient permissions or dead symlinks." )] pub show_errors: bool, + /// Change current working directory #[arg( long, @@ -476,6 +510,7 @@ pub struct Opts { relative to this directory." )] pub base_directory: Option, + /// the search pattern (a regular expression, unless '--glob' is used; optional) #[arg( default_value = "", @@ -487,6 +522,7 @@ pub struct Opts { pass '--' first, or it will be considered as a flag (fd -- '-foo')." )] pub pattern: String, + /// Set path separator when printing file paths #[arg( long, @@ -496,6 +532,7 @@ pub struct Opts { the OS-specific separator ('/' on Unix, '\\' on Windows)." )] pub path_separator: Option, + /// the root directories for the filesystem search (optional) #[arg(action = ArgAction::Append, value_name = "path", @@ -503,6 +540,7 @@ pub struct Opts { omitted, search the current working directory.", )] path: Vec, + /// Provides paths to search as an alternative to the positional argument #[arg( long, @@ -514,6 +552,7 @@ pub struct Opts { --search-path []`" )] search_path: Vec, + /// By default, relative paths are prefixed with './' when -x/--exec, /// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a /// path starting with '-' being treated as a command line option. Use @@ -525,6 +564,7 @@ pub struct Opts { this flag to disable this behaviour.", )] pub strip_cwd_prefix: bool, + /// Filter by owning user and/or group #[cfg(unix)] #[arg(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group",