mirror of
https://github.com/sharkdp/fd.git
synced 2024-11-17 09:28:25 +01:00
commit
13a47c3a2c
15 changed files with 1082 additions and 1135 deletions
19
.github/workflows/CICD.yml
vendored
19
.github/workflows/CICD.yml
vendored
|
@ -1,7 +1,7 @@
|
||||||
name: CICD
|
name: CICD
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MIN_SUPPORTED_RUST_VERSION: "1.57.0"
|
MIN_SUPPORTED_RUST_VERSION: "1.60.0"
|
||||||
CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
@ -181,6 +181,11 @@ jobs:
|
||||||
command: test
|
command: test
|
||||||
args: --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
|
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
|
- name: Create tarball
|
||||||
id: package
|
id: package
|
||||||
shell: bash
|
shell: bash
|
||||||
|
@ -193,7 +198,6 @@ jobs:
|
||||||
PKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/package"
|
PKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/package"
|
||||||
ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/"
|
ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/"
|
||||||
mkdir -p "${ARCHIVE_DIR}"
|
mkdir -p "${ARCHIVE_DIR}"
|
||||||
mkdir -p "${ARCHIVE_DIR}/autocomplete"
|
|
||||||
|
|
||||||
# Binary
|
# Binary
|
||||||
cp "${{ steps.strip.outputs.BIN_PATH }}" "$ARCHIVE_DIR"
|
cp "${{ steps.strip.outputs.BIN_PATH }}" "$ARCHIVE_DIR"
|
||||||
|
@ -205,10 +209,7 @@ jobs:
|
||||||
cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR"
|
cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR"
|
||||||
|
|
||||||
# Autocompletion files
|
# Autocompletion files
|
||||||
cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'${{ env.PROJECT_NAME }}.bash' "$ARCHIVE_DIR/autocomplete/"
|
cp -r autocomplete "${ARCHIVE_DIR}"
|
||||||
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/"
|
|
||||||
|
|
||||||
# base compressed package
|
# base compressed package
|
||||||
pushd "${PKG_STAGING}/" >/dev/null
|
pushd "${PKG_STAGING}/" >/dev/null
|
||||||
|
@ -256,9 +257,9 @@ jobs:
|
||||||
gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ env.PROJECT_NAME }}.1"
|
gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ env.PROJECT_NAME }}.1"
|
||||||
|
|
||||||
# Autocompletion files
|
# 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 'autocomplete/fd.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 'autocomplete/fd.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' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ env.PROJECT_NAME }}"
|
||||||
|
|
||||||
# README and LICENSE
|
# README and LICENSE
|
||||||
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
|
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
target/
|
target/
|
||||||
|
/autocomplete/
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
67
Cargo.lock
generated
67
Cargo.lock
generated
|
@ -113,35 +113,47 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "3.2.22"
|
version = "4.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750"
|
checksum = "335867764ed2de42325fafe6d18b8af74ba97ee0c590fa016f157535b42ab04b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atty",
|
"atty",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"clap_derive",
|
||||||
"clap_lex",
|
"clap_lex",
|
||||||
"indexmap",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"strsim",
|
"strsim",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"terminal_size 0.2.1",
|
"terminal_size",
|
||||||
"textwrap",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_complete"
|
name = "clap_complete"
|
||||||
version = "3.2.5"
|
version = "4.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f7a2e0a962c45ce25afce14220bc24f9dade0a1787f185cecf96bfba7847cd8"
|
checksum = "dfe581a2035db4174cdbdc91265e1aba50f381577f0510d0ad36c7bc59cc84a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_derive"
|
||||||
version = "0.2.4"
|
version = "4.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"os_str_bytes",
|
"os_str_bytes",
|
||||||
]
|
]
|
||||||
|
@ -384,10 +396,10 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "heck"
|
||||||
version = "0.12.3"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
|
@ -446,16 +458,6 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indexmap"
|
|
||||||
version = "1.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"hashbrown",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "io-lifetimes"
|
name = "io-lifetimes"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
|
@ -806,16 +808,6 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "terminal_size"
|
|
||||||
version = "0.1.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "terminal_size"
|
name = "terminal_size"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -848,15 +840,6 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "textwrap"
|
|
||||||
version = "0.15.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d"
|
|
||||||
dependencies = [
|
|
||||||
"terminal_size 0.1.17",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.37"
|
version = "1.0.37"
|
||||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -30,8 +30,6 @@ name = "fd"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
clap = { version = "3.1", features = ["cargo"] }
|
|
||||||
clap_complete = "3.1"
|
|
||||||
version_check = "0.9"
|
version_check = "0.9"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -52,10 +50,11 @@ normpath = "0.3.2"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
once_cell = "1.15.0"
|
once_cell = "1.15.0"
|
||||||
crossbeam-channel = "0.5.6"
|
crossbeam-channel = "0.5.6"
|
||||||
|
clap_complete = {version = "4.0", optional = true}
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
version = "3.1"
|
version = "4.0.12"
|
||||||
features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped"]
|
features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped", "derive"]
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
users = "0.11.0"
|
users = "0.11.0"
|
||||||
|
@ -85,4 +84,6 @@ codegen-units = 1
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
use-jemalloc = ["jemallocator"]
|
use-jemalloc = ["jemallocator"]
|
||||||
default = ["use-jemalloc"]
|
completions = ["clap_complete"]
|
||||||
|
base = ["use-jemalloc"]
|
||||||
|
default = ["use-jemalloc", "completions"]
|
||||||
|
|
37
Makefile
Normal file
37
Makefile
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
PROFILE=release
|
||||||
|
EXE=target/$(PROFILE)/fd
|
||||||
|
prefix=/usr/local
|
||||||
|
bindir=$(prefix)/bin
|
||||||
|
datadir=$(prefix)/share
|
||||||
|
exe_name=fd
|
||||||
|
|
||||||
|
$(EXE): Cargo.toml src/**/*.rs
|
||||||
|
cargo build --profile $(PROFILE)
|
||||||
|
|
||||||
|
.PHONY: completions
|
||||||
|
completions: autocomplete/fd.bash autocomplete/fd.fish autocomplete/fd.ps1 autocomplete/_fd
|
||||||
|
|
||||||
|
comp_dir=@mkdir -p autocomplete
|
||||||
|
|
||||||
|
autocomplete/fd.bash: $(EXE)
|
||||||
|
$(comp_dir)
|
||||||
|
$(EXE) --gen-completions bash > $@
|
||||||
|
|
||||||
|
autocomplete/fd.fish: $(EXE)
|
||||||
|
$(comp_dir)
|
||||||
|
$(EXE) --gen-completions fish > $@
|
||||||
|
|
||||||
|
autocomplete/fd.ps1: $(EXE)
|
||||||
|
$(comp_dir)
|
||||||
|
$(EXE) --gen-completions powershell > $@
|
||||||
|
|
||||||
|
autocomplete/_fd: contrib/completion/_fd
|
||||||
|
$(comp_dir)
|
||||||
|
cp $< $@
|
||||||
|
|
||||||
|
install: $(EXE) completions
|
||||||
|
install -Dm755 $(EXE) $(DESTDIR)$(bindir)/fd
|
||||||
|
install -Dm644 autocomplete/fd.bash $(DESTDIR)/$(datadir)/bash-completion/completions/$(exe_name)
|
||||||
|
install -Dm644 autocomplete/fd.fish $(DESTDIR)/$(datadir)/fish/vendor_completions.d/$(exe_name).fish
|
||||||
|
install -Dm644 autocomplete/_fd $(DESTDIR)/$(datadir)/zsh/site-functions/_$(exe_name)
|
||||||
|
install -Dm644 doc/fd.1 $(DESTDIR)/$(datadir)/man/man1/$(exe_name).1
|
23
build.rs
23
build.rs
|
@ -1,13 +1,5 @@
|
||||||
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() {
|
fn main() {
|
||||||
let min_version = "1.57";
|
let min_version = "1.60";
|
||||||
|
|
||||||
match version_check::is_min_version(min_version) {
|
match version_check::is_min_version(min_version) {
|
||||||
Some(true) => {}
|
Some(true) => {}
|
||||||
|
@ -17,17 +9,4 @@ fn main() {
|
||||||
std::process::exit(1);
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
msrv = "1.57.0"
|
msrv = "1.60.0"
|
||||||
|
|
779
src/app.rs
779
src/app.rs
|
@ -1,779 +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.",
|
|
||||||
)
|
|
||||||
.after_long_help(
|
|
||||||
"Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues"
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("hidden")
|
|
||||||
.long("hidden")
|
|
||||||
.short('H')
|
|
||||||
.overrides_with("hidden")
|
|
||||||
.help("Search hidden files and directories")
|
|
||||||
.long_help(
|
|
||||||
"Include hidden directories and files in the search results (default: \
|
|
||||||
hidden files and directories are skipped). Files and directories are \
|
|
||||||
considered to be hidden if their name starts with a `.` sign (dot). \
|
|
||||||
The flag can be overridden with --no-hidden.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-hidden")
|
|
||||||
.long("no-hidden")
|
|
||||||
.overrides_with("hidden")
|
|
||||||
.hide(true)
|
|
||||||
.long_help(
|
|
||||||
"Overrides --hidden.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-ignore")
|
|
||||||
.long("no-ignore")
|
|
||||||
.short('I')
|
|
||||||
.overrides_with("no-ignore")
|
|
||||||
.help("Do not respect .(git|fd)ignore files")
|
|
||||||
.long_help(
|
|
||||||
"Show search results from files and directories that would otherwise be \
|
|
||||||
ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file. \
|
|
||||||
The flag can be overridden with --ignore.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("ignore")
|
|
||||||
.long("ignore")
|
|
||||||
.overrides_with("no-ignore")
|
|
||||||
.hide(true)
|
|
||||||
.long_help(
|
|
||||||
"Overrides --no-ignore.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-ignore-vcs")
|
|
||||||
.long("no-ignore-vcs")
|
|
||||||
.overrides_with("no-ignore-vcs")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Do not respect .gitignore files")
|
|
||||||
.long_help(
|
|
||||||
"Show search results from files and directories that would otherwise be \
|
|
||||||
ignored by '.gitignore' files. The flag can be overridden with --ignore-vcs.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("ignore-vcs")
|
|
||||||
.long("ignore-vcs")
|
|
||||||
.overrides_with("no-ignore-vcs")
|
|
||||||
.hide(true)
|
|
||||||
.long_help(
|
|
||||||
"Overrides --no-ignore-vcs.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-ignore-parent")
|
|
||||||
.long("no-ignore-parent")
|
|
||||||
.overrides_with("no-ignore-parent")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Do not respect .(git|fd)ignore files in parent directories")
|
|
||||||
.long_help(
|
|
||||||
"Show search results from files and directories that would otherwise be \
|
|
||||||
ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-global-ignore-file")
|
|
||||||
.long("no-global-ignore-file")
|
|
||||||
.hide(true)
|
|
||||||
.help("Do not respect the global ignore file")
|
|
||||||
.long_help("Do not respect the global ignore file."),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("rg-alias-hidden-ignore")
|
|
||||||
.short('u')
|
|
||||||
.long("unrestricted")
|
|
||||||
.overrides_with_all(&["ignore", "no-hidden"])
|
|
||||||
.multiple_occurrences(true) // Allowed for historical reasons
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Unrestricted search, alias for '--no-ignore --hidden'")
|
|
||||||
.long_help(
|
|
||||||
"Perform an unrestricted search, including ignored and hidden files. This is \
|
|
||||||
an alias for '--no-ignore --hidden'."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("case-sensitive")
|
|
||||||
.long("case-sensitive")
|
|
||||||
.short('s')
|
|
||||||
.overrides_with_all(&["ignore-case", "case-sensitive"])
|
|
||||||
.help("Case-sensitive search (default: smart case)")
|
|
||||||
.long_help(
|
|
||||||
"Perform a case-sensitive search. By default, fd uses case-insensitive \
|
|
||||||
searches, unless the pattern contains an uppercase character (smart \
|
|
||||||
case).",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("ignore-case")
|
|
||||||
.long("ignore-case")
|
|
||||||
.short('i')
|
|
||||||
.overrides_with_all(&["case-sensitive", "ignore-case"])
|
|
||||||
.help("Case-insensitive search (default: smart case)")
|
|
||||||
.long_help(
|
|
||||||
"Perform a case-insensitive search. By default, fd uses case-insensitive \
|
|
||||||
searches, unless the pattern contains an uppercase character (smart \
|
|
||||||
case).",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("glob")
|
|
||||||
.long("glob")
|
|
||||||
.short('g')
|
|
||||||
.conflicts_with("fixed-strings")
|
|
||||||
.overrides_with("glob")
|
|
||||||
.help("Glob-based search (default: regular expression)")
|
|
||||||
.long_help("Perform a glob-based search instead of a regular expression search."),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("regex")
|
|
||||||
.long("regex")
|
|
||||||
.overrides_with_all(&["glob", "regex"])
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Regular-expression based search (default)")
|
|
||||||
.long_help(
|
|
||||||
"Perform a regular-expression based search (default). This can be used to \
|
|
||||||
override --glob.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("fixed-strings")
|
|
||||||
.long("fixed-strings")
|
|
||||||
.short('F')
|
|
||||||
.alias("literal")
|
|
||||||
.overrides_with("fixed-strings")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Treat pattern as literal string instead of regex")
|
|
||||||
.long_help(
|
|
||||||
"Treat the pattern as a literal string instead of a regular expression. Note \
|
|
||||||
that this also performs substring comparison. If you want to match on an \
|
|
||||||
exact filename, consider using '--glob'.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("absolute-path")
|
|
||||||
.long("absolute-path")
|
|
||||||
.short('a')
|
|
||||||
.overrides_with("absolute-path")
|
|
||||||
.help("Show absolute instead of relative paths")
|
|
||||||
.long_help(
|
|
||||||
"Shows the full path starting from the root as opposed to relative paths. \
|
|
||||||
The flag can be overridden with --relative-path.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("relative-path")
|
|
||||||
.long("relative-path")
|
|
||||||
.overrides_with("absolute-path")
|
|
||||||
.hide(true)
|
|
||||||
.long_help(
|
|
||||||
"Overrides --absolute-path.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("list-details")
|
|
||||||
.long("list-details")
|
|
||||||
.short('l')
|
|
||||||
.conflicts_with("absolute-path")
|
|
||||||
.help("Use a long listing format with file metadata")
|
|
||||||
.long_help(
|
|
||||||
"Use a detailed listing format like 'ls -l'. This is basically an alias \
|
|
||||||
for '--exec-batch ls -l' with some additional 'ls' options. This can be \
|
|
||||||
used to see more metadata, to show symlink targets and to achieve a \
|
|
||||||
deterministic sort order.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("follow")
|
|
||||||
.long("follow")
|
|
||||||
.short('L')
|
|
||||||
.alias("dereference")
|
|
||||||
.overrides_with("follow")
|
|
||||||
.help("Follow symbolic links")
|
|
||||||
.long_help(
|
|
||||||
"By default, fd does not descend into symlinked directories. Using this \
|
|
||||||
flag, symbolic links are also traversed. \
|
|
||||||
Flag can be overriden with --no-follow.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-follow")
|
|
||||||
.long("no-follow")
|
|
||||||
.overrides_with("follow")
|
|
||||||
.hide(true)
|
|
||||||
.long_help(
|
|
||||||
"Overrides --follow.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("full-path")
|
|
||||||
.long("full-path")
|
|
||||||
.short('p')
|
|
||||||
.overrides_with("full-path")
|
|
||||||
.help("Search full abs. path (default: filename only)")
|
|
||||||
.long_help(
|
|
||||||
"By default, the search pattern is only matched against the filename (or \
|
|
||||||
directory name). Using this flag, the pattern is matched against the full \
|
|
||||||
(absolute) path. Example:\n \
|
|
||||||
fd --glob -p '**/.git/config'",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("null_separator")
|
|
||||||
.long("print0")
|
|
||||||
.short('0')
|
|
||||||
.overrides_with("print0")
|
|
||||||
.conflicts_with("list-details")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Separate results by the null character")
|
|
||||||
.long_help(
|
|
||||||
"Separate search results by the null character (instead of newlines). \
|
|
||||||
Useful for piping results to 'xargs'.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("max-depth")
|
|
||||||
.long("max-depth")
|
|
||||||
.short('d')
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("depth")
|
|
||||||
.help("Set maximum search depth (default: none)")
|
|
||||||
.long_help(
|
|
||||||
"Limit the directory traversal to a given depth. By default, there is no \
|
|
||||||
limit on the search depth.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// support --maxdepth as well, for compatibility with rg
|
|
||||||
.arg(
|
|
||||||
Arg::new("rg-depth")
|
|
||||||
.long("maxdepth")
|
|
||||||
.hide(true)
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Set maximum search depth (default: none)")
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("min-depth")
|
|
||||||
.long("min-depth")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("depth")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Only show results starting at given depth")
|
|
||||||
.long_help(
|
|
||||||
"Only show search results starting at the given depth. \
|
|
||||||
See also: '--max-depth' and '--exact-depth'",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("exact-depth")
|
|
||||||
.long("exact-depth")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("depth")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.conflicts_with_all(&["max-depth", "min-depth"])
|
|
||||||
.help("Only show results at exact given depth")
|
|
||||||
.long_help(
|
|
||||||
"Only show search results at the exact given depth. This is an alias for \
|
|
||||||
'--min-depth <depth> --max-depth <depth>'.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("prune")
|
|
||||||
.long("prune")
|
|
||||||
.conflicts_with_all(&["size", "exact-depth"])
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Do not traverse into matching directories")
|
|
||||||
.long_help("Do not traverse into directories that match the search criteria. If \
|
|
||||||
you want to exclude specific directories, use the '--exclude=…' option.")
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("file-type")
|
|
||||||
.long("type")
|
|
||||||
.short('t')
|
|
||||||
.multiple_occurrences(true)
|
|
||||||
.number_of_values(1)
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("filetype")
|
|
||||||
.possible_values(&[
|
|
||||||
"f",
|
|
||||||
"file",
|
|
||||||
"d",
|
|
||||||
"directory",
|
|
||||||
"l",
|
|
||||||
"symlink",
|
|
||||||
"x",
|
|
||||||
"executable",
|
|
||||||
"e",
|
|
||||||
"empty",
|
|
||||||
"s",
|
|
||||||
"socket",
|
|
||||||
"p",
|
|
||||||
"pipe",
|
|
||||||
])
|
|
||||||
.hide_possible_values(true)
|
|
||||||
.help(
|
|
||||||
"Filter by type: file (f), directory (d), symlink (l),\nexecutable (x), \
|
|
||||||
empty (e), socket (s), pipe (p)",
|
|
||||||
)
|
|
||||||
.long_help(
|
|
||||||
"Filter the search by type:\n \
|
|
||||||
'f' or 'file': regular files\n \
|
|
||||||
'd' or 'directory': directories\n \
|
|
||||||
'l' or 'symlink': symbolic links\n \
|
|
||||||
's' or 'socket': socket\n \
|
|
||||||
'p' or 'pipe': named pipe (FIFO)\n\n \
|
|
||||||
'x' or 'executable': executables\n \
|
|
||||||
'e' or 'empty': empty files or directories\n\n\
|
|
||||||
This option can be specified more than once to include multiple file types. \
|
|
||||||
Searching for '--type file --type symlink' will show both regular files as \
|
|
||||||
well as symlinks. Note that the 'executable' and 'empty' filters work differently: \
|
|
||||||
'--type executable' implies '--type file' by default. And '--type empty' searches \
|
|
||||||
for empty files and directories, unless either '--type file' or '--type directory' \
|
|
||||||
is specified in addition.\n\n\
|
|
||||||
Examples:\n \
|
|
||||||
- Only search for files:\n \
|
|
||||||
fd --type file …\n \
|
|
||||||
fd -tf …\n \
|
|
||||||
- Find both files and symlinks\n \
|
|
||||||
fd --type file --type symlink …\n \
|
|
||||||
fd -tf -tl …\n \
|
|
||||||
- Find executable files:\n \
|
|
||||||
fd --type executable\n \
|
|
||||||
fd -tx\n \
|
|
||||||
- Find empty files:\n \
|
|
||||||
fd --type empty --type file\n \
|
|
||||||
fd -te -tf\n \
|
|
||||||
- Find empty directories:\n \
|
|
||||||
fd --type empty --type directory\n \
|
|
||||||
fd -te -td"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("extension")
|
|
||||||
.long("extension")
|
|
||||||
.short('e')
|
|
||||||
.multiple_occurrences(true)
|
|
||||||
.number_of_values(1)
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("ext")
|
|
||||||
.help("Filter by file extension")
|
|
||||||
.long_help(
|
|
||||||
"(Additionally) filter search results by their file extension. Multiple \
|
|
||||||
allowable file extensions can be specified.\n\
|
|
||||||
If you want to search for files without extension, \
|
|
||||||
you can use the regex '^[^.]+$' as a normal search pattern.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("exec")
|
|
||||||
.long("exec")
|
|
||||||
.short('x')
|
|
||||||
.min_values(1)
|
|
||||||
.multiple_occurrences(true)
|
|
||||||
.allow_hyphen_values(true)
|
|
||||||
.value_terminator(";")
|
|
||||||
.value_name("cmd")
|
|
||||||
.conflicts_with("list-details")
|
|
||||||
.help("Execute a command for each search result")
|
|
||||||
.long_help(
|
|
||||||
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
|
|
||||||
All positional arguments following --exec are considered to be arguments to the command - not to fd. \
|
|
||||||
It is therefore recommended to place the '-x'/'--exec' option last.\n\
|
|
||||||
The following placeholders are substituted before the command is executed:\n \
|
|
||||||
'{}': path (of the current search result)\n \
|
|
||||||
'{/}': basename\n \
|
|
||||||
'{//}': parent directory\n \
|
|
||||||
'{.}': path without file extension\n \
|
|
||||||
'{/.}': basename without file extension\n\n\
|
|
||||||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
|
||||||
Examples:\n\n \
|
|
||||||
- find all *.zip files and unzip them:\n\n \
|
|
||||||
fd -e zip -x unzip\n\n \
|
|
||||||
- find *.h and *.cpp files and run \"clang-format -i ..\" for each of them:\n\n \
|
|
||||||
fd -e h -e cpp -x clang-format -i\n\n \
|
|
||||||
- Convert all *.jpg files to *.png files:\n\n \
|
|
||||||
fd -e jpg -x convert {} {.}.png\
|
|
||||||
",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("exec-batch")
|
|
||||||
.long("exec-batch")
|
|
||||||
.short('X')
|
|
||||||
.min_values(1)
|
|
||||||
.multiple_occurrences(true)
|
|
||||||
.allow_hyphen_values(true)
|
|
||||||
.value_terminator(";")
|
|
||||||
.value_name("cmd")
|
|
||||||
.conflicts_with_all(&["exec", "list-details"])
|
|
||||||
.help("Execute a command with all search results at once")
|
|
||||||
.long_help(
|
|
||||||
"Execute the given command once, with all search results as arguments.\n\
|
|
||||||
One of the following placeholders is substituted before the command is executed:\n \
|
|
||||||
'{}': path (of all search results)\n \
|
|
||||||
'{/}': basename\n \
|
|
||||||
'{//}': parent directory\n \
|
|
||||||
'{.}': path without file extension\n \
|
|
||||||
'{/.}': basename without file extension\n\n\
|
|
||||||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
|
||||||
Examples:\n\n \
|
|
||||||
- Find all test_*.py files and open them in your favorite editor:\n\n \
|
|
||||||
fd -g 'test_*.py' -X vim\n\n \
|
|
||||||
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
|
|
||||||
fd -e rs -X wc -l\
|
|
||||||
"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("batch-size")
|
|
||||||
.long("batch-size")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("size")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.requires("exec-batch")
|
|
||||||
.help("Max number of arguments to run as a batch with -X")
|
|
||||||
.long_help(
|
|
||||||
"Maximum number of arguments to pass to the command given with -X. \
|
|
||||||
If the number of results is greater than the given size, \
|
|
||||||
the command given with -X is run again with remaining arguments. \
|
|
||||||
A batch size of zero means there is no limit (default), but note \
|
|
||||||
that batching might still happen due to OS restrictions on the \
|
|
||||||
maximum length of command lines.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("exclude")
|
|
||||||
.long("exclude")
|
|
||||||
.short('E')
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("pattern")
|
|
||||||
.number_of_values(1)
|
|
||||||
.multiple_occurrences(true)
|
|
||||||
.help("Exclude entries that match the given glob pattern")
|
|
||||||
.long_help(
|
|
||||||
"Exclude files/directories that match the given glob pattern. This \
|
|
||||||
overrides any other ignore logic. Multiple exclude patterns can be \
|
|
||||||
specified.\n\n\
|
|
||||||
Examples:\n \
|
|
||||||
--exclude '*.pyc'\n \
|
|
||||||
--exclude node_modules",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("ignore-file")
|
|
||||||
.long("ignore-file")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("path")
|
|
||||||
.number_of_values(1)
|
|
||||||
.multiple_occurrences(true)
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Add custom ignore-file in '.gitignore' format")
|
|
||||||
.long_help(
|
|
||||||
"Add a custom ignore-file in '.gitignore' format. These files have a low \
|
|
||||||
precedence.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("color")
|
|
||||||
.long("color")
|
|
||||||
.short('c')
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("when")
|
|
||||||
.possible_values(&["never", "auto", "always"])
|
|
||||||
.hide_possible_values(true)
|
|
||||||
.help("When to use colors: never, *auto*, always")
|
|
||||||
.long_help(
|
|
||||||
"Declare when to use color for the pattern match output:\n \
|
|
||||||
'auto': show colors if the output goes to an interactive console (default)\n \
|
|
||||||
'never': do not use colorized output\n \
|
|
||||||
'always': always use colorized output",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("threads")
|
|
||||||
.long("threads")
|
|
||||||
.short('j')
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("num")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Set number of threads")
|
|
||||||
.long_help(
|
|
||||||
"Set number of threads to use for searching & executing (default: number \
|
|
||||||
of available CPU cores)",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("size")
|
|
||||||
.long("size")
|
|
||||||
.short('S')
|
|
||||||
.takes_value(true)
|
|
||||||
.number_of_values(1)
|
|
||||||
.allow_hyphen_values(true)
|
|
||||||
.multiple_occurrences(true)
|
|
||||||
.help("Limit results based on the size of files")
|
|
||||||
.long_help(
|
|
||||||
"Limit results based on the size of files using the format <+-><NUM><UNIT>.\n \
|
|
||||||
'+': file size must be greater than or equal to this\n \
|
|
||||||
'-': file size must be less than or equal to this\n\
|
|
||||||
If neither '+' nor '-' is specified, file size must be exactly equal to this.\n \
|
|
||||||
'NUM': The numeric size (e.g. 500)\n \
|
|
||||||
'UNIT': The units for NUM. They are not case-sensitive.\n\
|
|
||||||
Allowed unit values:\n \
|
|
||||||
'b': bytes\n \
|
|
||||||
'k': kilobytes (base ten, 10^3 = 1000 bytes)\n \
|
|
||||||
'm': megabytes\n \
|
|
||||||
'g': gigabytes\n \
|
|
||||||
't': terabytes\n \
|
|
||||||
'ki': kibibytes (base two, 2^10 = 1024 bytes)\n \
|
|
||||||
'mi': mebibytes\n \
|
|
||||||
'gi': gibibytes\n \
|
|
||||||
'ti': tebibytes",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("max-buffer-time")
|
|
||||||
.long("max-buffer-time")
|
|
||||||
.takes_value(true)
|
|
||||||
.hide(true)
|
|
||||||
.help("Milliseconds to buffer before streaming search results to console")
|
|
||||||
.long_help(
|
|
||||||
"Amount of time in milliseconds to buffer, before streaming the search \
|
|
||||||
results to the console.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("changed-within")
|
|
||||||
.long("changed-within")
|
|
||||||
.alias("change-newer-than")
|
|
||||||
.alias("newer")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("date|dur")
|
|
||||||
.number_of_values(1)
|
|
||||||
.help("Filter by file modification time (newer than)")
|
|
||||||
.long_help(
|
|
||||||
"Filter results based on the file modification time. The argument can be provided \
|
|
||||||
as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min). \
|
|
||||||
If the time is not specified, it defaults to 00:00:00. \
|
|
||||||
'--change-newer-than' or '--newer' can be used as aliases.\n\
|
|
||||||
Examples:\n \
|
|
||||||
--changed-within 2weeks\n \
|
|
||||||
--change-newer-than '2018-10-27 10:00:00'\n \
|
|
||||||
--newer 2018-10-27",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("changed-before")
|
|
||||||
.long("changed-before")
|
|
||||||
.alias("change-older-than")
|
|
||||||
.alias("older")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("date|dur")
|
|
||||||
.number_of_values(1)
|
|
||||||
.help("Filter by file modification time (older than)")
|
|
||||||
.long_help(
|
|
||||||
"Filter results based on the file modification time. The argument can be provided \
|
|
||||||
as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min). \
|
|
||||||
'--change-older-than' or '--older' can be used as aliases.\n\
|
|
||||||
Examples:\n \
|
|
||||||
--changed-before '2018-10-27 10:00:00'\n \
|
|
||||||
--change-older-than 2weeks\n \
|
|
||||||
--older 2018-10-27",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("max-results")
|
|
||||||
.long("max-results")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("count")
|
|
||||||
// We currently do not support --max-results in combination with
|
|
||||||
// program execution because the results that come up in a --max-results
|
|
||||||
// search are non-deterministic. Users might think that they can run the
|
|
||||||
// same search with `--exec rm` attached and get a reliable removal of
|
|
||||||
// the files they saw in the previous search.
|
|
||||||
.conflicts_with_all(&["exec", "exec-batch", "list-details"])
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Limit number of search results")
|
|
||||||
.long_help("Limit the number of search results to 'count' and quit immediately."),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("max-one-result")
|
|
||||||
.short('1')
|
|
||||||
.hide_short_help(true)
|
|
||||||
.overrides_with("max-results")
|
|
||||||
.conflicts_with_all(&["exec", "exec-batch", "list-details"])
|
|
||||||
.help("Limit search to a single result")
|
|
||||||
.long_help("Limit the search to a single result and quit immediately. \
|
|
||||||
This is an alias for '--max-results=1'.")
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("quiet")
|
|
||||||
.long("quiet")
|
|
||||||
.short('q')
|
|
||||||
.alias("has-results")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.conflicts_with_all(&["exec", "exec-batch", "list-details", "max-results"])
|
|
||||||
.help("Print nothing, exit code 0 if match found, 1 otherwise")
|
|
||||||
.long_help(
|
|
||||||
"When the flag is present, the program does not print anything and will \
|
|
||||||
return with an exit code of 0 if there is at least one match. Otherwise, the \
|
|
||||||
exit code will be 1. \
|
|
||||||
'--has-results' can be used as an alias."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("show-errors")
|
|
||||||
.long("show-errors")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.overrides_with("show-errors")
|
|
||||||
.help("Show filesystem errors")
|
|
||||||
.long_help(
|
|
||||||
"Enable the display of filesystem errors for situations such as \
|
|
||||||
insufficient permissions or dead symlinks.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("base-directory")
|
|
||||||
.long("base-directory")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("path")
|
|
||||||
.number_of_values(1)
|
|
||||||
.allow_invalid_utf8(true)
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Change current working directory")
|
|
||||||
.long_help(
|
|
||||||
"Change the current working directory of fd to the provided path. This \
|
|
||||||
means that search results will be shown with respect to the given base \
|
|
||||||
path. Note that relative paths which are passed to fd via the positional \
|
|
||||||
<path> argument or the '--search-path' option will also be resolved \
|
|
||||||
relative to this directory.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("pattern")
|
|
||||||
.allow_invalid_utf8(true)
|
|
||||||
.help(
|
|
||||||
"the search pattern (a regular expression, unless '--glob' is used; optional)",
|
|
||||||
).long_help(
|
|
||||||
"the search pattern which is either a regular expression (default) or a glob \
|
|
||||||
pattern (if --glob is used). If no pattern has been specified, every entry \
|
|
||||||
is considered a match. If your pattern starts with a dash (-), make sure to \
|
|
||||||
pass '--' first, or it will be considered as a flag (fd -- '-foo').")
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("path-separator")
|
|
||||||
.takes_value(true)
|
|
||||||
.value_name("separator")
|
|
||||||
.long("path-separator")
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("Set path separator when printing file paths")
|
|
||||||
.long_help(
|
|
||||||
"Set the path separator to use when printing file paths. The default is \
|
|
||||||
the OS-specific separator ('/' on Unix, '\\' on Windows).",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("path")
|
|
||||||
.multiple_occurrences(true)
|
|
||||||
.allow_invalid_utf8(true)
|
|
||||||
.help("the root directory for the filesystem search (optional)")
|
|
||||||
.long_help(
|
|
||||||
"The directory where the filesystem search is rooted (optional). If \
|
|
||||||
omitted, search the current working directory.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("search-path")
|
|
||||||
.long("search-path")
|
|
||||||
.takes_value(true)
|
|
||||||
.conflicts_with("path")
|
|
||||||
.multiple_occurrences(true)
|
|
||||||
.hide_short_help(true)
|
|
||||||
.number_of_values(1)
|
|
||||||
.allow_invalid_utf8(true)
|
|
||||||
.help("Provide paths to search as an alternative to the positional <path>")
|
|
||||||
.long_help(
|
|
||||||
"Provide paths to search as an alternative to the positional <path> \
|
|
||||||
argument. Changes the usage to `fd [OPTIONS] --search-path <path> \
|
|
||||||
--search-path <path2> [<pattern>]`",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("strip-cwd-prefix")
|
|
||||||
.long("strip-cwd-prefix")
|
|
||||||
.conflicts_with_all(&["path", "search-path"])
|
|
||||||
.hide_short_help(true)
|
|
||||||
.help("strip './' prefix from -0/--print0 output")
|
|
||||||
.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."
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
845
src/cli.rs
Normal file
845
src/cli.rs
Normal file
|
@ -0,0 +1,845 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[cfg(feature = "completions")]
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use clap::{
|
||||||
|
error::ErrorKind, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, Parser,
|
||||||
|
ValueEnum,
|
||||||
|
};
|
||||||
|
#[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::error::Result<Self> {
|
||||||
|
Ok(Negations)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_from_arg_matches(&mut self, _: &ArgMatches) -> clap::error::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")
|
||||||
|
.action(ArgAction::Count)
|
||||||
|
.long("no-hidden")
|
||||||
|
.overrides_with("hidden")
|
||||||
|
.hide(true)
|
||||||
|
.long_help("Overrides --hidden."),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("ignore")
|
||||||
|
.action(ArgAction::Count)
|
||||||
|
.long("ignore")
|
||||||
|
.overrides_with("no_ignore")
|
||||||
|
.hide(true)
|
||||||
|
.long_help("Overrides --no-ignore."),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("ignore_vcs")
|
||||||
|
.action(ArgAction::Count)
|
||||||
|
.long("ignore-vcs")
|
||||||
|
.overrides_with("no_ignore_vcs")
|
||||||
|
.hide(true)
|
||||||
|
.long_help("Overrides --no-ignore-vcs."),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("relative_path")
|
||||||
|
.action(ArgAction::Count)
|
||||||
|
.long("relative-path")
|
||||||
|
.overrides_with("absolute_path")
|
||||||
|
.hide(true)
|
||||||
|
.long_help("Overrides --absolute-path."),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("no_follow")
|
||||||
|
.action(ArgAction::Count)
|
||||||
|
.long("no-follow")
|
||||||
|
.overrides_with("follow")
|
||||||
|
.hide(true)
|
||||||
|
.long_help("Overrides --follow."),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(
|
||||||
|
version,
|
||||||
|
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(&[
|
||||||
|
"max_results", "has_results", "count"])),
|
||||||
|
)]
|
||||||
|
pub struct Opts {
|
||||||
|
/// Search hidden files and directories
|
||||||
|
#[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
|
||||||
|
#[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
|
||||||
|
#[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
|
||||||
|
#[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,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
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)
|
||||||
|
#[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)
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short = 'g',
|
||||||
|
conflicts_with("fixed_strings"),
|
||||||
|
long_help = "Perform a glob-based search instead of a regular expression search."
|
||||||
|
)]
|
||||||
|
pub glob: bool,
|
||||||
|
|
||||||
|
/// 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 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
|
||||||
|
#[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
|
||||||
|
#[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
|
||||||
|
#[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)
|
||||||
|
#[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
|
||||||
|
#[arg(
|
||||||
|
long = "print0",
|
||||||
|
short = '0',
|
||||||
|
conflicts_with("list_details"),
|
||||||
|
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)
|
||||||
|
#[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<usize>,
|
||||||
|
|
||||||
|
/// Only show search results starting at the given depth.
|
||||||
|
#[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<usize>,
|
||||||
|
|
||||||
|
/// 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 <depth> --max-depth <depth>'.",
|
||||||
|
)]
|
||||||
|
exact_depth: Option<usize>,
|
||||||
|
|
||||||
|
/// 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"]),
|
||||||
|
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)
|
||||||
|
#[arg(
|
||||||
|
long = "type",
|
||||||
|
short = 't',
|
||||||
|
value_name = "filetype",
|
||||||
|
hide_possible_values = true,
|
||||||
|
value_enum,
|
||||||
|
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<Vec<FileType>>,
|
||||||
|
|
||||||
|
/// Filter by file extension
|
||||||
|
#[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<Vec<String>>,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub exec: Exec,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
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
|
||||||
|
#[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<String>,
|
||||||
|
|
||||||
|
/// 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<PathBuf>,
|
||||||
|
|
||||||
|
/// When to use colors
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short = 'c',
|
||||||
|
value_enum,
|
||||||
|
default_value_t = ColorWhen::Auto,
|
||||||
|
value_name = "when",
|
||||||
|
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<u32>,
|
||||||
|
|
||||||
|
/// 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 <+-><NUM><UNIT>.\n \
|
||||||
|
'+': file size must be greater than or equal to this\n \
|
||||||
|
'-': file size must be less than or equal to this\n\
|
||||||
|
If neither '+' nor '-' is specified, file size must be exactly equal to this.\n \
|
||||||
|
'NUM': The numeric size (e.g. 500)\n \
|
||||||
|
'UNIT': The units for NUM. They are not case-sensitive.\n\
|
||||||
|
Allowed unit values:\n \
|
||||||
|
'b': bytes\n \
|
||||||
|
'k': kilobytes (base ten, 10^3 = 1000 bytes)\n \
|
||||||
|
'm': megabytes\n \
|
||||||
|
'g': gigabytes\n \
|
||||||
|
't': terabytes\n \
|
||||||
|
'ki': kibibytes (base two, 2^10 = 1024 bytes)\n \
|
||||||
|
'mi': mebibytes\n \
|
||||||
|
'gi': gibibytes\n \
|
||||||
|
'ti': tebibytes",
|
||||||
|
)]
|
||||||
|
pub size: Vec<SizeFilter>,
|
||||||
|
|
||||||
|
/// Milliseconds to buffer before streaming search results to console
|
||||||
|
///
|
||||||
|
/// Amount of time in milliseconds to buffer, before streaming the search
|
||||||
|
/// results to the console.
|
||||||
|
#[arg(long, hide = true, value_parser = parse_millis)]
|
||||||
|
pub max_buffer_time: Option<Duration>,
|
||||||
|
|
||||||
|
/// Filter by file modification time (newer than)
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
alias("change-newer-than"),
|
||||||
|
alias("newer"),
|
||||||
|
value_name = "date|dur",
|
||||||
|
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<String>,
|
||||||
|
|
||||||
|
/// Filter by file modification time (older than)
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
alias("change-older-than"),
|
||||||
|
alias("older"),
|
||||||
|
value_name = "date|dur",
|
||||||
|
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<String>,
|
||||||
|
|
||||||
|
/// Limit number of search results
|
||||||
|
#[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<usize>,
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short = 'q',
|
||||||
|
alias = "has-results",
|
||||||
|
hide_short_help = true,
|
||||||
|
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
|
||||||
|
#[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 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 \
|
||||||
|
<path> argument or the '--search-path' option will also be resolved \
|
||||||
|
relative to this directory."
|
||||||
|
)]
|
||||||
|
pub base_directory: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// the search pattern (a regular expression, unless '--glob' is used; optional)
|
||||||
|
#[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
|
||||||
|
#[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<String>,
|
||||||
|
|
||||||
|
/// the root directories for the filesystem search (optional)
|
||||||
|
#[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<PathBuf>,
|
||||||
|
|
||||||
|
/// Provides paths to search as an alternative to the positional <path> argument
|
||||||
|
#[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 <path> \
|
||||||
|
argument. Changes the usage to `fd [OPTIONS] --search-path <path> \
|
||||||
|
--search-path <path2> [<pattern>]`"
|
||||||
|
)]
|
||||||
|
search_path: Vec<PathBuf>,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
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
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[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<OwnerFilter>,
|
||||||
|
#[cfg(any(unix, windows))]
|
||||||
|
#[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")]
|
||||||
|
#[arg(long, hide = true, exclusive = true)]
|
||||||
|
gen_completions: Option<Option<Shell>>,
|
||||||
|
|
||||||
|
#[clap(flatten)]
|
||||||
|
_negations: Negations,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Opts {
|
||||||
|
pub fn search_paths(&self) -> anyhow::Result<Vec<PathBuf>> {
|
||||||
|
// would it make sense to concatenate these?
|
||||||
|
let paths = if !self.path.is_empty() {
|
||||||
|
&self.path
|
||||||
|
} else if !self.search_path.is_empty() {
|
||||||
|
&self.search_path
|
||||||
|
} else {
|
||||||
|
let current_directory = Path::new(".");
|
||||||
|
ensure_current_directory_exists(current_directory)?;
|
||||||
|
return Ok(vec![self.normalize_path(current_directory)]);
|
||||||
|
};
|
||||||
|
Ok(paths
|
||||||
|
.iter()
|
||||||
|
.filter_map(|path| {
|
||||||
|
if filesystem::is_existing_directory(path) {
|
||||||
|
Some(self.normalize_path(path))
|
||||||
|
} else {
|
||||||
|
print_error(format!(
|
||||||
|
"Search path '{}' is not a directory.",
|
||||||
|
path.to_string_lossy()
|
||||||
|
));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_path(&self, path: &Path) -> PathBuf {
|
||||||
|
if self.absolute_path {
|
||||||
|
filesystem::absolute_path(path.normalize().unwrap().as_path()).unwrap()
|
||||||
|
} else {
|
||||||
|
path.to_path_buf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn no_search_paths(&self) -> bool {
|
||||||
|
self.path.is_empty() && self.search_path.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn rg_alias_ignore(&self) -> bool {
|
||||||
|
self.rg_alias_hidden_ignore > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_depth(&self) -> Option<usize> {
|
||||||
|
self.max_depth.or(self.exact_depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min_depth(&self) -> Option<usize> {
|
||||||
|
self.min_depth.or(self.exact_depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn threads(&self) -> usize {
|
||||||
|
// 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<usize> {
|
||||||
|
self.max_results
|
||||||
|
.filter(|&m| m > 0)
|
||||||
|
.or_else(|| self.max_one_result.then(|| 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "completions")]
|
||||||
|
pub fn gen_completions(&self) -> anyhow::Result<Option<Shell>> {
|
||||||
|
self.gen_completions
|
||||||
|
.map(|maybe_shell| match maybe_shell {
|
||||||
|
Some(sh) => Ok(sh),
|
||||||
|
None => guess_shell(),
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "completions")]
|
||||||
|
fn guess_shell() -> anyhow::Result<Shell> {
|
||||||
|
let env_shell = std::env::var_os("SHELL").map(PathBuf::from);
|
||||||
|
if let Some(shell) = env_shell
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.file_name())
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
{
|
||||||
|
shell
|
||||||
|
.parse::<Shell>()
|
||||||
|
.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)]
|
||||||
|
pub enum FileType {
|
||||||
|
#[value(alias = "f")]
|
||||||
|
File,
|
||||||
|
#[value(alias = "d")]
|
||||||
|
Directory,
|
||||||
|
#[value(alias = "l")]
|
||||||
|
Symlink,
|
||||||
|
#[value(alias = "x")]
|
||||||
|
Executable,
|
||||||
|
#[value(alias = "e")]
|
||||||
|
Empty,
|
||||||
|
#[value(alias = "s")]
|
||||||
|
Socket,
|
||||||
|
#[value(alias = "p")]
|
||||||
|
Pipe,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorWhen {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
use ColorWhen::*;
|
||||||
|
match *self {
|
||||||
|
Auto => "auto",
|
||||||
|
Never => "never",
|
||||||
|
Always => "always",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// there isn't a derive api for getting grouped values yet,
|
||||||
|
// so we have to use hand-rolled parsing for exec and exec-batch
|
||||||
|
pub struct Exec {
|
||||||
|
pub command: Option<CommandSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl clap::FromArgMatches for Exec {
|
||||||
|
fn from_arg_matches(matches: &ArgMatches) -> clap::error::Result<Self> {
|
||||||
|
let command = matches
|
||||||
|
.grouped_values_of("exec")
|
||||||
|
.map(CommandSet::new)
|
||||||
|
.or_else(|| {
|
||||||
|
matches
|
||||||
|
.grouped_values_of("exec_batch")
|
||||||
|
.map(CommandSet::new_batch)
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?;
|
||||||
|
Ok(Exec { command })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> clap::error::Result<()> {
|
||||||
|
*self = Self::from_arg_matches(matches)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl clap::Args for Exec {
|
||||||
|
fn augment_args(cmd: Command) -> Command {
|
||||||
|
cmd.arg(Arg::new("exec")
|
||||||
|
.action(ArgAction::Append)
|
||||||
|
.long("exec")
|
||||||
|
.short('x')
|
||||||
|
.num_args(1..)
|
||||||
|
.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")
|
||||||
|
.action(ArgAction::Append)
|
||||||
|
.long("exec-batch")
|
||||||
|
.short('X')
|
||||||
|
.num_args(1..)
|
||||||
|
.allow_hyphen_values(true)
|
||||||
|
.value_terminator(";")
|
||||||
|
.value_name("cmd")
|
||||||
|
.conflicts_with_all(&["exec", "list_details"])
|
||||||
|
.help("Execute a command with all search results at once")
|
||||||
|
.long_help(
|
||||||
|
"Execute the given command once, with all search results as arguments.\n\
|
||||||
|
One of the following placeholders is substituted before the command is executed:\n \
|
||||||
|
'{}': path (of all search results)\n \
|
||||||
|
'{/}': basename\n \
|
||||||
|
'{//}': parent directory\n \
|
||||||
|
'{.}': path without file extension\n \
|
||||||
|
'{/.}': basename without file extension\n\n\
|
||||||
|
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
||||||
|
Examples:\n\n \
|
||||||
|
- Find all test_*.py files and open them in your favorite editor:\n\n \
|
||||||
|
fd -g 'test_*.py' -X vim\n\n \
|
||||||
|
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
|
||||||
|
fd -e rs -X wc -l\
|
||||||
|
"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn augment_args_for_update(cmd: Command) -> Command {
|
||||||
|
Self::augment_args(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_millis(arg: &str) -> Result<Duration, std::num::ParseIntError> {
|
||||||
|
Ok(Duration::from_millis(arg.parse()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_current_directory_exists(current_directory: &Path) -> anyhow::Result<()> {
|
||||||
|
if filesystem::is_existing_directory(current_directory) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"Could not retrieve current directory (has it been deleted?)."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ pub fn job(
|
||||||
// Generate a command, execute it and store its exit code.
|
// Generate a command, execute it and store its exit code.
|
||||||
results.push(cmd.execute(
|
results.push(cmd.execute(
|
||||||
dir_entry.stripped_path(config),
|
dir_entry.stripped_path(config),
|
||||||
|
config.path_separator.as_deref(),
|
||||||
Arc::clone(&out_perm),
|
Arc::clone(&out_perm),
|
||||||
buffer_output,
|
buffer_output,
|
||||||
))
|
))
|
||||||
|
@ -61,5 +62,5 @@ pub fn batch(rx: Receiver<WorkerResult>, cmd: &CommandSet, config: &Config) -> E
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cmd.execute_batch(paths, config.batch_size)
|
cmd.execute_batch(paths, config.batch_size, config.path_separator.as_deref())
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,19 +35,17 @@ pub enum ExecutionMode {
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct CommandSet {
|
pub struct CommandSet {
|
||||||
mode: ExecutionMode,
|
mode: ExecutionMode,
|
||||||
path_separator: Option<String>,
|
|
||||||
commands: Vec<CommandTemplate>,
|
commands: Vec<CommandTemplate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandSet {
|
impl CommandSet {
|
||||||
pub fn new<I, S>(input: I, path_separator: Option<String>) -> Result<CommandSet>
|
pub fn new<I, S>(input: I) -> Result<CommandSet>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = Vec<S>>,
|
I: IntoIterator<Item = Vec<S>>,
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
Ok(CommandSet {
|
Ok(CommandSet {
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
path_separator,
|
|
||||||
commands: input
|
commands: input
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(CommandTemplate::new)
|
.map(CommandTemplate::new)
|
||||||
|
@ -55,14 +53,13 @@ impl CommandSet {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_batch<I, S>(input: I, path_separator: Option<String>) -> Result<CommandSet>
|
pub fn new_batch<I, S>(input: I) -> Result<CommandSet>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = Vec<S>>,
|
I: IntoIterator<Item = Vec<S>>,
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
Ok(CommandSet {
|
Ok(CommandSet {
|
||||||
mode: ExecutionMode::Batch,
|
mode: ExecutionMode::Batch,
|
||||||
path_separator,
|
|
||||||
commands: input
|
commands: input
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|args| {
|
.map(|args| {
|
||||||
|
@ -83,8 +80,13 @@ impl CommandSet {
|
||||||
self.mode == ExecutionMode::Batch
|
self.mode == ExecutionMode::Batch
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(&self, input: &Path, out_perm: Arc<Mutex<()>>, buffer_output: bool) -> ExitCode {
|
pub fn execute(
|
||||||
let path_separator = self.path_separator.as_deref();
|
&self,
|
||||||
|
input: &Path,
|
||||||
|
path_separator: Option<&str>,
|
||||||
|
out_perm: Arc<Mutex<()>>,
|
||||||
|
buffer_output: bool,
|
||||||
|
) -> ExitCode {
|
||||||
let commands = self
|
let commands = self
|
||||||
.commands
|
.commands
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -92,12 +94,10 @@ impl CommandSet {
|
||||||
execute_commands(commands, &out_perm, buffer_output)
|
execute_commands(commands, &out_perm, buffer_output)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_batch<I>(&self, paths: I, limit: usize) -> ExitCode
|
pub fn execute_batch<I>(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode
|
||||||
where
|
where
|
||||||
I: Iterator<Item = PathBuf>,
|
I: Iterator<Item = PathBuf>,
|
||||||
{
|
{
|
||||||
let path_separator = self.path_separator.as_deref();
|
|
||||||
|
|
||||||
let builders: io::Result<Vec<_>> = self
|
let builders: io::Result<Vec<_>> = self
|
||||||
.commands
|
.commands
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -413,7 +413,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_with_placeholder() {
|
fn tokens_with_placeholder() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]], None).unwrap(),
|
CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]]).unwrap(),
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
|
@ -423,7 +423,6 @@ mod tests {
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
path_separator: None,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -431,7 +430,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_with_no_extension() {
|
fn tokens_with_no_extension() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CommandSet::new(vec![vec!["echo", "{.}"]], None).unwrap(),
|
CommandSet::new(vec![vec!["echo", "{.}"]]).unwrap(),
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
|
@ -440,7 +439,6 @@ mod tests {
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
path_separator: None,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -448,7 +446,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_with_basename() {
|
fn tokens_with_basename() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CommandSet::new(vec![vec!["echo", "{/}"]], None).unwrap(),
|
CommandSet::new(vec![vec!["echo", "{/}"]]).unwrap(),
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
|
@ -457,7 +455,6 @@ mod tests {
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
path_separator: None,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -465,7 +462,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_with_parent() {
|
fn tokens_with_parent() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CommandSet::new(vec![vec!["echo", "{//}"]], None).unwrap(),
|
CommandSet::new(vec![vec!["echo", "{//}"]]).unwrap(),
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
|
@ -474,7 +471,6 @@ mod tests {
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
path_separator: None,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -482,7 +478,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_with_basename_no_extension() {
|
fn tokens_with_basename_no_extension() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CommandSet::new(vec![vec!["echo", "{/.}"]], None).unwrap(),
|
CommandSet::new(vec![vec!["echo", "{/.}"]]).unwrap(),
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
|
@ -491,7 +487,6 @@ mod tests {
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
path_separator: None,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -499,7 +494,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_multiple() {
|
fn tokens_multiple() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]], None).unwrap(),
|
CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]]).unwrap(),
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
|
@ -512,7 +507,6 @@ mod tests {
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
path_separator: None,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -520,7 +514,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_single_batch() {
|
fn tokens_single_batch() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CommandSet::new_batch(vec![vec!["echo", "{.}"]], None).unwrap(),
|
CommandSet::new_batch(vec![vec!["echo", "{.}"]]).unwrap(),
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
|
@ -529,14 +523,13 @@ mod tests {
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::Batch,
|
mode: ExecutionMode::Batch,
|
||||||
path_separator: None,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_multiple_batch() {
|
fn tokens_multiple_batch() {
|
||||||
assert!(CommandSet::new_batch(vec![vec!["echo", "{.}", "{}"]], None).is_err());
|
assert!(CommandSet::new_batch(vec![vec!["echo", "{.}", "{}"]]).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -546,7 +539,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_set_no_args() {
|
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]
|
#[test]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
|
@ -24,7 +25,12 @@ const GIBI: u64 = MEBI * 1024;
|
||||||
const TEBI: u64 = GIBI * 1024;
|
const TEBI: u64 = GIBI * 1024;
|
||||||
|
|
||||||
impl SizeFilter {
|
impl SizeFilter {
|
||||||
pub fn from_string(s: &str) -> Option<Self> {
|
pub fn from_string(s: &str) -> anyhow::Result<Self> {
|
||||||
|
SizeFilter::parse_opt(s)
|
||||||
|
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_opt(s: &str) -> Option<Self> {
|
||||||
if !SIZE_CAPTURES.is_match(s) {
|
if !SIZE_CAPTURES.is_match(s) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -165,7 +171,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn $name() {
|
fn $name() {
|
||||||
let i = SizeFilter::from_string($value);
|
let i = SizeFilter::from_string($value);
|
||||||
assert!(i.is_none());
|
assert!(i.is_err());
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
};
|
};
|
||||||
|
|
360
src/main.rs
360
src/main.rs
|
@ -1,4 +1,4 @@
|
||||||
mod app;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod dir_entry;
|
mod dir_entry;
|
||||||
mod error;
|
mod error;
|
||||||
|
@ -12,25 +12,25 @@ mod regex_helper;
|
||||||
mod walk;
|
mod walk;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use atty::Stream;
|
use atty::Stream;
|
||||||
|
use clap::{CommandFactory, Parser};
|
||||||
use globset::GlobBuilder;
|
use globset::GlobBuilder;
|
||||||
use lscolors::LsColors;
|
use lscolors::LsColors;
|
||||||
use normpath::PathExt;
|
|
||||||
use regex::bytes::{RegexBuilder, RegexSetBuilder};
|
use regex::bytes::{RegexBuilder, RegexSetBuilder};
|
||||||
|
|
||||||
|
use crate::cli::{ColorWhen, Opts};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::error::print_error;
|
|
||||||
use crate::exec::CommandSet;
|
use crate::exec::CommandSet;
|
||||||
use crate::exit_codes::ExitCode;
|
use crate::exit_codes::ExitCode;
|
||||||
use crate::filetypes::FileTypes;
|
use crate::filetypes::FileTypes;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use crate::filter::OwnerFilter;
|
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};
|
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
|
// We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/481
|
||||||
|
@ -67,23 +67,42 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<ExitCode> {
|
fn run() -> Result<ExitCode> {
|
||||||
let matches = app::build_app().get_matches_from(env::args_os());
|
let opts = Opts::parse();
|
||||||
|
|
||||||
set_working_dir(&matches)?;
|
#[cfg(feature = "completions")]
|
||||||
let search_paths = extract_search_paths(&matches)?;
|
if let Some(shell) = opts.gen_completions()? {
|
||||||
let pattern = extract_search_pattern(&matches)?;
|
return print_completions(shell);
|
||||||
ensure_search_pattern_is_not_a_path(&matches, pattern)?;
|
}
|
||||||
let pattern_regex = build_pattern_regex(&matches, pattern)?;
|
|
||||||
|
|
||||||
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)?;
|
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regex)?;
|
||||||
let re = build_regex(pattern_regex, &config)?;
|
let re = build_regex(pattern_regex, &config)?;
|
||||||
walk::scan(&search_paths, Arc::new(re), Arc::new(config))
|
walk::scan(&search_paths, Arc::new(re), Arc::new(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_working_dir(matches: &clap::ArgMatches) -> Result<()> {
|
#[cfg(feature = "completions")]
|
||||||
if let Some(base_directory) = matches.value_of_os("base-directory") {
|
#[cold]
|
||||||
let base_directory = Path::new(base_directory);
|
fn print_completions(shell: clap_complete::Shell) -> Result<ExitCode> {
|
||||||
|
// The program name is the first argument.
|
||||||
|
let program_name = env::args().next().unwrap_or_else(|| "fd".to_string());
|
||||||
|
let mut cmd = Opts::command();
|
||||||
|
cmd.build();
|
||||||
|
// TODO: fix panic
|
||||||
|
clap_complete::generate(shell, &mut cmd, &program_name, &mut std::io::stdout());
|
||||||
|
Ok(ExitCode::Success)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_working_dir(opts: &Opts) -> Result<()> {
|
||||||
|
if let Some(ref base_directory) = opts.base_directory {
|
||||||
if !filesystem::is_existing_directory(base_directory) {
|
if !filesystem::is_existing_directory(base_directory) {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"The '--base-directory' path '{}' is not a directory.",
|
"The '--base-directory' path '{}' is not a directory.",
|
||||||
|
@ -100,75 +119,11 @@ fn set_working_dir(matches: &clap::ArgMatches) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_current_directory_exists(current_directory: &Path) -> Result<()> {
|
|
||||||
if filesystem::is_existing_directory(current_directory) {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(anyhow!(
|
|
||||||
"Could not retrieve current directory (has it been deleted?)."
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_search_pattern(matches: &clap::ArgMatches) -> Result<&'_ str> {
|
|
||||||
let pattern = matches
|
|
||||||
.value_of_os("pattern")
|
|
||||||
.map(|p| {
|
|
||||||
p.to_str()
|
|
||||||
.ok_or_else(|| anyhow!("The search pattern includes invalid UTF-8 sequences."))
|
|
||||||
})
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or("");
|
|
||||||
Ok(pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_search_paths(matches: &clap::ArgMatches) -> Result<Vec<PathBuf>> {
|
|
||||||
let parameter_paths = matches
|
|
||||||
.values_of_os("path")
|
|
||||||
.or_else(|| matches.values_of_os("search-path"));
|
|
||||||
|
|
||||||
let mut search_paths = match parameter_paths {
|
|
||||||
Some(paths) => paths
|
|
||||||
.filter_map(|path| {
|
|
||||||
let path_buffer = PathBuf::from(path);
|
|
||||||
if filesystem::is_existing_directory(&path_buffer) {
|
|
||||||
Some(path_buffer)
|
|
||||||
} else {
|
|
||||||
print_error(format!(
|
|
||||||
"Search path '{}' is not a directory.",
|
|
||||||
path_buffer.to_string_lossy(),
|
|
||||||
));
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
None => {
|
|
||||||
let current_directory = Path::new(".");
|
|
||||||
ensure_current_directory_exists(current_directory)?;
|
|
||||||
vec![current_directory.to_path_buf()]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if search_paths.is_empty() {
|
|
||||||
return Err(anyhow!("No valid search paths given."));
|
|
||||||
}
|
|
||||||
if matches.is_present("absolute-path") {
|
|
||||||
update_to_absolute_paths(&mut search_paths);
|
|
||||||
}
|
|
||||||
Ok(search_paths)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_to_absolute_paths(search_paths: &mut [PathBuf]) {
|
|
||||||
for buffer in search_paths.iter_mut() {
|
|
||||||
*buffer = filesystem::absolute_path(buffer.normalize().unwrap().as_path()).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detect if the user accidentally supplied a path instead of a search pattern
|
/// 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<()> {
|
fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
|
||||||
if !matches.is_present("full-path")
|
if !opts.full_path
|
||||||
&& pattern.contains(std::path::MAIN_SEPARATOR)
|
&& opts.pattern.contains(std::path::MAIN_SEPARATOR)
|
||||||
&& Path::new(pattern).is_dir()
|
&& Path::new(&opts.pattern).is_dir()
|
||||||
{
|
{
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"The search pattern '{pattern}' contains a path-separation character ('{sep}') \
|
"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\
|
fd . '{pattern}'\n\n\
|
||||||
Instead, if you want your pattern to match the full file path, use:\n\n \
|
Instead, if you want your pattern to match the full file path, use:\n\n \
|
||||||
fd --full-path '{pattern}'",
|
fd --full-path '{pattern}'",
|
||||||
pattern = pattern,
|
pattern = &opts.pattern,
|
||||||
sep = std::path::MAIN_SEPARATOR,
|
sep = std::path::MAIN_SEPARATOR,
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
|
@ -185,11 +140,12 @@ fn ensure_search_pattern_is_not_a_path(matches: &clap::ArgMatches, pattern: &str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_pattern_regex(matches: &clap::ArgMatches, pattern: &str) -> Result<String> {
|
fn build_pattern_regex(opts: &Opts) -> Result<String> {
|
||||||
Ok(if matches.is_present("glob") && !pattern.is_empty() {
|
let pattern = &opts.pattern;
|
||||||
|
Ok(if opts.glob && !pattern.is_empty() {
|
||||||
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
|
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
|
||||||
glob.regex().to_owned()
|
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
|
// Treat pattern as literal string if '--fixed-strings' is used
|
||||||
regex::escape(pattern)
|
regex::escape(pattern)
|
||||||
} else {
|
} else {
|
||||||
|
@ -211,28 +167,25 @@ fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Config> {
|
fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
|
||||||
// The search will be case-sensitive if the command line flag is set or
|
// The search will be case-sensitive if the command line flag is set or
|
||||||
// if the pattern has an uppercase character (smart case).
|
// if the pattern has an uppercase character (smart case).
|
||||||
let case_sensitive = !matches.is_present("ignore-case")
|
let case_sensitive =
|
||||||
&& (matches.is_present("case-sensitive") || pattern_has_uppercase_char(pattern_regex));
|
!opts.ignore_case && (opts.case_sensitive || pattern_has_uppercase_char(pattern_regex));
|
||||||
|
|
||||||
let path_separator = matches
|
let path_separator = opts
|
||||||
.value_of("path-separator")
|
.path_separator
|
||||||
.map_or_else(filesystem::default_path_separator, |s| Some(s.to_owned()));
|
.take()
|
||||||
|
.or_else(filesystem::default_path_separator);
|
||||||
let actual_path_separator = path_separator
|
let actual_path_separator = path_separator
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| std::path::MAIN_SEPARATOR.to_string());
|
.unwrap_or_else(|| std::path::MAIN_SEPARATOR.to_string());
|
||||||
check_path_separator_length(path_separator.as_deref())?;
|
check_path_separator_length(path_separator.as_deref())?;
|
||||||
|
|
||||||
let size_limits = extract_size_limits(&matches)?;
|
let size_limits = std::mem::take(&mut opts.size);
|
||||||
let time_constraints = extract_time_constraints(&matches)?;
|
let time_constraints = extract_time_constraints(&opts)?;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let owner_constraint = matches
|
let owner_constraint: Option<OwnerFilter> = opts.owner;
|
||||||
.value_of("owner")
|
|
||||||
.map(OwnerFilter::from_string)
|
|
||||||
.transpose()?
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let ansi_colors_support =
|
let ansi_colors_support =
|
||||||
|
@ -241,10 +194,12 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
|
||||||
let ansi_colors_support = true;
|
let ansi_colors_support = true;
|
||||||
|
|
||||||
let interactive_terminal = atty::is(Stream::Stdout);
|
let interactive_terminal = atty::is(Stream::Stdout);
|
||||||
let colored_output = match matches.value_of("color") {
|
let colored_output = match opts.color {
|
||||||
Some("always") => true,
|
ColorWhen::Always => true,
|
||||||
Some("never") => false,
|
ColorWhen::Never => false,
|
||||||
_ => ansi_colors_support && env::var_os("NO_COLOR").is_none() && interactive_terminal,
|
ColorWhen::Auto => {
|
||||||
|
ansi_colors_support && env::var_os("NO_COLOR").is_none() && interactive_terminal
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let ls_colors = if colored_output {
|
let ls_colors = if colored_output {
|
||||||
|
@ -252,80 +207,43 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let command = extract_command(&matches, path_separator.as_deref(), colored_output)?;
|
let command = extract_command(&mut opts, colored_output)?;
|
||||||
|
let has_command = command.is_some();
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
case_sensitive,
|
case_sensitive,
|
||||||
search_full_path: matches.is_present("full-path"),
|
search_full_path: opts.full_path,
|
||||||
ignore_hidden: !(matches.is_present("hidden")
|
ignore_hidden: !(opts.hidden || opts.rg_alias_ignore()),
|
||||||
|| matches.is_present("rg-alias-hidden-ignore")),
|
read_fdignore: !(opts.no_ignore || opts.rg_alias_ignore()),
|
||||||
read_fdignore: !(matches.is_present("no-ignore")
|
read_vcsignore: !(opts.no_ignore || opts.rg_alias_ignore() || opts.no_ignore_vcs),
|
||||||
|| matches.is_present("rg-alias-hidden-ignore")),
|
read_parent_ignore: !opts.no_ignore_parent,
|
||||||
read_vcsignore: !(matches.is_present("no-ignore")
|
read_global_ignore: !opts.no_ignore || opts.rg_alias_ignore() || opts.no_global_ignore_file,
|
||||||
|| matches.is_present("rg-alias-hidden-ignore")
|
follow_links: opts.follow,
|
||||||
|| matches.is_present("no-ignore-vcs")),
|
one_file_system: opts.one_file_system,
|
||||||
read_parent_ignore: !matches.is_present("no-ignore-parent"),
|
null_separator: opts.null_separator,
|
||||||
read_global_ignore: !(matches.is_present("no-ignore")
|
quiet: opts.quiet,
|
||||||
|| matches.is_present("rg-alias-hidden-ignore")
|
max_depth: opts.max_depth(),
|
||||||
|| matches.is_present("no-global-ignore-file")),
|
min_depth: opts.min_depth(),
|
||||||
follow_links: matches.is_present("follow"),
|
prune: opts.prune,
|
||||||
one_file_system: matches.is_present("one-file-system"),
|
threads: opts.threads(),
|
||||||
null_separator: matches.is_present("null_separator"),
|
max_buffer_time: opts.max_buffer_time,
|
||||||
quiet: matches.is_present("quiet"),
|
|
||||||
max_depth: matches
|
|
||||||
.value_of("max-depth")
|
|
||||||
.or_else(|| matches.value_of("rg-depth"))
|
|
||||||
.or_else(|| matches.value_of("exact-depth"))
|
|
||||||
.map(|n| n.parse::<usize>())
|
|
||||||
.transpose()
|
|
||||||
.context("Failed to parse argument to --max-depth/--exact-depth")?,
|
|
||||||
min_depth: matches
|
|
||||||
.value_of("min-depth")
|
|
||||||
.or_else(|| matches.value_of("exact-depth"))
|
|
||||||
.map(|n| n.parse::<usize>())
|
|
||||||
.transpose()
|
|
||||||
.context("Failed to parse argument to --min-depth/--exact-depth")?,
|
|
||||||
prune: matches.is_present("prune"),
|
|
||||||
threads: std::cmp::max(
|
|
||||||
matches
|
|
||||||
.value_of("threads")
|
|
||||||
.map(|n| n.parse::<usize>())
|
|
||||||
.transpose()
|
|
||||||
.context("Failed to parse number of threads")?
|
|
||||||
.map(|n| {
|
|
||||||
if n > 0 {
|
|
||||||
Ok(n)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Number of threads must be positive."))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or_else(num_cpus::get),
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
max_buffer_time: matches
|
|
||||||
.value_of("max-buffer-time")
|
|
||||||
.map(|n| n.parse::<u64>())
|
|
||||||
.transpose()
|
|
||||||
.context("Failed to parse max. buffer time argument")?
|
|
||||||
.map(time::Duration::from_millis),
|
|
||||||
ls_colors,
|
ls_colors,
|
||||||
interactive_terminal,
|
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();
|
let mut file_types = FileTypes::default();
|
||||||
for value in values {
|
for value in values {
|
||||||
match value {
|
match value {
|
||||||
"f" | "file" => file_types.files = true,
|
File => file_types.files = true,
|
||||||
"d" | "directory" => file_types.directories = true,
|
Directory => file_types.directories = true,
|
||||||
"l" | "symlink" => file_types.symlinks = true,
|
Symlink => file_types.symlinks = true,
|
||||||
"x" | "executable" => {
|
Executable => {
|
||||||
file_types.executables_only = true;
|
file_types.executables_only = true;
|
||||||
file_types.files = true;
|
file_types.files = true;
|
||||||
}
|
}
|
||||||
"e" | "empty" => file_types.empty_only = true,
|
Empty => file_types.empty_only = true,
|
||||||
"s" | "socket" => file_types.sockets = true,
|
Socket => file_types.sockets = true,
|
||||||
"p" | "pipe" => file_types.pipes = true,
|
Pipe => file_types.pipes = true,
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,10 +255,12 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
|
||||||
|
|
||||||
file_types
|
file_types
|
||||||
}),
|
}),
|
||||||
extensions: matches
|
extensions: opts
|
||||||
.values_of("extension")
|
.extensions
|
||||||
|
.as_ref()
|
||||||
.map(|exts| {
|
.map(|exts| {
|
||||||
let patterns = exts
|
let patterns = exts
|
||||||
|
.iter()
|
||||||
.map(|e| e.trim_start_matches('.'))
|
.map(|e| e.trim_start_matches('.'))
|
||||||
.map(|e| format!(r".\.{}$", regex::escape(e)));
|
.map(|e| format!(r".\.{}$", regex::escape(e)));
|
||||||
RegexSetBuilder::new(patterns)
|
RegexSetBuilder::new(patterns)
|
||||||
|
@ -349,78 +269,38 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
|
||||||
})
|
})
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
command: command.map(Arc::new),
|
command: command.map(Arc::new),
|
||||||
batch_size: matches
|
batch_size: opts.batch_size,
|
||||||
.value_of("batch-size")
|
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
|
||||||
.map(|n| n.parse::<usize>())
|
ignore_files: std::mem::take(&mut opts.ignore_file),
|
||||||
.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),
|
|
||||||
size_constraints: size_limits,
|
size_constraints: size_limits,
|
||||||
time_constraints,
|
time_constraints,
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
owner_constraint,
|
owner_constraint,
|
||||||
show_filesystem_errors: matches.is_present("show-errors"),
|
show_filesystem_errors: opts.show_errors,
|
||||||
path_separator,
|
path_separator,
|
||||||
actual_path_separator,
|
actual_path_separator,
|
||||||
max_results: matches
|
max_results: opts.max_results(),
|
||||||
.value_of("max-results")
|
strip_cwd_prefix: (opts.no_search_paths()
|
||||||
.map(|n| n.parse::<usize>())
|
&& (opts.strip_cwd_prefix || !(opts.null_separator || has_command))),
|
||||||
.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")
|
|
||||||
&& (matches.is_present("strip-cwd-prefix")
|
|
||||||
|| !(matches.is_present("null_separator")
|
|
||||||
|| matches.is_present("exec")
|
|
||||||
|| matches.is_present("exec-batch"))),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_command(
|
fn extract_command(opts: &mut Opts, colored_output: bool) -> Result<Option<CommandSet>> {
|
||||||
matches: &clap::ArgMatches,
|
opts.exec
|
||||||
path_separator: Option<&str>,
|
.command
|
||||||
colored_output: bool,
|
.take()
|
||||||
) -> Result<Option<CommandSet>> {
|
.map(Ok)
|
||||||
None.or_else(|| {
|
.or_else(|| {
|
||||||
matches
|
if !opts.list_details {
|
||||||
.grouped_values_of("exec")
|
return None;
|
||||||
.map(|args| CommandSet::new(args, path_separator.map(str::to_string)))
|
}
|
||||||
})
|
let color_arg = format!("--color={}", opts.color.as_str());
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let color = matches.value_of("color").unwrap_or("auto");
|
let res = determine_ls_command(&color_arg, colored_output)
|
||||||
let color_arg = format!("--color={}", color);
|
.map(|cmd| CommandSet::new_batch([cmd]).unwrap());
|
||||||
|
Some(res)
|
||||||
let res = determine_ls_command(&color_arg, colored_output)
|
})
|
||||||
.map(|cmd| CommandSet::new_batch([cmd], path_separator.map(str::to_string)).unwrap());
|
.transpose()
|
||||||
|
|
||||||
Some(res)
|
|
||||||
})
|
|
||||||
.transpose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&str>> {
|
fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&str>> {
|
||||||
|
@ -502,20 +382,10 @@ fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&st
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_size_limits(matches: &clap::ArgMatches) -> Result<Vec<SizeFilter>> {
|
fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
|
||||||
matches.values_of("size").map_or(Ok(Vec::new()), |vs| {
|
|
||||||
vs.map(|sf| {
|
|
||||||
SizeFilter::from_string(sf)
|
|
||||||
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", sf))
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>>>()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_time_constraints(matches: &clap::ArgMatches) -> Result<Vec<TimeFilter>> {
|
|
||||||
let now = time::SystemTime::now();
|
let now = time::SystemTime::now();
|
||||||
let mut time_constraints: Vec<TimeFilter> = Vec::new();
|
let mut time_constraints: Vec<TimeFilter> = Vec::new();
|
||||||
if let Some(t) = matches.value_of("changed-within") {
|
if let Some(ref t) = opts.changed_within {
|
||||||
if let Some(f) = TimeFilter::after(&now, t) {
|
if let Some(f) = TimeFilter::after(&now, t) {
|
||||||
time_constraints.push(f);
|
time_constraints.push(f);
|
||||||
} else {
|
} else {
|
||||||
|
@ -525,7 +395,7 @@ fn extract_time_constraints(matches: &clap::ArgMatches) -> Result<Vec<TimeFilter
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(t) = matches.value_of("changed-before") {
|
if let Some(ref t) = opts.changed_before {
|
||||||
if let Some(f) = TimeFilter::before(&now, t) {
|
if let Some(f) = TimeFilter::before(&now, t) {
|
||||||
time_constraints.push(f);
|
time_constraints.push(f);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -360,7 +360,6 @@ fn spawn_receiver(
|
||||||
handles.push(handle);
|
handles.push(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all threads to exit before exiting the program.
|
|
||||||
let exit_codes = handles
|
let exit_codes = handles
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|handle| handle.join().unwrap())
|
.map(|handle| handle.join().unwrap())
|
||||||
|
|
|
@ -1479,7 +1479,12 @@ fn test_exec_batch() {
|
||||||
|
|
||||||
te.assert_failure_with_error(
|
te.assert_failure_with_error(
|
||||||
&["foo", "--exec-batch", "echo", "{}", "{}"],
|
&["foo", "--exec-batch", "echo", "{}", "{}"],
|
||||||
"[fd error]: Only one placeholder allowed for batch commands",
|
"error: Only one placeholder allowed for batch commands\n\
|
||||||
|
\n\
|
||||||
|
Usage: fd-find [OPTIONS] [pattern] [path]...\n\
|
||||||
|
\n\
|
||||||
|
For more information try '--help'\n\
|
||||||
|
",
|
||||||
);
|
);
|
||||||
|
|
||||||
te.assert_failure_with_error(
|
te.assert_failure_with_error(
|
||||||
|
@ -1494,7 +1499,12 @@ fn test_exec_batch() {
|
||||||
|
|
||||||
te.assert_failure_with_error(
|
te.assert_failure_with_error(
|
||||||
&["foo", "--exec-batch", "echo {}"],
|
&["foo", "--exec-batch", "echo {}"],
|
||||||
"[fd error]: First argument of exec-batch is expected to be a fixed executable",
|
"error: First argument of exec-batch is expected to be a fixed executable\n\
|
||||||
|
\n\
|
||||||
|
Usage: fd-find [OPTIONS] [pattern] [path]...\n\
|
||||||
|
\n\
|
||||||
|
For more information try '--help'\n\
|
||||||
|
",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue