Merge branch 'master' into master

This commit is contained in:
Thayne McCombs 2022-03-15 01:20:35 -06:00 committed by GitHub
commit 50c0fa812f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 933 additions and 447 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: [sharkdp, tavianator]

View File

@ -1,7 +1,7 @@
name: CICD name: CICD
env: env:
MIN_SUPPORTED_RUST_VERSION: "1.53.0" MIN_SUPPORTED_RUST_VERSION: "1.56.0"
CICD_INTERMEDIATES_DIR: "_cicd-intermediates" CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
on: on:
@ -46,11 +46,18 @@ jobs:
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }}) - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }} toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }}
default: true default: true
components: clippy
- name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
uses: actions-rs/cargo@v1
with:
command: clippy
args: --locked --all-targets --all-features
profile: minimal # minimal component installation (ie, no documentation) profile: minimal # minimal component installation (ie, no documentation)
- name: Run tests - name: Run tests
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@ -313,8 +320,11 @@ jobs:
Architecture: ${DPKG_ARCH} Architecture: ${DPKG_ARCH}
Provides: ${{ env.PROJECT_NAME }} Provides: ${{ env.PROJECT_NAME }}
Conflicts: ${DPKG_CONFLICTS} Conflicts: ${DPKG_CONFLICTS}
Description: cat(1) clone with wings. Description: simple, fast and user-friendly alternative to find
A cat(1) clone with syntax highlighting and Git integration. fd is a program to find entries in your filesystem.
It is a simple, fast and user-friendly alternative to find.
While it does not aim to support all of finds powerful functionality, it provides
sensible (opinionated) defaults for a majority of use cases.
EOF EOF
DPKG_PATH="${DPKG_STAGING}/${DPKG_NAME}" DPKG_PATH="${DPKG_STAGING}/${DPKG_NAME}"

View File

@ -2,16 +2,42 @@
## Performance improvements ## Performance improvements
## Features ## Features
## Bugfixes ## Bugfixes
## Changes ## Changes
- Directories are now printed with an additional path separator at the end: `foo/bar/` - Directories are now printed with an additional path separator at the end: `foo/bar/`
## Other ## Other
# v8.3.2
## Bugfixes
- Invalid absolute path on windows when searching from the drive root, see #931 and #936 (@gbarta)
# v8.3.1
## Bugfixes
- Stop implying `--no-ignore-parent` when `--no-vcs-ignore` is supplied, see #907, #901, #908 (@tmccombs)
- fd no longer waits for the whole traversal if the only matches arrive within max_buffer_time, see #868 and #895 (@tavianator)
- `--max-results=1` now immediately quits after the first result, see #867
- `fd -h` does not panic anymore when stdout is closed, see #897
## Changes
- Disable jemalloc on FreeBSD, see #896 (@xanderio)
- Updated man page, see #912 (@rlue)
- Updated zsh completions, see #932 (@tmccombs)
# v8.3.0 # v8.3.0

191
Cargo.lock generated
View File

@ -22,9 +22,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.51" version = "1.0.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
[[package]] [[package]]
name = "atty" name = "atty"
@ -39,9 +39,9 @@ dependencies = [
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.0.1" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@ -60,9 +60,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.72" version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -85,35 +85,35 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.34.0" version = "3.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" checksum = "ced1892c55c910c1219e98d6fc8d71f6bddba7905866ce740066d8bfea859312"
dependencies = [ dependencies = [
"ansi_term",
"atty", "atty",
"bitflags", "bitflags",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim", "strsim",
"term_size", "termcolor",
"terminal_size",
"textwrap", "textwrap",
"unicode-width",
"vec_map",
] ]
[[package]] [[package]]
name = "crossbeam-channel" name = "clap_complete"
version = "0.5.1" version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" checksum = "df6f3613c0a3cddfd78b41b10203eb322cb29b600cbdf808a7d3db95691b8e25"
dependencies = [ dependencies = [
"cfg-if", "clap",
"crossbeam-utils",
] ]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.5" version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"lazy_static", "lazy_static",
@ -158,14 +158,14 @@ dependencies = [
[[package]] [[package]]
name = "fd-find" name = "fd-find"
version = "8.3.0" version = "8.3.2"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"anyhow", "anyhow",
"atty", "atty",
"chrono", "chrono",
"clap", "clap",
"crossbeam-channel", "clap_complete",
"ctrlc", "ctrlc",
"diff", "diff",
"dirs-next", "dirs-next",
@ -220,9 +220,9 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.3" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@ -242,6 +242,12 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.1.19"
@ -275,6 +281,16 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "indexmap"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]] [[package]]
name = "jemalloc-sys" name = "jemalloc-sys"
version = "0.3.2" version = "0.3.2"
@ -304,9 +320,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.108" version = "0.2.119"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
[[package]] [[package]]
name = "log" name = "log"
@ -319,9 +335,9 @@ dependencies = [
[[package]] [[package]]
name = "lscolors" name = "lscolors"
version = "0.8.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd58d8727f3035fa6d5272f16b519741fd4875936b99d8a7cde21291b7d9174" checksum = "4e9323b3525d4efad2dead1837a105e313253bfdbad1d470994038eededa4d62"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
] ]
@ -334,18 +350,18 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.6.4" version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [ dependencies = [
"autocfg", "autocfg",
] ]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.23.0" version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188" checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cc", "cc",
@ -356,9 +372,9 @@ dependencies = [
[[package]] [[package]]
name = "normpath" name = "normpath"
version = "0.3.1" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "640c20e9df4a2d4a5adad5b47e17d76dac3e824346b181931c3ec9f7a85687b1" checksum = "04aaf5e9cb0fbf883cc0423159eacdf96a9878022084b35c462c428cab73bcaf"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]
@ -384,9 +400,9 @@ dependencies = [
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.13.0" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
@ -394,24 +410,57 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.8.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
dependencies = [
"memchr",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.32" version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.10" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -455,9 +504,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.10" version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@ -509,15 +558,15 @@ dependencies = [
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.82" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -535,10 +584,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "term_size" name = "termcolor"
version = "0.3.2" version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
dependencies = [ dependencies = [
"libc", "libc",
"winapi", "winapi",
@ -546,32 +604,31 @@ dependencies = [
[[package]] [[package]]
name = "test-case" name = "test-case"
version = "1.2.1" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7cad0a06f9a61e94355aa3b3dc92d85ab9c83406722b1ca5e918d4297c12c23" checksum = "4f7d58e237f65d5fe5eaf1105188c94c9a441e1fbc298ed5df45ec9c9af236d3"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
"version_check",
] ]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
dependencies = [ dependencies = [
"term_size", "terminal_size",
"unicode-width",
] ]
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.3" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
dependencies = [ dependencies = [
"once_cell", "once_cell",
] ]
@ -586,12 +643,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.2" version = "0.2.2"
@ -608,17 +659,11 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.3" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]] [[package]]
name = "walkdir" name = "walkdir"

View File

@ -16,8 +16,8 @@ license = "MIT/Apache-2.0"
name = "fd-find" name = "fd-find"
readme = "README.md" readme = "README.md"
repository = "https://github.com/sharkdp/fd" repository = "https://github.com/sharkdp/fd"
version = "8.3.0" version = "8.3.2"
edition= "2018" edition= "2021"
[badges.appveyor] [badges.appveyor]
repository = "sharkdp/fd" repository = "sharkdp/fd"
@ -30,34 +30,34 @@ name = "fd"
path = "src/main.rs" path = "src/main.rs"
[build-dependencies] [build-dependencies]
clap = "2.34.0" clap = { version = "3.1", features = ["cargo"] }
clap_complete = "3.1"
version_check = "0.9" version_check = "0.9"
[dependencies] [dependencies]
ansi_term = "0.12" ansi_term = "0.12"
atty = "0.2" atty = "0.2"
ignore = "0.4.3" ignore = "0.4.3"
num_cpus = "1.8" num_cpus = "1.13"
regex = "1.5.4" regex = "1.5.4"
regex-syntax = "0.6" regex-syntax = "0.6"
ctrlc = "3.2" ctrlc = "3.2"
humantime = "2.1" humantime = "2.1"
lscolors = "0.8" lscolors = "0.9"
globset = "0.4" globset = "0.4"
anyhow = "1.0" anyhow = "1.0"
dirs-next = "2.0" dirs-next = "2.0"
normpath = "0.3" normpath = "0.3.2"
chrono = "0.4" chrono = "0.4"
once_cell = "1.8.0" once_cell = "1.9.0"
crossbeam-channel = "0.5.1"
[dependencies.clap] [dependencies.clap]
version = "2.34.0" version = "3.1"
features = ["suggestions", "color", "wrap_help"] features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped"]
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
users = "0.11.0" users = "0.11.0"
nix = "0.23.0" nix = "0.23.1"
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies] [target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
libc = "0.2" libc = "0.2"
@ -65,15 +65,19 @@ libc = "0.2"
# FIXME: Re-enable jemalloc on macOS # FIXME: Re-enable jemalloc on macOS
# jemalloc is currently disabled on macOS due to a bug in jemalloc in combination with macOS # jemalloc is currently disabled on macOS due to a bug in jemalloc in combination with macOS
# Catalina. See https://github.com/sharkdp/fd/issues/498 for details. # Catalina. See https://github.com/sharkdp/fd/issues/498 for details.
[target.'cfg(all(not(windows), not(target_os = "android"), not(target_os = "macos"), not(target_os = "freebsd"), not(target_env = "musl")))'.dependencies] [target.'cfg(all(not(windows), not(target_os = "android"), not(target_os = "macos"), not(target_os = "freebsd"), not(target_env = "musl"), not(target_arch = "riscv64")))'.dependencies]
jemallocator = "0.3.0" jemallocator = {version = "0.3.0", optional = true}
[dev-dependencies] [dev-dependencies]
diff = "0.1" diff = "0.1"
tempdir = "0.3" tempdir = "0.3"
filetime = "0.2" filetime = "0.2"
test-case = "1.2" test-case = "2.0"
[profile.release] [profile.release]
lto = true lto = true
codegen-units = 1 codegen-units = 1
[features]
use-jemalloc = ["jemallocator"]
default = ["use-jemalloc"]

View File

@ -20,7 +20,7 @@ Quick links:
* Intuitive syntax: `fd PATTERN` instead of `find -iname '*PATTERN*'`. * Intuitive syntax: `fd PATTERN` instead of `find -iname '*PATTERN*'`.
* Regular expression (default) and glob-based patterns. * Regular expression (default) and glob-based patterns.
* [Very fast](#benchmark) due to parallelized directory traversal. * [Very fast](#benchmark) due to parallelized directory traversal.
* Uses colors to highlight different file types (same as *ls*). * Uses colors to highlight different file types (same as `ls`).
* Supports [parallel command execution](#command-execution) * Supports [parallel command execution](#command-execution)
* Smart case: the search is case-insensitive by default. It switches to * Smart case: the search is case-insensitive by default. It switches to
case-sensitive if the pattern contains an uppercase case-sensitive if the pattern contains an uppercase
@ -535,7 +535,7 @@ Make sure that `$HOME/.local/bin` is in your `$PATH`.
If you use an older version of Ubuntu, you can download the latest `.deb` package from the If you use an older version of Ubuntu, you can download the latest `.deb` package from the
[release page](https://github.com/sharkdp/fd/releases) and install it via: [release page](https://github.com/sharkdp/fd/releases) and install it via:
``` bash ``` bash
sudo dpkg -i fd_8.3.0_amd64.deb # adapt version number and architecture sudo dpkg -i fd_8.3.2_amd64.deb # adapt version number and architecture
``` ```
### On Debian ### On Debian
@ -557,12 +557,6 @@ Starting with Fedora 28, you can install `fd` from the official package sources:
dnf install fd-find dnf install fd-find
``` ```
For older versions, you can use this [Fedora copr](https://copr.fedorainfracloud.org/coprs/keefle/fd/) to install `fd`:
``` bash
dnf copr enable keefle/fd
dnf install fd
```
### On Alpine Linux ### On Alpine Linux
You can install [the fd package](https://pkgs.alpinelinux.org/packages?name=fd) You can install [the fd package](https://pkgs.alpinelinux.org/packages?name=fd)
@ -652,7 +646,7 @@ With Rust's package manager [cargo](https://github.com/rust-lang/cargo), you can
``` ```
cargo install fd-find cargo install fd-find
``` ```
Note that rust version *1.53.0* or later is required. Note that rust version *1.56.0* or later is required.
`make` is also needed for the build. `make` is also needed for the build.

View File

@ -1,11 +1,13 @@
use std::fs; use std::fs;
use clap::Shell; use clap_complete::{generate_to, Shell};
use Shell::*;
//use clap_complete::shells::Shel{Bash, Fish, PowerShell, Elvish};
include!("src/app.rs"); include!("src/app.rs");
fn main() { fn main() {
let min_version = "1.53"; let min_version = "1.56";
match version_check::is_min_version(min_version) { match version_check::is_min_version(min_version) {
Some(true) => {} Some(true) => {}
@ -24,7 +26,8 @@ fn main() {
fs::create_dir_all(&outdir).unwrap(); fs::create_dir_all(&outdir).unwrap();
let mut app = build_app(); let mut app = build_app();
app.gen_completions("fd", Shell::Bash, &outdir); // NOTE: zsh completions are hand written in contrib/completion/_fd
app.gen_completions("fd", Shell::Fish, &outdir); for shell in [Bash, PowerShell, Fish, Elvish] {
app.gen_completions("fd", Shell::PowerShell, &outdir); generate_to(shell, &mut app, "fd", &outdir).unwrap();
}
} }

View File

@ -1 +1 @@
msrv = "1.53.0" msrv = "1.56.0"

View File

@ -57,6 +57,7 @@ _fd() {
+ no-ignore-partial # some ignore files + no-ignore-partial # some ignore files
"(no-ignore-full --no-ignore-vcs)--no-ignore-vcs[don't respect .gitignore files]" "(no-ignore-full --no-ignore-vcs)--no-ignore-vcs[don't respect .gitignore files]"
"!(no-ignore-full --no-global-ignore-file)--no-global-ignore-file[don't respect the global ignore file]" "!(no-ignore-full --no-global-ignore-file)--no-global-ignore-file[don't respect the global ignore file]"
$no'(no-ignore-full --no-ignore-parent)--no-ignore-parent[]'
+ '(case)' # case-sensitivity + '(case)' # case-sensitivity
{-s,--case-sensitive}'[perform a case-sensitive search]' {-s,--case-sensitive}'[perform a case-sensitive search]'
@ -154,6 +155,9 @@ _fd() {
$no'(--base-directory)--base-directory=[change the current working directory to the given path]:directory:_files -/' $no'(--base-directory)--base-directory=[change the current working directory to the given path]:directory:_files -/'
$no'(*)*--search-path=[set search path (instead of positional <path> arguments)]:directory:_files -/' $no'(*)*--search-path=[set search path (instead of positional <path> arguments)]:directory:_files -/'
+ strip-cwd-prefix
$no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix[Strip ./ prefix when output is redirected]'
+ args # positional arguments + args # positional arguments
'1: :_guard "^-*" pattern' '1: :_guard "^-*" pattern'
'(--search-path)*:directory:_files -/' '(--search-path)*:directory:_files -/'

25
doc/fd.1 vendored
View File

@ -24,6 +24,11 @@ fd \- find entries in the filesystem
.B fd .B fd
is a simple, fast and user-friendly alternative to is a simple, fast and user-friendly alternative to
.BR find (1). .BR find (1).
.P
By default
.B fd
uses regular expressions for the pattern. However, this can be changed to use simple glob patterns
with the '\-\-glob' option.
.SH OPTIONS .SH OPTIONS
.TP .TP
.B \-H, \-\-hidden .B \-H, \-\-hidden
@ -66,6 +71,10 @@ git setting, which defaults to
.IR $HOME/.config/git/ignore ). .IR $HOME/.config/git/ignore ).
The flag can be overridden with '--ignore-vcs'. The flag can be overridden with '--ignore-vcs'.
.TP .TP
.B \-\-no\-ignore\-parent
Show search results from files and directories that would otherwise be ignored by gitignore files in
parent directories.
.TP
.B \-s, \-\-case\-sensitive .B \-s, \-\-case\-sensitive
Perform a case-sensitive search. By default, fd uses case-insensitive searches, unless the Perform a case-sensitive search. By default, fd uses case-insensitive searches, unless the
pattern contains an uppercase character (smart case). pattern contains an uppercase character (smart case).
@ -126,6 +135,10 @@ can be used as an alias.
Enable the display of filesystem errors for situations such as insufficient Enable the display of filesystem errors for situations such as insufficient
permissions or dead symlinks. permissions or dead symlinks.
.TP .TP
.B \-\-strip-cwd-prefix
By default, relative paths are prefixed with './' when the output goes to a non interactive terminal
(TTY). Use this flag to disable this behaviour.
.TP
.B \-\-one\-file\-system, \-\-mount, \-\-xdev .B \-\-one\-file\-system, \-\-mount, \-\-xdev
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). 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).
.TP .TP
@ -328,6 +341,9 @@ Note that all subsequent positional arguments are considered to be arguments to
It is therefore recommended to place the \-x/\-\-exec option last. Alternatively, you can supply It is therefore recommended to place the \-x/\-\-exec option last. Alternatively, you can supply
a ';' argument to end the argument list and continue with more fd options. a ';' argument to end the argument list and continue with more fd options.
Most shells require ';' to be escaped: '\\;'. Most shells require ';' to be escaped: '\\;'.
This option can be specified multiple times, in which case all commands are run for each
file found, in the order they are provided. In that case, you must supply a ';' argument for
all but the last commands.
The following placeholders are substituted before the command is executed: The following placeholders are substituted before the command is executed:
.RS .RS
@ -381,6 +397,9 @@ basename without file extension
If no placeholder is present, an implicit "{}" at the end is assumed. If no placeholder is present, an implicit "{}" at the end is assumed.
Like \-\-exec, this can be used multiple times, in which case each command will be run in
the order given.
Examples: Examples:
- Find all test_*.py files and open them in your favorite editor: - Find all test_*.py files and open them in your favorite editor:
@ -393,7 +412,11 @@ Examples:
fd -e rs -X wc -l fd -e rs -X wc -l
.RE .RE
.TP
.BI "\-\-batch-size " size
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.
.SH PATTERN SYNTAX .SH PATTERN SYNTAX
The regular expression syntax used by fd is documented here: The regular expression syntax used by fd is documented here:

BIN
doc/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

161
doc/logo.svg vendored Normal file
View File

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="66mm"
height="66mm"
viewBox="0 0 66 66.000001"
version="1.1"
id="svg5"
inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
sodipodi:docname="logo.svg"
inkscape:export-filename="/home/shark/Informatik/rust/fd/doc/logo.png"
inkscape:export-xdpi="192.42"
inkscape:export-ydpi="192.42"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="1"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="2.1795515"
inkscape:cx="114.47309"
inkscape:cy="176.18304"
inkscape:window-width="1920"
inkscape:window-height="1175"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="false"
inkscape:guide-bbox="true"
inkscape:snap-global="false"
fit-margin-top="2"
fit-margin-left="2"
fit-margin-right="2"
fit-margin-bottom="2"
lock-margins="true">
<sodipodi:guide
position="26.228232,26.126763"
orientation="0,-1"
id="guide47826" />
<sodipodi:guide
position="25.799494,2.3628924"
orientation="0,-1"
id="guide47828" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-21.358009,-148.28012)">
<g
id="g66267"
transform="matrix(0.84959471,0,0,0.84959471,7.9920783,43.351816)">
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:29.7126px;line-height:0;font-family:'Fira Sans Condensed';-inkscape-font-specification:'Fira Sans Condensed, ';white-space:pre;inline-size:37.3715;fill:#e5e5e5;fill-opacity:1;stroke-width:0.742816"
x="50.647034"
y="173.19841"
id="text50653"
transform="matrix(1.0604862,0,0,1.0604862,-3.3101428,-10.150043)"><tspan
x="50.647034"
y="173.19841"
id="tspan66635"><tspan
style="font-family:'Source Code Pro';-inkscape-font-specification:'Source Code Pro'"
id="tspan66633">fd</tspan></tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:29.7126px;line-height:0;font-family:'Fira Sans Condensed';-inkscape-font-specification:'Fira Sans Condensed, ';white-space:pre;inline-size:37.3715;fill:#00ccff;fill-opacity:0.996078;stroke-width:0.742816"
x="50.647034"
y="173.19841"
id="text1244"
transform="matrix(1.0604862,0,0,1.0604862,-2.8008599,-9.6407599)"><tspan
x="50.647034"
y="173.19841"
id="tspan66639"><tspan
style="font-family:'Source Code Pro';-inkscape-font-specification:'Source Code Pro'"
id="tspan66637">fd</tspan></tspan></text>
<g
id="g47824"
transform="translate(0.1724878,-0.35338542)">
<g
id="g42041">
<path
style="fill:none;stroke:#939dac;stroke-width:1.065;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 28.209616,155.51329 v 15.68758 H 40.83001"
id="path39763"
sodipodi:nodetypes="ccc" />
<path
style="fill:#b7bec8;stroke:#939dac;stroke-width:1.065;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 27.869464,161.83767 H 40.261291"
id="path39765" />
</g>
<g
id="g41945"
transform="translate(-1.0583333)">
<path
style="fill:#0088aa;fill-opacity:0.993797;stroke:none;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 42.164961,159.42939 -1.554274,-1.89462 -1.975227,-0.005 c -0.941841,0.014 -1.165466,0.27232 -1.14085,2.88812 z"
id="path40006"
sodipodi:nodetypes="ccccc" />
<rect
style="fill:#01ccff;fill-opacity:1;stroke:none;stroke-width:2.3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.80126;paint-order:stroke fill markers"
id="rect39949"
width="9.4925022"
height="6.2080379"
x="37.492516"
y="158.82776"
ry="0.90871465" />
</g>
<g
id="g41951"
transform="translate(-1.0583334,9.3665773)">
<path
style="fill:#373e48;fill-opacity:0.993797;stroke:none;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 42.164961,159.42939 -1.554274,-1.89462 -1.975227,-0.005 c -0.941841,0.014 -1.165466,0.27232 -1.14085,2.88812 z"
id="path41947"
sodipodi:nodetypes="ccccc" />
<rect
style="fill:#535d6c;fill-opacity:0.993797;stroke:none;stroke-width:2.3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.80126;paint-order:stroke fill markers"
id="rect41949"
width="9.4925022"
height="6.2080379"
x="37.492516"
y="158.82776"
ry="0.90871465" />
</g>
<g
id="g41957"
transform="translate(-14.306994,-6.8962642)">
<path
style="fill:#373e48;fill-opacity:0.993797;stroke:none;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 42.164961,159.42939 -1.554274,-1.89462 -1.975227,-0.005 c -0.941841,0.014 -1.165466,0.27232 -1.14085,2.88812 z"
id="path41953"
sodipodi:nodetypes="ccccc" />
<rect
style="fill:#535d6c;fill-opacity:0.993797;stroke:none;stroke-width:2.3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.80126;paint-order:stroke fill markers"
id="rect41955"
width="9.4925022"
height="6.2080379"
x="37.492516"
y="158.82776"
ry="0.90871465" />
</g>
</g>
<g
id="g65006"
transform="matrix(0.55302761,0,0,0.55302761,66.463548,117.45819)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -1,25 +1,25 @@
use clap::{crate_version, App, AppSettings, Arg}; use clap::{crate_version, AppSettings, Arg, ColorChoice, Command};
pub fn build_app() -> App<'static, 'static> { pub fn build_app() -> Command<'static> {
let clap_color_setting = if std::env::var_os("NO_COLOR").is_none() { let clap_color_choice = if std::env::var_os("NO_COLOR").is_none() {
AppSettings::ColoredHelp ColorChoice::Auto
} else { } else {
AppSettings::ColorNever ColorChoice::Never
}; };
let mut app = App::new("fd") let mut app = Command::new("fd")
.version(crate_version!()) .version(crate_version!())
.usage("fd [FLAGS/OPTIONS] [<pattern>] [<path>...]") .color(clap_color_choice)
.setting(clap_color_setting)
.setting(AppSettings::DeriveDisplayOrder) .setting(AppSettings::DeriveDisplayOrder)
.dont_collapse_args_in_usage(true)
.after_help( .after_help(
"Note: `fd -h` prints a short and concise overview while `fd --help` gives all \ "Note: `fd -h` prints a short and concise overview while `fd --help` gives all \
details.", details.",
) )
.arg( .arg(
Arg::with_name("hidden") Arg::new("hidden")
.long("hidden") .long("hidden")
.short("H") .short('H')
.overrides_with("hidden") .overrides_with("hidden")
.help("Search hidden files and directories") .help("Search hidden files and directories")
.long_help( .long_help(
@ -30,18 +30,18 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("no-hidden") Arg::new("no-hidden")
.long("no-hidden") .long("no-hidden")
.overrides_with("hidden") .overrides_with("hidden")
.hidden(true) .hide(true)
.long_help( .long_help(
"Overrides --hidden.", "Overrides --hidden.",
), ),
) )
.arg( .arg(
Arg::with_name("no-ignore") Arg::new("no-ignore")
.long("no-ignore") .long("no-ignore")
.short("I") .short('I')
.overrides_with("no-ignore") .overrides_with("no-ignore")
.help("Do not respect .(git|fd)ignore files") .help("Do not respect .(git|fd)ignore files")
.long_help( .long_help(
@ -51,19 +51,19 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("ignore") Arg::new("ignore")
.long("ignore") .long("ignore")
.overrides_with("no-ignore") .overrides_with("no-ignore")
.hidden(true) .hide(true)
.long_help( .long_help(
"Overrides --no-ignore.", "Overrides --no-ignore.",
), ),
) )
.arg( .arg(
Arg::with_name("no-ignore-vcs") Arg::new("no-ignore-vcs")
.long("no-ignore-vcs") .long("no-ignore-vcs")
.overrides_with("no-ignore-vcs") .overrides_with("no-ignore-vcs")
.hidden_short_help(true) .hide_short_help(true)
.help("Do not respect .gitignore files") .help("Do not respect .gitignore files")
.long_help( .long_help(
"Show search results from files and directories that would otherwise be \ "Show search results from files and directories that would otherwise be \
@ -71,19 +71,19 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("ignore-vcs") Arg::new("ignore-vcs")
.long("ignore-vcs") .long("ignore-vcs")
.overrides_with("no-ignore-vcs") .overrides_with("no-ignore-vcs")
.hidden(true) .hide(true)
.long_help( .long_help(
"Overrides --no-ignore-vcs.", "Overrides --no-ignore-vcs.",
), ),
) )
.arg( .arg(
Arg::with_name("no-ignore-parent") Arg::new("no-ignore-parent")
.long("no-ignore-parent") .long("no-ignore-parent")
.overrides_with("no-ignore-parent") .overrides_with("no-ignore-parent")
.hidden_short_help(true) .hide_short_help(true)
.help("Do not respect .(git|fd)ignore files in parent directories") .help("Do not respect .(git|fd)ignore files in parent directories")
.long_help( .long_help(
"Show search results from files and directories that would otherwise be \ "Show search results from files and directories that would otherwise be \
@ -91,19 +91,19 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("no-global-ignore-file") Arg::new("no-global-ignore-file")
.long("no-global-ignore-file") .long("no-global-ignore-file")
.hidden(true) .hide(true)
.help("Do not respect the global ignore file") .help("Do not respect the global ignore file")
.long_help("Do not respect the global ignore file."), .long_help("Do not respect the global ignore file."),
) )
.arg( .arg(
Arg::with_name("rg-alias-hidden-ignore") Arg::new("rg-alias-hidden-ignore")
.short("u") .short('u')
.long("unrestricted") .long("unrestricted")
.overrides_with_all(&["ignore", "no-hidden"]) .overrides_with_all(&["ignore", "no-hidden"])
.multiple(true) .multiple_occurrences(true)
.hidden_short_help(true) .hide_short_help(true)
.help("Alias for '--no-ignore', and '--hidden' when given twice") .help("Alias for '--no-ignore', and '--hidden' when given twice")
.long_help( .long_help(
"Alias for '--no-ignore'. Can be repeated. '-uu' is an alias for \ "Alias for '--no-ignore'. Can be repeated. '-uu' is an alias for \
@ -111,9 +111,9 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("case-sensitive") Arg::new("case-sensitive")
.long("case-sensitive") .long("case-sensitive")
.short("s") .short('s')
.overrides_with_all(&["ignore-case", "case-sensitive"]) .overrides_with_all(&["ignore-case", "case-sensitive"])
.help("Case-sensitive search (default: smart case)") .help("Case-sensitive search (default: smart case)")
.long_help( .long_help(
@ -123,9 +123,9 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("ignore-case") Arg::new("ignore-case")
.long("ignore-case") .long("ignore-case")
.short("i") .short('i')
.overrides_with_all(&["case-sensitive", "ignore-case"]) .overrides_with_all(&["case-sensitive", "ignore-case"])
.help("Case-insensitive search (default: smart case)") .help("Case-insensitive search (default: smart case)")
.long_help( .long_help(
@ -135,19 +135,19 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("glob") Arg::new("glob")
.long("glob") .long("glob")
.short("g") .short('g')
.conflicts_with("fixed-strings") .conflicts_with("fixed-strings")
.overrides_with("glob") .overrides_with("glob")
.help("Glob-based search (default: regular expression)") .help("Glob-based search (default: regular expression)")
.long_help("Perform a glob-based search instead of a regular expression search."), .long_help("Perform a glob-based search instead of a regular expression search."),
) )
.arg( .arg(
Arg::with_name("regex") Arg::new("regex")
.long("regex") .long("regex")
.overrides_with_all(&["glob", "regex"]) .overrides_with_all(&["glob", "regex"])
.hidden_short_help(true) .hide_short_help(true)
.help("Regular-expression based search (default)") .help("Regular-expression based search (default)")
.long_help( .long_help(
"Perform a regular-expression based search (default). This can be used to \ "Perform a regular-expression based search (default). This can be used to \
@ -155,12 +155,12 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("fixed-strings") Arg::new("fixed-strings")
.long("fixed-strings") .long("fixed-strings")
.short("F") .short('F')
.alias("literal") .alias("literal")
.overrides_with("fixed-strings") .overrides_with("fixed-strings")
.hidden_short_help(true) .hide_short_help(true)
.help("Treat pattern as literal string instead of regex") .help("Treat pattern as literal string instead of regex")
.long_help( .long_help(
"Treat the pattern as a literal string instead of a regular expression. Note \ "Treat the pattern as a literal string instead of a regular expression. Note \
@ -169,9 +169,9 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("absolute-path") Arg::new("absolute-path")
.long("absolute-path") .long("absolute-path")
.short("a") .short('a')
.overrides_with("absolute-path") .overrides_with("absolute-path")
.help("Show absolute instead of relative paths") .help("Show absolute instead of relative paths")
.long_help( .long_help(
@ -180,18 +180,18 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("relative-path") Arg::new("relative-path")
.long("relative-path") .long("relative-path")
.overrides_with("absolute-path") .overrides_with("absolute-path")
.hidden(true) .hide(true)
.long_help( .long_help(
"Overrides --absolute-path.", "Overrides --absolute-path.",
), ),
) )
.arg( .arg(
Arg::with_name("list-details") Arg::new("list-details")
.long("list-details") .long("list-details")
.short("l") .short('l')
.conflicts_with("absolute-path") .conflicts_with("absolute-path")
.help("Use a long listing format with file metadata") .help("Use a long listing format with file metadata")
.long_help( .long_help(
@ -202,9 +202,9 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("follow") Arg::new("follow")
.long("follow") .long("follow")
.short("L") .short('L')
.alias("dereference") .alias("dereference")
.overrides_with("follow") .overrides_with("follow")
.help("Follow symbolic links") .help("Follow symbolic links")
@ -215,18 +215,18 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("no-follow") Arg::new("no-follow")
.long("no-follow") .long("no-follow")
.overrides_with("follow") .overrides_with("follow")
.hidden(true) .hide(true)
.long_help( .long_help(
"Overrides --follow.", "Overrides --follow.",
), ),
) )
.arg( .arg(
Arg::with_name("full-path") Arg::new("full-path")
.long("full-path") .long("full-path")
.short("p") .short('p')
.overrides_with("full-path") .overrides_with("full-path")
.help("Search full abs. path (default: filename only)") .help("Search full abs. path (default: filename only)")
.long_help( .long_help(
@ -237,12 +237,12 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("null_separator") Arg::new("null_separator")
.long("print0") .long("print0")
.short("0") .short('0')
.overrides_with("print0") .overrides_with("print0")
.conflicts_with("list-details") .conflicts_with("list-details")
.hidden_short_help(true) .hide_short_help(true)
.help("Separate results by the null character") .help("Separate results by the null character")
.long_help( .long_help(
"Separate search results by the null character (instead of newlines). \ "Separate search results by the null character (instead of newlines). \
@ -250,9 +250,9 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("max-depth") Arg::new("max-depth")
.long("max-depth") .long("max-depth")
.short("d") .short('d')
.takes_value(true) .takes_value(true)
.value_name("depth") .value_name("depth")
.help("Set maximum search depth (default: none)") .help("Set maximum search depth (default: none)")
@ -263,18 +263,18 @@ pub fn build_app() -> App<'static, 'static> {
) )
// support --maxdepth as well, for compatibility with rg // support --maxdepth as well, for compatibility with rg
.arg( .arg(
Arg::with_name("rg-depth") Arg::new("rg-depth")
.long("maxdepth") .long("maxdepth")
.hidden(true) .hide(true)
.takes_value(true) .takes_value(true)
.help("Set maximum search depth (default: none)") .help("Set maximum search depth (default: none)")
) )
.arg( .arg(
Arg::with_name("min-depth") Arg::new("min-depth")
.long("min-depth") .long("min-depth")
.takes_value(true) .takes_value(true)
.value_name("depth") .value_name("depth")
.hidden_short_help(true) .hide_short_help(true)
.help("Only show results starting at given depth") .help("Only show results starting at given depth")
.long_help( .long_help(
"Only show search results starting at the given depth. \ "Only show search results starting at the given depth. \
@ -282,11 +282,11 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("exact-depth") Arg::new("exact-depth")
.long("exact-depth") .long("exact-depth")
.takes_value(true) .takes_value(true)
.value_name("depth") .value_name("depth")
.hidden_short_help(true) .hide_short_help(true)
.conflicts_with_all(&["max-depth", "min-depth"]) .conflicts_with_all(&["max-depth", "min-depth"])
.help("Only show results at exact given depth") .help("Only show results at exact given depth")
.long_help( .long_help(
@ -295,19 +295,19 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("prune") Arg::new("prune")
.long("prune") .long("prune")
.conflicts_with_all(&["size", "exact-depth"]) .conflicts_with_all(&["size", "exact-depth"])
.hidden_short_help(true) .hide_short_help(true)
.help("Do not traverse into matching directories") .help("Do not traverse into matching directories")
.long_help("Do not traverse into directories that match the search criteria. If \ .long_help("Do not traverse into directories that match the search criteria. If \
you want to exclude specific directories, use the '--exclude=' option.") you want to exclude specific directories, use the '--exclude=' option.")
) )
.arg( .arg(
Arg::with_name("file-type") Arg::new("file-type")
.long("type") .long("type")
.short("t") .short('t')
.multiple(true) .multiple_occurrences(true)
.number_of_values(1) .number_of_values(1)
.takes_value(true) .takes_value(true)
.value_name("filetype") .value_name("filetype")
@ -366,10 +366,10 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("extension") Arg::new("extension")
.long("extension") .long("extension")
.short("e") .short('e')
.multiple(true) .multiple_occurrences(true)
.number_of_values(1) .number_of_values(1)
.takes_value(true) .takes_value(true)
.value_name("ext") .value_name("ext")
@ -382,10 +382,11 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("exec") Arg::new("exec")
.long("exec") .long("exec")
.short("x") .short('x')
.min_values(1) .min_values(1)
.multiple_occurrences(true)
.allow_hyphen_values(true) .allow_hyphen_values(true)
.value_terminator(";") .value_terminator(";")
.value_name("cmd") .value_name("cmd")
@ -413,10 +414,11 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("exec-batch") Arg::new("exec-batch")
.long("exec-batch") .long("exec-batch")
.short("X") .short('X')
.min_values(1) .min_values(1)
.multiple_occurrences(true)
.allow_hyphen_values(true) .allow_hyphen_values(true)
.value_terminator(";") .value_terminator(";")
.value_name("cmd") .value_name("cmd")
@ -440,11 +442,11 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("batch-size") Arg::new("batch-size")
.long("batch-size") .long("batch-size")
.takes_value(true) .takes_value(true)
.value_name("size") .value_name("size")
.hidden_short_help(true) .hide_short_help(true)
.requires("exec-batch") .requires("exec-batch")
.help("Max number of arguments to run as a batch with -X") .help("Max number of arguments to run as a batch with -X")
.long_help( .long_help(
@ -455,13 +457,13 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("exclude") Arg::new("exclude")
.long("exclude") .long("exclude")
.short("E") .short('E')
.takes_value(true) .takes_value(true)
.value_name("pattern") .value_name("pattern")
.number_of_values(1) .number_of_values(1)
.multiple(true) .multiple_occurrences(true)
.help("Exclude entries that match the given glob pattern") .help("Exclude entries that match the given glob pattern")
.long_help( .long_help(
"Exclude files/directories that match the given glob pattern. This \ "Exclude files/directories that match the given glob pattern. This \
@ -473,13 +475,13 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("ignore-file") Arg::new("ignore-file")
.long("ignore-file") .long("ignore-file")
.takes_value(true) .takes_value(true)
.value_name("path") .value_name("path")
.number_of_values(1) .number_of_values(1)
.multiple(true) .multiple_occurrences(true)
.hidden_short_help(true) .hide_short_help(true)
.help("Add custom ignore-file in '.gitignore' format") .help("Add custom ignore-file in '.gitignore' format")
.long_help( .long_help(
"Add a custom ignore-file in '.gitignore' format. These files have a low \ "Add a custom ignore-file in '.gitignore' format. These files have a low \
@ -487,9 +489,9 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("color") Arg::new("color")
.long("color") .long("color")
.short("c") .short('c')
.takes_value(true) .takes_value(true)
.value_name("when") .value_name("when")
.possible_values(&["never", "auto", "always"]) .possible_values(&["never", "auto", "always"])
@ -503,12 +505,12 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("threads") Arg::new("threads")
.long("threads") .long("threads")
.short("j") .short('j')
.takes_value(true) .takes_value(true)
.value_name("num") .value_name("num")
.hidden_short_help(true) .hide_short_help(true)
.help("Set number of threads") .help("Set number of threads")
.long_help( .long_help(
"Set number of threads to use for searching & executing (default: number \ "Set number of threads to use for searching & executing (default: number \
@ -516,13 +518,13 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("size") Arg::new("size")
.long("size") .long("size")
.short("S") .short('S')
.takes_value(true) .takes_value(true)
.number_of_values(1) .number_of_values(1)
.allow_hyphen_values(true) .allow_hyphen_values(true)
.multiple(true) .multiple_occurrences(true)
.help("Limit results based on the size of files") .help("Limit results based on the size of files")
.long_help( .long_help(
"Limit results based on the size of files using the format <+-><NUM><UNIT>.\n \ "Limit results based on the size of files using the format <+-><NUM><UNIT>.\n \
@ -544,10 +546,10 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("max-buffer-time") Arg::new("max-buffer-time")
.long("max-buffer-time") .long("max-buffer-time")
.takes_value(true) .takes_value(true)
.hidden(true) .hide(true)
.help("Milliseconds to buffer before streaming search results to console") .help("Milliseconds to buffer before streaming search results to console")
.long_help( .long_help(
"Amount of time in milliseconds to buffer, before streaming the search \ "Amount of time in milliseconds to buffer, before streaming the search \
@ -555,7 +557,7 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("changed-within") Arg::new("changed-within")
.long("changed-within") .long("changed-within")
.alias("change-newer-than") .alias("change-newer-than")
.alias("newer") .alias("newer")
@ -575,7 +577,7 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("changed-before") Arg::new("changed-before")
.long("changed-before") .long("changed-before")
.alias("change-older-than") .alias("change-older-than")
.alias("older") .alias("older")
@ -594,7 +596,7 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("max-results") Arg::new("max-results")
.long("max-results") .long("max-results")
.takes_value(true) .takes_value(true)
.value_name("count") .value_name("count")
@ -604,14 +606,14 @@ pub fn build_app() -> App<'static, 'static> {
// same search with `--exec rm` attached and get a reliable removal of // same search with `--exec rm` attached and get a reliable removal of
// the files they saw in the previous search. // the files they saw in the previous search.
.conflicts_with_all(&["exec", "exec-batch", "list-details"]) .conflicts_with_all(&["exec", "exec-batch", "list-details"])
.hidden_short_help(true) .hide_short_help(true)
.help("Limit number of search results") .help("Limit number of search results")
.long_help("Limit the number of search results to 'count' and quit immediately."), .long_help("Limit the number of search results to 'count' and quit immediately."),
) )
.arg( .arg(
Arg::with_name("max-one-result") Arg::new("max-one-result")
.short("1") .short('1')
.hidden_short_help(true) .hide_short_help(true)
.overrides_with("max-results") .overrides_with("max-results")
.conflicts_with_all(&["exec", "exec-batch", "list-details"]) .conflicts_with_all(&["exec", "exec-batch", "list-details"])
.help("Limit search to a single result") .help("Limit search to a single result")
@ -619,11 +621,11 @@ pub fn build_app() -> App<'static, 'static> {
This is an alias for '--max-results=1'.") This is an alias for '--max-results=1'.")
) )
.arg( .arg(
Arg::with_name("quiet") Arg::new("quiet")
.long("quiet") .long("quiet")
.short("q") .short('q')
.alias("has-results") .alias("has-results")
.hidden_short_help(true) .hide_short_help(true)
.conflicts_with_all(&["exec", "exec-batch", "list-details", "max-results"]) .conflicts_with_all(&["exec", "exec-batch", "list-details", "max-results"])
.help("Print nothing, exit code 0 if match found, 1 otherwise") .help("Print nothing, exit code 0 if match found, 1 otherwise")
.long_help( .long_help(
@ -634,9 +636,9 @@ pub fn build_app() -> App<'static, 'static> {
) )
) )
.arg( .arg(
Arg::with_name("show-errors") Arg::new("show-errors")
.long("show-errors") .long("show-errors")
.hidden_short_help(true) .hide_short_help(true)
.overrides_with("show-errors") .overrides_with("show-errors")
.help("Show filesystem errors") .help("Show filesystem errors")
.long_help( .long_help(
@ -645,12 +647,13 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("base-directory") Arg::new("base-directory")
.long("base-directory") .long("base-directory")
.takes_value(true) .takes_value(true)
.value_name("path") .value_name("path")
.number_of_values(1) .number_of_values(1)
.hidden_short_help(true) .allow_invalid_utf8(true)
.hide_short_help(true)
.help("Change current working directory") .help("Change current working directory")
.long_help( .long_help(
"Change the current working directory of fd to the provided path. This \ "Change the current working directory of fd to the provided path. This \
@ -661,7 +664,9 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("pattern").help( Arg::new("pattern")
.allow_invalid_utf8(true)
.help(
"the search pattern (a regular expression, unless '--glob' is used; optional)", "the search pattern (a regular expression, unless '--glob' is used; optional)",
).long_help( ).long_help(
"the search pattern which is either a regular expression (default) or a glob \ "the search pattern which is either a regular expression (default) or a glob \
@ -670,11 +675,11 @@ pub fn build_app() -> App<'static, 'static> {
pass '--' first, or it will be considered as a flag (fd -- '-foo').") pass '--' first, or it will be considered as a flag (fd -- '-foo').")
) )
.arg( .arg(
Arg::with_name("path-separator") Arg::new("path-separator")
.takes_value(true) .takes_value(true)
.value_name("separator") .value_name("separator")
.long("path-separator") .long("path-separator")
.hidden_short_help(true) .hide_short_help(true)
.help("Set path separator when printing file paths") .help("Set path separator when printing file paths")
.long_help( .long_help(
"Set the path separator to use when printing file paths. The default is \ "Set the path separator to use when printing file paths. The default is \
@ -682,8 +687,9 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("path") Arg::new("path")
.multiple(true) .multiple_occurrences(true)
.allow_invalid_utf8(true)
.help("the root directory for the filesystem search (optional)") .help("the root directory for the filesystem search (optional)")
.long_help( .long_help(
"The directory where the filesystem search is rooted (optional). If \ "The directory where the filesystem search is rooted (optional). If \
@ -691,25 +697,26 @@ pub fn build_app() -> App<'static, 'static> {
), ),
) )
.arg( .arg(
Arg::with_name("search-path") Arg::new("search-path")
.long("search-path") .long("search-path")
.takes_value(true) .takes_value(true)
.conflicts_with("path") .conflicts_with("path")
.multiple(true) .multiple_occurrences(true)
.hidden_short_help(true) .hide_short_help(true)
.number_of_values(1) .number_of_values(1)
.allow_invalid_utf8(true)
.help("Provide paths to search as an alternative to the positional <path>") .help("Provide paths to search as an alternative to the positional <path>")
.long_help( .long_help(
"Provide paths to search as an alternative to the positional <path> \ "Provide paths to search as an alternative to the positional <path> \
argument. Changes the usage to `fd [FLAGS/OPTIONS] --search-path <path> \ argument. Changes the usage to `fd [OPTIONS] --search-path <path> \
--search-path <path2> [<pattern>]`", --search-path <path2> [<pattern>]`",
), ),
) )
.arg( .arg(
Arg::with_name("strip-cwd-prefix") Arg::new("strip-cwd-prefix")
.long("strip-cwd-prefix") .long("strip-cwd-prefix")
.conflicts_with_all(&["path", "search-path"]) .conflicts_with_all(&["path", "search-path"])
.hidden_short_help(true) .hide_short_help(true)
.help("strip './' prefix from non-tty outputs") .help("strip './' prefix from non-tty outputs")
.long_help( .long_help(
"By default, relative paths are prefixed with './' when the output goes to a non \ "By default, relative paths are prefixed with './' when the output goes to a non \
@ -719,9 +726,9 @@ pub fn build_app() -> App<'static, 'static> {
if cfg!(unix) { if cfg!(unix) {
app = app.arg( app = app.arg(
Arg::with_name("owner") Arg::new("owner")
.long("owner") .long("owner")
.short("o") .short('o')
.takes_value(true) .takes_value(true)
.value_name("user:group") .value_name("user:group")
.help("Filter by owning user and/or group") .help("Filter by owning user and/or group")
@ -742,10 +749,10 @@ pub fn build_app() -> App<'static, 'static> {
// Provide aliases `mount` and `xdev` for people coming from `find`. // Provide aliases `mount` and `xdev` for people coming from `find`.
if cfg!(any(unix, windows)) { if cfg!(any(unix, windows)) {
app = app.arg( app = app.arg(
Arg::with_name("one-file-system") Arg::new("one-file-system")
.long("one-file-system") .long("one-file-system")
.aliases(&["mount", "xdev"]) .aliases(&["mount", "xdev"])
.hidden_short_help(true) .hide_short_help(true)
.help("Do not descend into a different file system") .help("Do not descend into a different file system")
.long_help( .long_help(
"By default, fd will traverse the file system tree as far as other options \ "By default, fd will traverse the file system tree as far as other options \
@ -758,3 +765,8 @@ pub fn build_app() -> App<'static, 'static> {
app app
} }
#[test]
fn verify_app() {
build_app().debug_assert()
}

View File

@ -3,7 +3,7 @@ use std::{path::PathBuf, sync::Arc, time::Duration};
use lscolors::LsColors; use lscolors::LsColors;
use regex::bytes::RegexSet; use regex::bytes::RegexSet;
use crate::exec::CommandTemplate; use crate::exec::CommandSet;
use crate::filetypes::FileTypes; use crate::filetypes::FileTypes;
#[cfg(unix)] #[cfg(unix)]
use crate::filter::OwnerFilter; use crate::filter::OwnerFilter;
@ -83,7 +83,7 @@ pub struct Config {
pub extensions: Option<RegexSet>, pub extensions: Option<RegexSet>,
/// If a value is supplied, each item found will be used to generate and execute commands. /// If a value is supplied, each item found will be used to generate and execute commands.
pub command: Option<Arc<CommandTemplate>>, pub command: Option<Arc<CommandSet>>,
/// Maximum number of search results to pass to each `command`. If zero, the number is /// Maximum number of search results to pass to each `command`. If zero, the number is
/// unlimited. /// unlimited.

View File

@ -6,47 +6,93 @@ use std::sync::Mutex;
use crate::error::print_error; use crate::error::print_error;
use crate::exit_codes::ExitCode; use crate::exit_codes::ExitCode;
/// Executes a command. struct Outputs {
pub fn execute_command( stdout: Vec<u8>,
mut cmd: Command, stderr: Vec<u8>,
out_perm: &Mutex<()>, }
enable_output_buffering: bool, struct OutputBuffer<'a> {
) -> ExitCode { output_permission: &'a Mutex<()>,
// Spawn the supplied command. outputs: Vec<Outputs>,
let output = if enable_output_buffering { }
cmd.output()
} else {
// If running on only one thread, don't buffer output
// Allows for viewing and interacting with intermediate command output
cmd.spawn().and_then(|c| c.wait_with_output())
};
// Then wait for the command to exit, if it was spawned. impl<'a> OutputBuffer<'a> {
match output { fn new(output_permission: &'a Mutex<()>) -> Self {
Ok(output) => { Self {
// While this lock is active, this thread will be the only thread allowed output_permission,
// to write its outputs. outputs: Vec::new(),
let _lock = out_perm.lock().unwrap();
let stdout = io::stdout();
let stderr = io::stderr();
let _ = stdout.lock().write_all(&output.stdout);
let _ = stderr.lock().write_all(&output.stderr);
if output.status.code() == Some(0) {
ExitCode::Success
} else {
ExitCode::GeneralError
}
} }
Err(ref why) if why.kind() == io::ErrorKind::NotFound => { }
print_error(format!("Command not found: {:?}", cmd));
ExitCode::GeneralError fn push(&mut self, stdout: Vec<u8>, stderr: Vec<u8>) {
self.outputs.push(Outputs { stdout, stderr });
}
fn write(self) {
// avoid taking the lock if there is nothing to do
if self.outputs.is_empty() {
return;
} }
Err(why) => { // While this lock is active, this thread will be the only thread allowed
print_error(format!("Problem while executing command: {}", why)); // to write its outputs.
ExitCode::GeneralError let _lock = self.output_permission.lock().unwrap();
let stdout = io::stdout();
let stderr = io::stderr();
let mut stdout = stdout.lock();
let mut stderr = stderr.lock();
for output in self.outputs.iter() {
let _ = stdout.write_all(&output.stdout);
let _ = stderr.write_all(&output.stderr);
} }
} }
} }
/// Executes a command.
pub fn execute_commands<I: Iterator<Item = Command>>(
cmds: I,
out_perm: &Mutex<()>,
enable_output_buffering: bool,
) -> ExitCode {
let mut output_buffer = OutputBuffer::new(out_perm);
for mut cmd in cmds {
// Spawn the supplied command.
let output = if enable_output_buffering {
cmd.output()
} else {
// If running on only one thread, don't buffer output
// Allows for viewing and interacting with intermediate command output
cmd.spawn().and_then(|c| c.wait_with_output())
};
// Then wait for the command to exit, if it was spawned.
match output {
Ok(output) => {
if enable_output_buffering {
output_buffer.push(output.stdout, output.stderr);
}
if output.status.code() != Some(0) {
output_buffer.write();
return ExitCode::GeneralError;
}
}
Err(why) => {
output_buffer.write();
return handle_cmd_error(&cmd, why);
}
}
}
output_buffer.write();
ExitCode::Success
}
pub fn handle_cmd_error(cmd: &Command, err: io::Error) -> ExitCode {
if err.kind() == io::ErrorKind::NotFound {
print_error(format!("Command not found: {:?}", cmd));
ExitCode::GeneralError
} else {
print_error(format!("Problem while executing command: {}", err));
ExitCode::GeneralError
}
}

View File

@ -5,13 +5,13 @@ use crate::filesystem::strip_current_dir;
/// Removes the parent component of the path /// Removes the parent component of the path
pub fn basename(path: &Path) -> &OsStr { pub fn basename(path: &Path) -> &OsStr {
path.file_name().unwrap_or_else(|| path.as_os_str()) path.file_name().unwrap_or(path.as_os_str())
} }
/// Removes the extension from the path /// Removes the extension from the path
pub fn remove_extension(path: &Path) -> OsString { pub fn remove_extension(path: &Path) -> OsString {
let dirname = dirname(path); let dirname = dirname(path);
let stem = path.file_stem().unwrap_or_else(|| path.as_os_str()); let stem = path.file_stem().unwrap_or(path.as_os_str());
let path = PathBuf::from(dirname).join(stem); let path = PathBuf::from(dirname).join(stem);

View File

@ -1,20 +1,21 @@
use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crossbeam_channel::Receiver;
use crate::dir_entry::DirEntry; use crate::dir_entry::DirEntry;
use crate::error::print_error; use crate::error::print_error;
use crate::exit_codes::{merge_exitcodes, ExitCode}; use crate::exit_codes::{merge_exitcodes, ExitCode};
use crate::walk::WorkerResult; use crate::walk::WorkerResult;
use super::CommandTemplate; use super::CommandSet;
/// An event loop that listens for inputs from the `rx` receiver. Each received input will /// An event loop that listens for inputs from the `rx` receiver. Each received input will
/// generate a command with the supplied command template. The generated command will then /// generate a command with the supplied command template. The generated command will then
/// be executed, and this process will continue until the receiver's sender has closed. /// be executed, and this process will continue until the receiver's sender has closed.
pub fn job( pub fn job(
rx: Arc<Mutex<Receiver<WorkerResult>>>, rx: Arc<Mutex<Receiver<WorkerResult>>>,
cmd: Arc<CommandTemplate>, cmd: Arc<CommandSet>,
out_perm: Arc<Mutex<()>>, out_perm: Arc<Mutex<()>>,
show_filesystem_errors: bool, show_filesystem_errors: bool,
buffer_output: bool, buffer_output: bool,
@ -40,11 +41,7 @@ pub fn job(
// Drop the lock so that other threads can read from the receiver. // Drop the lock so that other threads can read from the receiver.
drop(lock); drop(lock);
// Generate a command, execute it and store its exit code. // Generate a command, execute it and store its exit code.
results.push(cmd.generate_and_execute( results.push(cmd.execute(&dir_entry, Arc::clone(&out_perm), buffer_output))
dir_entry.path(),
Arc::clone(&out_perm),
buffer_output,
))
} }
// Returns error in case of any error. // Returns error in case of any error.
merge_exitcodes(results) merge_exitcodes(results)
@ -52,9 +49,8 @@ pub fn job(
pub fn batch( pub fn batch(
rx: Receiver<WorkerResult>, rx: Receiver<WorkerResult>,
cmd: &CommandTemplate, cmd: &CommandSet,
show_filesystem_errors: bool, show_filesystem_errors: bool,
buffer_output: bool,
limit: usize, limit: usize,
) -> ExitCode { ) -> ExitCode {
let paths = rx let paths = rx
@ -70,14 +66,14 @@ pub fn batch(
}); });
if limit == 0 { if limit == 0 {
// no limit // no limit
return cmd.generate_and_execute_batch(paths, buffer_output); return cmd.execute_batch(paths);
} }
let mut exit_codes = Vec::new(); let mut exit_codes = Vec::new();
let mut peekable = paths.peekable(); let mut peekable = paths.peekable();
while peekable.peek().is_some() { while peekable.peek().is_some() {
let limited = peekable.by_ref().take(limit); let limited = peekable.by_ref().take(limit);
let exit_code = cmd.generate_and_execute_batch(limited, buffer_output); let exit_code = cmd.execute_batch(limited);
exit_codes.push(exit_code); exit_codes.push(exit_code);
} }
merge_exitcodes(exit_codes) merge_exitcodes(exit_codes)

View File

@ -9,13 +9,13 @@ use std::path::{Component, Path, PathBuf, Prefix};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use anyhow::{anyhow, Result}; use anyhow::{bail, Result};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use crate::exit_codes::ExitCode; use crate::exit_codes::ExitCode;
use self::command::execute_command; use self::command::{execute_commands, handle_cmd_error};
use self::input::{basename, dirname, remove_extension}; use self::input::{basename, dirname, remove_extension};
pub use self::job::{batch, job}; pub use self::job::{batch, job};
use self::token::Token; use self::token::Token;
@ -29,44 +29,94 @@ pub enum ExecutionMode {
Batch, Batch,
} }
#[derive(Debug, Clone, PartialEq)]
pub struct CommandSet {
mode: ExecutionMode,
path_separator: Option<String>,
commands: Vec<CommandTemplate>,
}
impl CommandSet {
pub fn new<I, S>(input: I, path_separator: Option<String>) -> Result<CommandSet>
where
I: IntoIterator<Item = Vec<S>>,
S: AsRef<str>,
{
Ok(CommandSet {
mode: ExecutionMode::OneByOne,
path_separator,
commands: input
.into_iter()
.map(CommandTemplate::new)
.collect::<Result<_>>()?,
})
}
pub fn new_batch<I, S>(input: I, path_separator: Option<String>) -> Result<CommandSet>
where
I: IntoIterator<Item = Vec<S>>,
S: AsRef<str>,
{
Ok(CommandSet {
mode: ExecutionMode::Batch,
path_separator,
commands: input
.into_iter()
.map(|args| {
let cmd = CommandTemplate::new(args)?;
if cmd.number_of_tokens() > 1 {
bail!("Only one placeholder allowed for batch commands");
}
if cmd.args[0].has_tokens() {
bail!("First argument of exec-batch is expected to be a fixed executable");
}
Ok(cmd)
})
.collect::<Result<Vec<_>>>()?,
})
}
pub fn in_batch_mode(&self) -> bool {
self.mode == ExecutionMode::Batch
}
pub fn execute(&self, input: &Path, out_perm: Arc<Mutex<()>>, buffer_output: bool) -> ExitCode {
let path_separator = self.path_separator.as_deref();
let commands = self
.commands
.iter()
.map(|c| c.generate(input, path_separator));
execute_commands(commands, &out_perm, buffer_output)
}
pub fn execute_batch<I>(&self, paths: I) -> ExitCode
where
I: Iterator<Item = PathBuf>,
{
let path_separator = self.path_separator.as_deref();
let mut paths = paths.collect::<Vec<_>>();
paths.sort();
for cmd in &self.commands {
let exit = cmd.generate_and_execute_batch(&paths, path_separator);
if exit != ExitCode::Success {
return exit;
}
}
ExitCode::Success
}
}
/// Represents a template that is utilized to generate command strings. /// Represents a template that is utilized to generate command strings.
/// ///
/// The template is meant to be coupled with an input in order to generate a command. The /// The template is meant to be coupled with an input in order to generate a command. The
/// `generate_and_execute()` method will be used to generate a command and execute it. /// `generate_and_execute()` method will be used to generate a command and execute it.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct CommandTemplate { struct CommandTemplate {
args: Vec<ArgumentTemplate>, args: Vec<ArgumentTemplate>,
mode: ExecutionMode,
path_separator: Option<String>,
} }
impl CommandTemplate { impl CommandTemplate {
pub fn new<I, S>(input: I, path_separator: Option<String>) -> CommandTemplate fn new<I, S>(input: I) -> Result<CommandTemplate>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self::build(input, ExecutionMode::OneByOne, path_separator)
}
pub fn new_batch<I, S>(input: I, path_separator: Option<String>) -> Result<CommandTemplate>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let cmd = Self::build(input, ExecutionMode::Batch, path_separator);
if cmd.number_of_tokens() > 1 {
return Err(anyhow!("Only one placeholder allowed for batch commands"));
}
if cmd.args[0].has_tokens() {
return Err(anyhow!(
"First argument of exec-batch is expected to be a fixed executable"
));
}
Ok(cmd)
}
fn build<I, S>(input: I, mode: ExecutionMode, path_separator: Option<String>) -> CommandTemplate
where where
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
S: AsRef<str>, S: AsRef<str>,
@ -117,16 +167,21 @@ impl CommandTemplate {
args.push(ArgumentTemplate::Tokens(tokens)); args.push(ArgumentTemplate::Tokens(tokens));
} }
// We need to check that we have at least one argument, because if not
// it will try to execute each file and directory it finds.
//
// Sadly, clap can't currently handle this for us, see
// https://github.com/clap-rs/clap/issues/3542
if args.is_empty() {
bail!("No executable provided for --exec or --exec-batch");
}
// If a placeholder token was not supplied, append one at the end of the command. // If a placeholder token was not supplied, append one at the end of the command.
if !has_placeholder { if !has_placeholder {
args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder])); args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder]));
} }
CommandTemplate { Ok(CommandTemplate { args })
args,
mode,
path_separator,
}
} }
fn number_of_tokens(&self) -> usize { fn number_of_tokens(&self) -> usize {
@ -136,45 +191,31 @@ impl CommandTemplate {
/// Generates and executes a command. /// Generates and executes a command.
/// ///
/// Using the internal `args` field, and a supplied `input` variable, a `Command` will be /// Using the internal `args` field, and a supplied `input` variable, a `Command` will be
/// build. Once all arguments have been processed, the command is executed. /// build.
pub fn generate_and_execute( fn generate(&self, input: &Path, path_separator: Option<&str>) -> Command {
&self, let mut cmd = Command::new(self.args[0].generate(&input, path_separator));
input: &Path,
out_perm: Arc<Mutex<()>>,
buffer_output: bool,
) -> ExitCode {
let mut cmd = Command::new(self.args[0].generate(&input, self.path_separator.as_deref()));
for arg in &self.args[1..] { for arg in &self.args[1..] {
cmd.arg(arg.generate(&input, self.path_separator.as_deref())); cmd.arg(arg.generate(&input, path_separator));
} }
cmd
execute_command(cmd, &out_perm, buffer_output)
} }
pub fn in_batch_mode(&self) -> bool { fn generate_and_execute_batch(
self.mode == ExecutionMode::Batch &self,
} paths: &[PathBuf],
path_separator: Option<&str>,
pub fn generate_and_execute_batch<I>(&self, paths: I, buffer_output: bool) -> ExitCode ) -> ExitCode {
where
I: Iterator<Item = PathBuf>,
{
let mut cmd = Command::new(self.args[0].generate("", None)); let mut cmd = Command::new(self.args[0].generate("", None));
cmd.stdin(Stdio::inherit()); cmd.stdin(Stdio::inherit());
cmd.stdout(Stdio::inherit()); cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit()); cmd.stderr(Stdio::inherit());
let mut paths: Vec<_> = paths.collect();
let mut has_path = false; let mut has_path = false;
for arg in &self.args[1..] { for arg in &self.args[1..] {
if arg.has_tokens() { if arg.has_tokens() {
paths.sort(); for path in paths {
cmd.arg(arg.generate(path, path_separator));
// A single `Tokens` is expected
// So we can directly consume the iterator once and for all
for path in &mut paths {
cmd.arg(arg.generate(path, self.path_separator.as_deref()));
has_path = true; has_path = true;
} }
} else { } else {
@ -183,7 +224,10 @@ impl CommandTemplate {
} }
if has_path { if has_path {
execute_command(cmd, &Mutex::new(()), buffer_output) match cmd.spawn().and_then(|mut c| c.wait()) {
Ok(_) => ExitCode::Success,
Err(e) => handle_cmd_error(&cmd, e),
}
} else { } else {
ExitCode::Success ExitCode::Success
} }
@ -302,13 +346,15 @@ mod tests {
#[test] #[test]
fn tokens_with_placeholder() { fn tokens_with_placeholder() {
assert_eq!( assert_eq!(
CommandTemplate::new(&[&"echo", &"${SHELL}:"], None), CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]], None).unwrap(),
CommandTemplate { CommandSet {
args: vec![ commands: vec![CommandTemplate {
ArgumentTemplate::Text("echo".into()), args: vec![
ArgumentTemplate::Text("${SHELL}:".into()), ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Tokens(vec![Token::Placeholder]), ArgumentTemplate::Text("${SHELL}:".into()),
], ArgumentTemplate::Tokens(vec![Token::Placeholder]),
]
}],
mode: ExecutionMode::OneByOne, mode: ExecutionMode::OneByOne,
path_separator: None, path_separator: None,
} }
@ -318,12 +364,14 @@ mod tests {
#[test] #[test]
fn tokens_with_no_extension() { fn tokens_with_no_extension() {
assert_eq!( assert_eq!(
CommandTemplate::new(&["echo", "{.}"], None), CommandSet::new(vec![vec!["echo", "{.}"]], None).unwrap(),
CommandTemplate { CommandSet {
args: vec![ commands: vec![CommandTemplate {
ArgumentTemplate::Text("echo".into()), args: vec![
ArgumentTemplate::Tokens(vec![Token::NoExt]), ArgumentTemplate::Text("echo".into()),
], ArgumentTemplate::Tokens(vec![Token::NoExt]),
],
}],
mode: ExecutionMode::OneByOne, mode: ExecutionMode::OneByOne,
path_separator: None, path_separator: None,
} }
@ -333,12 +381,14 @@ mod tests {
#[test] #[test]
fn tokens_with_basename() { fn tokens_with_basename() {
assert_eq!( assert_eq!(
CommandTemplate::new(&["echo", "{/}"], None), CommandSet::new(vec![vec!["echo", "{/}"]], None).unwrap(),
CommandTemplate { CommandSet {
args: vec![ commands: vec![CommandTemplate {
ArgumentTemplate::Text("echo".into()), args: vec![
ArgumentTemplate::Tokens(vec![Token::Basename]), ArgumentTemplate::Text("echo".into()),
], ArgumentTemplate::Tokens(vec![Token::Basename]),
],
}],
mode: ExecutionMode::OneByOne, mode: ExecutionMode::OneByOne,
path_separator: None, path_separator: None,
} }
@ -348,12 +398,14 @@ mod tests {
#[test] #[test]
fn tokens_with_parent() { fn tokens_with_parent() {
assert_eq!( assert_eq!(
CommandTemplate::new(&["echo", "{//}"], None), CommandSet::new(vec![vec!["echo", "{//}"]], None).unwrap(),
CommandTemplate { CommandSet {
args: vec![ commands: vec![CommandTemplate {
ArgumentTemplate::Text("echo".into()), args: vec![
ArgumentTemplate::Tokens(vec![Token::Parent]), ArgumentTemplate::Text("echo".into()),
], ArgumentTemplate::Tokens(vec![Token::Parent]),
],
}],
mode: ExecutionMode::OneByOne, mode: ExecutionMode::OneByOne,
path_separator: None, path_separator: None,
} }
@ -363,12 +415,14 @@ mod tests {
#[test] #[test]
fn tokens_with_basename_no_extension() { fn tokens_with_basename_no_extension() {
assert_eq!( assert_eq!(
CommandTemplate::new(&["echo", "{/.}"], None), CommandSet::new(vec![vec!["echo", "{/.}"]], None).unwrap(),
CommandTemplate { CommandSet {
args: vec![ commands: vec![CommandTemplate {
ArgumentTemplate::Text("echo".into()), args: vec![
ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]), ArgumentTemplate::Text("echo".into()),
], ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]),
],
}],
mode: ExecutionMode::OneByOne, mode: ExecutionMode::OneByOne,
path_separator: None, path_separator: None,
} }
@ -378,16 +432,18 @@ mod tests {
#[test] #[test]
fn tokens_multiple() { fn tokens_multiple() {
assert_eq!( assert_eq!(
CommandTemplate::new(&["cp", "{}", "{/.}.ext"], None), CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]], None).unwrap(),
CommandTemplate { CommandSet {
args: vec![ commands: vec![CommandTemplate {
ArgumentTemplate::Text("cp".into()), args: vec![
ArgumentTemplate::Tokens(vec![Token::Placeholder]), ArgumentTemplate::Text("cp".into()),
ArgumentTemplate::Tokens(vec![ ArgumentTemplate::Tokens(vec![Token::Placeholder]),
Token::BasenameNoExt, ArgumentTemplate::Tokens(vec![
Token::Text(".ext".into()) Token::BasenameNoExt,
]), Token::Text(".ext".into())
], ]),
],
}],
mode: ExecutionMode::OneByOne, mode: ExecutionMode::OneByOne,
path_separator: None, path_separator: None,
} }
@ -397,12 +453,14 @@ mod tests {
#[test] #[test]
fn tokens_single_batch() { fn tokens_single_batch() {
assert_eq!( assert_eq!(
CommandTemplate::new_batch(&["echo", "{.}"], None).unwrap(), CommandSet::new_batch(vec![vec!["echo", "{.}"]], None).unwrap(),
CommandTemplate { CommandSet {
args: vec![ commands: vec![CommandTemplate {
ArgumentTemplate::Text("echo".into()), args: vec![
ArgumentTemplate::Tokens(vec![Token::NoExt]), ArgumentTemplate::Text("echo".into()),
], ArgumentTemplate::Tokens(vec![Token::NoExt]),
],
}],
mode: ExecutionMode::Batch, mode: ExecutionMode::Batch,
path_separator: None, path_separator: None,
} }
@ -411,7 +469,17 @@ mod tests {
#[test] #[test]
fn tokens_multiple_batch() { fn tokens_multiple_batch() {
assert!(CommandTemplate::new_batch(&["echo", "{.}", "{}"], None).is_err()); assert!(CommandSet::new_batch(vec![vec!["echo", "{.}", "{}"]], None).is_err());
}
#[test]
fn template_no_args() {
assert!(CommandTemplate::new::<Vec<_>, &'static str>(vec![]).is_err());
}
#[test]
fn command_set_no_args() {
assert!(CommandSet::new(vec![vec!["echo"], vec![]], None).is_err());
} }
#[test] #[test]

View File

@ -25,7 +25,7 @@ use regex::bytes::{RegexBuilder, RegexSetBuilder};
use crate::config::Config; use crate::config::Config;
use crate::error::print_error; use crate::error::print_error;
use crate::exec::CommandTemplate; 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)]
@ -40,7 +40,9 @@ use crate::regex_helper::{pattern_has_uppercase_char, pattern_matches_strings_wi
not(target_os = "android"), not(target_os = "android"),
not(target_os = "macos"), not(target_os = "macos"),
not(target_os = "freebsd"), not(target_os = "freebsd"),
not(target_env = "musl") not(target_env = "musl"),
not(target_arch = "riscv64"),
feature = "use-jemalloc"
))] ))]
#[global_allocator] #[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
@ -111,7 +113,7 @@ fn ensure_current_directory_exists(current_directory: &Path) -> Result<()> {
} }
} }
fn extract_search_pattern<'a>(matches: &'a clap::ArgMatches) -> Result<&'a str> { fn extract_search_pattern(matches: &clap::ArgMatches) -> Result<&'_ str> {
let pattern = matches let pattern = matches
.value_of_os("pattern") .value_of_os("pattern")
.map(|p| { .map(|p| {
@ -261,10 +263,7 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
read_vcsignore: !(matches.is_present("no-ignore") read_vcsignore: !(matches.is_present("no-ignore")
|| matches.is_present("rg-alias-hidden-ignore") || matches.is_present("rg-alias-hidden-ignore")
|| matches.is_present("no-ignore-vcs")), || matches.is_present("no-ignore-vcs")),
read_parent_ignore: !(matches.is_present("no-ignore") read_parent_ignore: !matches.is_present("no-ignore-parent"),
|| matches.is_present("rg-alias-hidden-ignore")
|| matches.is_present("no-ignore-vcs")
|| matches.is_present("no-ignore-parent")),
read_global_ignore: !(matches.is_present("no-ignore") read_global_ignore: !(matches.is_present("no-ignore")
|| matches.is_present("rg-alias-hidden-ignore") || matches.is_present("rg-alias-hidden-ignore")
|| matches.is_present("no-global-ignore-file")), || matches.is_present("no-global-ignore-file")),
@ -392,19 +391,16 @@ fn extract_command(
matches: &clap::ArgMatches, matches: &clap::ArgMatches,
path_separator: Option<&str>, path_separator: Option<&str>,
colored_output: bool, colored_output: bool,
) -> Result<Option<CommandTemplate>> { ) -> Result<Option<CommandSet>> {
None.or_else(|| { None.or_else(|| {
matches.values_of("exec").map(|args| { matches
Ok(CommandTemplate::new( .grouped_values_of("exec")
args, .map(|args| CommandSet::new(args, path_separator.map(str::to_string)))
path_separator.map(str::to_string),
))
})
}) })
.or_else(|| { .or_else(|| {
matches matches
.values_of("exec-batch") .grouped_values_of("exec-batch")
.map(|args| CommandTemplate::new_batch(args, path_separator.map(str::to_string))) .map(|args| CommandSet::new_batch(args, path_separator.map(str::to_string)))
}) })
.or_else(|| { .or_else(|| {
if !matches.is_present("list-details") { if !matches.is_present("list-details") {
@ -414,9 +410,8 @@ fn extract_command(
let color = matches.value_of("color").unwrap_or("auto"); let color = matches.value_of("color").unwrap_or("auto");
let color_arg = format!("--color={}", color); let color_arg = format!("--color={}", color);
let res = determine_ls_command(&color_arg, colored_output).map(|cmd| { let res = determine_ls_command(&color_arg, colored_output)
CommandTemplate::new_batch(cmd, path_separator.map(str::to_string)).unwrap() .map(|cmd| CommandSet::new_batch([cmd], path_separator.map(str::to_string)).unwrap());
});
Some(res) Some(res)
}) })

View File

@ -3,13 +3,13 @@ use std::io;
use std::mem; use std::mem;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError, Sender};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{borrow::Cow, io::Write}; use std::{borrow::Cow, io::Write};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Sender};
use ignore::overrides::OverrideBuilder; use ignore::overrides::OverrideBuilder;
use ignore::{self, WalkBuilder}; use ignore::{self, WalkBuilder};
use regex::bytes::Regex; use regex::bytes::Regex;
@ -54,7 +54,7 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> R
let first_path_buf = path_iter let first_path_buf = path_iter
.next() .next()
.expect("Error: Path vector can not be empty"); .expect("Error: Path vector can not be empty");
let (tx, rx) = unbounded(); let (tx, rx) = channel();
let mut override_builder = OverrideBuilder::new(first_path_buf.as_path()); let mut override_builder = OverrideBuilder::new(first_path_buf.as_path());
@ -71,7 +71,7 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> R
walker walker
.hidden(config.ignore_hidden) .hidden(config.ignore_hidden)
.ignore(config.read_fdignore) .ignore(config.read_fdignore)
.parents(config.read_parent_ignore) .parents(config.read_parent_ignore && (config.read_fdignore || config.read_vcsignore))
.git_ignore(config.read_vcsignore) .git_ignore(config.read_vcsignore)
.git_global(config.read_vcsignore) .git_global(config.read_vcsignore)
.git_exclude(config.read_vcsignore) .git_exclude(config.read_vcsignore)
@ -103,10 +103,7 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> R
match result { match result {
Some(ignore::Error::Partial(_)) => (), Some(ignore::Error::Partial(_)) => (),
Some(err) => { Some(err) => {
print_error(format!( print_error(format!("Malformed pattern in global ignore file. {}.", err));
"Malformed pattern in global ignore file. {}.",
err.to_string()
));
} }
None => (), None => (),
} }
@ -118,10 +115,7 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> R
match result { match result {
Some(ignore::Error::Partial(_)) => (), Some(ignore::Error::Partial(_)) => (),
Some(err) => { Some(err) => {
print_error(format!( print_error(format!("Malformed pattern in custom ignore file. {}.", err));
"Malformed pattern in custom ignore file. {}.",
err.to_string()
));
} }
None => (), None => (),
} }
@ -231,7 +225,11 @@ impl<W: Write> ReceiverBuffer<W> {
match self.mode { match self.mode {
ReceiverMode::Buffering => { ReceiverMode::Buffering => {
// Wait at most until we should switch to streaming // Wait at most until we should switch to streaming
self.rx.recv_deadline(self.deadline) let now = Instant::now();
self.deadline
.checked_duration_since(now)
.ok_or(RecvTimeoutError::Timeout)
.and_then(|t| self.rx.recv_timeout(t))
} }
ReceiverMode::Streaming => { ReceiverMode::Streaming => {
// Wait however long it takes for a result // Wait however long it takes for a result
@ -351,13 +349,7 @@ fn spawn_receiver(
// This will be set to `Some` if the `--exec` argument was supplied. // This will be set to `Some` if the `--exec` argument was supplied.
if let Some(ref cmd) = config.command { if let Some(ref cmd) = config.command {
if cmd.in_batch_mode() { if cmd.in_batch_mode() {
exec::batch( exec::batch(rx, cmd, show_filesystem_errors, config.batch_size)
rx,
cmd,
show_filesystem_errors,
enable_output_buffering,
config.batch_size,
)
} else { } else {
let shared_rx = Arc::new(Mutex::new(rx)); let shared_rx = Arc::new(Mutex::new(rx));

View File

@ -130,6 +130,17 @@ fn normalize_output(s: &str, trim_start: bool, normalize_line: bool) -> String {
lines.join("\n") lines.join("\n")
} }
/// Trim whitespace from the beginning of each line.
fn trim_lines(s: &str) -> String {
s.lines()
.map(|line| line.trim_start())
.fold(String::new(), |mut str, line| {
str.push_str(line);
str.push('\n');
str
})
}
impl TestEnv { impl TestEnv {
pub fn new(directories: &[&'static str], files: &[&'static str]) -> TestEnv { pub fn new(directories: &[&'static str], files: &[&'static str]) -> TestEnv {
let temp_dir = create_working_directory(directories, files).expect("working directory"); let temp_dir = create_working_directory(directories, files).expect("working directory");
@ -203,6 +214,19 @@ impl TestEnv {
output output
} }
pub fn assert_success_and_get_normalized_output<P: AsRef<Path>>(
&self,
path: P,
args: &[&str],
) -> String {
let output = self.assert_success_and_get_output(path, args);
normalize_output(
&String::from_utf8_lossy(&output.stdout),
false,
self.normalize_line,
)
}
/// Assert that calling *fd* with the specified arguments produces the expected output. /// Assert that calling *fd* with the specified arguments produces the expected output.
pub fn assert_output(&self, args: &[&str], expected: &str) { pub fn assert_output(&self, args: &[&str], expected: &str) {
self.assert_output_subdirectory(".", args, expected) self.assert_output_subdirectory(".", args, expected)
@ -224,15 +248,9 @@ impl TestEnv {
args: &[&str], args: &[&str],
expected: &str, expected: &str,
) { ) {
let output = self.assert_success_and_get_output(path, args);
// Normalize both expected and actual output. // Normalize both expected and actual output.
let expected = normalize_output(expected, true, self.normalize_line); let expected = normalize_output(expected, true, self.normalize_line);
let actual = normalize_output( let actual = self.assert_success_and_get_normalized_output(path, args);
&String::from_utf8_lossy(&output.stdout),
false,
self.normalize_line,
);
// Compare actual output to expected output. // Compare actual output to expected output.
if expected != actual { if expected != actual {
@ -280,12 +298,8 @@ impl TestEnv {
if let Some(expected) = expected { if let Some(expected) = expected {
// Normalize both expected and actual output. // Normalize both expected and actual output.
let expected_error = normalize_output(expected, true, self.normalize_line); let expected_error = trim_lines(expected);
let actual_err = normalize_output( let actual_err = trim_lines(&String::from_utf8_lossy(&output.stderr));
&String::from_utf8_lossy(&output.stderr),
false,
self.normalize_line,
);
// Compare actual output to expected output. // Compare actual output to expected output.
if !actual_err.trim_start().starts_with(&expected_error) { if !actual_err.trim_start().starts_with(&expected_error) {

View File

@ -614,6 +614,22 @@ fn test_no_ignore_vcs() {
); );
} }
/// Test that --no-ignore-vcs still respects .fdignored in parent directory
#[test]
fn test_no_ignore_vcs_child_dir() {
let te = TestEnv::new(
&["inner"],
&["inner/fdignored.foo", "inner/foo", "inner/gitignored.foo"],
);
te.assert_output_subdirectory(
"inner",
&["--no-ignore-vcs", "foo"],
"./foo
./gitignored.foo",
);
}
/// Custom ignore files (--ignore-file) /// Custom ignore files (--ignore-file)
#[test] #[test]
fn test_custom_ignore_files() { fn test_custom_ignore_files() {
@ -1367,6 +1383,67 @@ fn test_exec() {
} }
} }
#[test]
fn test_exec_multi() {
// TODO test for windows
if cfg!(windows) {
return;
}
let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES);
te.assert_output(
&[
"--absolute-path",
"foo",
"--exec",
"echo",
";",
"--exec",
"echo",
"test",
"{/}",
],
&format!(
"{abs_path}/a.foo
{abs_path}/one/b.foo
{abs_path}/one/two/C.Foo2
{abs_path}/one/two/c.foo
{abs_path}/one/two/three/d.foo
{abs_path}/one/two/three/directory_foo
test a.foo
test b.foo
test C.Foo2
test c.foo
test d.foo
test directory_foo",
abs_path = &abs_path
),
);
te.assert_output(
&[
"e1", "--exec", "echo", "{.}", ";", "--exec", "echo", "{/}", ";", "--exec", "echo",
"{//}", ";", "--exec", "echo", "{/.}",
],
"e1 e2
e1 e2
.
e1 e2",
);
te.assert_output(
&[
"foo", "--exec", "echo", "-n", "{/}: ", ";", "--exec", "echo", "{//}",
],
"a.foo: .
b.foo: ./one
C.Foo2: ./one/two
c.foo: ./one/two
d.foo: ./one/two/three
directory_foo: ./one/two/three",
);
}
#[test] #[test]
fn test_exec_batch() { fn test_exec_batch() {
let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES); let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES);
@ -1404,12 +1481,12 @@ fn test_exec_batch() {
te.assert_failure_with_error( te.assert_failure_with_error(
&["foo", "--exec-batch", "echo", "{/}", ";", "-x", "echo"], &["foo", "--exec-batch", "echo", "{/}", ";", "-x", "echo"],
"error: The argument '--exec <cmd>' cannot be used with '--exec-batch <cmd>'", "error: The argument '--exec-batch <cmd>...' cannot be used with '--exec <cmd>...'",
); );
te.assert_failure_with_error( te.assert_failure_with_error(
&["foo", "--exec-batch"], &["foo", "--exec-batch"],
"error: The argument '--exec-batch <cmd>' requires a value but none was supplied", "error: The argument '--exec-batch <cmd>...' requires a value but none was supplied",
); );
te.assert_failure_with_error( te.assert_failure_with_error(
@ -1419,6 +1496,21 @@ fn test_exec_batch() {
} }
} }
#[test]
fn test_exec_batch_multi() {
// TODO test for windows
if cfg!(windows) {
return;
}
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
te.assert_output(
&["foo", "--exec-batch", "echo", "{}", ";", "--exec-batch", "echo", "{/}"],
"./a.foo ./one/b.foo ./one/two/C.Foo2 ./one/two/c.foo ./one/two/three/d.foo ./one/two/three/directory_foo
a.foo b.foo C.Foo2 c.foo d.foo directory_foo",
);
}
#[test] #[test]
fn test_exec_batch_with_limit() { fn test_exec_batch_with_limit() {
// TODO Test for windows // TODO Test for windows
@ -1954,8 +2046,8 @@ fn test_opposing(flag: &str, opposing_flags: &[&str]) {
let mut flags = vec![flag]; let mut flags = vec![flag];
flags.extend_from_slice(opposing_flags); flags.extend_from_slice(opposing_flags);
let out_no_flags = te.assert_success_and_get_output(".", &[]); let out_no_flags = te.assert_success_and_get_normalized_output(".", &[]);
let out_opposing_flags = te.assert_success_and_get_output(".", &flags); let out_opposing_flags = te.assert_success_and_get_normalized_output(".", &flags);
assert_eq!( assert_eq!(
out_no_flags, out_no_flags,