mirror of https://github.com/sharkdp/fd.git
Compare commits
354 Commits
Author | SHA1 | Date |
---|---|---|
Bryan Honof | 4e9672250b | |
Thayne McCombs | f8270a6a44 | |
Thayne McCombs | 7042dff969 | |
Thayne McCombs | f477c4f2c9 | |
Thayne McCombs | be815c261a | |
Thayne McCombs | d8a808c0e3 | |
Thayne McCombs | c92290c3d7 | |
Thayne McCombs | 0649c2b379 | |
dependabot[bot] | 755970deba | |
dependabot[bot] | f897b82d76 | |
dependabot[bot] | dbbabed606 | |
dependabot[bot] | ac178e20af | |
David Peter | 29936f0fba | |
David Peter | bfc16a1dee | |
Thayne McCombs | 289a68bac3 | |
Thayne McCombs | fcaff0f385 | |
Thayne McCombs | 36163f9c3a | |
Thayne McCombs | d90ec1758e | |
Thayne McCombs | ea22cbd712 | |
Thayne McCombs | d44badc190 | |
Thayne McCombs | 6becb66185 | |
Thayne McCombs | 1a1f057e5d | |
Thayne McCombs | 10a269bd3f | |
Thayne McCombs | 90d3381814 | |
Thayne McCombs | b1f83a0bb0 | |
Thayne McCombs | 3bc70925a9 | |
Thayne McCombs | f287f08b9f | |
Tavian Barnes | 0e4488e9dc | |
Tavian Barnes | d7d63eddbe | |
Thayne McCombs | 8acd7722f0 | |
Thayne McCombs | 92fab6e058 | |
Thayne McCombs | a0ee0856db | |
Thayne McCombs | b8df500a70 | |
Thayne McCombs | cd96ca071d | |
Tavian Barnes | 216472ff9f | |
Thayne McCombs | 3680d10e5c | |
dependabot[bot] | abe3b9cd78 | |
Thayne McCombs | 7aad6c9edf | |
Thayne McCombs | ddd3aae249 | |
dependabot[bot] | 6d3bb68faf | |
dependabot[bot] | 21d50dae8c | |
David Peter | 9279b1f0af | |
Thayne McCombs | 6647085015 | |
Thayne McCombs | 6af8f092ee | |
Thayne McCombs | c4094c7a05 | |
AlbydS | 6d58df5f0c | |
AlbydS | ffecccf209 | |
Thayne McCombs | 31f2839751 | |
Thayne McCombs | e10a4eab2b | |
Thayne McCombs | 8eb047945e | |
Tavian Barnes | 1031325cca | |
Tavian Barnes | 9fc2167cf9 | |
Tavian Barnes | ae1de4de24 | |
Thayne McCombs | 7e5d14b733 | |
Thayne McCombs | 85cbea8dcb | |
Thayne McCombs | bc6782624e | |
Thayne McCombs | cf6ff87c7d | |
Thayne McCombs | 3cd73d7927 | |
binlingyu | 7794c4aae5 | |
Thayne McCombs | 8c7a84ea30 | |
Thayne McCombs | e262ade74e | |
Thayne McCombs | 11069e284a | |
Thayne McCombs | 6e2e86decb | |
Thayne McCombs | 15d3b63ccc | |
dependabot[bot] | 453577651e | |
dependabot[bot] | 39c07b7b4c | |
dependabot[bot] | 5910285db0 | |
Thayne McCombs | 68fe31da3f | |
Jian Wang | f875ea9a52 | |
one230six | 138919907b | |
Thayne McCombs | b8744626e7 | |
dependabot[bot] | b08d78f6fc | |
Tavian Barnes | 4efc05ef27 | |
garlic-hub | 0788c43c3f | |
Thayne McCombs | 3b2fd158b5 | |
Thayne McCombs | c38dbacbd0 | |
Thayne McCombs | 728b3200c0 | |
dependabot[bot] | 7f74cd9e56 | |
dependabot[bot] | 6ae8da6a39 | |
dependabot[bot] | f699c8bb6a | |
Nathan Bellows | ffde94c10e | |
Nathan Bellows | b0a8848f68 | |
AlbydS | d651a595d4 | |
David Peter | 969316cc0e | |
David Peter | 5b46867507 | |
Thayne McCombs | e117a373a7 | |
dependabot[bot] | a4aed14337 | |
Thayne McCombs | 9cde3c12a2 | |
Thayne McCombs | 906e7a933e | |
dependabot[bot] | 077d28d13a | |
dependabot[bot] | b55bb1e9be | |
Thayne McCombs | 7a6cc92d6d | |
Thayne McCombs | b694c6e673 | |
dependabot[bot] | 17895538a0 | |
dependabot[bot] | 72ff1f9a87 | |
Thayne McCombs | ef3194a510 | |
Maksim Bondarenkov | 8773402246 | |
Rob | ff3fc81db4 | |
Tavian Barnes | 0dc3342c33 | |
Thayne McCombs | c66fc812ac | |
Thayne McCombs | 14ed023875 | |
Tavian Barnes | 58284b8dbe | |
Tavian Barnes | 60889d0b99 | |
dependabot[bot] | 7e19bad0a4 | |
Thayne McCombs | 4b1d73d39d | |
dependabot[bot] | 03e19a1ad2 | |
Thayne McCombs | 8fb9499c20 | |
Thayne McCombs | 38fb6a5958 | |
dependabot[bot] | 49cd62d65e | |
dependabot[bot] | 24bb5216bb | |
dependabot[bot] | 7f8760fd1f | |
Alexandru-Constantin Atomei | 3cb6b9d93a | |
Atomei Alexandru | c591106b86 | |
Alexandru-Constantin Atomei | 9f096737db | |
Alexandru-Constantin Atomei | 1bda165b25 | |
Thayne McCombs | f48372624d | |
Roshan Jossy | 5cd15536b6 | |
Sayan Goswami | aeb4a5fdad | |
Thayne McCombs | 9529f30129 | |
Thayne McCombs | 266311ca33 | |
Tavian Barnes | 954a3900b9 | |
David Peter | 07343b5baf | |
David Peter | a03ed8b300 | |
David Peter | 13a93e5cbe | |
David Peter | d9c4e6239f | |
David Peter | 61ebd9be6a | |
David Peter | e3b40208d5 | |
Tavian Barnes | 16c2d1e1d0 | |
Tavian Barnes | fea1622724 | |
David Peter | 00b64f3ccb | |
Thayne McCombs | 74b850a642 | |
dependabot[bot] | 4202f3939e | |
dependabot[bot] | e1ecba2ce4 | |
dependabot[bot] | 0853e35e1f | |
dependabot[bot] | 4b4a74c988 | |
Tavian Barnes | 84f032eba8 | |
Tavian Barnes | b8a5f95cf2 | |
Tavian Barnes | 73260c0e35 | |
Tavian Barnes | 5903dec289 | |
Tavian Barnes | 571ebb349b | |
Tavian Barnes | d62bbbbcd1 | |
Lena | ad5fb44ddc | |
Tavian Barnes | 8bbbd7679b | |
David Peter | cd32a3827d | |
Tavian Barnes | 66c0637c90 | |
Tavian Barnes | c9df4296f9 | |
Tavian Barnes | 7c5cf28ace | |
Tavian Barnes | 51002c842d | |
Tavian Barnes | 8e582971fa | |
Tavian Barnes | 6daa72f929 | |
dependabot[bot] | 8355d78359 | |
dependabot[bot] | dbc1818073 | |
dependabot[bot] | e57ce7f2a4 | |
dependabot[bot] | d8f89fa59e | |
dependabot[bot] | 350003d8da | |
tkb-github | 15329f9cfa | |
Thayne McCombs | 95b4dff379 | |
Thayne McCombs | c96b1af3be | |
Thayne McCombs | 5ee6365510 | |
Thayne McCombs | 1d57b3a064 | |
Thayne McCombs | 325d419e39 | |
Thayne McCombs | 8b5532d8dd | |
João Marcos P. Bezerra | 7263b5e01d | |
Thayne McCombs | c6fcdbe000 | |
Thayne McCombs | 306dacd0b4 | |
Thayne McCombs | 08910e4e3f | |
Thayne McCombs | 8897659607 | |
Thayne McCombs | 53fd416c47 | |
Thayne McCombs | 5e0018fb1f | |
Thayne McCombs | 054bae01ef | |
David Peter | 8f32a758a4 | |
David Peter | 0fc8facfb7 | |
Thayne McCombs | 069b181625 | |
Thayne McCombs | d9b69c8405 | |
Thayne McCombs | a11f8426d4 | |
Thayne McCombs | e6aa8e82f6 | |
sitiom | 978866d983 | |
Christian Göttsche | 36bc84041b | |
Thayne McCombs | 3ed4ea7538 | |
Thayne McCombs | 6b5fe1c634 | |
Thayne McCombs | 7c39fff969 | |
Thayne McCombs | b922ca18f0 | |
Thayne McCombs | b8e7cbd5e3 | |
Karthik Prakash | 9df9a489f0 | |
Thayne McCombs | fa01a280ed | |
Thayne McCombs | e6b5a4ef9d | |
Thayne McCombs | 19832fcbd3 | |
sonke | d371b10039 | |
skoriop | 8c50bc733d | |
skoriop | 3f9794cd1a | |
Thayne McCombs | fc240f7b2a | |
Eden Mikitas | dea9110b90 | |
Thayne McCombs | 93cdb2628e | |
Thayne McCombs | 817c0bc512 | |
Thayne McCombs | e97dec777c | |
Thayne McCombs | 5f494b0925 | |
Thayne McCombs | 59feb7d6ab | |
dependabot[bot] | 97f5326393 | |
dependabot[bot] | e2a298a84f | |
dependabot[bot] | 3317362e78 | |
dependabot[bot] | 39d0a3ff3c | |
dependabot[bot] | d36c59920d | |
Tavian Barnes | 995d2f5e44 | |
Tavian Barnes | 3884f054f1 | |
Collin Styles | 32504fa3d5 | |
Josh Taylor | afd0efa291 | |
Thayne McCombs | 737b5bc42e | |
dependabot[bot] | 601d2bb13e | |
Thayne McCombs | 917c56b120 | |
Thayne McCombs | 9ffd57f4ef | |
dependabot[bot] | 08a8723ee7 | |
Thayne McCombs | efdba804ac | |
Thayne McCombs | 6f0632273b | |
Thayne McCombs | c848af33d5 | |
dependabot[bot] | f33de6544f | |
dependabot[bot] | d7e5dcf9d2 | |
dependabot[bot] | b38ba68ccc | |
dependabot[bot] | e55907dc8b | |
Thayne McCombs | a248607bee | |
dependabot[bot] | ed23fb9054 | |
Thayne McCombs | 7d357a6cec | |
dependabot[bot] | 1feed8816a | |
Tavian Barnes | 9ce43b2d7b | |
dependabot[bot] | a6a78e1c65 | |
Thayne McCombs | cd14bb8a2c | |
Thayne McCombs | 7162f28a5b | |
dependabot[bot] | 2328e9cd17 | |
dependabot[bot] | 2a6026b25d | |
tkb-github | c62224d2c3 | |
Thayne McCombs | 9a40d21ceb | |
Andrea Frigido | d019b02829 | |
Thayne McCombs | 2f813601aa | |
Thayne McCombs | aae8519a1d | |
Thayne McCombs | 4bfb903b22 | |
dependabot[bot] | d91b2a202e | |
dependabot[bot] | a74a43987a | |
dependabot[bot] | 2a588a0171 | |
Utkarsh Gupta | 3ae04546ea | |
Thayne McCombs | a0370aaf25 | |
Thayne McCombs | 740edeb73f | |
Thayne McCombs | 91e3c3cba5 | |
Thayne McCombs | d6e9cbfff3 | |
dependabot[bot] | 8d3172f987 | |
Thayne McCombs | 5be58f0f76 | |
Thayne McCombs | 8d30d6a4fe | |
Thayne McCombs | 5ff866aa26 | |
dependabot[bot] | 4ecf013527 | |
dependabot[bot] | 1c3a38b423 | |
dependabot[bot] | a3a4912ced | |
Thayne McCombs | 0d32bebcc2 | |
Thayne McCombs | 0884b837b2 | |
dependabot[bot] | 11199079c3 | |
Thayne McCombs | 69521a1057 | |
Thayne McCombs | 59a487b524 | |
dependabot[bot] | 0e2a4bac72 | |
dependabot[bot] | 35aa52538c | |
dependabot[bot] | b680a9de9f | |
Nathan Houghton | 42244e5f32 | |
dependabot[bot] | 072c9e56e1 | |
dependabot[bot] | f7bb60aba5 | |
dependabot[bot] | b019d8f1bf | |
dependabot[bot] | 15c795d2e1 | |
dependabot[bot] | a428f7eb13 | |
dependabot[bot] | 02c9efba28 | |
dependabot[bot] | aebe7537c3 | |
Ryan Caezar Itang | 4356ba3c43 | |
Ryan Caezar Itang | c9afbc5b70 | |
Ryan Caezar Itang | e38e3078ac | |
David Peter | 5439326aa4 | |
Thayne McCombs | 35bc1f95fb | |
Thayne McCombs | 161ee64399 | |
Thayne McCombs | 399bf3a931 | |
Tavian Barnes | bae0a1bfa6 | |
Tavian Barnes | e4bca1033c | |
Thayne McCombs | da40e76aae | |
Thayne McCombs | 31ac4a3f5c | |
Thayne McCombs | 424d6efcc0 | |
Thayne McCombs | ccf8e69650 | |
Thayne McCombs | ee44c1ed90 | |
David Peter | 3ac2e13a25 | |
Thayne McCombs | 06a6a118a1 | |
Thayne McCombs | c095867154 | |
Tavian Barnes | 324005fb3a | |
cyqsimon | d8166907e6 | |
cyqsimon | 7cbfb8e29c | |
dependabot[bot] | 1c5ce0a661 | |
dependabot[bot] | f98496abcd | |
David Peter | 82aa17f9fb | |
David Peter | 9b8457aeb3 | |
dependabot[bot] | 535b34e48a | |
dependabot[bot] | 0909d413d0 | |
Thayne McCombs | 284ee3d0c6 | |
John Purnell | f3e6536d59 | |
Tavian Barnes | 002645d7ac | |
Thayne McCombs | 9f6abded0e | |
Thayne McCombs | 840a565d3a | |
David Peter | 3cf5ac0b9a | |
sitiom | a217823510 | |
Frank_Shek | f867c28a2c | |
David Peter | 73a693ef28 | |
David Peter | 9955e20d01 | |
David Peter | 03052757a7 | |
dependabot[bot] | bdcc24ed04 | |
dependabot[bot] | 8478a2c7eb | |
Thayne McCombs | c34bfa30fe | |
David Peter | af9daff4ee | |
Thayne McCombs | 10ba34f78b | |
Thayne McCombs | 503ede7535 | |
Max 👨🏽💻 Coplan | 08c0d427bf | |
Thayne McCombs | ab7d5eff87 | |
Thayne McCombs | 686318c005 | |
Thayne McCombs | c04ab74744 | |
Thayne McCombs | 8fdfc6c2ef | |
dependabot[bot] | 71393fa1be | |
Thayne McCombs | 5e50825af2 | |
dependabot[bot] | 8fed650de9 | |
dependabot[bot] | 4d8569ad6b | |
dependabot[bot] | 2f0677b556 | |
dependabot[bot] | 0a8a72d4f3 | |
David Peter | de611c8835 | |
Thayne McCombs | a36f2cf61c | |
Thayne McCombs | b6c7ebc4f1 | |
Thayne McCombs | fd707b42c2 | |
Thayne McCombs | 7c86c7d585 | |
Thayne McCombs | 27013537c9 | |
David Peter | addf00cb16 | |
Thayne McCombs | 1964e434e6 | |
Thayne McCombs | d5bca085dd | |
David Peter | 8ecfdfee43 | |
Thayne McCombs | b7a2f68d59 | |
Thayne McCombs | e98a6c6755 | |
David Peter | 614e637dbc | |
Thayne McCombs | 7ec795cd57 | |
Tavian Barnes | 8f510265fc | |
Thayne McCombs | 39d80a59b6 | |
Tavian Barnes | 6e3eb26af3 | |
Thayne McCombs | 4a66d8fcd8 | |
Thayne McCombs | daa986ea35 | |
Thayne McCombs | 0a575763a1 | |
dependabot[bot] | 547d08c1ef | |
Thayne McCombs | bbd66b3240 | |
dependabot[bot] | 2ddc2f6c18 | |
dependabot[bot] | 58a9dde73f | |
Thayne McCombs | d441516c9d | |
Thayne McCombs | d991beb942 | |
Thayne McCombs | 650a511fa4 | |
Thayne McCombs | 2aa966cb3c | |
Ptipiak | cd5fad3cf3 | |
David Peter | c9d3968475 | |
David Peter | 36e60223eb | |
David Peter | 781bd4bcf2 | |
David Peter | 0d9926de40 | |
David Peter | e147ba901b | |
Kasper Gałkowski | 7e26925933 |
|
@ -4,3 +4,7 @@ updates:
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "monthly"
|
interval: "monthly"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
name: CICD
|
name: CICD
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MIN_SUPPORTED_RUST_VERSION: "1.60.0"
|
|
||||||
CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||||
|
MSRV_FEATURES: "--all-features"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -14,68 +14,90 @@ on:
|
||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
code_quality:
|
crate_metadata:
|
||||||
name: Code quality
|
name: Extract crate metadata
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Extract crate information
|
||||||
|
id: crate_metadata
|
||||||
|
run: |
|
||||||
|
echo "name=fd" | tee -a $GITHUB_OUTPUT
|
||||||
|
cargo metadata --no-deps --format-version 1 | jq -r '"version=" + .packages[0].version' | tee -a $GITHUB_OUTPUT
|
||||||
|
cargo metadata --no-deps --format-version 1 | jq -r '"maintainer=" + .packages[0].authors[0]' | tee -a $GITHUB_OUTPUT
|
||||||
|
cargo metadata --no-deps --format-version 1 | jq -r '"homepage=" + .packages[0].homepage' | tee -a $GITHUB_OUTPUT
|
||||||
|
cargo metadata --no-deps --format-version 1 | jq -r '"msrv=" + .packages[0].rust_version' | tee -a $GITHUB_OUTPUT
|
||||||
|
outputs:
|
||||||
|
name: ${{ steps.crate_metadata.outputs.name }}
|
||||||
|
version: ${{ steps.crate_metadata.outputs.version }}
|
||||||
|
maintainer: ${{ steps.crate_metadata.outputs.maintainer }}
|
||||||
|
homepage: ${{ steps.crate_metadata.outputs.homepage }}
|
||||||
|
msrv: ${{ steps.crate_metadata.outputs.msrv }}
|
||||||
|
|
||||||
|
ensure_cargo_fmt:
|
||||||
|
name: Ensure 'cargo fmt' has been run
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
uses: actions/checkout@v3
|
with:
|
||||||
- name: Install rust toolchain
|
components: rustfmt
|
||||||
run: |
|
- uses: actions/checkout@v4
|
||||||
rm -f "${HOME}/.cargo/bin/"{rustfmt,cargo-fmt}
|
- run: cargo fmt -- --check
|
||||||
rustup set profile minimal
|
|
||||||
rustup toolchain install stable -c "clippy,rustfmt"
|
lint_check:
|
||||||
rustup default stable
|
name: Ensure 'cargo clippy' has no warnings
|
||||||
- name: Rust cache
|
runs-on: ubuntu-latest
|
||||||
uses: Swatinem/rust-cache@v2
|
steps:
|
||||||
- name: Ensure `cargo fmt` has been run
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
run: cargo fmt --check
|
with:
|
||||||
- name: Ensure MSRV is set in `clippy.toml`
|
components: clippy
|
||||||
run: grep "^msrv = \"${{ env.MIN_SUPPORTED_RUST_VERSION }}\"\$" clippy.toml
|
- uses: actions/checkout@v4
|
||||||
- name: Run clippy
|
- run: cargo clippy --all-targets --all-features -- -Dwarnings
|
||||||
run: cargo clippy --locked --all-targets --all-features
|
|
||||||
|
|
||||||
min_version:
|
min_version:
|
||||||
name: Minimum supported rust version
|
name: Minimum supported rust version
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
needs: crate_metadata
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
|
- name: Install rust toolchain (v${{ needs.crate_metadata.outputs.msrv }})
|
||||||
run: |
|
uses: dtolnay/rust-toolchain@master
|
||||||
rustup set profile minimal
|
with:
|
||||||
rustup toolchain install ${{ env.MIN_SUPPORTED_RUST_VERSION }} -c clippy
|
toolchain: ${{ needs.crate_metadata.outputs.msrv }}
|
||||||
rustup default ${{ env.MIN_SUPPORTED_RUST_VERSION }}
|
components: clippy
|
||||||
- name: Rust cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
- name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
|
- name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
|
||||||
run: cargo clippy --locked --all-targets --all-features
|
run: cargo clippy --locked --all-targets ${{ env.MSRV_FEATURES }}
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --locked
|
run: cargo test --locked ${{ env.MSRV_FEATURES }}
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: ${{ matrix.job.os }} (${{ matrix.job.target }})
|
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
||||||
runs-on: ${{ matrix.job.os }}
|
runs-on: ${{ matrix.job.os }}
|
||||||
|
needs: crate_metadata
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
job:
|
job:
|
||||||
- { os: ubuntu-20.04, target: arm-unknown-linux-gnueabihf , use-cross: true }
|
- { target: aarch64-unknown-linux-gnu , os: ubuntu-22.04, use-cross: true }
|
||||||
- { os: ubuntu-20.04, target: arm-unknown-linux-musleabihf, use-cross: true }
|
- { target: aarch64-unknown-linux-musl , os: ubuntu-22.04, use-cross: true }
|
||||||
- { os: ubuntu-20.04, target: aarch64-unknown-linux-gnu , use-cross: true }
|
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-22.04, use-cross: true }
|
||||||
- { os: ubuntu-20.04, target: i686-unknown-linux-gnu , use-cross: true }
|
- { target: arm-unknown-linux-musleabihf, os: ubuntu-22.04, use-cross: true }
|
||||||
- { os: ubuntu-20.04, target: i686-unknown-linux-musl , use-cross: true }
|
- { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||||
- { os: ubuntu-20.04, target: x86_64-unknown-linux-gnu , use-cross: true }
|
- { target: i686-unknown-linux-gnu , os: ubuntu-22.04, use-cross: true }
|
||||||
- { os: ubuntu-20.04, target: x86_64-unknown-linux-musl , use-cross: true }
|
- { target: i686-unknown-linux-musl , os: ubuntu-22.04, use-cross: true }
|
||||||
- { os: macos-12 , target: x86_64-apple-darwin }
|
- { target: x86_64-apple-darwin , os: macos-12 }
|
||||||
# - { os: windows-2019, target: i686-pc-windows-gnu } ## disabled; error: linker `i686-w64-mingw32-gcc` not found
|
- { target: aarch64-apple-darwin , os: macos-14 }
|
||||||
- { os: windows-2019, target: i686-pc-windows-msvc }
|
- { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||||
- { os: windows-2019, target: x86_64-pc-windows-gnu }
|
- { target: x86_64-pc-windows-msvc , os: windows-2022 }
|
||||||
- { os: windows-2019, target: x86_64-pc-windows-msvc }
|
- { target: x86_64-unknown-linux-gnu , os: ubuntu-22.04, use-cross: true }
|
||||||
|
- { target: x86_64-unknown-linux-musl , os: ubuntu-22.04, use-cross: true }
|
||||||
|
env:
|
||||||
|
BUILD_CMD: cargo
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install prerequisites
|
- name: Install prerequisites
|
||||||
shell: bash
|
shell: bash
|
||||||
|
@ -85,20 +107,24 @@ jobs:
|
||||||
aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
|
aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Extract crate information
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "PROJECT_NAME=fd" >> $GITHUB_ENV
|
|
||||||
echo "PROJECT_VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV
|
|
||||||
echo "PROJECT_MAINTAINER=$(sed -n 's/^authors = \["\(.*\)"\]/\1/p' Cargo.toml)" >> $GITHUB_ENV
|
|
||||||
echo "PROJECT_HOMEPAGE=$(sed -n 's/^homepage = "\(.*\)"/\1/p' Cargo.toml)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
run: |
|
uses: dtolnay/rust-toolchain@stable
|
||||||
rustup set profile minimal
|
with:
|
||||||
rustup toolchain install stable
|
targets: ${{ matrix.job.target }}
|
||||||
rustup override set stable
|
# On windows, for now build with 1.77.2, so that it works on windows 7.
|
||||||
rustup target add ${{ matrix.job.target }}
|
# When we update the MSRV again, we'll need to revisit this, and probably drop support for Win7
|
||||||
|
toolchain: "${{ contains(matrix.job.target, 'windows-') && '1.77.2' || 'stable' }}"
|
||||||
|
|
||||||
|
- name: Install cross
|
||||||
|
if: matrix.job.use-cross
|
||||||
|
uses: taiki-e/install-action@v2
|
||||||
|
with:
|
||||||
|
tool: cross
|
||||||
|
|
||||||
|
- name: Overwrite build command env variable
|
||||||
|
if: matrix.job.use-cross
|
||||||
|
shell: bash
|
||||||
|
run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Show version information (Rust, cargo, GCC)
|
- name: Show version information (Rust, cargo, GCC)
|
||||||
shell: bash
|
shell: bash
|
||||||
|
@ -110,29 +136,12 @@ jobs:
|
||||||
cargo -V
|
cargo -V
|
||||||
rustc -V
|
rustc -V
|
||||||
|
|
||||||
- name: Set cargo cmd
|
|
||||||
shell: bash
|
|
||||||
run: echo "CARGO_CMD=cargo" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set cargo cmd to cross
|
|
||||||
shell: bash
|
|
||||||
if: ${{ matrix.job.use-cross == true }}
|
|
||||||
run: echo "CARGO_CMD=cross" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Rust cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
|
|
||||||
|
|
||||||
- name: Install cross
|
|
||||||
if: ${{ matrix.job.use-cross == true }}
|
|
||||||
run: cargo install cross
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ${{ env.CARGO_CMD }} build --locked --release --target=${{ matrix.job.target }}
|
shell: bash
|
||||||
|
run: $BUILD_CMD build --locked --release --target=${{ matrix.job.target }}
|
||||||
|
|
||||||
- name: Strip debug information from executable
|
- name: Set binary name & path
|
||||||
id: strip
|
id: bin
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
# Figure out suffix of binary
|
# Figure out suffix of binary
|
||||||
|
@ -141,29 +150,11 @@ jobs:
|
||||||
*-pc-windows-*) EXE_suffix=".exe" ;;
|
*-pc-windows-*) EXE_suffix=".exe" ;;
|
||||||
esac;
|
esac;
|
||||||
|
|
||||||
# Figure out what strip tool to use if any
|
|
||||||
STRIP="strip"
|
|
||||||
case ${{ matrix.job.target }} in
|
|
||||||
arm-unknown-linux-*) STRIP="arm-linux-gnueabihf-strip" ;;
|
|
||||||
aarch64-unknown-linux-gnu) STRIP="aarch64-linux-gnu-strip" ;;
|
|
||||||
*-pc-windows-msvc) STRIP="" ;;
|
|
||||||
esac;
|
|
||||||
|
|
||||||
# Setup paths
|
# Setup paths
|
||||||
BIN_DIR="${{ env.CICD_INTERMEDIATES_DIR }}/stripped-release-bin/"
|
BIN_NAME="${{ needs.crate_metadata.outputs.name }}${EXE_suffix}"
|
||||||
mkdir -p "${BIN_DIR}"
|
BIN_PATH="target/${{ matrix.job.target }}/release/${BIN_NAME}"
|
||||||
BIN_NAME="${{ env.PROJECT_NAME }}${EXE_suffix}"
|
|
||||||
BIN_PATH="${BIN_DIR}/${BIN_NAME}"
|
|
||||||
|
|
||||||
# Copy the release build binary to the result location
|
# Let subsequent steps know where to find the binary
|
||||||
cp "target/${{ matrix.job.target }}/release/${BIN_NAME}" "${BIN_DIR}"
|
|
||||||
|
|
||||||
# Also strip if possible
|
|
||||||
if [ -n "${STRIP}" ]; then
|
|
||||||
"${STRIP}" "${BIN_PATH}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Let subsequent steps know where to find the (stripped) bin
|
|
||||||
echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT
|
echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT
|
||||||
echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT
|
echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
@ -173,11 +164,12 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
# test only library unit tests and binary for arm-type targets
|
# test only library unit tests and binary for arm-type targets
|
||||||
unset CARGO_TEST_OPTIONS
|
unset CARGO_TEST_OPTIONS
|
||||||
unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--bin ${PROJECT_NAME}" ;; esac;
|
unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--bin ${{ needs.crate_metadata.outputs.name }}" ;; esac;
|
||||||
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
|
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: ${{ env.CARGO_CMD }} test --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
|
shell: bash
|
||||||
|
run: $BUILD_CMD test --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
|
||||||
|
|
||||||
- name: Generate completions
|
- name: Generate completions
|
||||||
id: completions
|
id: completions
|
||||||
|
@ -189,7 +181,7 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac;
|
PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac;
|
||||||
PKG_BASENAME=${PROJECT_NAME}-v${PROJECT_VERSION}-${{ matrix.job.target }}
|
PKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-v${{ needs.crate_metadata.outputs.version }}-${{ matrix.job.target }}
|
||||||
PKG_NAME=${PKG_BASENAME}${PKG_suffix}
|
PKG_NAME=${PKG_BASENAME}${PKG_suffix}
|
||||||
echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT
|
echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
@ -198,14 +190,14 @@ jobs:
|
||||||
mkdir -p "${ARCHIVE_DIR}"
|
mkdir -p "${ARCHIVE_DIR}"
|
||||||
|
|
||||||
# Binary
|
# Binary
|
||||||
cp "${{ steps.strip.outputs.BIN_PATH }}" "$ARCHIVE_DIR"
|
cp "${{ steps.bin.outputs.BIN_PATH }}" "$ARCHIVE_DIR"
|
||||||
|
|
||||||
# Man page
|
|
||||||
cp 'doc/${{ env.PROJECT_NAME }}.1' "$ARCHIVE_DIR"
|
|
||||||
|
|
||||||
# README, LICENSE and CHANGELOG files
|
# README, LICENSE and CHANGELOG files
|
||||||
cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR"
|
cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR"
|
||||||
|
|
||||||
|
# Man page
|
||||||
|
cp 'doc/${{ needs.crate_metadata.outputs.name }}.1' "$ARCHIVE_DIR"
|
||||||
|
|
||||||
# Autocompletion files
|
# Autocompletion files
|
||||||
cp -r autocomplete "${ARCHIVE_DIR}"
|
cp -r autocomplete "${ARCHIVE_DIR}"
|
||||||
|
|
||||||
|
@ -230,10 +222,10 @@ jobs:
|
||||||
DPKG_DIR="${DPKG_STAGING}/dpkg"
|
DPKG_DIR="${DPKG_STAGING}/dpkg"
|
||||||
mkdir -p "${DPKG_DIR}"
|
mkdir -p "${DPKG_DIR}"
|
||||||
|
|
||||||
DPKG_BASENAME=${PROJECT_NAME}
|
DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}
|
||||||
DPKG_CONFLICTS=${PROJECT_NAME}-musl
|
DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl
|
||||||
case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${PROJECT_NAME}-musl ; DPKG_CONFLICTS=${PROJECT_NAME} ;; esac;
|
case ${{ matrix.job.target }} in *-musl*) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac;
|
||||||
DPKG_VERSION=${PROJECT_VERSION}
|
DPKG_VERSION=${{ needs.crate_metadata.outputs.version }}
|
||||||
|
|
||||||
unset DPKG_ARCH
|
unset DPKG_ARCH
|
||||||
case ${{ matrix.job.target }} in
|
case ${{ matrix.job.target }} in
|
||||||
|
@ -248,16 +240,16 @@ jobs:
|
||||||
echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT
|
echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Binary
|
# Binary
|
||||||
install -Dm755 "${{ steps.strip.outputs.BIN_PATH }}" "${DPKG_DIR}/usr/bin/${{ steps.strip.outputs.BIN_NAME }}"
|
install -Dm755 "${{ steps.bin.outputs.BIN_PATH }}" "${DPKG_DIR}/usr/bin/${{ steps.bin.outputs.BIN_NAME }}"
|
||||||
|
|
||||||
# Man page
|
# Man page
|
||||||
install -Dm644 'doc/${{ env.PROJECT_NAME }}.1' "${DPKG_DIR}/usr/share/man/man1/${{ env.PROJECT_NAME }}.1"
|
install -Dm644 'doc/${{ needs.crate_metadata.outputs.name }}.1' "${DPKG_DIR}/usr/share/man/man1/${{ needs.crate_metadata.outputs.name }}.1"
|
||||||
gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ env.PROJECT_NAME }}.1"
|
gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ needs.crate_metadata.outputs.name }}.1"
|
||||||
|
|
||||||
# Autocompletion files
|
# Autocompletion files
|
||||||
install -Dm644 'autocomplete/fd.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/${{ env.PROJECT_NAME }}"
|
install -Dm644 'autocomplete/fd.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/${{ needs.crate_metadata.outputs.name }}"
|
||||||
install -Dm644 'autocomplete/fd.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/${{ env.PROJECT_NAME }}.fish"
|
install -Dm644 'autocomplete/fd.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/${{ needs.crate_metadata.outputs.name }}.fish"
|
||||||
install -Dm644 'autocomplete/_fd' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ env.PROJECT_NAME }}"
|
install -Dm644 'autocomplete/_fd' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ needs.crate_metadata.outputs.name }}"
|
||||||
|
|
||||||
# README and LICENSE
|
# README and LICENSE
|
||||||
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
|
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
|
||||||
|
@ -268,12 +260,12 @@ jobs:
|
||||||
|
|
||||||
cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" <<EOF
|
cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" <<EOF
|
||||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
Upstream-Name: ${{ env.PROJECT_NAME }}
|
Upstream-Name: ${{ needs.crate_metadata.outputs.name }}
|
||||||
Source: ${{ env.PROJECT_HOMEPAGE }}
|
Source: ${{ needs.crate_metadata.outputs.homepage }}
|
||||||
|
|
||||||
Files: *
|
Files: *
|
||||||
Copyright: ${{ env.PROJECT_MAINTAINER }}
|
Copyright: ${{ needs.crate_metadata.outputs.maintainer }}
|
||||||
Copyright: $COPYRIGHT_YEARS ${{ env.PROJECT_MAINTAINER }}
|
Copyright: $COPYRIGHT_YEARS ${{ needs.crate_metadata.outputs.maintainer }}
|
||||||
License: Apache-2.0 or MIT
|
License: Apache-2.0 or MIT
|
||||||
|
|
||||||
License: Apache-2.0
|
License: Apache-2.0
|
||||||
|
@ -314,10 +306,10 @@ jobs:
|
||||||
Version: ${DPKG_VERSION}
|
Version: ${DPKG_VERSION}
|
||||||
Section: utils
|
Section: utils
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Maintainer: ${{ env.PROJECT_MAINTAINER }}
|
Maintainer: ${{ needs.crate_metadata.outputs.maintainer }}
|
||||||
Homepage: ${{ env.PROJECT_HOMEPAGE }}
|
Homepage: ${{ needs.crate_metadata.outputs.homepage }}
|
||||||
Architecture: ${DPKG_ARCH}
|
Architecture: ${DPKG_ARCH}
|
||||||
Provides: ${{ env.PROJECT_NAME }}
|
Provides: ${{ needs.crate_metadata.outputs.name }}
|
||||||
Conflicts: ${DPKG_CONFLICTS}
|
Conflicts: ${DPKG_CONFLICTS}
|
||||||
Description: simple, fast and user-friendly alternative to find
|
Description: simple, fast and user-friendly alternative to find
|
||||||
fd is a program to find entries in your filesystem.
|
fd is a program to find entries in your filesystem.
|
||||||
|
@ -353,7 +345,7 @@ jobs:
|
||||||
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
|
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Publish archives and packages
|
- name: Publish archives and packages
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
if: steps.is-release.outputs.IS_RELEASE
|
if: steps.is-release.outputs.IS_RELEASE
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
@ -361,3 +353,15 @@ jobs:
|
||||||
${{ steps.debian-package.outputs.DPKG_PATH }}
|
${{ steps.debian-package.outputs.DPKG_PATH }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
winget:
|
||||||
|
name: Publish to Winget
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
steps:
|
||||||
|
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||||
|
with:
|
||||||
|
identifier: sharkdp.fd
|
||||||
|
installers-regex: '-pc-windows-msvc\.zip$'
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|
104
CHANGELOG.md
104
CHANGELOG.md
|
@ -1,7 +1,109 @@
|
||||||
# Upcoming release
|
# 10.1.0
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
- Allow passing an optional argument to `--strip-cwd-prefix` of "always", "never", or "auto". to force whether the cwd prefix is stripped or not.
|
||||||
|
- Add a `--format` option which allows using a format template for direct ouput similar to the template used for `--exec`. (#1043)
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
- Fix aarch64 page size again. This time it should actually work. (#1085, #1549) (@tavianator)
|
||||||
|
|
||||||
|
|
||||||
|
## Other
|
||||||
|
|
||||||
|
- aarch64-apple-darwin target added to builds on the release page. Note that this is a tier 2 rust target.
|
||||||
|
|
||||||
|
# v10.0.0
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Add `dir` as an alias to `directory` when using `-t` \ `--type`, see #1460 and #1464 (@Ato2207).
|
||||||
|
- Add support for @%s date format in time filters similar to GNU date (seconds since Unix epoch for --older/--newer), see #1493 (@nabellows)
|
||||||
|
- Breaking: No longer automatically ignore `.git` when using `--hidden` with vcs ignore enabled. This reverts the change in v9.0.0. While this feature
|
||||||
|
was often useful, it also broke some existing workflows, and there wasn't a good way to opt out of it. And there isn't really a good way for us to add
|
||||||
|
a way to opt out of it. And you can easily get similar behavior by adding `.git/` to your global fdignore file.
|
||||||
|
See #1457.
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
- Respect NO_COLOR environment variable with `--list-details` option. (#1455)
|
||||||
|
- Fix bug that would cause hidden files to be included despite gitignore rules
|
||||||
|
if search path is "." (#1461, BurntSushi/ripgrep#2711).
|
||||||
|
- aarch64 builds now use 64k page sizes with jemalloc. This fixes issues on some systems, such as ARM Macs that
|
||||||
|
have a larger system page size than the system that the binary was built on. (#1547)
|
||||||
|
- Address [CVE-2024-24576](https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html), by increasing minimum rust version.
|
||||||
|
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
- Minimum supported rust version is now 1.77.2
|
||||||
|
|
||||||
|
|
||||||
|
# v9.0.0
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- Performance has been *significantly improved*, both due to optimizations in the underlying `ignore`
|
||||||
|
crate (#1429), and in `fd` itself (#1422, #1408, #1362) - @tavianator.
|
||||||
|
[Benchmarks results](https://gist.github.com/tavianator/32edbe052f33ef60570cf5456b59de81) show gains
|
||||||
|
of 6-8x for full traversals of smaller directories (100k files) and up to 13x for larger directories (1M files).
|
||||||
|
|
||||||
|
- The default number of threads is now constrained to be at most 64. This should improve startup time on
|
||||||
|
systems with many CPU cores. (#1203, #1410, #1412, #1431) - @tmccombs and @tavianator
|
||||||
|
|
||||||
|
- New flushing behavior when writing output to stdout, providing better performance for TTY and non-TTY
|
||||||
|
use cases, see #1452 and #1313 (@tavianator).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Support character and block device file types, see #1213 and #1336 (@cgzones)
|
||||||
|
- Breaking: `.git/` is now ignored by default when using `--hidden` / `-H`, use `--no-ignore` / `-I` or
|
||||||
|
`--no-ignore-vcs` to override, see #1387 and #1396 (@skoriop)
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
- Fix `NO_COLOR` support, see #1421 (@acuteenvy)
|
||||||
|
|
||||||
|
## Other
|
||||||
|
|
||||||
|
- Fixed documentation typos, see #1409 (@marcospb19)
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
Special thanks to @tavianator for his incredible work on performance in the `ignore` crate and `fd` itself.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# v8.7.1
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
- `-1` properly conflicts with the exec family of options.
|
||||||
|
- `--max-results` overrides `-1`
|
||||||
|
- `--quiet` properly conflicts with the exec family of options. This used to be the case, but broke during the switch to clap-derive
|
||||||
|
- `--changed-within` now accepts a space as well as a "T" as the separator between date and time (due to update of chrono dependency)
|
||||||
|
|
||||||
|
## Other
|
||||||
|
- Many dependencies were updated
|
||||||
|
- Some documentation was updated and fixed
|
||||||
|
|
||||||
|
# v8.7.0
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Add flag --no-require-git to always respect gitignore files, see #1216 (@vegerot)
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
- Fix logic for when to use global ignore file. There was a bug where the only case where the
|
||||||
|
global ignore file wasn't processed was if `--no-ignore` was passed, but neither `--unrestricted`
|
||||||
|
nor `--no-global-ignore-file` is passed. See #1209
|
||||||
|
|
||||||
|
# v8.6.0
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- New `--and <pattern>` option to add additional patterns that must also be matched. See #315
|
||||||
|
and #1139 (@Uthar)
|
||||||
- Added `--changed-after` as alias for `--changed-within`, to have a name consistent with `--changed-before`.
|
- Added `--changed-after` as alias for `--changed-within`, to have a name consistent with `--changed-before`.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,11 +13,11 @@ give us the chance to discuss any potential changes first.
|
||||||
## Add an entry to the changelog
|
## Add an entry to the changelog
|
||||||
|
|
||||||
If your contribution changes the behavior of `fd` (as opposed to a typo-fix
|
If your contribution changes the behavior of `fd` (as opposed to a typo-fix
|
||||||
in the documentation), please update the [`CHANGELOG.md`](CHANGELOG.md) file
|
in the documentation), please update the [`CHANGELOG.md`](CHANGELOG.md#upcoming-release) file
|
||||||
and describe your changes. This makes the release process much easier and
|
and describe your changes. This makes the release process much easier and
|
||||||
therefore helps to get your changes into a new `fd` release faster.
|
therefore helps to get your changes into a new `fd` release faster.
|
||||||
|
|
||||||
The top of the `CHANGELOG` contains a *"unreleased"* section with a few
|
The top of the `CHANGELOG` contains an *"Upcoming release"* section with a few
|
||||||
subsections (Features, Bugfixes, …). Please add your entry to the subsection
|
subsections (Features, Bugfixes, …). Please add your entry to the subsection
|
||||||
that best describes your change.
|
that best describes your change.
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
53
Cargo.toml
53
Cargo.toml
|
@ -12,12 +12,13 @@ keywords = [
|
||||||
"filesystem",
|
"filesystem",
|
||||||
"tool",
|
"tool",
|
||||||
]
|
]
|
||||||
license = "MIT/Apache-2.0"
|
license = "MIT OR 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.5.3"
|
version = "10.1.0"
|
||||||
edition= "2021"
|
edition= "2021"
|
||||||
|
rust-version = "1.77.2"
|
||||||
|
|
||||||
[badges.appveyor]
|
[badges.appveyor]
|
||||||
repository = "sharkdp/fd"
|
repository = "sharkdp/fd"
|
||||||
|
@ -33,33 +34,38 @@ path = "src/main.rs"
|
||||||
version_check = "0.9"
|
version_check = "0.9"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ansi_term = "0.12"
|
aho-corasick = "1.1"
|
||||||
|
nu-ansi-term = "0.50"
|
||||||
argmax = "0.3.1"
|
argmax = "0.3.1"
|
||||||
atty = "0.2"
|
ignore = "0.4.22"
|
||||||
ignore = "0.4.3"
|
regex = "1.10.3"
|
||||||
num_cpus = "1.13"
|
regex-syntax = "0.8"
|
||||||
regex = "1.6.0"
|
|
||||||
regex-syntax = "0.6"
|
|
||||||
ctrlc = "3.2"
|
ctrlc = "3.2"
|
||||||
humantime = "2.1"
|
humantime = "2.1"
|
||||||
lscolors = "0.12"
|
|
||||||
globset = "0.4"
|
globset = "0.4"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
dirs-next = "2.0"
|
etcetera = "0.8"
|
||||||
normpath = "0.3.2"
|
normpath = "1.1.1"
|
||||||
chrono = "0.4"
|
crossbeam-channel = "0.5.13"
|
||||||
once_cell = "1.15.0"
|
clap_complete = {version = "4.4.9", optional = true}
|
||||||
crossbeam-channel = "0.5.6"
|
|
||||||
clap_complete = {version = "4.0.5", optional = true}
|
|
||||||
faccess = "0.2.4"
|
faccess = "0.2.4"
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
version = "4.0.22"
|
version = "4.4.13"
|
||||||
features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped", "derive"]
|
features = ["suggestions", "color", "wrap_help", "cargo", "derive"]
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
version = "0.4.38"
|
||||||
|
default-features = false
|
||||||
|
features = ["std", "clock"]
|
||||||
|
|
||||||
|
[dependencies.lscolors]
|
||||||
|
version = "0.17"
|
||||||
|
default-features = false
|
||||||
|
features = ["nu-ansi-term"]
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
users = "0.11.0"
|
nix = { version = "0.29.0", default-features = false, features = ["signal", "user"] }
|
||||||
nix = { version = "0.24.2", default-features = false, features = ["signal"] }
|
|
||||||
|
|
||||||
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
@ -67,17 +73,18 @@ 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(all(target_env = "musl", target_pointer_width = "32")), not(target_arch = "riscv64")))'.dependencies]
|
[target.'cfg(all(not(windows), not(target_os = "android"), not(target_os = "macos"), not(target_os = "freebsd"), not(target_os = "openbsd"), not(all(target_env = "musl", target_pointer_width = "32")), not(target_arch = "riscv64")))'.dependencies]
|
||||||
jemallocator = {version = "0.5.0", optional = true}
|
jemallocator = {version = "0.5.4", optional = true}
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
diff = "0.1"
|
diff = "0.1"
|
||||||
tempdir = "0.3"
|
tempfile = "3.10"
|
||||||
filetime = "0.2"
|
filetime = "0.2"
|
||||||
test-case = "2.2"
|
test-case = "3.3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
strip = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
# https://github.com/sharkdp/fd/issues/1085
|
||||||
|
[target.aarch64-unknown-linux-gnu.env]
|
||||||
|
passthrough = ["JEMALLOC_SYS_WITH_LG_PAGE=16"]
|
||||||
|
|
||||||
|
[target.aarch64-unknown-linux-musl.env]
|
||||||
|
passthrough = ["JEMALLOC_SYS_WITH_LG_PAGE=16"]
|
2
Makefile
2
Makefile
|
@ -6,7 +6,7 @@ datadir=$(prefix)/share
|
||||||
exe_name=fd
|
exe_name=fd
|
||||||
|
|
||||||
$(EXE): Cargo.toml src/**/*.rs
|
$(EXE): Cargo.toml src/**/*.rs
|
||||||
cargo build --profile $(PROFILE)
|
cargo build --profile $(PROFILE) --locked
|
||||||
|
|
||||||
.PHONY: completions
|
.PHONY: completions
|
||||||
completions: autocomplete/fd.bash autocomplete/fd.fish autocomplete/fd.ps1 autocomplete/_fd
|
completions: autocomplete/fd.bash autocomplete/fd.fish autocomplete/fd.ps1 autocomplete/_fd
|
||||||
|
|
190
README.md
190
README.md
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
[![CICD](https://github.com/sharkdp/fd/actions/workflows/CICD.yml/badge.svg)](https://github.com/sharkdp/fd/actions/workflows/CICD.yml)
|
[![CICD](https://github.com/sharkdp/fd/actions/workflows/CICD.yml/badge.svg)](https://github.com/sharkdp/fd/actions/workflows/CICD.yml)
|
||||||
[![Version info](https://img.shields.io/crates/v/fd-find.svg)](https://crates.io/crates/fd-find)
|
[![Version info](https://img.shields.io/crates/v/fd-find.svg)](https://crates.io/crates/fd-find)
|
||||||
[[中文](https://github.com/chinanf-boy/fd-zh)]
|
[[中文](https://github.com/cha0ran/fd-zh)]
|
||||||
[[한국어](https://github.com/spearkkk/fd-kor)]
|
[[한국어](https://github.com/spearkkk/fd-kor)]
|
||||||
|
|
||||||
`fd` is a program to find entries in your filesystem.
|
`fd` is a program to find entries in your filesystem.
|
||||||
|
@ -10,10 +10,7 @@ It is a simple, fast and user-friendly alternative to [`find`](https://www.gnu.o
|
||||||
While it does not aim to support all of `find`'s powerful functionality, it provides sensible
|
While it does not aim to support all of `find`'s powerful functionality, it provides sensible
|
||||||
(opinionated) defaults for a majority of use cases.
|
(opinionated) defaults for a majority of use cases.
|
||||||
|
|
||||||
Quick links:
|
[Installation](#installation) • [How to use](#how-to-use) • [Troubleshooting](#troubleshooting)
|
||||||
* [How to use](#how-to-use)
|
|
||||||
* [Installation](#installation)
|
|
||||||
* [Troubleshooting](#troubleshooting)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -143,7 +140,7 @@ target/debug/deps/libnum_cpus-f5ce7ef99006aa05.rlib
|
||||||
```
|
```
|
||||||
|
|
||||||
To really search *all* files and directories, simply combine the hidden and ignore features to show
|
To really search *all* files and directories, simply combine the hidden and ignore features to show
|
||||||
everything (`-HI`).
|
everything (`-HI`) or use `-u`/`--unrestricted`.
|
||||||
|
|
||||||
### Matching the full path
|
### Matching the full path
|
||||||
By default, *fd* only matches the filename of each file. However, using the `--full-path` or `-p` option,
|
By default, *fd* only matches the filename of each file. However, using the `--full-path` or `-p` option,
|
||||||
|
@ -261,12 +258,17 @@ To make exclude-patterns like these permanent, you can create a `.fdignore` file
|
||||||
/mnt/external-drive
|
/mnt/external-drive
|
||||||
*.bak
|
*.bak
|
||||||
```
|
```
|
||||||
Note: `fd` also supports `.ignore` files that are used by other programs such as `rg` or `ag`.
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `fd` also supports `.ignore` files that are used by other programs such as `rg` or `ag`.
|
||||||
|
|
||||||
If you want `fd` to ignore these patterns globally, you can put them in `fd`'s global ignore file.
|
If you want `fd` to ignore these patterns globally, you can put them in `fd`'s global ignore file.
|
||||||
This is usually located in `~/.config/fd/ignore` in macOS or Linux, and `%APPDATA%\fd\ignore` in
|
This is usually located in `~/.config/fd/ignore` in macOS or Linux, and `%APPDATA%\fd\ignore` in
|
||||||
Windows.
|
Windows.
|
||||||
|
|
||||||
|
You may wish to include `.git/` in your `fd/ignore` file so that `.git` directories, and their contents
|
||||||
|
are not included in output if you use the `--hidden` option.
|
||||||
|
|
||||||
### Deleting files
|
### Deleting files
|
||||||
|
|
||||||
You can use `fd` to remove all files and directories that are matched by your search pattern.
|
You can use `fd` to remove all files and directories that are matched by your search pattern.
|
||||||
|
@ -284,7 +286,8 @@ option:
|
||||||
If you also want to remove a certain class of directories, you can use the same technique. You will
|
If you also want to remove a certain class of directories, you can use the same technique. You will
|
||||||
have to use `rm`s `--recursive`/`-r` flag to remove directories.
|
have to use `rm`s `--recursive`/`-r` flag to remove directories.
|
||||||
|
|
||||||
Note: there are scenarios where using `fd … -X rm -r` can cause race conditions: if you have a
|
> [!NOTE]
|
||||||
|
> There are scenarios where using `fd … -X rm -r` can cause race conditions: if you have a
|
||||||
path like `…/foo/bar/foo/…` and want to remove all directories named `foo`, you can end up in a
|
path like `…/foo/bar/foo/…` and want to remove all directories named `foo`, you can end up in a
|
||||||
situation where the outer `foo` directory is removed first, leading to (harmless) *"'foo/bar/foo':
|
situation where the outer `foo` directory is removed first, leading to (harmless) *"'foo/bar/foo':
|
||||||
No such file or directory"* errors in the `rm` call.
|
No such file or directory"* errors in the `rm` call.
|
||||||
|
@ -313,81 +316,76 @@ Options:
|
||||||
-p, --full-path Search full abs. path (default: filename only)
|
-p, --full-path Search full abs. path (default: filename only)
|
||||||
-d, --max-depth <depth> Set maximum search depth (default: none)
|
-d, --max-depth <depth> Set maximum search depth (default: none)
|
||||||
-E, --exclude <pattern> Exclude entries that match the given glob pattern
|
-E, --exclude <pattern> Exclude entries that match the given glob pattern
|
||||||
-t, --type <filetype> Filter by type: file (f), directory (d), symlink (l),
|
-t, --type <filetype> Filter by type: file (f), directory (d/dir), symlink (l),
|
||||||
executable (x), empty (e), socket (s), pipe (p)
|
executable (x), empty (e), socket (s), pipe (p), char-device
|
||||||
|
(c), block-device (b)
|
||||||
-e, --extension <ext> Filter by file extension
|
-e, --extension <ext> Filter by file extension
|
||||||
-S, --size <size> Limit results based on the size of files
|
-S, --size <size> Limit results based on the size of files
|
||||||
--changed-within <date|dur> Filter by file modification time (newer than)
|
--changed-within <date|dur> Filter by file modification time (newer than)
|
||||||
--changed-before <date|dur> Filter by file modification time (older than)
|
--changed-before <date|dur> Filter by file modification time (older than)
|
||||||
-o, --owner <user:group> Filter by owning user and/or group
|
-o, --owner <user:group> Filter by owning user and/or group
|
||||||
|
--format <fmt> Print results according to template
|
||||||
-x, --exec <cmd>... Execute a command for each search result
|
-x, --exec <cmd>... Execute a command for each search result
|
||||||
-X, --exec-batch <cmd>... Execute a command with all search results at once
|
-X, --exec-batch <cmd>... Execute a command with all search results at once
|
||||||
-c, --color <when> When to use colors [default: auto] [possible values: auto,
|
-c, --color <when> When to use colors [default: auto] [possible values: auto,
|
||||||
always, never]
|
always, never]
|
||||||
-h, --help Print help information (use `--help` for more detail)
|
-h, --help Print help (see more with '--help')
|
||||||
-V, --version Print version information
|
-V, --version Print version
|
||||||
```
|
```
|
||||||
|
|
||||||
## Benchmark
|
## Benchmark
|
||||||
|
|
||||||
Let's search my home folder for files that end in `[0-9].jpg`. It contains ~190.000
|
Let's search my home folder for files that end in `[0-9].jpg`. It contains ~750.000
|
||||||
subdirectories and about a million files. For averaging and statistical analysis, I'm using
|
subdirectories and about a 4 million files. For averaging and statistical analysis, I'm using
|
||||||
[hyperfine](https://github.com/sharkdp/hyperfine). The following benchmarks are performed
|
[hyperfine](https://github.com/sharkdp/hyperfine). The following benchmarks are performed
|
||||||
with a "warm"/pre-filled disk-cache (results for a "cold" disk-cache show the same trends).
|
with a "warm"/pre-filled disk-cache (results for a "cold" disk-cache show the same trends).
|
||||||
|
|
||||||
Let's start with `find`:
|
Let's start with `find`:
|
||||||
```
|
```
|
||||||
Benchmark #1: find ~ -iregex '.*[0-9]\.jpg$'
|
Benchmark 1: find ~ -iregex '.*[0-9]\.jpg$'
|
||||||
|
Time (mean ± σ): 19.922 s ± 0.109 s
|
||||||
Time (mean ± σ): 7.236 s ± 0.090 s
|
Range (min … max): 19.765 s … 20.065 s
|
||||||
|
|
||||||
Range (min … max): 7.133 s … 7.385 s
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`find` is much faster if it does not need to perform a regular-expression search:
|
`find` is much faster if it does not need to perform a regular-expression search:
|
||||||
```
|
```
|
||||||
Benchmark #2: find ~ -iname '*[0-9].jpg'
|
Benchmark 2: find ~ -iname '*[0-9].jpg'
|
||||||
|
Time (mean ± σ): 11.226 s ± 0.104 s
|
||||||
Time (mean ± σ): 3.914 s ± 0.027 s
|
Range (min … max): 11.119 s … 11.466 s
|
||||||
|
|
||||||
Range (min … max): 3.876 s … 3.964 s
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now let's try the same for `fd`. Note that `fd` *always* performs a regular expression
|
Now let's try the same for `fd`. Note that `fd` performs a regular expression
|
||||||
search. The options `--hidden` and `--no-ignore` are needed for a fair comparison,
|
search by default. The options `-u`/`--unrestricted` option is needed here for
|
||||||
otherwise `fd` does not have to traverse hidden folders and ignored paths (see below):
|
a fair comparison. Otherwise `fd` does not have to traverse hidden folders and
|
||||||
|
ignored paths (see below):
|
||||||
```
|
```
|
||||||
Benchmark #3: fd -HI '.*[0-9]\.jpg$' ~
|
Benchmark 3: fd -u '[0-9]\.jpg$' ~
|
||||||
|
Time (mean ± σ): 854.8 ms ± 10.0 ms
|
||||||
Time (mean ± σ): 811.6 ms ± 26.9 ms
|
Range (min … max): 839.2 ms … 868.9 ms
|
||||||
|
|
||||||
Range (min … max): 786.0 ms … 870.7 ms
|
|
||||||
```
|
```
|
||||||
For this particular example, `fd` is approximately nine times faster than `find -iregex`
|
For this particular example, `fd` is approximately **23 times faster** than `find -iregex`
|
||||||
and about five times faster than `find -iname`. By the way, both tools found the exact
|
and about **13 times faster** than `find -iname`. By the way, both tools found the exact
|
||||||
same 20880 files :smile:.
|
same 546 files :smile:.
|
||||||
|
|
||||||
Finally, let's run `fd` without `--hidden` and `--no-ignore` (this can lead to different
|
**Note**: This is *one particular* benchmark on *one particular* machine. While we have
|
||||||
search results, of course). If *fd* does not have to traverse the hidden and git-ignored
|
performed a lot of different tests (and found consistent results), things might
|
||||||
folders, it is almost an order of magnitude faster:
|
be different for you! We encourage everyone to try it out on their own. See
|
||||||
```
|
|
||||||
Benchmark #4: fd '[0-9]\.jpg$' ~
|
|
||||||
|
|
||||||
Time (mean ± σ): 123.7 ms ± 6.0 ms
|
|
||||||
|
|
||||||
Range (min … max): 118.8 ms … 140.0 ms
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: This is *one particular* benchmark on *one particular* machine. While I have
|
|
||||||
performed quite a lot of different tests (and found consistent results), things might
|
|
||||||
be different for you! I encourage everyone to try it out on their own. See
|
|
||||||
[this repository](https://github.com/sharkdp/fd-benchmarks) for all necessary scripts.
|
[this repository](https://github.com/sharkdp/fd-benchmarks) for all necessary scripts.
|
||||||
|
|
||||||
Concerning *fd*'s speed, the main credit goes to the `regex` and `ignore` crates that are also used
|
Concerning *fd*'s speed, a lot of credit goes to the `regex` and `ignore` crates that are
|
||||||
in [ripgrep](https://github.com/BurntSushi/ripgrep) (check it out!).
|
also used in [ripgrep](https://github.com/BurntSushi/ripgrep) (check it out!).
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### `fd` does not find my file!
|
||||||
|
|
||||||
|
Remember that `fd` ignores hidden directories and files by default. It also ignores patterns
|
||||||
|
from `.gitignore` files. If you want to make sure to find absolutely every possible file, always
|
||||||
|
use the options `-u`/`--unrestricted` option (or `-HI` to enable hidden and ignored files):
|
||||||
|
``` bash
|
||||||
|
> fd -u …
|
||||||
|
```
|
||||||
|
|
||||||
### Colorized output
|
### Colorized output
|
||||||
|
|
||||||
`fd` can colorize files by extension, just like `ls`. In order for this to work, the environment
|
`fd` can colorize files by extension, just like `ls`. In order for this to work, the environment
|
||||||
|
@ -401,15 +399,6 @@ for alternative, more complete (or more colorful) variants, see [here](https://g
|
||||||
|
|
||||||
`fd` also honors the [`NO_COLOR`](https://no-color.org/) environment variable.
|
`fd` also honors the [`NO_COLOR`](https://no-color.org/) environment variable.
|
||||||
|
|
||||||
### `fd` does not find my file!
|
|
||||||
|
|
||||||
Remember that `fd` ignores hidden directories and files by default. It also ignores patterns
|
|
||||||
from `.gitignore` files. If you want to make sure to find absolutely every possible file, always
|
|
||||||
use the options `-H` and `-I` to disable these two features:
|
|
||||||
``` bash
|
|
||||||
> fd -HI …
|
|
||||||
```
|
|
||||||
|
|
||||||
### `fd` doesn't seem to interpret my regex pattern correctly
|
### `fd` doesn't seem to interpret my regex pattern correctly
|
||||||
|
|
||||||
A lot of special regex characters (like `[]`, `^`, `$`, ..) are also special characters in your
|
A lot of special regex characters (like `[]`, `^`, `$`, ..) are also special characters in your
|
||||||
|
@ -488,16 +477,17 @@ In emacs, run `M-x find-file-in-project-by-selected` to find matching files. Alt
|
||||||
|
|
||||||
### Printing the output as a tree
|
### Printing the output as a tree
|
||||||
|
|
||||||
To format the output of `fd` similar to the `tree` command, install [`as-tree`] and pipe the output
|
To format the output of `fd` as a file-tree you can use the `tree` command with
|
||||||
of `fd` to `as-tree`:
|
`--fromfile`:
|
||||||
```bash
|
```bash
|
||||||
fd | as-tree
|
❯ fd | tree --fromfile
|
||||||
```
|
```
|
||||||
|
|
||||||
This can be more useful than running `tree` by itself because `tree` does not ignore any files by
|
This can be more useful than running `tree` by itself because `tree` does not
|
||||||
default, nor does it support as rich a set of options as `fd` does to control what to print:
|
ignore any files by default, nor does it support as rich a set of options as
|
||||||
|
`fd` does to control what to print:
|
||||||
```bash
|
```bash
|
||||||
❯ fd --extension rs | as-tree
|
❯ fd --extension rs | tree --fromfile
|
||||||
.
|
.
|
||||||
├── build.rs
|
├── build.rs
|
||||||
└── src
|
└── src
|
||||||
|
@ -505,9 +495,10 @@ default, nor does it support as rich a set of options as `fd` does to control wh
|
||||||
└── error.rs
|
└── error.rs
|
||||||
```
|
```
|
||||||
|
|
||||||
For more information about `as-tree`, see [the `as-tree` README][`as-tree`].
|
On bash and similar you can simply create an alias:
|
||||||
|
```bash
|
||||||
[`as-tree`]: https://github.com/jez/as-tree
|
❯ alias as-tree='tree --fromfile'
|
||||||
|
```
|
||||||
|
|
||||||
### Using fd with `xargs` or `parallel`
|
### Using fd with `xargs` or `parallel`
|
||||||
|
|
||||||
|
@ -530,7 +521,7 @@ newlines). In the same way, the `-0` option of `xargs` tells it to read the inpu
|
||||||
If you run Ubuntu 19.04 (Disco Dingo) or newer, you can install the
|
If you run Ubuntu 19.04 (Disco Dingo) or newer, you can install the
|
||||||
[officially maintained package](https://packages.ubuntu.com/fd-find):
|
[officially maintained package](https://packages.ubuntu.com/fd-find):
|
||||||
```
|
```
|
||||||
sudo apt install fd-find
|
apt install fd-find
|
||||||
```
|
```
|
||||||
Note that the binary is called `fdfind` as the binary name `fd` is already used by another package.
|
Note that the binary is called `fdfind` as the binary name `fd` is already used by another package.
|
||||||
It is recommended that after installation, you add a link to `fd` by executing command
|
It is recommended that after installation, you add a link to `fd` by executing command
|
||||||
|
@ -540,7 +531,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.5.3_amd64.deb # adapt version number and architecture
|
dpkg -i fd_9.0.0_amd64.deb # adapt version number and architecture
|
||||||
```
|
```
|
||||||
|
|
||||||
### On Debian
|
### On Debian
|
||||||
|
@ -548,7 +539,7 @@ sudo dpkg -i fd_8.5.3_amd64.deb # adapt version number and architecture
|
||||||
If you run Debian Buster or newer, you can install the
|
If you run Debian Buster or newer, you can install the
|
||||||
[officially maintained Debian package](https://tracker.debian.org/pkg/rust-fd-find):
|
[officially maintained Debian package](https://tracker.debian.org/pkg/rust-fd-find):
|
||||||
```
|
```
|
||||||
sudo apt-get install fd-find
|
apt-get install fd-find
|
||||||
```
|
```
|
||||||
Note that the binary is called `fdfind` as the binary name `fd` is already used by another package.
|
Note that the binary is called `fdfind` as the binary name `fd` is already used by another package.
|
||||||
It is recommended that after installation, you add a link to `fd` by executing command
|
It is recommended that after installation, you add a link to `fd` by executing command
|
||||||
|
@ -576,6 +567,8 @@ You can install [the fd package](https://www.archlinux.org/packages/community/x8
|
||||||
```
|
```
|
||||||
pacman -S fd
|
pacman -S fd
|
||||||
```
|
```
|
||||||
|
You can also install fd [from the AUR](https://aur.archlinux.org/packages/fd-git).
|
||||||
|
|
||||||
### On Gentoo Linux
|
### On Gentoo Linux
|
||||||
|
|
||||||
You can use [the fd ebuild](https://packages.gentoo.org/packages/sys-apps/fd) from the official repo:
|
You can use [the fd ebuild](https://packages.gentoo.org/packages/sys-apps/fd) from the official repo:
|
||||||
|
@ -597,22 +590,31 @@ You can install `fd` via xbps-install:
|
||||||
xbps-install -S fd
|
xbps-install -S fd
|
||||||
```
|
```
|
||||||
|
|
||||||
### On RedHat Enterprise Linux 8 (RHEL8), Almalinux 8, EuroLinux 8 or Rocky Linux 8
|
### On ALT Linux
|
||||||
|
|
||||||
Get the latest fd-v*-x86_64-unknown-linux-gnu.tar.gz file from [sharkdp on github](https://github.com/sharkdp/fd/releases)
|
You can install [the fd package](https://packages.altlinux.org/en/sisyphus/srpms/fd/) from the official repo:
|
||||||
```
|
```
|
||||||
tar xf fd-v*-x86_64-unknown-linux-gnu.tar.gz
|
apt-get install fd
|
||||||
chown -R root:root fd-v*-x86_64-unknown-linux-gnu
|
|
||||||
cd fd-v*-x86_64-unknown-linux-gnu
|
|
||||||
sudo cp fd /bin
|
|
||||||
gzip fd.1
|
|
||||||
chown root:root fd.1.gz
|
|
||||||
sudo cp fd.1.gz /usr/share/man/man1
|
|
||||||
sudo cp autocomplete/fd.bash /usr/share/bash-completion/completions/fd
|
|
||||||
source /usr/share/bash-completion/completions/fd
|
|
||||||
fd
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### On Solus
|
||||||
|
|
||||||
|
You can install [the fd package](https://github.com/getsolus/packages/tree/main/packages/f/fd) from the official repo:
|
||||||
|
```
|
||||||
|
eopkg install fd
|
||||||
|
```
|
||||||
|
|
||||||
|
### On RedHat Enterprise Linux 8/9 (RHEL8/9), Almalinux 8/9, EuroLinux 8/9 or Rocky Linux 8/9
|
||||||
|
|
||||||
|
You can install [the `fd` package](https://copr.fedorainfracloud.org/coprs/tkbcopr/fd/) from Fedora Copr.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dnf copr enable tkbcopr/fd
|
||||||
|
dnf install fd
|
||||||
|
```
|
||||||
|
|
||||||
|
A different version using the [slower](https://github.com/sharkdp/fd/pull/481#issuecomment-534494592) malloc [instead of jemalloc](https://bugzilla.redhat.com/show_bug.cgi?id=2216193#c1) is also available from the EPEL8/9 repo as the package `fd-find`.
|
||||||
|
|
||||||
### On macOS
|
### On macOS
|
||||||
|
|
||||||
You can install `fd` with [Homebrew](https://formulae.brew.sh/formula/fd):
|
You can install `fd` with [Homebrew](https://formulae.brew.sh/formula/fd):
|
||||||
|
@ -622,7 +624,7 @@ brew install fd
|
||||||
|
|
||||||
… or with MacPorts:
|
… or with MacPorts:
|
||||||
```
|
```
|
||||||
sudo port install fd
|
port install fd
|
||||||
```
|
```
|
||||||
|
|
||||||
### On Windows
|
### On Windows
|
||||||
|
@ -639,6 +641,11 @@ Or via [Chocolatey](https://chocolatey.org):
|
||||||
choco install fd
|
choco install fd
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or via [Winget](https://learn.microsoft.com/en-us/windows/package-manager/):
|
||||||
|
```
|
||||||
|
winget install sharkdp.fd
|
||||||
|
```
|
||||||
|
|
||||||
### On GuixOS
|
### On GuixOS
|
||||||
|
|
||||||
You can install [the fd package](https://guix.gnu.org/en/packages/fd-8.1.1/) from the official repo:
|
You can install [the fd package](https://guix.gnu.org/en/packages/fd-8.1.1/) from the official repo:
|
||||||
|
@ -653,6 +660,13 @@ You can use the [Nix package manager](https://nixos.org/nix/) to install `fd`:
|
||||||
nix-env -i fd
|
nix-env -i fd
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Via Flox
|
||||||
|
|
||||||
|
You can use [Flox](https://flox.dev) to install `fd` into a Flox environment:
|
||||||
|
```
|
||||||
|
flox install fd
|
||||||
|
```
|
||||||
|
|
||||||
### On FreeBSD
|
### On FreeBSD
|
||||||
|
|
||||||
You can install [the fd-find package](https://www.freshports.org/sysutils/fd) from the official repo:
|
You can install [the fd-find package](https://www.freshports.org/sysutils/fd) from the official repo:
|
||||||
|
@ -662,7 +676,7 @@ pkg install fd-find
|
||||||
|
|
||||||
### From npm
|
### From npm
|
||||||
|
|
||||||
On linux and macOS, you can install the [fd-find](https://npm.im/fd-find) package:
|
On Linux and macOS, you can install the [fd-find](https://npm.im/fd-find) package:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install -g fd-find
|
npm install -g fd-find
|
||||||
|
@ -674,7 +688,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.60.0* or later is required.
|
Note that rust version *1.77.2* or later is required.
|
||||||
|
|
||||||
`make` is also needed for the build.
|
`make` is also needed for the build.
|
||||||
|
|
||||||
|
@ -705,8 +719,6 @@ cargo install --path .
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2017-2021 The fd developers
|
|
||||||
|
|
||||||
`fd` is distributed under the terms of both the MIT License and the Apache License 2.0.
|
`fd` is distributed under the terms of both the MIT License and the Apache License 2.0.
|
||||||
|
|
||||||
See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details.
|
See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details.
|
||||||
|
|
2
build.rs
2
build.rs
|
@ -1,5 +1,5 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
let min_version = "1.60";
|
let min_version = "1.64";
|
||||||
|
|
||||||
match version_check::is_min_version(min_version) {
|
match version_check::is_min_version(min_version) {
|
||||||
Some(true) => {}
|
Some(true) => {}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
msrv = "1.60.0"
|
|
|
@ -26,6 +26,8 @@ _fd() {
|
||||||
{l,symlink}'\:"symbolic links"'
|
{l,symlink}'\:"symbolic links"'
|
||||||
{e,empty}'\:"empty files or directories"'
|
{e,empty}'\:"empty files or directories"'
|
||||||
{x,executable}'\:"executable (files)"'
|
{x,executable}'\:"executable (files)"'
|
||||||
|
{b,block-device}'\:"block devices"'
|
||||||
|
{c,char-device}'\:"character devices"'
|
||||||
{s,socket}'\:"sockets"'
|
{s,socket}'\:"sockets"'
|
||||||
{p,pipe}'\:"named pipes (FIFOs)"'
|
{p,pipe}'\:"named pipes (FIFOs)"'
|
||||||
)
|
)
|
||||||
|
@ -36,7 +38,7 @@ _fd() {
|
||||||
# for all of the potential negation options listed below!
|
# for all of the potential negation options listed below!
|
||||||
if
|
if
|
||||||
# (--[bpsu]* => match all options marked with '$no')
|
# (--[bpsu]* => match all options marked with '$no')
|
||||||
[[ $PREFIX$SUFFIX == --[bopsu]* ]] ||
|
[[ $PREFIX$SUFFIX == --[bopsun]* ]] ||
|
||||||
zstyle -t ":complete:$curcontext:*" complete-all
|
zstyle -t ":complete:$curcontext:*" complete-all
|
||||||
then
|
then
|
||||||
no=
|
no=
|
||||||
|
@ -70,6 +72,9 @@ _fd() {
|
||||||
{-g,--glob}'[perform a glob-based search]'
|
{-g,--glob}'[perform a glob-based search]'
|
||||||
{-F,--fixed-strings}'[treat pattern as literal string instead of a regex]'
|
{-F,--fixed-strings}'[treat pattern as literal string instead of a regex]'
|
||||||
|
|
||||||
|
+ '(no-require-git)'
|
||||||
|
"$no(no-ignore-full --no-ignore-vcs --no-require-git)--no-require-git[don't require git repo to respect gitignores]"
|
||||||
|
|
||||||
+ '(match-full)' # match against full path
|
+ '(match-full)' # match against full path
|
||||||
{-p,--full-path}'[match the pattern against the full path instead of the basename]'
|
{-p,--full-path}'[match the pattern against the full path instead of the basename]'
|
||||||
|
|
||||||
|
@ -118,6 +123,7 @@ _fd() {
|
||||||
|
|
||||||
+ '(filter-mtime-newer)' # filter by files modified after than
|
+ '(filter-mtime-newer)' # filter by files modified after than
|
||||||
'--changed-within=[limit search to files/directories modified within the given date/duration]:date or duration'
|
'--changed-within=[limit search to files/directories modified within the given date/duration]:date or duration'
|
||||||
|
'--changed-after=[alias for --changed-within]:date/duration'
|
||||||
'!--change-newer-than=:date/duration'
|
'!--change-newer-than=:date/duration'
|
||||||
'!--newer=:date/duration'
|
'!--newer=:date/duration'
|
||||||
|
|
||||||
|
@ -156,7 +162,11 @@ _fd() {
|
||||||
$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
|
+ strip-cwd-prefix
|
||||||
$no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix[Strip ./ prefix when output is redirected]'
|
$no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix=[When to strip ./]:when:(always never auto)'
|
||||||
|
|
||||||
|
+ and
|
||||||
|
'--and=[additional required search path]:pattern'
|
||||||
|
|
||||||
|
|
||||||
+ args # positional arguments
|
+ args # positional arguments
|
||||||
'1: :_guard "^-*" pattern'
|
'1: :_guard "^-*" pattern'
|
||||||
|
|
|
@ -29,11 +29,19 @@ By default
|
||||||
.B fd
|
.B fd
|
||||||
uses regular expressions for the pattern. However, this can be changed to use simple glob patterns
|
uses regular expressions for the pattern. However, this can be changed to use simple glob patterns
|
||||||
with the '\-\-glob' option.
|
with the '\-\-glob' option.
|
||||||
|
.P
|
||||||
|
By default
|
||||||
|
.B fd
|
||||||
|
will exclude hidden files and directories, as well as any files that match gitignore rules
|
||||||
|
or ignore rules in .ignore or .fdignore files.
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
.TP
|
.TP
|
||||||
.B \-H, \-\-hidden
|
.B \-H, \-\-hidden
|
||||||
Include hidden files and directories in the search results
|
Include hidden files and directories in the search results
|
||||||
(default: hidden files and directories are skipped). The flag can be overridden with '--no-hidden'.
|
(default: hidden files and directories are skipped). The flag can be overridden with '--no-hidden'.
|
||||||
|
.IP
|
||||||
|
Ignored files are still excluded unless \-\-no\-ignore or \-\-no\-ignore\-vcs
|
||||||
|
is also used.
|
||||||
.TP
|
.TP
|
||||||
.B \-I, \-\-no\-ignore
|
.B \-I, \-\-no\-ignore
|
||||||
Show search results from files and directories that would otherwise be ignored by
|
Show search results from files and directories that would otherwise be ignored by
|
||||||
|
@ -71,6 +79,14 @@ 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\-require\-git
|
||||||
|
Do not require a git repository to respect gitignores. By default, fd will only
|
||||||
|
respect global gitignore rules, .gitignore rules and local exclude rules if fd
|
||||||
|
detects that you are searching inside a git repository. This flag allows you to
|
||||||
|
relax this restriction such that fd will respect all git related ignore rules
|
||||||
|
regardless of whether you’re searching in a git repository or not. The flag can
|
||||||
|
be overridden with '--require-git'.
|
||||||
|
.TP
|
||||||
.B \-\-no\-ignore\-parent
|
.B \-\-no\-ignore\-parent
|
||||||
Show search results from files and directories that would otherwise be ignored by gitignore files in
|
Show search results from files and directories that would otherwise be ignored by gitignore files in
|
||||||
parent directories.
|
parent directories.
|
||||||
|
@ -94,6 +110,11 @@ Perform a regular-expression based search (default). This can be used to overrid
|
||||||
Treat the pattern as a literal string instead of a regular expression. Note that this also
|
Treat the pattern as a literal string instead of a regular expression. Note that this also
|
||||||
performs substring comparison. If you want to match on an exact filename, consider using '\-\-glob'.
|
performs substring comparison. If you want to match on an exact filename, consider using '\-\-glob'.
|
||||||
.TP
|
.TP
|
||||||
|
.BI "\-\-and " pattern
|
||||||
|
Add additional required search patterns, all of which must be matched. Multiple additional
|
||||||
|
patterns can be specified. The patterns are regular expressions, unless '\-\-glob'
|
||||||
|
or '\-\-fixed\-strings' is used.
|
||||||
|
.TP
|
||||||
.B \-a, \-\-absolute\-path
|
.B \-a, \-\-absolute\-path
|
||||||
Shows the full path starting from the root as opposed to relative paths.
|
Shows the full path starting from the root as opposed to relative paths.
|
||||||
The flag can be overridden with '--relative-path'.
|
The flag can be overridden with '--relative-path'.
|
||||||
|
@ -135,9 +156,20 @@ 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
|
.B \-\-strip-cwd-prefix [when]
|
||||||
By default, relative paths are prefixed with './' when the output goes to a non interactive terminal
|
By default, relative paths are prefixed with './' when -x/--exec,
|
||||||
(TTY). Use this flag to disable this behaviour.
|
-X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
||||||
|
path starting with '-' being treated as a command line option. Use
|
||||||
|
this flag to change this behavior. If this flag is used without a value,
|
||||||
|
it is equivalent to passing "always". Possible values are:
|
||||||
|
.RS
|
||||||
|
.IP never
|
||||||
|
Never strip the ./ at the beginning of paths
|
||||||
|
.IP always
|
||||||
|
Always strip the ./ at the beginning of paths
|
||||||
|
.IP auto
|
||||||
|
Only strip if used with --exec, --exec-batch, or --print0. That is, it resets to the default behavior.
|
||||||
|
.RE
|
||||||
.TP
|
.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).
|
||||||
|
@ -167,10 +199,14 @@ Filter search by type:
|
||||||
.RS
|
.RS
|
||||||
.IP "f, file"
|
.IP "f, file"
|
||||||
regular files
|
regular files
|
||||||
.IP "d, directory"
|
.IP "d, dir, directory"
|
||||||
directories
|
directories
|
||||||
.IP "l, symlink"
|
.IP "l, symlink"
|
||||||
symbolic links
|
symbolic links
|
||||||
|
.IP "b, block-device"
|
||||||
|
block devices
|
||||||
|
.IP "c, char-device"
|
||||||
|
character devices
|
||||||
.IP "s, socket"
|
.IP "s, socket"
|
||||||
sockets
|
sockets
|
||||||
.IP "p, pipe"
|
.IP "p, pipe"
|
||||||
|
@ -284,8 +320,9 @@ tebibytes
|
||||||
Filter results based on the file modification time.
|
Filter results based on the file modification time.
|
||||||
Files with modification times greater than the argument will be returned.
|
Files with modification times greater than the argument will be returned.
|
||||||
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
||||||
in time in either full RFC3339 format with time zone, or as a date or datetime in the
|
in time as full RFC3339 format with time zone, as a date or datetime in the
|
||||||
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR).
|
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR), or as the prefix '@'
|
||||||
|
followed by the number of seconds since the Unix epoch (@[0-9]+).
|
||||||
\fB\-\-change-newer-than\fR,
|
\fB\-\-change-newer-than\fR,
|
||||||
.B --newer
|
.B --newer
|
||||||
or
|
or
|
||||||
|
@ -296,13 +333,15 @@ Examples:
|
||||||
\-\-changed-within 2weeks
|
\-\-changed-within 2weeks
|
||||||
\-\-change-newer-than "2018-10-27 10:00:00"
|
\-\-change-newer-than "2018-10-27 10:00:00"
|
||||||
\-\-newer 2018-10-27
|
\-\-newer 2018-10-27
|
||||||
|
\-\-changed-after @1704067200
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-changed-before " date|duration
|
.BI "\-\-changed-before " date|duration
|
||||||
Filter results based on the file modification time.
|
Filter results based on the file modification time.
|
||||||
Files with modification times less than the argument will be returned.
|
Files with modification times less than the argument will be returned.
|
||||||
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
||||||
in time in either full RFC3339 format with time zone, or as a date or datetime in the
|
in time as full RFC3339 format with time zone, as a date or datetime in the
|
||||||
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR).
|
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR), or as the prefix '@'
|
||||||
|
followed by the number of seconds since the Unix epoch (@[0-9]+).
|
||||||
.B --change-older-than
|
.B --change-older-than
|
||||||
or
|
or
|
||||||
.B --older
|
.B --older
|
||||||
|
@ -311,6 +350,7 @@ can be used as aliases.
|
||||||
Examples:
|
Examples:
|
||||||
\-\-changed-before "2018-10-27 10:00:00"
|
\-\-changed-before "2018-10-27 10:00:00"
|
||||||
\-\-change-older-than 2weeks
|
\-\-change-older-than 2weeks
|
||||||
|
\-\-older @1704067200
|
||||||
.TP
|
.TP
|
||||||
.BI "-o, \-\-owner " [user][:group]
|
.BI "-o, \-\-owner " [user][:group]
|
||||||
Filter files by their user and/or group. Format: [(user|uid)][:(group|gid)]. Either side
|
Filter files by their user and/or group. Format: [(user|uid)][:(group|gid)]. Either side
|
||||||
|
@ -335,6 +375,30 @@ Set the path separator to use when printing file paths. The default is the OS-sp
|
||||||
Provide paths to search as an alternative to the positional \fIpath\fR argument. Changes the usage to
|
Provide paths to search as an alternative to the positional \fIpath\fR argument. Changes the usage to
|
||||||
\'fd [FLAGS/OPTIONS] \-\-search\-path PATH \-\-search\-path PATH2 [PATTERN]\'
|
\'fd [FLAGS/OPTIONS] \-\-search\-path PATH \-\-search\-path PATH2 [PATTERN]\'
|
||||||
.TP
|
.TP
|
||||||
|
.BI "\-\-format " fmt
|
||||||
|
Specify a template string that is used for printing a line for each file found.
|
||||||
|
|
||||||
|
The following placeholders are substituted into the string for each file before printing:
|
||||||
|
.RS
|
||||||
|
.IP {}
|
||||||
|
path (of the current search result)
|
||||||
|
.IP {/}
|
||||||
|
basename
|
||||||
|
.IP {//}
|
||||||
|
parent directory
|
||||||
|
.IP {.}
|
||||||
|
path without file extension
|
||||||
|
.IP {/.}
|
||||||
|
basename without file extension
|
||||||
|
.IP {{
|
||||||
|
literal '{' (an escape sequence)
|
||||||
|
.IP }}
|
||||||
|
literal '}' (an escape sequence)
|
||||||
|
.P
|
||||||
|
Notice that you can use "{{" and "}}" to escape "{" and "}" respectively, which is especially
|
||||||
|
useful if you need to include the literal text of one of the above placeholders.
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.BI "\-x, \-\-exec " command
|
.BI "\-x, \-\-exec " command
|
||||||
.RS
|
.RS
|
||||||
Execute
|
Execute
|
||||||
|
@ -351,19 +415,13 @@ This option can be specified multiple times, in which case all commands are run
|
||||||
file found, in the order they are provided. In that case, you must supply a ';' argument for
|
file found, in the order they are provided. In that case, you must supply a ';' argument for
|
||||||
all but the last commands.
|
all but the last commands.
|
||||||
|
|
||||||
The following placeholders are substituted before the command is executed:
|
If parallelism is enabled, the order commands will be executed in is non-deterministic. And even with
|
||||||
.RS
|
--threads=1, the order is determined by the operating system and may not be what you expect. Thus, it is
|
||||||
.IP {}
|
recommended that you don't rely on any ordering of the results.
|
||||||
path (of the current search result)
|
|
||||||
.IP {/}
|
Before executing the command, any placeholder patterns in the command are replaced with the
|
||||||
basename
|
corresponding values for the current file. The same placeholders are used as in the "\-\-format"
|
||||||
.IP {//}
|
option.
|
||||||
parent directory
|
|
||||||
.IP {.}
|
|
||||||
path without file extension
|
|
||||||
.IP {/.}
|
|
||||||
basename without file extension
|
|
||||||
.RE
|
|
||||||
|
|
||||||
If no placeholder is present, an implicit "{}" at the end is assumed.
|
If no placeholder is present, an implicit "{}" at the end is assumed.
|
||||||
|
|
||||||
|
@ -387,19 +445,12 @@ Examples:
|
||||||
Execute
|
Execute
|
||||||
.I command
|
.I command
|
||||||
once, with all search results as arguments.
|
once, with all search results as arguments.
|
||||||
One of the following placeholders is substituted before the command is executed:
|
|
||||||
.RS
|
The order of the arguments is non-deterministic and should not be relied upon.
|
||||||
.IP {}
|
|
||||||
path (of all search results)
|
This uses the same placeholders as "\-\-format" and "\-\-exec", but instead of expanding
|
||||||
.IP {/}
|
once per command invocation each argument containing a placeholder is expanding for every
|
||||||
basename
|
file in a batch and passed as separate arguments.
|
||||||
.IP {//}
|
|
||||||
parent directory
|
|
||||||
.IP {.}
|
|
||||||
path without file extension
|
|
||||||
.IP {/.}
|
|
||||||
basename without file extension
|
|
||||||
.RE
|
|
||||||
|
|
||||||
If no placeholder is present, an implicit "{}" at the end is assumed.
|
If no placeholder is present, an implicit "{}" at the end is assumed.
|
||||||
|
|
||||||
|
@ -448,6 +499,17 @@ is set, use
|
||||||
.IR $XDG_CONFIG_HOME/fd/ignore .
|
.IR $XDG_CONFIG_HOME/fd/ignore .
|
||||||
Otherwise, use
|
Otherwise, use
|
||||||
.IR $HOME/.config/fd/ignore .
|
.IR $HOME/.config/fd/ignore .
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.B .fdignore
|
||||||
|
This file works similarly to a .gitignore file anywhere in the searched tree and specifies patterns
|
||||||
|
that should be excluded from the search. However, this file is specific to fd, and will be used even
|
||||||
|
if the --no-ignore-vcs option is used.
|
||||||
|
.TP
|
||||||
|
.B $XDG_CONFIG_HOME/fd/ignore
|
||||||
|
Global ignore file. Unless ignore mode is turned off (such as with --no-ignore)
|
||||||
|
ignore entries in this file will be ignored, as if it was an .fdignore file in the
|
||||||
|
current directory.
|
||||||
.SH EXAMPLES
|
.SH EXAMPLES
|
||||||
.TP
|
.TP
|
||||||
.RI "Find files and directories that match the pattern '" needle "':"
|
.RI "Find files and directories that match the pattern '" needle "':"
|
||||||
|
@ -461,6 +523,16 @@ $ fd -e py
|
||||||
.TP
|
.TP
|
||||||
.RI "Open all search results with vim:"
|
.RI "Open all search results with vim:"
|
||||||
$ fd pattern -X vim
|
$ fd pattern -X vim
|
||||||
|
.SH Tips and Tricks
|
||||||
|
.IP \[bu]
|
||||||
|
If you add ".git/" to your global ignore file ($XDG_CONFIG_HOME/fd/ignore), then
|
||||||
|
".git" folders will be ignored by default, even when the --hidden option is used.
|
||||||
|
.IP \[bu]
|
||||||
|
You can use a shell alias or a wrapper script in order to pass desired flags to fd
|
||||||
|
by default. For example if you do not like the default behavior of respecting gitignore,
|
||||||
|
you can use
|
||||||
|
`alias fd="/usr/bin/fd --no-ignore-vcs"`
|
||||||
|
in your .bashrc to create an alias for fd that doesn't ignore git files by default.
|
||||||
.SH BUGS
|
.SH BUGS
|
||||||
Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues
|
Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues
|
||||||
.SH SEE ALSO
|
.SH SEE ALSO
|
||||||
|
|
|
@ -9,7 +9,7 @@ necessary changes for the upcoming release.
|
||||||
- [ ] Update version in `Cargo.toml`. Run `cargo build` to update `Cargo.lock`.
|
- [ ] Update version in `Cargo.toml`. Run `cargo build` to update `Cargo.lock`.
|
||||||
Make sure to `git add` the `Cargo.lock` changes as well.
|
Make sure to `git add` the `Cargo.lock` changes as well.
|
||||||
- [ ] Find the current min. supported Rust version by running
|
- [ ] Find the current min. supported Rust version by running
|
||||||
`grep '^\s*MIN_SUPPORTED_RUST_VERSION' .github/workflows/CICD.yml`.
|
`grep rust-version Cargo.toml`.
|
||||||
- [ ] Update the `fd` version and the min. supported Rust version in `README.md`.
|
- [ ] Update the `fd` version and the min. supported Rust version in `README.md`.
|
||||||
- [ ] Update `CHANGELOG.md`. Change the heading of the *"Upcoming release"* section
|
- [ ] Update `CHANGELOG.md`. Change the heading of the *"Upcoming release"* section
|
||||||
to the version of this release.
|
to the version of this release.
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
`fd` development is sponsored by many individuals and companies. Thank you very much!
|
||||||
|
|
||||||
|
Please note, that being sponsored does not affect the individuality of the `fd`
|
||||||
|
project or affect the maintainers' actions in any way.
|
||||||
|
We remain impartial and continue to assess pull requests solely on merit - the
|
||||||
|
features added, bugs solved, and effect on the overall complexity of the code.
|
||||||
|
No issue will have a different priority based on sponsorship status of the
|
||||||
|
reporter.
|
||||||
|
|
||||||
|
Contributions from anybody are most welcomed, please see our [`CONTRIBUTING.md`](../CONTRIBUTING.md) guide.
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.2 KiB |
|
@ -0,0 +1 @@
|
||||||
|
# Defaults are used
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# This script automates the "Version bump" section
|
||||||
|
|
||||||
|
version="$1"
|
||||||
|
|
||||||
|
if [[ -z $version ]]; then
|
||||||
|
echo "Usage: must supply version as first argument" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git switch -C "release-$version"
|
||||||
|
sed -i -e "0,/^\[badges/{s/^version =.*/version = \"$version\"/}" Cargo.toml
|
||||||
|
|
||||||
|
msrv="$(grep -F rust-version Cargo.toml | sed -e 's/^rust-version= "\(.*\)"/\1/')"
|
||||||
|
|
||||||
|
sed -i -e "s/Note that rust version \*[0-9.]+\* or later/Note that rust version *$msrv* or later/" README.md
|
||||||
|
|
||||||
|
sed -i -e "s/^# Upcoming release/# $version/" CHANGELOG.md
|
||||||
|
|
593
src/cli.rs
593
src/cli.rs
|
@ -1,3 +1,4 @@
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -26,17 +27,20 @@ use crate::filter::SizeFilter;
|
||||||
max_term_width = 98,
|
max_term_width = 98,
|
||||||
args_override_self = true,
|
args_override_self = true,
|
||||||
group(ArgGroup::new("execs").args(&["exec", "exec_batch", "list_details"]).conflicts_with_all(&[
|
group(ArgGroup::new("execs").args(&["exec", "exec_batch", "list_details"]).conflicts_with_all(&[
|
||||||
"max_results", "has_results", "count"])),
|
"max_results", "quiet", "max_one_result"])),
|
||||||
)]
|
)]
|
||||||
pub struct Opts {
|
pub struct Opts {
|
||||||
/// Search hidden files and directories
|
/// Include hidden directories and files in the search results (default:
|
||||||
|
/// hidden files and directories are skipped). Files and directories are
|
||||||
|
/// considered to be hidden if their name starts with a `.` sign (dot).
|
||||||
|
/// Any files or directories that are ignored due to the rules described by
|
||||||
|
/// --no-ignore are still ignored unless otherwise specified.
|
||||||
|
/// The flag can be overridden with --no-hidden.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'H',
|
short = 'H',
|
||||||
long_help = "Include hidden directories and files in the search results (default: \
|
help = "Search hidden files and directories",
|
||||||
hidden files and directories are skipped). Files and directories are \
|
long_help
|
||||||
considered to be hidden if their name starts with a `.` sign (dot). \
|
|
||||||
The flag can be overridden with --no-hidden."
|
|
||||||
)]
|
)]
|
||||||
pub hidden: bool,
|
pub hidden: bool,
|
||||||
|
|
||||||
|
@ -44,13 +48,14 @@ pub struct Opts {
|
||||||
#[arg(long, overrides_with = "hidden", hide = true, action = ArgAction::SetTrue)]
|
#[arg(long, overrides_with = "hidden", hide = true, action = ArgAction::SetTrue)]
|
||||||
no_hidden: (),
|
no_hidden: (),
|
||||||
|
|
||||||
/// Do not respect .(git|fd)ignore files
|
/// Show search results from files and directories that would otherwise be
|
||||||
|
/// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file,
|
||||||
|
/// The flag can be overridden with --ignore.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'I',
|
short = 'I',
|
||||||
long_help = "Show search results from files and directories that would otherwise be \
|
help = "Do not respect .(git|fd)ignore files",
|
||||||
ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file. \
|
long_help
|
||||||
The flag can be overridden with --ignore."
|
|
||||||
)]
|
)]
|
||||||
pub no_ignore: bool,
|
pub no_ignore: bool,
|
||||||
|
|
||||||
|
@ -58,12 +63,14 @@ pub struct Opts {
|
||||||
#[arg(long, overrides_with = "no_ignore", hide = true, action = ArgAction::SetTrue)]
|
#[arg(long, overrides_with = "no_ignore", hide = true, action = ArgAction::SetTrue)]
|
||||||
ignore: (),
|
ignore: (),
|
||||||
|
|
||||||
/// Do not respect .gitignore files
|
///Show search results from files and directories that
|
||||||
|
///would otherwise be ignored by '.gitignore' files.
|
||||||
|
///The flag can be overridden with --ignore-vcs.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Show search results from files and directories that would otherwise be \
|
help = "Do not respect .gitignore files",
|
||||||
ignored by '.gitignore' files. The flag can be overridden with --ignore-vcs."
|
long_help
|
||||||
)]
|
)]
|
||||||
pub no_ignore_vcs: bool,
|
pub no_ignore_vcs: bool,
|
||||||
|
|
||||||
|
@ -71,12 +78,35 @@ pub struct Opts {
|
||||||
#[arg(long, overrides_with = "no_ignore_vcs", hide = true, action = ArgAction::SetTrue)]
|
#[arg(long, overrides_with = "no_ignore_vcs", hide = true, action = ArgAction::SetTrue)]
|
||||||
ignore_vcs: (),
|
ignore_vcs: (),
|
||||||
|
|
||||||
/// Do not respect .(git|fd)ignore files in parent directories
|
/// Do not require a git repository to respect gitignores.
|
||||||
|
/// By default, fd will only respect global gitignore rules, .gitignore rules,
|
||||||
|
/// and local exclude rules if fd detects that you are searching inside a
|
||||||
|
/// git repository. This flag allows you to relax this restriction such that
|
||||||
|
/// fd will respect all git related ignore rules regardless of whether you're
|
||||||
|
/// searching in a git repository or not.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// This flag can be disabled with --require-git.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
overrides_with = "require_git",
|
||||||
|
hide_short_help = true,
|
||||||
|
// same description as ripgrep's flag: ripgrep/crates/core/app.rs
|
||||||
|
long_help
|
||||||
|
)]
|
||||||
|
pub no_require_git: bool,
|
||||||
|
|
||||||
|
/// Overrides --no-require-git
|
||||||
|
#[arg(long, overrides_with = "no_require_git", hide = true, action = ArgAction::SetTrue)]
|
||||||
|
require_git: (),
|
||||||
|
|
||||||
|
/// Show search results from files and directories that would otherwise be
|
||||||
|
/// ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Show search results from files and directories that would otherwise be \
|
help = "Do not respect .(git|fd)ignore files in parent directories",
|
||||||
ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories."
|
long_help
|
||||||
)]
|
)]
|
||||||
pub no_ignore_parent: bool,
|
pub no_ignore_parent: bool,
|
||||||
|
|
||||||
|
@ -84,10 +114,11 @@ pub struct Opts {
|
||||||
#[arg(long, hide = true)]
|
#[arg(long, hide = true)]
|
||||||
pub no_global_ignore_file: bool,
|
pub no_global_ignore_file: bool,
|
||||||
|
|
||||||
/// Unrestricted search, alias for '--no-ignore --hidden'
|
/// Perform an unrestricted search, including ignored and hidden files. This is
|
||||||
|
/// an alias for '--no-ignore --hidden'.
|
||||||
#[arg(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no_hidden"]), action(ArgAction::Count), hide_short_help = true,
|
#[arg(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no_hidden"]), action(ArgAction::Count), hide_short_help = true,
|
||||||
long_help = "Perform an unrestricted search, including ignored and hidden files. This is \
|
help = "Unrestricted search, alias for '--no-ignore --hidden'",
|
||||||
an alias for '--no-ignore --hidden'."
|
long_help,
|
||||||
)]
|
)]
|
||||||
rg_alias_hidden_ignore: u8,
|
rg_alias_hidden_ignore: u8,
|
||||||
|
|
||||||
|
@ -102,54 +133,72 @@ pub struct Opts {
|
||||||
)]
|
)]
|
||||||
pub case_sensitive: bool,
|
pub case_sensitive: bool,
|
||||||
|
|
||||||
/// Case-insensitive search (default: smart case)
|
/// Perform a case-insensitive search. By default, fd uses case-insensitive
|
||||||
|
/// searches, unless the pattern contains an uppercase character (smart
|
||||||
|
/// case).
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'i',
|
short = 'i',
|
||||||
overrides_with("case_sensitive"),
|
overrides_with("case_sensitive"),
|
||||||
long_help = "Perform a case-insensitive search. By default, fd uses case-insensitive \
|
help = "Case-insensitive search (default: smart case)",
|
||||||
searches, unless the pattern contains an uppercase character (smart \
|
long_help
|
||||||
case)."
|
|
||||||
)]
|
)]
|
||||||
pub ignore_case: bool,
|
pub ignore_case: bool,
|
||||||
|
|
||||||
/// Glob-based search (default: regular expression)
|
/// Perform a glob-based search instead of a regular expression search.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'g',
|
short = 'g',
|
||||||
conflicts_with("fixed_strings"),
|
conflicts_with("fixed_strings"),
|
||||||
long_help = "Perform a glob-based search instead of a regular expression search."
|
help = "Glob-based search (default: regular expression)",
|
||||||
|
long_help
|
||||||
)]
|
)]
|
||||||
pub glob: bool,
|
pub glob: bool,
|
||||||
|
|
||||||
/// Regular-expression based search (default)
|
/// Perform a regular-expression based search (default). This can be used to
|
||||||
|
/// override --glob.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
overrides_with("glob"),
|
overrides_with("glob"),
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Perform a regular-expression based search (default). This can be used to \
|
help = "Regular-expression based search (default)",
|
||||||
override --glob."
|
long_help
|
||||||
)]
|
)]
|
||||||
pub regex: bool,
|
pub regex: bool,
|
||||||
|
|
||||||
/// Treat pattern as literal string stead of regex
|
/// Treat the pattern as a literal string instead of a regular expression. Note
|
||||||
|
/// that this also performs substring comparison. If you want to match on an
|
||||||
|
/// exact filename, consider using '--glob'.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'F',
|
short = 'F',
|
||||||
alias = "literal",
|
alias = "literal",
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Treat the pattern as a literal string instead of a regular expression. Note \
|
help = "Treat pattern as literal string stead of regex",
|
||||||
that this also performs substring comparison. If you want to match on an \
|
long_help
|
||||||
exact filename, consider using '--glob'."
|
|
||||||
)]
|
)]
|
||||||
pub fixed_strings: bool,
|
pub fixed_strings: bool,
|
||||||
|
|
||||||
/// Show absolute instead of relative paths
|
/// Add additional required search patterns, all of which must be matched. Multiple
|
||||||
|
/// additional patterns can be specified. The patterns are regular
|
||||||
|
/// expressions, unless '--glob' or '--fixed-strings' is used.
|
||||||
|
#[arg(
|
||||||
|
long = "and",
|
||||||
|
value_name = "pattern",
|
||||||
|
help = "Additional search patterns that need to be matched",
|
||||||
|
long_help,
|
||||||
|
hide_short_help = true,
|
||||||
|
allow_hyphen_values = true
|
||||||
|
)]
|
||||||
|
pub exprs: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Shows the full path starting from the root as opposed to relative paths.
|
||||||
|
/// The flag can be overridden with --relative-path.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'a',
|
short = 'a',
|
||||||
long_help = "Shows the full path starting from the root as opposed to relative paths. \
|
help = "Show absolute instead of relative paths",
|
||||||
The flag can be overridden with --relative-path."
|
long_help
|
||||||
)]
|
)]
|
||||||
pub absolute_path: bool,
|
pub absolute_path: bool,
|
||||||
|
|
||||||
|
@ -157,15 +206,16 @@ pub struct Opts {
|
||||||
#[arg(long, overrides_with = "absolute_path", hide = true, action = ArgAction::SetTrue)]
|
#[arg(long, overrides_with = "absolute_path", hide = true, action = ArgAction::SetTrue)]
|
||||||
relative_path: (),
|
relative_path: (),
|
||||||
|
|
||||||
/// Use a long listing format with file metadata
|
/// Use a detailed listing format like 'ls -l'. This is basically an alias
|
||||||
|
/// for '--exec-batch ls -l' with some additional 'ls' options. This can be
|
||||||
|
/// used to see more metadata, to show symlink targets and to achieve a
|
||||||
|
/// deterministic sort order.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'l',
|
short = 'l',
|
||||||
conflicts_with("absolute_path"),
|
conflicts_with("absolute_path"),
|
||||||
long_help = "Use a detailed listing format like 'ls -l'. This is basically an alias \
|
help = "Use a long listing format with file metadata",
|
||||||
for '--exec-batch ls -l' with some additional 'ls' options. This can be \
|
long_help
|
||||||
used to see more metadata, to show symlink targets and to achieve a \
|
|
||||||
deterministic sort order."
|
|
||||||
)]
|
)]
|
||||||
pub list_details: bool,
|
pub list_details: bool,
|
||||||
|
|
||||||
|
@ -176,7 +226,7 @@ pub struct Opts {
|
||||||
alias = "dereference",
|
alias = "dereference",
|
||||||
long_help = "By default, fd does not descend into symlinked directories. Using this \
|
long_help = "By default, fd does not descend into symlinked directories. Using this \
|
||||||
flag, symbolic links are also traversed. \
|
flag, symbolic links are also traversed. \
|
||||||
Flag can be overriden with --no-follow."
|
Flag can be overridden with --no-follow."
|
||||||
)]
|
)]
|
||||||
pub follow: bool,
|
pub follow: bool,
|
||||||
|
|
||||||
|
@ -184,207 +234,247 @@ pub struct Opts {
|
||||||
#[arg(long, overrides_with = "follow", hide = true, action = ArgAction::SetTrue)]
|
#[arg(long, overrides_with = "follow", hide = true, action = ArgAction::SetTrue)]
|
||||||
no_follow: (),
|
no_follow: (),
|
||||||
|
|
||||||
/// Search full abs. path (default: filename only)
|
/// By default, the search pattern is only matched against the filename (or directory name). Using this flag, the pattern is matched against the full (absolute) path. Example:
|
||||||
|
/// fd --glob -p '**/.git/config'
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'p',
|
short = 'p',
|
||||||
long_help = "By default, the search pattern is only matched against the filename (or \
|
help = "Search full abs. path (default: filename only)",
|
||||||
directory name). Using this flag, the pattern is matched against the full \
|
long_help,
|
||||||
(absolute) path. Example:\n \
|
verbatim_doc_comment
|
||||||
fd --glob -p '**/.git/config'"
|
|
||||||
)]
|
)]
|
||||||
pub full_path: bool,
|
pub full_path: bool,
|
||||||
|
|
||||||
/// Separate search results by the null character
|
/// Separate search results by the null character (instead of newlines).
|
||||||
|
/// Useful for piping results to 'xargs'.
|
||||||
#[arg(
|
#[arg(
|
||||||
long = "print0",
|
long = "print0",
|
||||||
short = '0',
|
short = '0',
|
||||||
conflicts_with("list_details"),
|
conflicts_with("list_details"),
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Separate search results by the null character (instead of newlines). \
|
help = "Separate search results by the null character",
|
||||||
Useful for piping results to 'xargs'."
|
long_help
|
||||||
)]
|
)]
|
||||||
pub null_separator: bool,
|
pub null_separator: bool,
|
||||||
|
|
||||||
/// Set maximum search depth (default: none)
|
/// Limit the directory traversal to a given depth. By default, there is no
|
||||||
|
/// limit on the search depth.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'd',
|
short = 'd',
|
||||||
value_name = "depth",
|
value_name = "depth",
|
||||||
alias("maxdepth"),
|
alias("maxdepth"),
|
||||||
long_help = "Limit the directory traversal to a given depth. By default, there is no \
|
help = "Set maximum search depth (default: none)",
|
||||||
limit on the search depth."
|
long_help
|
||||||
)]
|
)]
|
||||||
max_depth: Option<usize>,
|
max_depth: Option<usize>,
|
||||||
|
|
||||||
/// Only show search results starting at the given depth.
|
/// Only show search results starting at the given depth.
|
||||||
|
/// See also: '--max-depth' and '--exact-depth'
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
value_name = "depth",
|
value_name = "depth",
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Only show search results starting at the given depth. \
|
help = "Only show search results starting at the given depth.",
|
||||||
See also: '--max-depth' and '--exact-depth'"
|
long_help
|
||||||
)]
|
)]
|
||||||
min_depth: Option<usize>,
|
min_depth: Option<usize>,
|
||||||
|
|
||||||
/// Only show search results at the exact given depth
|
/// Only show search results at the exact given depth. This is an alias for
|
||||||
|
/// '--min-depth <depth> --max-depth <depth>'.
|
||||||
#[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]),
|
#[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]),
|
||||||
long_help = "Only show search results at the exact given depth. This is an alias for \
|
help = "Only show search results at the exact given depth",
|
||||||
'--min-depth <depth> --max-depth <depth>'.",
|
long_help,
|
||||||
)]
|
)]
|
||||||
exact_depth: Option<usize>,
|
exact_depth: Option<usize>,
|
||||||
|
|
||||||
/// Exclude entries that match the given glob pattern
|
/// Exclude files/directories that match the given glob pattern. This
|
||||||
|
/// overrides any other ignore logic. Multiple exclude patterns can be
|
||||||
|
/// specified.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// {n} --exclude '*.pyc'
|
||||||
|
/// {n} --exclude node_modules
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'E',
|
short = 'E',
|
||||||
value_name = "pattern",
|
value_name = "pattern",
|
||||||
long_help = "Exclude files/directories that match the given glob pattern. This \
|
help = "Exclude entries that match the given glob pattern",
|
||||||
overrides any other ignore logic. Multiple exclude patterns can be \
|
long_help
|
||||||
specified.\n\n\
|
|
||||||
Examples:\n \
|
|
||||||
--exclude '*.pyc'\n \
|
|
||||||
--exclude node_modules"
|
|
||||||
)]
|
)]
|
||||||
pub exclude: Vec<String>,
|
pub exclude: Vec<String>,
|
||||||
|
|
||||||
/// Do not traverse into directories that match the search criteria. If
|
/// 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(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]),
|
#[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]),
|
||||||
long_help = "Do not traverse into directories that match the search criteria. If \
|
long_help,
|
||||||
you want to exclude specific directories, use the '--exclude=…' option.",
|
|
||||||
)]
|
)]
|
||||||
pub prune: bool,
|
pub prune: bool,
|
||||||
|
|
||||||
/// Filter by type: file (f), directory (d), symlink (l),
|
/// Filter the search by type:
|
||||||
/// executable (x), empty (e), socket (s), pipe (p)
|
/// {n} 'f' or 'file': regular files
|
||||||
|
/// {n} 'd' or 'dir' or 'directory': directories
|
||||||
|
/// {n} 'l' or 'symlink': symbolic links
|
||||||
|
/// {n} 's' or 'socket': socket
|
||||||
|
/// {n} 'p' or 'pipe': named pipe (FIFO)
|
||||||
|
/// {n} 'b' or 'block-device': block device
|
||||||
|
/// {n} 'c' or 'char-device': character device
|
||||||
|
/// {n}{n} 'x' or 'executable': executables
|
||||||
|
/// {n} 'e' or 'empty': empty files or directories
|
||||||
|
///
|
||||||
|
/// This option can be specified more than once to include multiple file types.
|
||||||
|
/// Searching for '--type file --type symlink' will show both regular files as
|
||||||
|
/// well as symlinks. Note that the 'executable' and 'empty' filters work differently:
|
||||||
|
/// '--type executable' implies '--type file' by default. And '--type empty' searches
|
||||||
|
/// for empty files and directories, unless either '--type file' or '--type directory'
|
||||||
|
/// is specified in addition.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// {n} - Only search for files:
|
||||||
|
/// {n} fd --type file …
|
||||||
|
/// {n} fd -tf …
|
||||||
|
/// {n} - Find both files and symlinks
|
||||||
|
/// {n} fd --type file --type symlink …
|
||||||
|
/// {n} fd -tf -tl …
|
||||||
|
/// {n} - Find executable files:
|
||||||
|
/// {n} fd --type executable
|
||||||
|
/// {n} fd -tx
|
||||||
|
/// {n} - Find empty files:
|
||||||
|
/// {n} fd --type empty --type file
|
||||||
|
/// {n} fd -te -tf
|
||||||
|
/// {n} - Find empty directories:
|
||||||
|
/// {n} fd --type empty --type directory
|
||||||
|
/// {n} fd -te -td
|
||||||
#[arg(
|
#[arg(
|
||||||
long = "type",
|
long = "type",
|
||||||
short = 't',
|
short = 't',
|
||||||
value_name = "filetype",
|
value_name = "filetype",
|
||||||
hide_possible_values = true,
|
hide_possible_values = true,
|
||||||
value_enum,
|
value_enum,
|
||||||
long_help = "Filter the search by type:\n \
|
help = "Filter by type: file (f), directory (d/dir), symlink (l), \
|
||||||
'f' or 'file': regular files\n \
|
executable (x), empty (e), socket (s), pipe (p), \
|
||||||
'd' or 'directory': directories\n \
|
char-device (c), block-device (b)",
|
||||||
'l' or 'symlink': symbolic links\n \
|
long_help
|
||||||
's' or 'socket': socket\n \
|
|
||||||
'p' or 'pipe': named pipe (FIFO)\n\n \
|
|
||||||
'x' or 'executable': executables\n \
|
|
||||||
'e' or 'empty': empty files or directories\n\n\
|
|
||||||
This option can be specified more than once to include multiple file types. \
|
|
||||||
Searching for '--type file --type symlink' will show both regular files as \
|
|
||||||
well as symlinks. Note that the 'executable' and 'empty' filters work differently: \
|
|
||||||
'--type executable' implies '--type file' by default. And '--type empty' searches \
|
|
||||||
for empty files and directories, unless either '--type file' or '--type directory' \
|
|
||||||
is specified in addition.\n\n\
|
|
||||||
Examples:\n \
|
|
||||||
- Only search for files:\n \
|
|
||||||
fd --type file …\n \
|
|
||||||
fd -tf …\n \
|
|
||||||
- Find both files and symlinks\n \
|
|
||||||
fd --type file --type symlink …\n \
|
|
||||||
fd -tf -tl …\n \
|
|
||||||
- Find executable files:\n \
|
|
||||||
fd --type executable\n \
|
|
||||||
fd -tx\n \
|
|
||||||
- Find empty files:\n \
|
|
||||||
fd --type empty --type file\n \
|
|
||||||
fd -te -tf\n \
|
|
||||||
- Find empty directories:\n \
|
|
||||||
fd --type empty --type directory\n \
|
|
||||||
fd -te -td"
|
|
||||||
)]
|
)]
|
||||||
pub filetype: Option<Vec<FileType>>,
|
pub filetype: Option<Vec<FileType>>,
|
||||||
|
|
||||||
/// Filter by file extension
|
/// (Additionally) filter search results by their file extension. Multiple
|
||||||
|
/// allowable file extensions can be specified.
|
||||||
|
///
|
||||||
|
/// If you want to search for files without extension,
|
||||||
|
/// you can use the regex '^[^.]+$' as a normal search pattern.
|
||||||
#[arg(
|
#[arg(
|
||||||
long = "extension",
|
long = "extension",
|
||||||
short = 'e',
|
short = 'e',
|
||||||
value_name = "ext",
|
value_name = "ext",
|
||||||
long_help = "(Additionally) filter search results by their file extension. Multiple \
|
help = "Filter by file extension",
|
||||||
allowable file extensions can be specified.\n\
|
long_help
|
||||||
If you want to search for files without extension, \
|
|
||||||
you can use the regex '^[^.]+$' as a normal search pattern."
|
|
||||||
)]
|
)]
|
||||||
pub extensions: Option<Vec<String>>,
|
pub extensions: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Limit results based on the size of files
|
/// Limit results based on the size of files using the format <+-><NUM><UNIT>.
|
||||||
|
/// '+': file size must be greater than or equal to this
|
||||||
|
/// '-': file size must be less than or equal to this
|
||||||
|
///
|
||||||
|
/// If neither '+' nor '-' is specified, file size must be exactly equal to this.
|
||||||
|
/// 'NUM': The numeric size (e.g. 500)
|
||||||
|
/// 'UNIT': The units for NUM. They are not case-sensitive.
|
||||||
|
/// Allowed unit values:
|
||||||
|
/// 'b': bytes
|
||||||
|
/// 'k': kilobytes (base ten, 10^3 = 1000 bytes)
|
||||||
|
/// 'm': megabytes
|
||||||
|
/// 'g': gigabytes
|
||||||
|
/// 't': terabytes
|
||||||
|
/// 'ki': kibibytes (base two, 2^10 = 1024 bytes)
|
||||||
|
/// 'mi': mebibytes
|
||||||
|
/// 'gi': gibibytes
|
||||||
|
/// 'ti': tebibytes
|
||||||
#[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment, value_name = "size",
|
#[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment, value_name = "size",
|
||||||
long_help = "Limit results based on the size of files using the format <+-><NUM><UNIT>.\n \
|
help = "Limit results based on the size of files",
|
||||||
'+': file size must be greater than or equal to this\n \
|
long_help,
|
||||||
'-': file size must be less than or equal to this\n\
|
verbatim_doc_comment,
|
||||||
If neither '+' nor '-' is specified, file size must be exactly equal to this.\n \
|
|
||||||
'NUM': The numeric size (e.g. 500)\n \
|
|
||||||
'UNIT': The units for NUM. They are not case-sensitive.\n\
|
|
||||||
Allowed unit values:\n \
|
|
||||||
'b': bytes\n \
|
|
||||||
'k': kilobytes (base ten, 10^3 = 1000 bytes)\n \
|
|
||||||
'm': megabytes\n \
|
|
||||||
'g': gigabytes\n \
|
|
||||||
't': terabytes\n \
|
|
||||||
'ki': kibibytes (base two, 2^10 = 1024 bytes)\n \
|
|
||||||
'mi': mebibytes\n \
|
|
||||||
'gi': gibibytes\n \
|
|
||||||
'ti': tebibytes",
|
|
||||||
)]
|
)]
|
||||||
pub size: Vec<SizeFilter>,
|
pub size: Vec<SizeFilter>,
|
||||||
|
|
||||||
/// Filter by file modification time (newer than)
|
/// Filter results based on the file modification time. Files with modification times
|
||||||
|
/// greater than the argument are returned. The argument can be provided
|
||||||
|
/// as a specific point in time (YYYY-MM-DD HH:MM:SS or @timestamp) or as a duration (10h, 1d, 35min).
|
||||||
|
/// If the time is not specified, it defaults to 00:00:00.
|
||||||
|
/// '--change-newer-than', '--newer', or '--changed-after' can be used as aliases.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// {n} --changed-within 2weeks
|
||||||
|
/// {n} --change-newer-than '2018-10-27 10:00:00'
|
||||||
|
/// {n} --newer 2018-10-27
|
||||||
|
/// {n} --changed-after 1day
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
alias("change-newer-than"),
|
alias("change-newer-than"),
|
||||||
alias("newer"),
|
alias("newer"),
|
||||||
alias("changed-after"),
|
alias("changed-after"),
|
||||||
value_name = "date|dur",
|
value_name = "date|dur",
|
||||||
long_help = "Filter results based on the file modification time. \
|
help = "Filter by file modification time (newer than)",
|
||||||
Files with modification times greater than the argument are returned. \
|
long_help
|
||||||
The argument can be provided \
|
|
||||||
as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min). \
|
|
||||||
If the time is not specified, it defaults to 00:00:00. \
|
|
||||||
'--change-newer-than' or '--newer' can be used as aliases.\n\
|
|
||||||
Examples:\n \
|
|
||||||
--changed-within 2weeks\n \
|
|
||||||
--change-newer-than '2018-10-27 10:00:00'\n \
|
|
||||||
--newer 2018-10-27"
|
|
||||||
)]
|
)]
|
||||||
pub changed_within: Option<String>,
|
pub changed_within: Option<String>,
|
||||||
|
|
||||||
/// Filter by file modification time (older than)
|
/// Filter results based on the file modification time. Files with modification times
|
||||||
|
/// less than the argument are returned. The argument can be provided
|
||||||
|
/// as a specific point in time (YYYY-MM-DD HH:MM:SS or @timestamp) or as a duration (10h, 1d, 35min).
|
||||||
|
/// '--change-older-than' or '--older' can be used as aliases.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// {n} --changed-before '2018-10-27 10:00:00'
|
||||||
|
/// {n} --change-older-than 2weeks
|
||||||
|
/// {n} --older 2018-10-27
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
alias("change-older-than"),
|
alias("change-older-than"),
|
||||||
alias("older"),
|
alias("older"),
|
||||||
value_name = "date|dur",
|
value_name = "date|dur",
|
||||||
long_help = "Filter results based on the file modification time. \
|
help = "Filter by file modification time (older than)",
|
||||||
Files with modification times less than the argument are returned. \
|
long_help
|
||||||
The argument can be provided \
|
|
||||||
as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min). \
|
|
||||||
'--change-older-than' or '--older' can be used as aliases.\n\
|
|
||||||
Examples:\n \
|
|
||||||
--changed-before '2018-10-27 10:00:00'\n \
|
|
||||||
--change-older-than 2weeks\n \
|
|
||||||
--older 2018-10-27"
|
|
||||||
)]
|
)]
|
||||||
pub changed_before: Option<String>,
|
pub changed_before: Option<String>,
|
||||||
|
|
||||||
/// Filter by owning user and/or group
|
/// Filter files by their user and/or group.
|
||||||
|
/// Format: [(user|uid)][:(group|gid)]. Either side is optional.
|
||||||
|
/// Precede either side with a '!' to exclude files instead.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// {n} --owner john
|
||||||
|
/// {n} --owner :students
|
||||||
|
/// {n} --owner '!john:students'
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[arg(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group",
|
#[arg(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group",
|
||||||
long_help = "Filter files by their user and/or group. \
|
help = "Filter by owning user and/or group",
|
||||||
Format: [(user|uid)][:(group|gid)]. Either side is optional. \
|
long_help,
|
||||||
Precede either side with a '!' to exclude files instead.\n\
|
|
||||||
Examples:\n \
|
|
||||||
--owner john\n \
|
|
||||||
--owner :students\n \
|
|
||||||
--owner '!john:students'"
|
|
||||||
)]
|
)]
|
||||||
pub owner: Option<OwnerFilter>,
|
pub owner: Option<OwnerFilter>,
|
||||||
|
|
||||||
|
/// Instead of printing the file normally, print the format string with the following placeholders replaced:
|
||||||
|
/// '{}': path (of the current search result)
|
||||||
|
/// '{/}': basename
|
||||||
|
/// '{//}': parent directory
|
||||||
|
/// '{.}': path without file extension
|
||||||
|
/// '{/.}': basename without file extension
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
value_name = "fmt",
|
||||||
|
help = "Print results according to template",
|
||||||
|
conflicts_with = "list_details"
|
||||||
|
)]
|
||||||
|
pub format: Option<String>,
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub exec: Exec,
|
pub exec: Exec,
|
||||||
|
|
||||||
/// Max number of arguments to run as a batch size with -X
|
/// Maximum number of arguments to pass to the command given with -X.
|
||||||
|
/// If the number of results is greater than the given size,
|
||||||
|
/// the command given with -X is run again with remaining arguments.
|
||||||
|
/// A batch size of zero means there is no limit (default), but note
|
||||||
|
/// that batching might still happen due to OS restrictions on the
|
||||||
|
/// maximum length of command lines.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
value_name = "size",
|
value_name = "size",
|
||||||
|
@ -392,39 +482,37 @@ pub struct Opts {
|
||||||
requires("exec_batch"),
|
requires("exec_batch"),
|
||||||
value_parser = value_parser!(usize),
|
value_parser = value_parser!(usize),
|
||||||
default_value_t,
|
default_value_t,
|
||||||
long_help = "Maximum number of arguments to pass to the command given with -X. \
|
help = "Max number of arguments to run as a batch size with -X",
|
||||||
If the number of results is greater than the given size, \
|
long_help,
|
||||||
the command given with -X is run again with remaining arguments. \
|
|
||||||
A batch size of zero means there is no limit (default), but note \
|
|
||||||
that batching might still happen due to OS restrictions on the \
|
|
||||||
maximum length of command lines.",
|
|
||||||
)]
|
)]
|
||||||
pub batch_size: usize,
|
pub batch_size: usize,
|
||||||
|
|
||||||
/// Add a custom ignore-file in '.gitignore' format
|
/// Add a custom ignore-file in '.gitignore' format. These files have a low precedence.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
value_name = "path",
|
value_name = "path",
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Add a custom ignore-file in '.gitignore' format. These files have a low precedence."
|
help = "Add a custom ignore-file in '.gitignore' format",
|
||||||
|
long_help
|
||||||
)]
|
)]
|
||||||
pub ignore_file: Vec<PathBuf>,
|
pub ignore_file: Vec<PathBuf>,
|
||||||
|
|
||||||
/// When to use colors
|
/// Declare when to use color for the pattern match output
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'c',
|
short = 'c',
|
||||||
value_enum,
|
value_enum,
|
||||||
default_value_t = ColorWhen::Auto,
|
default_value_t = ColorWhen::Auto,
|
||||||
value_name = "when",
|
value_name = "when",
|
||||||
long_help = "Declare when to use color for the pattern match output",
|
help = "When to use colors",
|
||||||
|
long_help,
|
||||||
)]
|
)]
|
||||||
pub color: ColorWhen,
|
pub color: ColorWhen,
|
||||||
|
|
||||||
/// Set number of threads to use for searching & executing (default: number
|
/// Set number of threads to use for searching & executing (default: number
|
||||||
/// of available CPU cores)
|
/// of available CPU cores)
|
||||||
#[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = clap::value_parser!(u32).range(1..))]
|
#[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = str::parse::<NonZeroUsize>)]
|
||||||
pub threads: Option<u32>,
|
pub threads: Option<NonZeroUsize>,
|
||||||
|
|
||||||
/// Milliseconds to buffer before streaming search results to console
|
/// Milliseconds to buffer before streaming search results to console
|
||||||
///
|
///
|
||||||
|
@ -433,121 +521,127 @@ pub struct Opts {
|
||||||
#[arg(long, hide = true, value_parser = parse_millis)]
|
#[arg(long, hide = true, value_parser = parse_millis)]
|
||||||
pub max_buffer_time: Option<Duration>,
|
pub max_buffer_time: Option<Duration>,
|
||||||
|
|
||||||
/// Limit number of search results
|
///Limit the number of search results to 'count' and quit immediately.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
value_name = "count",
|
value_name = "count",
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Limit the number of search results to 'count' and quit immediately."
|
overrides_with("max_one_result"),
|
||||||
|
help = "Limit the number of search results",
|
||||||
|
long_help
|
||||||
)]
|
)]
|
||||||
max_results: Option<usize>,
|
max_results: Option<usize>,
|
||||||
|
|
||||||
/// Limit search to a single result
|
/// Limit the search to a single result and quit immediately.
|
||||||
|
/// This is an alias for '--max-results=1'.
|
||||||
#[arg(
|
#[arg(
|
||||||
short = '1',
|
short = '1',
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
overrides_with("max_results"),
|
overrides_with("max_results"),
|
||||||
long_help = "Limit the search to a single result and quit immediately. \
|
help = "Limit search to a single result",
|
||||||
This is an alias for '--max-results=1'."
|
long_help
|
||||||
)]
|
)]
|
||||||
max_one_result: bool,
|
max_one_result: bool,
|
||||||
|
|
||||||
/// Print nothing, exit code 0 if match found, 1 otherwise
|
/// When the flag is present, the program does not print anything and will
|
||||||
|
/// return with an exit code of 0 if there is at least one match. Otherwise, the
|
||||||
|
/// exit code will be 1.
|
||||||
|
/// '--has-results' can be used as an alias.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short = 'q',
|
short = 'q',
|
||||||
alias = "has-results",
|
alias = "has-results",
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
conflicts_with("max_results"),
|
conflicts_with("max_results"),
|
||||||
long_help = "When the flag is present, the program does not print anything and will \
|
help = "Print nothing, exit code 0 if match found, 1 otherwise",
|
||||||
return with an exit code of 0 if there is at least one match. Otherwise, the \
|
long_help
|
||||||
exit code will be 1. \
|
|
||||||
'--has-results' can be used as an alias."
|
|
||||||
)]
|
)]
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
|
|
||||||
/// Show filesystem errors
|
/// Enable the display of filesystem errors for situations such as
|
||||||
|
/// insufficient permissions or dead symlinks.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Enable the display of filesystem errors for situations such as \
|
help = "Show filesystem errors",
|
||||||
insufficient permissions or dead symlinks."
|
long_help
|
||||||
)]
|
)]
|
||||||
pub show_errors: bool,
|
pub show_errors: bool,
|
||||||
|
|
||||||
/// Change current working directory
|
/// Change the current working directory of fd to the provided path. This
|
||||||
|
/// means that search results will be shown with respect to the given base
|
||||||
|
/// path. Note that relative paths which are passed to fd via the positional
|
||||||
|
/// <path> argument or the '--search-path' option will also be resolved
|
||||||
|
/// relative to this directory.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
value_name = "path",
|
value_name = "path",
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Change the current working directory of fd to the provided path. This \
|
help = "Change current working directory",
|
||||||
means that search results will be shown with respect to the given base \
|
long_help
|
||||||
path. Note that relative paths which are passed to fd via the positional \
|
|
||||||
<path> argument or the '--search-path' option will also be resolved \
|
|
||||||
relative to this directory."
|
|
||||||
)]
|
)]
|
||||||
pub base_directory: Option<PathBuf>,
|
pub base_directory: Option<PathBuf>,
|
||||||
|
|
||||||
/// the search pattern (a regular expression, unless '--glob' is used; optional)
|
/// the search pattern which is either a regular expression (default) or a glob
|
||||||
|
/// pattern (if --glob is used). If no pattern has been specified, every entry
|
||||||
|
/// is considered a match. If your pattern starts with a dash (-), make sure to
|
||||||
|
/// pass '--' first, or it will be considered as a flag (fd -- '-foo').
|
||||||
#[arg(
|
#[arg(
|
||||||
default_value = "",
|
default_value = "",
|
||||||
hide_default_value = true,
|
hide_default_value = true,
|
||||||
value_name = "pattern",
|
value_name = "pattern",
|
||||||
long_help = "the search pattern which is either a regular expression (default) or a glob \
|
help = "the search pattern (a regular expression, unless '--glob' is used; optional)",
|
||||||
pattern (if --glob is used). If no pattern has been specified, every entry \
|
long_help
|
||||||
is considered a match. If your pattern starts with a dash (-), make sure to \
|
|
||||||
pass '--' first, or it will be considered as a flag (fd -- '-foo')."
|
|
||||||
)]
|
)]
|
||||||
pub pattern: String,
|
pub pattern: String,
|
||||||
|
|
||||||
/// Set path separator when printing file paths
|
/// Set the path separator to use when printing file paths. The default is
|
||||||
|
/// the OS-specific separator ('/' on Unix, '\' on Windows).
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
value_name = "separator",
|
value_name = "separator",
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Set the path separator to use when printing file paths. The default is \
|
help = "Set path separator when printing file paths",
|
||||||
the OS-specific separator ('/' on Unix, '\\' on Windows)."
|
long_help
|
||||||
)]
|
)]
|
||||||
pub path_separator: Option<String>,
|
pub path_separator: Option<String>,
|
||||||
|
|
||||||
/// the root directories for the filesystem search (optional)
|
/// The directory where the filesystem search is rooted (optional). If
|
||||||
|
/// omitted, search the current working directory.
|
||||||
#[arg(action = ArgAction::Append,
|
#[arg(action = ArgAction::Append,
|
||||||
value_name = "path",
|
value_name = "path",
|
||||||
long_help = "The directory where the filesystem search is rooted (optional). If \
|
help = "the root directories for the filesystem search (optional)",
|
||||||
omitted, search the current working directory.",
|
long_help,
|
||||||
)]
|
)]
|
||||||
path: Vec<PathBuf>,
|
path: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Provides paths to search as an alternative to the positional <path> argument
|
/// Provide paths to search as an alternative to the positional <path>
|
||||||
|
/// argument. Changes the usage to `fd [OPTIONS] --search-path <path>
|
||||||
|
/// --search-path <path2> [<pattern>]`
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
conflicts_with("path"),
|
conflicts_with("path"),
|
||||||
value_name = "search-path",
|
value_name = "search-path",
|
||||||
hide_short_help = true,
|
hide_short_help = true,
|
||||||
long_help = "Provide paths to search as an alternative to the positional <path> \
|
help = "Provides paths to search as an alternative to the positional <path> argument",
|
||||||
argument. Changes the usage to `fd [OPTIONS] --search-path <path> \
|
long_help
|
||||||
--search-path <path2> [<pattern>]`"
|
|
||||||
)]
|
)]
|
||||||
search_path: Vec<PathBuf>,
|
search_path: Vec<PathBuf>,
|
||||||
|
|
||||||
/// By default, relative paths are prefixed with './' when -x/--exec,
|
/// By default, relative paths are prefixed with './' when -x/--exec,
|
||||||
/// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
/// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
||||||
/// path starting with '-' being treated as a command line option. Use
|
/// path starting with '-' being treated as a command line option. Use
|
||||||
/// this flag to disable this behaviour.
|
/// this flag to change this behavior. If this flag is used without a value,
|
||||||
#[arg(long, conflicts_with_all(&["path", "search_path"]), hide_short_help = true,
|
/// it is equivalent to passing "always".
|
||||||
long_help = "By default, relative paths are prefixed with './' when -x/--exec, \
|
#[arg(long, conflicts_with_all(&["path", "search_path"]), value_name = "when", hide_short_help = true, require_equals = true, long_help)]
|
||||||
-X/--exec-batch, or -0/--print0 are given, to reduce the risk of a \
|
strip_cwd_prefix: Option<Option<StripCwdWhen>>,
|
||||||
path starting with '-' being treated as a command line option. Use \
|
|
||||||
this flag to disable this behaviour.",
|
|
||||||
)]
|
|
||||||
pub strip_cwd_prefix: bool,
|
|
||||||
|
|
||||||
|
/// By default, fd will traverse the file system tree as far as other options
|
||||||
|
/// dictate. With this flag, fd ensures that it does not descend into a
|
||||||
|
/// different file system than the one it started in. Comparable to the -mount
|
||||||
|
/// or -xdev filters of find(1).
|
||||||
#[cfg(any(unix, windows))]
|
#[cfg(any(unix, windows))]
|
||||||
#[arg(long, aliases(&["mount", "xdev"]), hide_short_help = true,
|
#[arg(long, aliases(&["mount", "xdev"]), hide_short_help = true, long_help)]
|
||||||
long_help = "By default, fd will traverse the file system tree as far as other options \
|
|
||||||
dictate. With this flag, fd ensures that it does not descend into a \
|
|
||||||
different file system than the one it started in. Comparable to the -mount \
|
|
||||||
or -xdev filters of find(1).")]
|
|
||||||
pub one_file_system: bool,
|
pub one_file_system: bool,
|
||||||
|
|
||||||
#[cfg(feature = "completions")]
|
#[cfg(feature = "completions")]
|
||||||
|
@ -563,7 +657,7 @@ impl Opts {
|
||||||
} else if !self.search_path.is_empty() {
|
} else if !self.search_path.is_empty() {
|
||||||
&self.search_path
|
&self.search_path
|
||||||
} else {
|
} else {
|
||||||
let current_directory = Path::new(".");
|
let current_directory = Path::new("./");
|
||||||
ensure_current_directory_exists(current_directory)?;
|
ensure_current_directory_exists(current_directory)?;
|
||||||
return Ok(vec![self.normalize_path(current_directory)]);
|
return Ok(vec![self.normalize_path(current_directory)]);
|
||||||
};
|
};
|
||||||
|
@ -586,6 +680,9 @@ impl Opts {
|
||||||
fn normalize_path(&self, path: &Path) -> PathBuf {
|
fn normalize_path(&self, path: &Path) -> PathBuf {
|
||||||
if self.absolute_path {
|
if self.absolute_path {
|
||||||
filesystem::absolute_path(path.normalize().unwrap().as_path()).unwrap()
|
filesystem::absolute_path(path.normalize().unwrap().as_path()).unwrap()
|
||||||
|
} else if path == Path::new(".") {
|
||||||
|
// Change "." to "./" as a workaround for https://github.com/BurntSushi/ripgrep/pull/2711
|
||||||
|
PathBuf::from("./")
|
||||||
} else {
|
} else {
|
||||||
path.to_path_buf()
|
path.to_path_buf()
|
||||||
}
|
}
|
||||||
|
@ -608,23 +705,24 @@ impl Opts {
|
||||||
self.min_depth.or(self.exact_depth)
|
self.min_depth.or(self.exact_depth)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn threads(&self) -> usize {
|
pub fn threads(&self) -> NonZeroUsize {
|
||||||
// This will panic if the number of threads passed in is more than usize::MAX in an environment
|
self.threads.unwrap_or_else(default_num_threads)
|
||||||
// where usize is less than 32 bits (for example 16-bit architectures). It's pretty
|
|
||||||
// unlikely fd will be running in such an environment, and even more unlikely someone would
|
|
||||||
// be trying to use that many threads on such an environment, so I think panicing is an
|
|
||||||
// appropriate way to handle that.
|
|
||||||
std::cmp::max(
|
|
||||||
self.threads
|
|
||||||
.map_or_else(num_cpus::get, |n| n.try_into().expect("too many threads")),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn max_results(&self) -> Option<usize> {
|
pub fn max_results(&self) -> Option<usize> {
|
||||||
self.max_results
|
self.max_results
|
||||||
.filter(|&m| m > 0)
|
.filter(|&m| m > 0)
|
||||||
.or_else(|| self.max_one_result.then(|| 1))
|
.or_else(|| self.max_one_result.then_some(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn strip_cwd_prefix<P: FnOnce() -> bool>(&self, auto_pred: P) -> bool {
|
||||||
|
use self::StripCwdWhen::*;
|
||||||
|
self.no_search_paths()
|
||||||
|
&& match self.strip_cwd_prefix.map_or(Auto, |o| o.unwrap_or(Always)) {
|
||||||
|
Auto => auto_pred(),
|
||||||
|
Always => true,
|
||||||
|
Never => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "completions")]
|
#[cfg(feature = "completions")]
|
||||||
|
@ -640,14 +738,32 @@ impl Opts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the default number of threads to use, if not explicitly specified.
|
||||||
|
fn default_num_threads() -> NonZeroUsize {
|
||||||
|
// If we can't get the amount of parallelism for some reason, then
|
||||||
|
// default to a single thread, because that is safe.
|
||||||
|
let fallback = NonZeroUsize::MIN;
|
||||||
|
// To limit startup overhead on massively parallel machines, don't use more
|
||||||
|
// than 64 threads.
|
||||||
|
let limit = NonZeroUsize::new(64).unwrap();
|
||||||
|
|
||||||
|
std::thread::available_parallelism()
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
.min(limit)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
|
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
|
||||||
pub enum FileType {
|
pub enum FileType {
|
||||||
#[value(alias = "f")]
|
#[value(alias = "f")]
|
||||||
File,
|
File,
|
||||||
#[value(alias = "d")]
|
#[value(alias = "d", alias = "dir")]
|
||||||
Directory,
|
Directory,
|
||||||
#[value(alias = "l")]
|
#[value(alias = "l")]
|
||||||
Symlink,
|
Symlink,
|
||||||
|
#[value(alias = "b")]
|
||||||
|
BlockDevice,
|
||||||
|
#[value(alias = "c")]
|
||||||
|
CharDevice,
|
||||||
/// A file which is executable by the current effective user
|
/// A file which is executable by the current effective user
|
||||||
#[value(alias = "x")]
|
#[value(alias = "x")]
|
||||||
Executable,
|
Executable,
|
||||||
|
@ -669,15 +785,14 @@ pub enum ColorWhen {
|
||||||
Never,
|
Never,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColorWhen {
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub enum StripCwdWhen {
|
||||||
use ColorWhen::*;
|
/// Use the default behavior
|
||||||
match *self {
|
Auto,
|
||||||
Auto => "auto",
|
/// Always strip the ./ at the beginning of paths
|
||||||
Never => "never",
|
Always,
|
||||||
Always => "always",
|
/// Never strip the ./
|
||||||
}
|
Never,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// there isn't a derive api for getting grouped values yet,
|
// there isn't a derive api for getting grouped values yet,
|
||||||
|
@ -689,11 +804,11 @@ pub struct Exec {
|
||||||
impl clap::FromArgMatches for Exec {
|
impl clap::FromArgMatches for Exec {
|
||||||
fn from_arg_matches(matches: &ArgMatches) -> clap::error::Result<Self> {
|
fn from_arg_matches(matches: &ArgMatches) -> clap::error::Result<Self> {
|
||||||
let command = matches
|
let command = matches
|
||||||
.grouped_values_of("exec")
|
.get_occurrences::<String>("exec")
|
||||||
.map(CommandSet::new)
|
.map(CommandSet::new)
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
matches
|
matches
|
||||||
.grouped_values_of("exec_batch")
|
.get_occurrences::<String>("exec_batch")
|
||||||
.map(CommandSet::new_batch)
|
.map(CommandSet::new_batch)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
|
@ -721,6 +836,7 @@ impl clap::Args for Exec {
|
||||||
.help("Execute a command for each search result")
|
.help("Execute a command for each search result")
|
||||||
.long_help(
|
.long_help(
|
||||||
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
|
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
|
||||||
|
There is no guarantee of the order commands are executed in, and the order should not be depended upon. \
|
||||||
All positional arguments following --exec are considered to be arguments to the command - not to fd. \
|
All positional arguments following --exec are considered to be arguments to the command - not to fd. \
|
||||||
It is therefore recommended to place the '-x'/'--exec' option last.\n\
|
It is therefore recommended to place the '-x'/'--exec' option last.\n\
|
||||||
The following placeholders are substituted before the command is executed:\n \
|
The following placeholders are substituted before the command is executed:\n \
|
||||||
|
@ -728,7 +844,9 @@ impl clap::Args for Exec {
|
||||||
'{/}': basename\n \
|
'{/}': basename\n \
|
||||||
'{//}': parent directory\n \
|
'{//}': parent directory\n \
|
||||||
'{.}': path without file extension\n \
|
'{.}': path without file extension\n \
|
||||||
'{/.}': basename without file extension\n\n\
|
'{/.}': basename without file extension\n \
|
||||||
|
'{{': literal '{' (for escaping)\n \
|
||||||
|
'}}': literal '}' (for escaping)\n\n\
|
||||||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
||||||
Examples:\n\n \
|
Examples:\n\n \
|
||||||
- find all *.zip files and unzip them:\n\n \
|
- find all *.zip files and unzip them:\n\n \
|
||||||
|
@ -753,12 +871,15 @@ impl clap::Args for Exec {
|
||||||
.help("Execute a command with all search results at once")
|
.help("Execute a command with all search results at once")
|
||||||
.long_help(
|
.long_help(
|
||||||
"Execute the given command once, with all search results as arguments.\n\
|
"Execute the given command once, with all search results as arguments.\n\
|
||||||
|
The order of the arguments is non-deterministic, and should not be relied upon.\n\
|
||||||
One of the following placeholders is substituted before the command is executed:\n \
|
One of the following placeholders is substituted before the command is executed:\n \
|
||||||
'{}': path (of all search results)\n \
|
'{}': path (of all search results)\n \
|
||||||
'{/}': basename\n \
|
'{/}': basename\n \
|
||||||
'{//}': parent directory\n \
|
'{//}': parent directory\n \
|
||||||
'{.}': path without file extension\n \
|
'{.}': path without file extension\n \
|
||||||
'{/.}': basename without file extension\n\n\
|
'{/.}': basename without file extension\n \
|
||||||
|
'{{': literal '{' (for escaping)\n \
|
||||||
|
'}}': literal '}' (for escaping)\n\n\
|
||||||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
||||||
Examples:\n\n \
|
Examples:\n\n \
|
||||||
- Find all test_*.py files and open them in your favorite editor:\n\n \
|
- Find all test_*.py files and open them in your favorite editor:\n\n \
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::filetypes::FileTypes;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use crate::filter::OwnerFilter;
|
use crate::filter::OwnerFilter;
|
||||||
use crate::filter::{SizeFilter, TimeFilter};
|
use crate::filter::{SizeFilter, TimeFilter};
|
||||||
|
use crate::fmt::FormatTemplate;
|
||||||
|
|
||||||
/// Configuration options for *fd*.
|
/// Configuration options for *fd*.
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -30,6 +31,9 @@ pub struct Config {
|
||||||
/// Whether to respect VCS ignore files (`.gitignore`, ..) or not.
|
/// Whether to respect VCS ignore files (`.gitignore`, ..) or not.
|
||||||
pub read_vcsignore: bool,
|
pub read_vcsignore: bool,
|
||||||
|
|
||||||
|
/// Whether to require a `.git` directory to respect gitignore files.
|
||||||
|
pub require_git_to_read_vcsignore: bool,
|
||||||
|
|
||||||
/// Whether to respect the global ignore file or not.
|
/// Whether to respect the global ignore file or not.
|
||||||
pub read_global_ignore: bool,
|
pub read_global_ignore: bool,
|
||||||
|
|
||||||
|
@ -82,6 +86,9 @@ pub struct Config {
|
||||||
/// The value (if present) will be a lowercase string without leading dots.
|
/// The value (if present) will be a lowercase string without leading dots.
|
||||||
pub extensions: Option<RegexSet>,
|
pub extensions: Option<RegexSet>,
|
||||||
|
|
||||||
|
/// A format string to use to format results, similarly to exec
|
||||||
|
pub format: Option<FormatTemplate>,
|
||||||
|
|
||||||
/// 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<CommandSet>>,
|
pub command: Option<Arc<CommandSet>>,
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
|
use std::cell::OnceCell;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs::{FileType, Metadata};
|
use std::fs::{FileType, Metadata};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use lscolors::{Colorable, LsColors, Style};
|
use lscolors::{Colorable, LsColors, Style};
|
||||||
|
|
||||||
use once_cell::unsync::OnceCell;
|
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::filesystem::strip_current_dir;
|
use crate::filesystem::strip_current_dir;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum DirEntryInner {
|
enum DirEntryInner {
|
||||||
Normal(ignore::DirEntry),
|
Normal(ignore::DirEntry),
|
||||||
BrokenSymlink(PathBuf),
|
BrokenSymlink(PathBuf),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct DirEntry {
|
pub struct DirEntry {
|
||||||
inner: DirEntryInner,
|
inner: DirEntryInner,
|
||||||
metadata: OnceCell<Option<Metadata>>,
|
metadata: OnceCell<Option<Metadata>>,
|
||||||
|
@ -112,7 +113,7 @@ impl Eq for DirEntry {}
|
||||||
impl PartialOrd for DirEntry {
|
impl PartialOrd for DirEntry {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
self.path().partial_cmp(other.path())
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crossbeam_channel::Receiver;
|
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
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;
|
||||||
|
@ -14,43 +11,47 @@ use super::CommandSet;
|
||||||
/// 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: Receiver<WorkerResult>,
|
results: impl IntoIterator<Item = WorkerResult>,
|
||||||
cmd: Arc<CommandSet>,
|
cmd: &CommandSet,
|
||||||
out_perm: Arc<Mutex<()>>,
|
out_perm: &Mutex<()>,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> ExitCode {
|
) -> ExitCode {
|
||||||
// Output should be buffered when only running a single thread
|
// Output should be buffered when only running a single thread
|
||||||
let buffer_output: bool = config.threads > 1;
|
let buffer_output: bool = config.threads > 1;
|
||||||
|
|
||||||
let mut results: Vec<ExitCode> = Vec::new();
|
let mut ret = ExitCode::Success;
|
||||||
loop {
|
for result in results {
|
||||||
// Obtain the next result from the receiver, else if the channel
|
// Obtain the next result from the receiver, else if the channel
|
||||||
// has closed, exit from the loop
|
// has closed, exit from the loop
|
||||||
let dir_entry: DirEntry = match rx.recv() {
|
let dir_entry = match result {
|
||||||
Ok(WorkerResult::Entry(dir_entry)) => dir_entry,
|
WorkerResult::Entry(dir_entry) => dir_entry,
|
||||||
Ok(WorkerResult::Error(err)) => {
|
WorkerResult::Error(err) => {
|
||||||
if config.show_filesystem_errors {
|
if config.show_filesystem_errors {
|
||||||
print_error(err.to_string());
|
print_error(err.to_string());
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(_) => break,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate a command, execute it and store its exit code.
|
// Generate a command, execute it and store its exit code.
|
||||||
results.push(cmd.execute(
|
let code = cmd.execute(
|
||||||
dir_entry.stripped_path(config),
|
dir_entry.stripped_path(config),
|
||||||
config.path_separator.as_deref(),
|
config.path_separator.as_deref(),
|
||||||
Arc::clone(&out_perm),
|
out_perm,
|
||||||
buffer_output,
|
buffer_output,
|
||||||
))
|
);
|
||||||
|
ret = merge_exitcodes([ret, code]);
|
||||||
}
|
}
|
||||||
// Returns error in case of any error.
|
// Returns error in case of any error.
|
||||||
merge_exitcodes(results)
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn batch(rx: Receiver<WorkerResult>, cmd: &CommandSet, config: &Config) -> ExitCode {
|
pub fn batch(
|
||||||
let paths = rx
|
results: impl IntoIterator<Item = WorkerResult>,
|
||||||
|
cmd: &CommandSet,
|
||||||
|
config: &Config,
|
||||||
|
) -> ExitCode {
|
||||||
|
let paths = results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|worker_result| match worker_result {
|
.filter_map(|worker_result| match worker_result {
|
||||||
WorkerResult::Entry(dir_entry) => Some(dir_entry.into_stripped_path(config)),
|
WorkerResult::Entry(dir_entry) => Some(dir_entry.into_stripped_path(config)),
|
||||||
|
|
239
src/exec/mod.rs
239
src/exec/mod.rs
|
@ -1,27 +1,21 @@
|
||||||
mod command;
|
mod command;
|
||||||
mod input;
|
|
||||||
mod job;
|
mod job;
|
||||||
mod token;
|
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::ffi::OsString;
|
||||||
use std::ffi::{OsStr, OsString};
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::path::{Component, Path, PathBuf, Prefix};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use argmax::Command;
|
use argmax::Command;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use crate::exit_codes::{merge_exitcodes, ExitCode};
|
use crate::exit_codes::{merge_exitcodes, ExitCode};
|
||||||
|
use crate::fmt::{FormatTemplate, Token};
|
||||||
|
|
||||||
use self::command::{execute_commands, handle_cmd_error};
|
use self::command::{execute_commands, handle_cmd_error};
|
||||||
use self::input::{basename, dirname, remove_extension};
|
|
||||||
pub use self::job::{batch, job};
|
pub use self::job::{batch, job};
|
||||||
use self::token::Token;
|
|
||||||
|
|
||||||
/// Execution mode of the command
|
/// Execution mode of the command
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
@ -39,9 +33,10 @@ pub struct CommandSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandSet {
|
impl CommandSet {
|
||||||
pub fn new<I, S>(input: I) -> Result<CommandSet>
|
pub fn new<I, T, S>(input: I) -> Result<CommandSet>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = Vec<S>>,
|
I: IntoIterator<Item = T>,
|
||||||
|
T: IntoIterator<Item = S>,
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
Ok(CommandSet {
|
Ok(CommandSet {
|
||||||
|
@ -53,9 +48,10 @@ impl CommandSet {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_batch<I, S>(input: I) -> Result<CommandSet>
|
pub fn new_batch<I, T, S>(input: I) -> Result<CommandSet>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = Vec<S>>,
|
I: IntoIterator<Item = T>,
|
||||||
|
T: IntoIterator<Item = S>,
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
Ok(CommandSet {
|
Ok(CommandSet {
|
||||||
|
@ -84,14 +80,14 @@ impl CommandSet {
|
||||||
&self,
|
&self,
|
||||||
input: &Path,
|
input: &Path,
|
||||||
path_separator: Option<&str>,
|
path_separator: Option<&str>,
|
||||||
out_perm: Arc<Mutex<()>>,
|
out_perm: &Mutex<()>,
|
||||||
buffer_output: bool,
|
buffer_output: bool,
|
||||||
) -> ExitCode {
|
) -> ExitCode {
|
||||||
let commands = self
|
let commands = self
|
||||||
.commands
|
.commands
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.generate(input, path_separator));
|
.map(|c| c.generate(input, path_separator));
|
||||||
execute_commands(commands, &out_perm, buffer_output)
|
execute_commands(commands, out_perm, buffer_output)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_batch<I>(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode
|
pub fn execute_batch<I>(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode
|
||||||
|
@ -131,7 +127,7 @@ impl CommandSet {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct CommandBuilder {
|
struct CommandBuilder {
|
||||||
pre_args: Vec<OsString>,
|
pre_args: Vec<OsString>,
|
||||||
path_arg: ArgumentTemplate,
|
path_arg: FormatTemplate,
|
||||||
post_args: Vec<OsString>,
|
post_args: Vec<OsString>,
|
||||||
cmd: Command,
|
cmd: Command,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
@ -220,7 +216,7 @@ impl CommandBuilder {
|
||||||
/// `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)]
|
||||||
struct CommandTemplate {
|
struct CommandTemplate {
|
||||||
args: Vec<ArgumentTemplate>,
|
args: Vec<FormatTemplate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandTemplate {
|
impl CommandTemplate {
|
||||||
|
@ -229,50 +225,15 @@ impl CommandTemplate {
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
static PLACEHOLDER_PATTERN: Lazy<Regex> =
|
|
||||||
Lazy::new(|| Regex::new(r"\{(/?\.?|//)\}").unwrap());
|
|
||||||
|
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
let mut has_placeholder = false;
|
let mut has_placeholder = false;
|
||||||
|
|
||||||
for arg in input {
|
for arg in input {
|
||||||
let arg = arg.as_ref();
|
let arg = arg.as_ref();
|
||||||
|
|
||||||
let mut tokens = Vec::new();
|
let tmpl = FormatTemplate::parse(arg);
|
||||||
let mut start = 0;
|
has_placeholder |= tmpl.has_tokens();
|
||||||
|
args.push(tmpl);
|
||||||
for placeholder in PLACEHOLDER_PATTERN.find_iter(arg) {
|
|
||||||
// Leading text before the placeholder.
|
|
||||||
if placeholder.start() > start {
|
|
||||||
tokens.push(Token::Text(arg[start..placeholder.start()].to_owned()));
|
|
||||||
}
|
|
||||||
|
|
||||||
start = placeholder.end();
|
|
||||||
|
|
||||||
match placeholder.as_str() {
|
|
||||||
"{}" => tokens.push(Token::Placeholder),
|
|
||||||
"{.}" => tokens.push(Token::NoExt),
|
|
||||||
"{/}" => tokens.push(Token::Basename),
|
|
||||||
"{//}" => tokens.push(Token::Parent),
|
|
||||||
"{/.}" => tokens.push(Token::BasenameNoExt),
|
|
||||||
_ => unreachable!("Unhandled placeholder"),
|
|
||||||
}
|
|
||||||
|
|
||||||
has_placeholder = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Without a placeholder, the argument is just fixed text.
|
|
||||||
if tokens.is_empty() {
|
|
||||||
args.push(ArgumentTemplate::Text(arg.to_owned()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if start < arg.len() {
|
|
||||||
// Trailing text after last placeholder.
|
|
||||||
tokens.push(Token::Text(arg[start..].to_owned()));
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(ArgumentTemplate::Tokens(tokens));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to check that we have at least one argument, because if not
|
// We need to check that we have at least one argument, because if not
|
||||||
|
@ -286,7 +247,7 @@ impl CommandTemplate {
|
||||||
|
|
||||||
// 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(FormatTemplate::Tokens(vec![Token::Placeholder]));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(CommandTemplate { args })
|
Ok(CommandTemplate { args })
|
||||||
|
@ -309,115 +270,18 @@ impl CommandTemplate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a template for a single command argument.
|
|
||||||
///
|
|
||||||
/// The argument is either a collection of `Token`s including at least one placeholder variant, or
|
|
||||||
/// a fixed text.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
enum ArgumentTemplate {
|
|
||||||
Tokens(Vec<Token>),
|
|
||||||
Text(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ArgumentTemplate {
|
|
||||||
pub fn has_tokens(&self) -> bool {
|
|
||||||
matches!(self, ArgumentTemplate::Tokens(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate an argument from this template. If path_separator is Some, then it will replace
|
|
||||||
/// the path separator in all placeholder tokens. Text arguments and tokens are not affected by
|
|
||||||
/// path separator substitution.
|
|
||||||
pub fn generate(&self, path: impl AsRef<Path>, path_separator: Option<&str>) -> OsString {
|
|
||||||
use self::Token::*;
|
|
||||||
let path = path.as_ref();
|
|
||||||
|
|
||||||
match *self {
|
|
||||||
ArgumentTemplate::Tokens(ref tokens) => {
|
|
||||||
let mut s = OsString::new();
|
|
||||||
for token in tokens {
|
|
||||||
match *token {
|
|
||||||
Basename => s.push(Self::replace_separator(basename(path), path_separator)),
|
|
||||||
BasenameNoExt => s.push(Self::replace_separator(
|
|
||||||
&remove_extension(basename(path).as_ref()),
|
|
||||||
path_separator,
|
|
||||||
)),
|
|
||||||
NoExt => s.push(Self::replace_separator(
|
|
||||||
&remove_extension(path),
|
|
||||||
path_separator,
|
|
||||||
)),
|
|
||||||
Parent => s.push(Self::replace_separator(&dirname(path), path_separator)),
|
|
||||||
Placeholder => {
|
|
||||||
s.push(Self::replace_separator(path.as_ref(), path_separator))
|
|
||||||
}
|
|
||||||
Text(ref string) => s.push(string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s
|
|
||||||
}
|
|
||||||
ArgumentTemplate::Text(ref text) => OsString::from(text),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the path separator in the input with the custom separator string. If path_separator
|
|
||||||
/// is None, simply return a borrowed Cow<OsStr> of the input. Otherwise, the input is
|
|
||||||
/// interpreted as a Path and its components are iterated through and re-joined into a new
|
|
||||||
/// OsString.
|
|
||||||
fn replace_separator<'a>(path: &'a OsStr, path_separator: Option<&str>) -> Cow<'a, OsStr> {
|
|
||||||
// fast-path - no replacement necessary
|
|
||||||
if path_separator.is_none() {
|
|
||||||
return Cow::Borrowed(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
let path_separator = path_separator.unwrap();
|
|
||||||
let mut out = OsString::with_capacity(path.len());
|
|
||||||
let mut components = Path::new(path).components().peekable();
|
|
||||||
|
|
||||||
while let Some(comp) = components.next() {
|
|
||||||
match comp {
|
|
||||||
// Absolute paths on Windows are tricky. A Prefix component is usually a drive
|
|
||||||
// letter or UNC path, and is usually followed by RootDir. There are also
|
|
||||||
// "verbatim" prefixes beginning with "\\?\" that skip normalization. We choose to
|
|
||||||
// ignore verbatim path prefixes here because they're very rare, might be
|
|
||||||
// impossible to reach here, and there's no good way to deal with them. If users
|
|
||||||
// are doing something advanced involving verbatim windows paths, they can do their
|
|
||||||
// own output filtering with a tool like sed.
|
|
||||||
Component::Prefix(prefix) => {
|
|
||||||
if let Prefix::UNC(server, share) = prefix.kind() {
|
|
||||||
// Prefix::UNC is a parsed version of '\\server\share'
|
|
||||||
out.push(path_separator);
|
|
||||||
out.push(path_separator);
|
|
||||||
out.push(server);
|
|
||||||
out.push(path_separator);
|
|
||||||
out.push(share);
|
|
||||||
} else {
|
|
||||||
// All other Windows prefix types are rendered as-is. This results in e.g. "C:" for
|
|
||||||
// drive letters. DeviceNS and Verbatim* prefixes won't have backslashes converted,
|
|
||||||
// but they're not returned by directories fd can search anyway so we don't worry
|
|
||||||
// about them.
|
|
||||||
out.push(comp.as_os_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root directory is always replaced with the custom separator.
|
|
||||||
Component::RootDir => out.push(path_separator),
|
|
||||||
|
|
||||||
// Everything else is joined normally, with a trailing separator if we're not last
|
|
||||||
_ => {
|
|
||||||
out.push(comp.as_os_str());
|
|
||||||
if components.peek().is_some() {
|
|
||||||
out.push(path_separator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Cow::Owned(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn generate_str(template: &CommandTemplate, input: &str) -> Vec<String> {
|
||||||
|
template
|
||||||
|
.args
|
||||||
|
.iter()
|
||||||
|
.map(|arg| arg.generate(input, None).into_string().unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_with_placeholder() {
|
fn tokens_with_placeholder() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -425,9 +289,9 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Text("${SHELL}:".into()),
|
FormatTemplate::Text("${SHELL}:".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
|
FormatTemplate::Tokens(vec![Token::Placeholder]),
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
|
@ -442,8 +306,8 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
FormatTemplate::Tokens(vec![Token::NoExt]),
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
|
@ -458,8 +322,8 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::Basename]),
|
FormatTemplate::Tokens(vec![Token::Basename]),
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
|
@ -474,8 +338,8 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::Parent]),
|
FormatTemplate::Tokens(vec![Token::Parent]),
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
|
@ -490,8 +354,8 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]),
|
FormatTemplate::Tokens(vec![Token::BasenameNoExt]),
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
|
@ -499,6 +363,21 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokens_with_literal_braces() {
|
||||||
|
let template = CommandTemplate::new(vec!["{{}}", "{{", "{.}}"]).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
generate_str(&template, "foo"),
|
||||||
|
vec!["{}", "{", "{.}", "foo"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokens_with_literal_braces_and_placeholder() {
|
||||||
|
let template = CommandTemplate::new(vec!["{{{},end}"]).unwrap();
|
||||||
|
assert_eq!(generate_str(&template, "foo"), vec!["{foo,end}"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tokens_multiple() {
|
fn tokens_multiple() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -506,9 +385,9 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("cp".into()),
|
FormatTemplate::Text("cp".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
|
FormatTemplate::Tokens(vec![Token::Placeholder]),
|
||||||
ArgumentTemplate::Tokens(vec![
|
FormatTemplate::Tokens(vec![
|
||||||
Token::BasenameNoExt,
|
Token::BasenameNoExt,
|
||||||
Token::Text(".ext".into())
|
Token::Text(".ext".into())
|
||||||
]),
|
]),
|
||||||
|
@ -526,8 +405,8 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
FormatTemplate::Tokens(vec![Token::NoExt]),
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::Batch,
|
mode: ExecutionMode::Batch,
|
||||||
|
@ -552,7 +431,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generate_custom_path_separator() {
|
fn generate_custom_path_separator() {
|
||||||
let arg = ArgumentTemplate::Tokens(vec![Token::Placeholder]);
|
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||||
macro_rules! check {
|
macro_rules! check {
|
||||||
($input:expr, $expected:expr) => {
|
($input:expr, $expected:expr) => {
|
||||||
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
||||||
|
@ -567,7 +446,7 @@ mod tests {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[test]
|
#[test]
|
||||||
fn generate_custom_path_separator_windows() {
|
fn generate_custom_path_separator_windows() {
|
||||||
let arg = ArgumentTemplate::Tokens(vec![Token::Placeholder]);
|
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||||
macro_rules! check {
|
macro_rules! check {
|
||||||
($input:expr, $expected:expr) => {
|
($input:expr, $expected:expr) => {
|
||||||
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
use std::fmt::{self, Display, Formatter};
|
|
||||||
|
|
||||||
/// Designates what should be written to a buffer
|
|
||||||
///
|
|
||||||
/// Each `Token` contains either text, or a placeholder variant, which will be used to generate
|
|
||||||
/// commands after all tokens for a given command template have been collected.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Token {
|
|
||||||
Placeholder,
|
|
||||||
Basename,
|
|
||||||
Parent,
|
|
||||||
NoExt,
|
|
||||||
BasenameNoExt,
|
|
||||||
Text(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Token {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
Token::Placeholder => f.write_str("{}")?,
|
|
||||||
Token::Basename => f.write_str("{/}")?,
|
|
||||||
Token::Parent => f.write_str("{//}")?,
|
|
||||||
Token::NoExt => f.write_str("{.}")?,
|
|
||||||
Token::BasenameNoExt => f.write_str("{/.}")?,
|
|
||||||
Token::Text(ref string) => f.write_str(string)?,
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -59,6 +59,26 @@ pub fn is_empty(entry: &dir_entry::DirEntry) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(unix, target_os = "redox"))]
|
||||||
|
pub fn is_block_device(ft: fs::FileType) -> bool {
|
||||||
|
ft.is_block_device()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn is_block_device(_: fs::FileType) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(unix, target_os = "redox"))]
|
||||||
|
pub fn is_char_device(ft: fs::FileType) -> bool {
|
||||||
|
ft.is_char_device()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn is_char_device(_: fs::FileType) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(unix, target_os = "redox"))]
|
#[cfg(any(unix, target_os = "redox"))]
|
||||||
pub fn is_socket(ft: fs::FileType) -> bool {
|
pub fn is_socket(ft: fs::FileType) -> bool {
|
||||||
ft.is_socket()
|
ft.is_socket()
|
||||||
|
@ -108,14 +128,12 @@ pub fn strip_current_dir(path: &Path) -> &Path {
|
||||||
pub fn default_path_separator() -> Option<String> {
|
pub fn default_path_separator() -> Option<String> {
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
let msystem = env::var("MSYSTEM").ok()?;
|
let msystem = env::var("MSYSTEM").ok()?;
|
||||||
match msystem.as_str() {
|
if !msystem.is_empty() {
|
||||||
"MINGW64" | "MINGW32" | "MSYS" => Some("/".to_owned()),
|
return Some("/".to_owned());
|
||||||
_ => None,
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
@ -9,6 +9,8 @@ pub struct FileTypes {
|
||||||
pub files: bool,
|
pub files: bool,
|
||||||
pub directories: bool,
|
pub directories: bool,
|
||||||
pub symlinks: bool,
|
pub symlinks: bool,
|
||||||
|
pub block_devices: bool,
|
||||||
|
pub char_devices: bool,
|
||||||
pub sockets: bool,
|
pub sockets: bool,
|
||||||
pub pipes: bool,
|
pub pipes: bool,
|
||||||
pub executables_only: bool,
|
pub executables_only: bool,
|
||||||
|
@ -21,6 +23,8 @@ impl FileTypes {
|
||||||
(!self.files && entry_type.is_file())
|
(!self.files && entry_type.is_file())
|
||||||
|| (!self.directories && entry_type.is_dir())
|
|| (!self.directories && entry_type.is_dir())
|
||||||
|| (!self.symlinks && entry_type.is_symlink())
|
|| (!self.symlinks && entry_type.is_symlink())
|
||||||
|
|| (!self.block_devices && filesystem::is_block_device(*entry_type))
|
||||||
|
|| (!self.char_devices && filesystem::is_char_device(*entry_type))
|
||||||
|| (!self.sockets && filesystem::is_socket(*entry_type))
|
|| (!self.sockets && filesystem::is_socket(*entry_type))
|
||||||
|| (!self.pipes && filesystem::is_pipe(*entry_type))
|
|| (!self.pipes && filesystem::is_pipe(*entry_type))
|
||||||
|| (self.executables_only && !entry.path().executable())
|
|| (self.executables_only && !entry.path().executable())
|
||||||
|
@ -28,6 +32,8 @@ impl FileTypes {
|
||||||
|| !(entry_type.is_file()
|
|| !(entry_type.is_file()
|
||||||
|| entry_type.is_dir()
|
|| entry_type.is_dir()
|
||||||
|| entry_type.is_symlink()
|
|| entry_type.is_symlink()
|
||||||
|
|| filesystem::is_block_device(*entry_type)
|
||||||
|
|| filesystem::is_char_device(*entry_type)
|
||||||
|| filesystem::is_socket(*entry_type)
|
|| filesystem::is_socket(*entry_type)
|
||||||
|| filesystem::is_pipe(*entry_type))
|
|| filesystem::is_pipe(*entry_type))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use nix::unistd::{Group, User};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
@ -35,16 +36,22 @@ impl OwnerFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
let uid = Check::parse(fst, |s| {
|
let uid = Check::parse(fst, |s| {
|
||||||
s.parse()
|
if let Ok(uid) = s.parse() {
|
||||||
.ok()
|
Ok(uid)
|
||||||
.or_else(|| users::get_user_by_name(s).map(|user| user.uid()))
|
} else {
|
||||||
|
User::from_name(s)?
|
||||||
|
.map(|user| user.uid.as_raw())
|
||||||
.ok_or_else(|| anyhow!("'{}' is not a recognized user name", s))
|
.ok_or_else(|| anyhow!("'{}' is not a recognized user name", s))
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
let gid = Check::parse(snd, |s| {
|
let gid = Check::parse(snd, |s| {
|
||||||
s.parse()
|
if let Ok(gid) = s.parse() {
|
||||||
.ok()
|
Ok(gid)
|
||||||
.or_else(|| users::get_group_by_name(s).map(|group| group.gid()))
|
} else {
|
||||||
|
Group::from_name(s)?
|
||||||
|
.map(|group| group.gid.as_raw())
|
||||||
.ok_or_else(|| anyhow!("'{}' is not a recognized group name", s))
|
.ok_or_else(|| anyhow!("'{}' is not a recognized group name", s))
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(OwnerFilter { uid, gid })
|
Ok(OwnerFilter { uid, gid })
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
static SIZE_CAPTURES: Lazy<Regex> =
|
static SIZE_CAPTURES: OnceLock<Regex> = OnceLock::new();
|
||||||
Lazy::new(|| Regex::new(r"(?i)^([+-]?)(\d+)(b|[kmgt]i?b?)$").unwrap());
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum SizeFilter {
|
pub enum SizeFilter {
|
||||||
|
@ -31,11 +31,13 @@ impl SizeFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_opt(s: &str) -> Option<Self> {
|
fn parse_opt(s: &str) -> Option<Self> {
|
||||||
if !SIZE_CAPTURES.is_match(s) {
|
let pattern =
|
||||||
|
SIZE_CAPTURES.get_or_init(|| Regex::new(r"(?i)^([+-]?)(\d+)(b|[kmgt]i?b?)$").unwrap());
|
||||||
|
if !pattern.is_match(s) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let captures = SIZE_CAPTURES.captures(s)?;
|
let captures = pattern.captures(s)?;
|
||||||
let limit_kind = captures.get(1).map_or("+", |m| m.as_str());
|
let limit_kind = captures.get(1).map_or("+", |m| m.as_str());
|
||||||
let quantity = captures
|
let quantity = captures
|
||||||
.get(2)
|
.get(2)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use chrono::{offset::TimeZone, DateTime, Local, NaiveDate};
|
use chrono::{DateTime, Local, NaiveDate, NaiveDateTime};
|
||||||
|
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
@ -20,11 +20,21 @@ impl TimeFilter {
|
||||||
.ok()
|
.ok()
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
NaiveDate::parse_from_str(s, "%F")
|
NaiveDate::parse_from_str(s, "%F")
|
||||||
.map(|nd| nd.and_hms(0, 0, 0))
|
.ok()?
|
||||||
.ok()
|
.and_hms_opt(0, 0, 0)?
|
||||||
.and_then(|ndt| Local.from_local_datetime(&ndt).single())
|
.and_local_timezone(Local)
|
||||||
|
.latest()
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
NaiveDateTime::parse_from_str(s, "%F %T")
|
||||||
|
.ok()?
|
||||||
|
.and_local_timezone(Local)
|
||||||
|
.latest()
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
let timestamp_secs = s.strip_prefix('@')?.parse().ok()?;
|
||||||
|
DateTime::from_timestamp(timestamp_secs, 0).map(Into::into)
|
||||||
})
|
})
|
||||||
.or_else(|| Local.datetime_from_str(s, "%F %T").ok())
|
|
||||||
.map(|dt| dt.into())
|
.map(|dt| dt.into())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -52,8 +62,10 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn is_time_filter_applicable() {
|
fn is_time_filter_applicable() {
|
||||||
let ref_time = Local
|
let ref_time = NaiveDateTime::parse_from_str("2010-10-10 10:10:10", "%F %T")
|
||||||
.datetime_from_str("2010-10-10 10:10:10", "%F %T")
|
.unwrap()
|
||||||
|
.and_local_timezone(Local)
|
||||||
|
.latest()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
|
@ -127,5 +139,32 @@ mod tests {
|
||||||
assert!(!TimeFilter::after(&ref_time, t10s_before)
|
assert!(!TimeFilter::after(&ref_time, t10s_before)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.applies_to(&t1m_ago));
|
.applies_to(&t1m_ago));
|
||||||
|
|
||||||
|
let ref_timestamp = 1707723412u64; // Mon Feb 12 07:36:52 UTC 2024
|
||||||
|
let ref_time = DateTime::parse_from_rfc3339("2024-02-12T07:36:52+00:00")
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
let t1m_ago = ref_time - Duration::from_secs(60);
|
||||||
|
let t1s_later = ref_time + Duration::from_secs(1);
|
||||||
|
// Timestamp only supported via '@' prefix
|
||||||
|
assert!(TimeFilter::before(&ref_time, &ref_timestamp.to_string()).is_none());
|
||||||
|
assert!(
|
||||||
|
TimeFilter::before(&ref_time, &format!("@{}", ref_timestamp))
|
||||||
|
.unwrap()
|
||||||
|
.applies_to(&t1m_ago)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!TimeFilter::before(&ref_time, &format!("@{}", ref_timestamp))
|
||||||
|
.unwrap()
|
||||||
|
.applies_to(&t1s_later)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!TimeFilter::after(&ref_time, &format!("@{}", ref_timestamp))
|
||||||
|
.unwrap()
|
||||||
|
.applies_to(&t1m_ago)
|
||||||
|
);
|
||||||
|
assert!(TimeFilter::after(&ref_time, &format!("@{}", ref_timestamp))
|
||||||
|
.unwrap()
|
||||||
|
.applies_to(&t1s_later));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,10 +34,10 @@ pub fn dirname(path: &Path) -> OsString {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod path_tests {
|
mod path_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::path::MAIN_SEPARATOR;
|
use std::path::MAIN_SEPARATOR_STR;
|
||||||
|
|
||||||
fn correct(input: &str) -> String {
|
fn correct(input: &str) -> String {
|
||||||
input.replace('/', &MAIN_SEPARATOR.to_string())
|
input.replace('/', MAIN_SEPARATOR_STR)
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! func_tests {
|
macro_rules! func_tests {
|
|
@ -0,0 +1,281 @@
|
||||||
|
mod input;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::path::{Component, Path, Prefix};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use aho_corasick::AhoCorasick;
|
||||||
|
|
||||||
|
use self::input::{basename, dirname, remove_extension};
|
||||||
|
|
||||||
|
/// Designates what should be written to a buffer
|
||||||
|
///
|
||||||
|
/// Each `Token` contains either text, or a placeholder variant, which will be used to generate
|
||||||
|
/// commands after all tokens for a given command template have been collected.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Token {
|
||||||
|
Placeholder,
|
||||||
|
Basename,
|
||||||
|
Parent,
|
||||||
|
NoExt,
|
||||||
|
BasenameNoExt,
|
||||||
|
Text(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Token {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Token::Placeholder => f.write_str("{}")?,
|
||||||
|
Token::Basename => f.write_str("{/}")?,
|
||||||
|
Token::Parent => f.write_str("{//}")?,
|
||||||
|
Token::NoExt => f.write_str("{.}")?,
|
||||||
|
Token::BasenameNoExt => f.write_str("{/.}")?,
|
||||||
|
Token::Text(ref string) => f.write_str(string)?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A parsed format string
|
||||||
|
///
|
||||||
|
/// This is either a collection of `Token`s including at least one placeholder variant,
|
||||||
|
/// or a fixed text.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum FormatTemplate {
|
||||||
|
Tokens(Vec<Token>),
|
||||||
|
Text(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
static PLACEHOLDERS: OnceLock<AhoCorasick> = OnceLock::new();
|
||||||
|
|
||||||
|
impl FormatTemplate {
|
||||||
|
pub fn has_tokens(&self) -> bool {
|
||||||
|
matches!(self, FormatTemplate::Tokens(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(fmt: &str) -> Self {
|
||||||
|
// NOTE: we assume that { and } have the same length
|
||||||
|
const BRACE_LEN: usize = '{'.len_utf8();
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
let mut remaining = fmt;
|
||||||
|
let mut buf = String::new();
|
||||||
|
let placeholders = PLACEHOLDERS.get_or_init(|| {
|
||||||
|
AhoCorasick::new(["{{", "}}", "{}", "{/}", "{//}", "{.}", "{/.}"]).unwrap()
|
||||||
|
});
|
||||||
|
while let Some(m) = placeholders.find(remaining) {
|
||||||
|
match m.pattern().as_u32() {
|
||||||
|
0 | 1 => {
|
||||||
|
// we found an escaped {{ or }}, so add
|
||||||
|
// everything up to the first char to the buffer
|
||||||
|
// then skip the second one.
|
||||||
|
buf += &remaining[..m.start() + BRACE_LEN];
|
||||||
|
remaining = &remaining[m.end()..];
|
||||||
|
}
|
||||||
|
id if !remaining[m.end()..].starts_with('}') => {
|
||||||
|
buf += &remaining[..m.start()];
|
||||||
|
if !buf.is_empty() {
|
||||||
|
tokens.push(Token::Text(std::mem::take(&mut buf)));
|
||||||
|
}
|
||||||
|
tokens.push(token_from_pattern_id(id));
|
||||||
|
remaining = &remaining[m.end()..];
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// We got a normal pattern, but the final "}"
|
||||||
|
// is escaped, so add up to that to the buffer, then
|
||||||
|
// skip the final }
|
||||||
|
buf += &remaining[..m.end()];
|
||||||
|
remaining = &remaining[m.end() + BRACE_LEN..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the rest of the string to the buffer, and add the final buffer to the tokens
|
||||||
|
if !remaining.is_empty() {
|
||||||
|
buf += remaining;
|
||||||
|
}
|
||||||
|
if tokens.is_empty() {
|
||||||
|
// No placeholders were found, so just return the text
|
||||||
|
return FormatTemplate::Text(buf);
|
||||||
|
}
|
||||||
|
// Add final text segment
|
||||||
|
if !buf.is_empty() {
|
||||||
|
tokens.push(Token::Text(buf));
|
||||||
|
}
|
||||||
|
debug_assert!(!tokens.is_empty());
|
||||||
|
FormatTemplate::Tokens(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a result string from this template. If path_separator is Some, then it will replace
|
||||||
|
/// the path separator in all placeholder tokens. Fixed text and tokens are not affected by
|
||||||
|
/// path separator substitution.
|
||||||
|
pub fn generate(&self, path: impl AsRef<Path>, path_separator: Option<&str>) -> OsString {
|
||||||
|
use Token::*;
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
match *self {
|
||||||
|
Self::Tokens(ref tokens) => {
|
||||||
|
let mut s = OsString::new();
|
||||||
|
for token in tokens {
|
||||||
|
match token {
|
||||||
|
Basename => s.push(Self::replace_separator(basename(path), path_separator)),
|
||||||
|
BasenameNoExt => s.push(Self::replace_separator(
|
||||||
|
&remove_extension(basename(path).as_ref()),
|
||||||
|
path_separator,
|
||||||
|
)),
|
||||||
|
NoExt => s.push(Self::replace_separator(
|
||||||
|
&remove_extension(path),
|
||||||
|
path_separator,
|
||||||
|
)),
|
||||||
|
Parent => s.push(Self::replace_separator(&dirname(path), path_separator)),
|
||||||
|
Placeholder => {
|
||||||
|
s.push(Self::replace_separator(path.as_ref(), path_separator))
|
||||||
|
}
|
||||||
|
Text(ref string) => s.push(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
Self::Text(ref text) => OsString::from(text),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the path separator in the input with the custom separator string. If path_separator
|
||||||
|
/// is None, simply return a borrowed Cow<OsStr> of the input. Otherwise, the input is
|
||||||
|
/// interpreted as a Path and its components are iterated through and re-joined into a new
|
||||||
|
/// OsString.
|
||||||
|
fn replace_separator<'a>(path: &'a OsStr, path_separator: Option<&str>) -> Cow<'a, OsStr> {
|
||||||
|
// fast-path - no replacement necessary
|
||||||
|
if path_separator.is_none() {
|
||||||
|
return Cow::Borrowed(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_separator = path_separator.unwrap();
|
||||||
|
let mut out = OsString::with_capacity(path.len());
|
||||||
|
let mut components = Path::new(path).components().peekable();
|
||||||
|
|
||||||
|
while let Some(comp) = components.next() {
|
||||||
|
match comp {
|
||||||
|
// Absolute paths on Windows are tricky. A Prefix component is usually a drive
|
||||||
|
// letter or UNC path, and is usually followed by RootDir. There are also
|
||||||
|
// "verbatim" prefixes beginning with "\\?\" that skip normalization. We choose to
|
||||||
|
// ignore verbatim path prefixes here because they're very rare, might be
|
||||||
|
// impossible to reach here, and there's no good way to deal with them. If users
|
||||||
|
// are doing something advanced involving verbatim windows paths, they can do their
|
||||||
|
// own output filtering with a tool like sed.
|
||||||
|
Component::Prefix(prefix) => {
|
||||||
|
if let Prefix::UNC(server, share) = prefix.kind() {
|
||||||
|
// Prefix::UNC is a parsed version of '\\server\share'
|
||||||
|
out.push(path_separator);
|
||||||
|
out.push(path_separator);
|
||||||
|
out.push(server);
|
||||||
|
out.push(path_separator);
|
||||||
|
out.push(share);
|
||||||
|
} else {
|
||||||
|
// All other Windows prefix types are rendered as-is. This results in e.g. "C:" for
|
||||||
|
// drive letters. DeviceNS and Verbatim* prefixes won't have backslashes converted,
|
||||||
|
// but they're not returned by directories fd can search anyway so we don't worry
|
||||||
|
// about them.
|
||||||
|
out.push(comp.as_os_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root directory is always replaced with the custom separator.
|
||||||
|
Component::RootDir => out.push(path_separator),
|
||||||
|
|
||||||
|
// Everything else is joined normally, with a trailing separator if we're not last
|
||||||
|
_ => {
|
||||||
|
out.push(comp.as_os_str());
|
||||||
|
if components.peek().is_some() {
|
||||||
|
out.push(path_separator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cow::Owned(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the id from an aho-corasick match to the
|
||||||
|
// appropriate token
|
||||||
|
fn token_from_pattern_id(id: u32) -> Token {
|
||||||
|
use Token::*;
|
||||||
|
match id {
|
||||||
|
2 => Placeholder,
|
||||||
|
3 => Basename,
|
||||||
|
4 => Parent,
|
||||||
|
5 => NoExt,
|
||||||
|
6 => BasenameNoExt,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod fmt_tests {
|
||||||
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_no_placeholders() {
|
||||||
|
let templ = FormatTemplate::parse("This string has no placeholders");
|
||||||
|
assert_eq!(
|
||||||
|
templ,
|
||||||
|
FormatTemplate::Text("This string has no placeholders".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_only_brace_escapes() {
|
||||||
|
let templ = FormatTemplate::parse("This string only has escapes like {{ and }}");
|
||||||
|
assert_eq!(
|
||||||
|
templ,
|
||||||
|
FormatTemplate::Text("This string only has escapes like { and }".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_placeholders() {
|
||||||
|
use Token::*;
|
||||||
|
|
||||||
|
let templ = FormatTemplate::parse(
|
||||||
|
"{{path={} \
|
||||||
|
basename={/} \
|
||||||
|
parent={//} \
|
||||||
|
noExt={.} \
|
||||||
|
basenameNoExt={/.} \
|
||||||
|
}}",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
templ,
|
||||||
|
FormatTemplate::Tokens(vec![
|
||||||
|
Text("{path=".into()),
|
||||||
|
Placeholder,
|
||||||
|
Text(" basename=".into()),
|
||||||
|
Basename,
|
||||||
|
Text(" parent=".into()),
|
||||||
|
Parent,
|
||||||
|
Text(" noExt=".into()),
|
||||||
|
NoExt,
|
||||||
|
Text(" basenameNoExt=".into()),
|
||||||
|
BasenameNoExt,
|
||||||
|
Text(" }".into()),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut path = PathBuf::new();
|
||||||
|
path.push("a");
|
||||||
|
path.push("folder");
|
||||||
|
path.push("file.txt");
|
||||||
|
|
||||||
|
let expanded = templ.generate(&path, Some("/")).into_string().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
expanded,
|
||||||
|
"{path=a/folder/file.txt \
|
||||||
|
basename=file.txt \
|
||||||
|
parent=a/folder \
|
||||||
|
noExt=a/folder/file \
|
||||||
|
basenameNoExt=file }"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
92
src/main.rs
92
src/main.rs
|
@ -7,21 +7,22 @@ mod exit_codes;
|
||||||
mod filesystem;
|
mod filesystem;
|
||||||
mod filetypes;
|
mod filetypes;
|
||||||
mod filter;
|
mod filter;
|
||||||
|
mod fmt;
|
||||||
mod output;
|
mod output;
|
||||||
mod regex_helper;
|
mod regex_helper;
|
||||||
mod walk;
|
mod walk;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::io::IsTerminal;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use atty::Stream;
|
|
||||||
use clap::{CommandFactory, Parser};
|
use clap::{CommandFactory, Parser};
|
||||||
use globset::GlobBuilder;
|
use globset::GlobBuilder;
|
||||||
use lscolors::LsColors;
|
use lscolors::LsColors;
|
||||||
use regex::bytes::{RegexBuilder, RegexSetBuilder};
|
use regex::bytes::{Regex, RegexBuilder, RegexSetBuilder};
|
||||||
|
|
||||||
use crate::cli::{ColorWhen, Opts};
|
use crate::cli::{ColorWhen, Opts};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
@ -40,6 +41,7 @@ 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_os = "openbsd"),
|
||||||
not(all(target_env = "musl", target_pointer_width = "32")),
|
not(all(target_env = "musl", target_pointer_width = "32")),
|
||||||
not(target_arch = "riscv64"),
|
not(target_arch = "riscv64"),
|
||||||
feature = "use-jemalloc"
|
feature = "use-jemalloc"
|
||||||
|
@ -81,12 +83,28 @@ fn run() -> Result<ExitCode> {
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_search_pattern_is_not_a_path(&opts)?;
|
ensure_search_pattern_is_not_a_path(&opts)?;
|
||||||
let pattern_regex = build_pattern_regex(&opts)?;
|
let pattern = &opts.pattern;
|
||||||
|
let exprs = &opts.exprs;
|
||||||
|
let empty = Vec::new();
|
||||||
|
|
||||||
let config = construct_config(opts, &pattern_regex)?;
|
let pattern_regexps = exprs
|
||||||
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regex)?;
|
.as_ref()
|
||||||
let re = build_regex(pattern_regex, &config)?;
|
.unwrap_or(&empty)
|
||||||
walk::scan(&search_paths, Arc::new(re), Arc::new(config))
|
.iter()
|
||||||
|
.chain([pattern])
|
||||||
|
.map(|pat| build_pattern_regex(pat, &opts))
|
||||||
|
.collect::<Result<Vec<String>>>()?;
|
||||||
|
|
||||||
|
let config = construct_config(opts, &pattern_regexps)?;
|
||||||
|
|
||||||
|
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regexps)?;
|
||||||
|
|
||||||
|
let regexps = pattern_regexps
|
||||||
|
.into_iter()
|
||||||
|
.map(|pat| build_regex(pat, &config))
|
||||||
|
.collect::<Result<Vec<Regex>>>()?;
|
||||||
|
|
||||||
|
walk::scan(&search_paths, regexps, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "completions")]
|
#[cfg(feature = "completions")]
|
||||||
|
@ -145,8 +163,7 @@ fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_pattern_regex(opts: &Opts) -> Result<String> {
|
fn build_pattern_regex(pattern: &str, opts: &Opts) -> Result<String> {
|
||||||
let pattern = &opts.pattern;
|
|
||||||
Ok(if opts.glob && !pattern.is_empty() {
|
Ok(if opts.glob && !pattern.is_empty() {
|
||||||
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
|
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
|
||||||
glob.regex().to_owned()
|
glob.regex().to_owned()
|
||||||
|
@ -172,11 +189,14 @@ fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
|
fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config> {
|
||||||
// The search will be case-sensitive if the command line flag is set or
|
// The search will be case-sensitive if the command line flag is set or
|
||||||
// if the pattern has an uppercase character (smart case).
|
// if any of the patterns has an uppercase character (smart case).
|
||||||
let case_sensitive =
|
let case_sensitive = !opts.ignore_case
|
||||||
!opts.ignore_case && (opts.case_sensitive || pattern_has_uppercase_char(pattern_regex));
|
&& (opts.case_sensitive
|
||||||
|
|| pattern_regexps
|
||||||
|
.iter()
|
||||||
|
.any(|pat| pattern_has_uppercase_char(pat)));
|
||||||
|
|
||||||
let path_separator = opts
|
let path_separator = opts
|
||||||
.path_separator
|
.path_separator
|
||||||
|
@ -194,16 +214,18 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let ansi_colors_support =
|
let ansi_colors_support =
|
||||||
ansi_term::enable_ansi_support().is_ok() || std::env::var_os("TERM").is_some();
|
nu_ansi_term::enable_ansi_support().is_ok() || std::env::var_os("TERM").is_some();
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let ansi_colors_support = true;
|
let ansi_colors_support = true;
|
||||||
|
|
||||||
let interactive_terminal = atty::is(Stream::Stdout);
|
let interactive_terminal = std::io::stdout().is_terminal();
|
||||||
|
|
||||||
let colored_output = match opts.color {
|
let colored_output = match opts.color {
|
||||||
ColorWhen::Always => true,
|
ColorWhen::Always => true,
|
||||||
ColorWhen::Never => false,
|
ColorWhen::Never => false,
|
||||||
ColorWhen::Auto => {
|
ColorWhen::Auto => {
|
||||||
ansi_colors_support && env::var_os("NO_COLOR").is_none() && interactive_terminal
|
let no_color = env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty());
|
||||||
|
ansi_colors_support && !no_color && interactive_terminal
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -221,8 +243,11 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
|
||||||
ignore_hidden: !(opts.hidden || opts.rg_alias_ignore()),
|
ignore_hidden: !(opts.hidden || opts.rg_alias_ignore()),
|
||||||
read_fdignore: !(opts.no_ignore || opts.rg_alias_ignore()),
|
read_fdignore: !(opts.no_ignore || opts.rg_alias_ignore()),
|
||||||
read_vcsignore: !(opts.no_ignore || opts.rg_alias_ignore() || opts.no_ignore_vcs),
|
read_vcsignore: !(opts.no_ignore || opts.rg_alias_ignore() || opts.no_ignore_vcs),
|
||||||
|
require_git_to_read_vcsignore: !opts.no_require_git,
|
||||||
read_parent_ignore: !opts.no_ignore_parent,
|
read_parent_ignore: !opts.no_ignore_parent,
|
||||||
read_global_ignore: !opts.no_ignore || opts.rg_alias_ignore() || opts.no_global_ignore_file,
|
read_global_ignore: !(opts.no_ignore
|
||||||
|
|| opts.rg_alias_ignore()
|
||||||
|
|| opts.no_global_ignore_file),
|
||||||
follow_links: opts.follow,
|
follow_links: opts.follow,
|
||||||
one_file_system: opts.one_file_system,
|
one_file_system: opts.one_file_system,
|
||||||
null_separator: opts.null_separator,
|
null_separator: opts.null_separator,
|
||||||
|
@ -230,7 +255,7 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
|
||||||
max_depth: opts.max_depth(),
|
max_depth: opts.max_depth(),
|
||||||
min_depth: opts.min_depth(),
|
min_depth: opts.min_depth(),
|
||||||
prune: opts.prune,
|
prune: opts.prune,
|
||||||
threads: opts.threads(),
|
threads: opts.threads().get(),
|
||||||
max_buffer_time: opts.max_buffer_time,
|
max_buffer_time: opts.max_buffer_time,
|
||||||
ls_colors,
|
ls_colors,
|
||||||
interactive_terminal,
|
interactive_terminal,
|
||||||
|
@ -247,6 +272,8 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
|
||||||
file_types.files = true;
|
file_types.files = true;
|
||||||
}
|
}
|
||||||
Empty => file_types.empty_only = true,
|
Empty => file_types.empty_only = true,
|
||||||
|
BlockDevice => file_types.block_devices = true,
|
||||||
|
CharDevice => file_types.char_devices = true,
|
||||||
Socket => file_types.sockets = true,
|
Socket => file_types.sockets = true,
|
||||||
Pipe => file_types.pipes = true,
|
Pipe => file_types.pipes = true,
|
||||||
}
|
}
|
||||||
|
@ -273,6 +300,10 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
|
||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
|
format: opts
|
||||||
|
.format
|
||||||
|
.as_deref()
|
||||||
|
.map(crate::fmt::FormatTemplate::parse),
|
||||||
command: command.map(Arc::new),
|
command: command.map(Arc::new),
|
||||||
batch_size: opts.batch_size,
|
batch_size: opts.batch_size,
|
||||||
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
|
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
|
||||||
|
@ -285,8 +316,7 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
|
||||||
path_separator,
|
path_separator,
|
||||||
actual_path_separator,
|
actual_path_separator,
|
||||||
max_results: opts.max_results(),
|
max_results: opts.max_results(),
|
||||||
strip_cwd_prefix: (opts.no_search_paths()
|
strip_cwd_prefix: opts.strip_cwd_prefix(|| !(opts.null_separator || has_command)),
|
||||||
&& (opts.strip_cwd_prefix || !(opts.null_separator || has_command))),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,18 +329,22 @@ fn extract_command(opts: &mut Opts, colored_output: bool) -> Result<Option<Comma
|
||||||
if !opts.list_details {
|
if !opts.list_details {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let color_arg = format!("--color={}", opts.color.as_str());
|
|
||||||
|
|
||||||
let res = determine_ls_command(&color_arg, colored_output)
|
let res = determine_ls_command(colored_output)
|
||||||
.map(|cmd| CommandSet::new_batch([cmd]).unwrap());
|
.map(|cmd| CommandSet::new_batch([cmd]).unwrap());
|
||||||
Some(res)
|
Some(res)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&str>> {
|
fn determine_ls_command(colored_output: bool) -> Result<Vec<&'static str>> {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
let gnu_ls = |command_name| {
|
let gnu_ls = |command_name| {
|
||||||
|
let color_arg = if colored_output {
|
||||||
|
"--color=always"
|
||||||
|
} else {
|
||||||
|
"--color=never"
|
||||||
|
};
|
||||||
// Note: we use short options here (instead of --long-options) to support more
|
// Note: we use short options here (instead of --long-options) to support more
|
||||||
// platforms (like BusyBox).
|
// platforms (like BusyBox).
|
||||||
vec![
|
vec![
|
||||||
|
@ -415,14 +449,18 @@ fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
|
||||||
|
|
||||||
fn ensure_use_hidden_option_for_leading_dot_pattern(
|
fn ensure_use_hidden_option_for_leading_dot_pattern(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
pattern_regex: &str,
|
pattern_regexps: &[String],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if cfg!(unix) && config.ignore_hidden && pattern_matches_strings_with_leading_dot(pattern_regex)
|
if cfg!(unix)
|
||||||
|
&& config.ignore_hidden
|
||||||
|
&& pattern_regexps
|
||||||
|
.iter()
|
||||||
|
.any(|pat| pattern_matches_strings_with_leading_dot(pat))
|
||||||
{
|
{
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"The pattern seems to only match files with a leading dot, but hidden files are \
|
"The pattern(s) seems to only match files with a leading dot, but hidden files are \
|
||||||
filtered by default. Consider adding -H/--hidden to search hidden files as well \
|
filtered by default. Consider adding -H/--hidden to search hidden files as well \
|
||||||
or adjust your search pattern."
|
or adjust your search pattern(s)."
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::config::Config;
|
||||||
use crate::dir_entry::DirEntry;
|
use crate::dir_entry::DirEntry;
|
||||||
use crate::error::print_error;
|
use crate::error::print_error;
|
||||||
use crate::exit_codes::ExitCode;
|
use crate::exit_codes::ExitCode;
|
||||||
|
use crate::fmt::FormatTemplate;
|
||||||
|
|
||||||
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
|
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
|
||||||
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
|
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
|
||||||
|
@ -14,7 +15,10 @@ fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
|
||||||
|
|
||||||
// TODO: this function is performance critical and can probably be optimized
|
// TODO: this function is performance critical and can probably be optimized
|
||||||
pub fn print_entry<W: Write>(stdout: &mut W, entry: &DirEntry, config: &Config) {
|
pub fn print_entry<W: Write>(stdout: &mut W, entry: &DirEntry, config: &Config) {
|
||||||
let r = if let Some(ref ls_colors) = config.ls_colors {
|
// TODO: use format if supplied
|
||||||
|
let r = if let Some(ref format) = config.format {
|
||||||
|
print_entry_format(stdout, entry, config, format)
|
||||||
|
} else if let Some(ref ls_colors) = config.ls_colors {
|
||||||
print_entry_colorized(stdout, entry, config, ls_colors)
|
print_entry_colorized(stdout, entry, config, ls_colors)
|
||||||
} else {
|
} else {
|
||||||
print_entry_uncolorized(stdout, entry, config)
|
print_entry_uncolorized(stdout, entry, config)
|
||||||
|
@ -46,7 +50,7 @@ fn print_trailing_slash<W: Write>(
|
||||||
stdout,
|
stdout,
|
||||||
"{}",
|
"{}",
|
||||||
style
|
style
|
||||||
.map(Style::to_ansi_term_style)
|
.map(Style::to_nu_ansi_term_style)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.paint(&config.actual_path_separator)
|
.paint(&config.actual_path_separator)
|
||||||
)?;
|
)?;
|
||||||
|
@ -54,6 +58,22 @@ fn print_trailing_slash<W: Write>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this function is performance critical and can probably be optimized
|
||||||
|
fn print_entry_format<W: Write>(
|
||||||
|
stdout: &mut W,
|
||||||
|
entry: &DirEntry,
|
||||||
|
config: &Config,
|
||||||
|
format: &FormatTemplate,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let separator = if config.null_separator { "\0" } else { "\n" };
|
||||||
|
let output = format.generate(
|
||||||
|
entry.stripped_path(config),
|
||||||
|
config.path_separator.as_deref(),
|
||||||
|
);
|
||||||
|
// TODO: support writing raw bytes on unix?
|
||||||
|
write!(stdout, "{}{}", output.to_string_lossy(), separator)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: this function is performance critical and can probably be optimized
|
// TODO: this function is performance critical and can probably be optimized
|
||||||
fn print_entry_colorized<W: Write>(
|
fn print_entry_colorized<W: Write>(
|
||||||
stdout: &mut W,
|
stdout: &mut W,
|
||||||
|
@ -85,14 +105,14 @@ fn print_entry_colorized<W: Write>(
|
||||||
|
|
||||||
let style = ls_colors
|
let style = ls_colors
|
||||||
.style_for_indicator(Indicator::Directory)
|
.style_for_indicator(Indicator::Directory)
|
||||||
.map(Style::to_ansi_term_style)
|
.map(Style::to_nu_ansi_term_style)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
write!(stdout, "{}", style.paint(parent_str))?;
|
write!(stdout, "{}", style.paint(parent_str))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let style = entry
|
let style = entry
|
||||||
.style(ls_colors)
|
.style(ls_colors)
|
||||||
.map(Style::to_ansi_term_style)
|
.map(Style::to_nu_ansi_term_style)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
write!(stdout, "{}", style.paint(&path_str[offset..]))?;
|
write!(stdout, "{}", style.paint(&path_str[offset..]))?;
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use regex_syntax::ParserBuilder;
|
||||||
|
|
||||||
/// Determine if a regex pattern contains a literal uppercase character.
|
/// Determine if a regex pattern contains a literal uppercase character.
|
||||||
pub fn pattern_has_uppercase_char(pattern: &str) -> bool {
|
pub fn pattern_has_uppercase_char(pattern: &str) -> bool {
|
||||||
let mut parser = ParserBuilder::new().allow_invalid_utf8(true).build();
|
let mut parser = ParserBuilder::new().utf8(false).build();
|
||||||
|
|
||||||
parser
|
parser
|
||||||
.parse(pattern)
|
.parse(pattern)
|
||||||
|
@ -16,16 +16,18 @@ fn hir_has_uppercase_char(hir: &Hir) -> bool {
|
||||||
use regex_syntax::hir::*;
|
use regex_syntax::hir::*;
|
||||||
|
|
||||||
match hir.kind() {
|
match hir.kind() {
|
||||||
HirKind::Literal(Literal::Unicode(c)) => c.is_uppercase(),
|
HirKind::Literal(Literal(bytes)) => match std::str::from_utf8(bytes) {
|
||||||
HirKind::Literal(Literal::Byte(b)) => char::from(*b).is_uppercase(),
|
Ok(s) => s.chars().any(|c| c.is_uppercase()),
|
||||||
|
Err(_) => bytes.iter().any(|b| char::from(*b).is_uppercase()),
|
||||||
|
},
|
||||||
HirKind::Class(Class::Unicode(ranges)) => ranges
|
HirKind::Class(Class::Unicode(ranges)) => ranges
|
||||||
.iter()
|
.iter()
|
||||||
.any(|r| r.start().is_uppercase() || r.end().is_uppercase()),
|
.any(|r| r.start().is_uppercase() || r.end().is_uppercase()),
|
||||||
HirKind::Class(Class::Bytes(ranges)) => ranges
|
HirKind::Class(Class::Bytes(ranges)) => ranges
|
||||||
.iter()
|
.iter()
|
||||||
.any(|r| char::from(r.start()).is_uppercase() || char::from(r.end()).is_uppercase()),
|
.any(|r| char::from(r.start()).is_uppercase() || char::from(r.end()).is_uppercase()),
|
||||||
HirKind::Group(Group { hir, .. }) | HirKind::Repetition(Repetition { hir, .. }) => {
|
HirKind::Capture(Capture { sub, .. }) | HirKind::Repetition(Repetition { sub, .. }) => {
|
||||||
hir_has_uppercase_char(hir)
|
hir_has_uppercase_char(sub)
|
||||||
}
|
}
|
||||||
HirKind::Concat(hirs) | HirKind::Alternation(hirs) => {
|
HirKind::Concat(hirs) | HirKind::Alternation(hirs) => {
|
||||||
hirs.iter().any(hir_has_uppercase_char)
|
hirs.iter().any(hir_has_uppercase_char)
|
||||||
|
@ -36,7 +38,7 @@ fn hir_has_uppercase_char(hir: &Hir) -> bool {
|
||||||
|
|
||||||
/// Determine if a regex pattern only matches strings starting with a literal dot (hidden files)
|
/// Determine if a regex pattern only matches strings starting with a literal dot (hidden files)
|
||||||
pub fn pattern_matches_strings_with_leading_dot(pattern: &str) -> bool {
|
pub fn pattern_matches_strings_with_leading_dot(pattern: &str) -> bool {
|
||||||
let mut parser = ParserBuilder::new().allow_invalid_utf8(true).build();
|
let mut parser = ParserBuilder::new().utf8(false).build();
|
||||||
|
|
||||||
parser
|
parser
|
||||||
.parse(pattern)
|
.parse(pattern)
|
||||||
|
@ -56,7 +58,7 @@ fn hir_matches_strings_with_leading_dot(hir: &Hir) -> bool {
|
||||||
HirKind::Concat(hirs) => {
|
HirKind::Concat(hirs) => {
|
||||||
let mut hirs = hirs.iter();
|
let mut hirs = hirs.iter();
|
||||||
if let Some(hir) = hirs.next() {
|
if let Some(hir) = hirs.next() {
|
||||||
if hir.kind() != &HirKind::Anchor(Anchor::StartText) {
|
if hir.kind() != &HirKind::Look(Look::Start) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -64,7 +66,10 @@ fn hir_matches_strings_with_leading_dot(hir: &Hir) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(hir) = hirs.next() {
|
if let Some(hir) = hirs.next() {
|
||||||
hir.kind() == &HirKind::Literal(Literal::Unicode('.'))
|
match hir.kind() {
|
||||||
|
HirKind::Literal(Literal(bytes)) => bytes.starts_with(&[b'.']),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
548
src/walk.rs
548
src/walk.rs
|
@ -1,17 +1,18 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::io;
|
use std::io::{self, Write};
|
||||||
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::{Arc, Mutex};
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{borrow::Cow, io::Write};
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender};
|
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, SendError, Sender};
|
||||||
use ignore::overrides::OverrideBuilder;
|
use etcetera::BaseStrategy;
|
||||||
use ignore::{self, WalkBuilder};
|
use ignore::overrides::{Override, OverrideBuilder};
|
||||||
|
use ignore::{WalkBuilder, WalkParallel, WalkState};
|
||||||
use regex::bytes::Regex;
|
use regex::bytes::Regex;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
@ -35,6 +36,7 @@ enum ReceiverMode {
|
||||||
|
|
||||||
/// The Worker threads can result in a valid entry having PathBuf or an error.
|
/// The Worker threads can result in a valid entry having PathBuf or an error.
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum WorkerResult {
|
pub enum WorkerResult {
|
||||||
// Errors should be rare, so it's probably better to allow large_enum_variant than
|
// Errors should be rare, so it's probably better to allow large_enum_variant than
|
||||||
// to box the Entry variant
|
// to box the Entry variant
|
||||||
|
@ -42,139 +44,98 @@ pub enum WorkerResult {
|
||||||
Error(ignore::Error),
|
Error(ignore::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A batch of WorkerResults to send over a channel.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Batch {
|
||||||
|
items: Arc<Mutex<Option<Vec<WorkerResult>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Batch {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
items: Arc::new(Mutex::new(Some(vec![]))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lock(&self) -> MutexGuard<'_, Option<Vec<WorkerResult>>> {
|
||||||
|
self.items.lock().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Batch {
|
||||||
|
type Item = WorkerResult;
|
||||||
|
type IntoIter = std::vec::IntoIter<WorkerResult>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.lock().take().unwrap().into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper that sends batches of items at once over a channel.
|
||||||
|
struct BatchSender {
|
||||||
|
batch: Batch,
|
||||||
|
tx: Sender<Batch>,
|
||||||
|
limit: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BatchSender {
|
||||||
|
fn new(tx: Sender<Batch>, limit: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
batch: Batch::new(),
|
||||||
|
tx,
|
||||||
|
limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if we need to flush a batch.
|
||||||
|
fn needs_flush(&self, batch: Option<&Vec<WorkerResult>>) -> bool {
|
||||||
|
match batch {
|
||||||
|
// Limit the batch size to provide some backpressure
|
||||||
|
Some(vec) => vec.len() >= self.limit,
|
||||||
|
// Batch was already taken by the receiver, so make a new one
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an item to a batch.
|
||||||
|
fn send(&mut self, item: WorkerResult) -> Result<(), SendError<()>> {
|
||||||
|
let mut batch = self.batch.lock();
|
||||||
|
|
||||||
|
if self.needs_flush(batch.as_ref()) {
|
||||||
|
drop(batch);
|
||||||
|
self.batch = Batch::new();
|
||||||
|
batch = self.batch.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = batch.as_mut().unwrap();
|
||||||
|
items.push(item);
|
||||||
|
|
||||||
|
if items.len() == 1 {
|
||||||
|
// New batch, send it over the channel
|
||||||
|
self.tx
|
||||||
|
.send(self.batch.clone())
|
||||||
|
.map_err(|_| SendError(()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Maximum size of the output buffer before flushing results to the console
|
/// Maximum size of the output buffer before flushing results to the console
|
||||||
pub const MAX_BUFFER_LENGTH: usize = 1000;
|
const MAX_BUFFER_LENGTH: usize = 1000;
|
||||||
/// Default duration until output buffering switches to streaming.
|
/// Default duration until output buffering switches to streaming.
|
||||||
pub const DEFAULT_MAX_BUFFER_TIME: Duration = Duration::from_millis(100);
|
const DEFAULT_MAX_BUFFER_TIME: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
/// Recursively scan the given search path for files / pathnames matching the pattern.
|
|
||||||
///
|
|
||||||
/// If the `--exec` argument was supplied, this will create a thread pool for executing
|
|
||||||
/// jobs in parallel from a given command line and the discovered paths. Otherwise, each
|
|
||||||
/// path will simply be written to standard output.
|
|
||||||
pub fn scan(paths: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> Result<ExitCode> {
|
|
||||||
let first_path = &paths[0];
|
|
||||||
|
|
||||||
// Channel capacity was chosen empircally to perform similarly to an unbounded channel
|
|
||||||
let (tx, rx) = bounded(0x4000 * config.threads);
|
|
||||||
|
|
||||||
let mut override_builder = OverrideBuilder::new(first_path);
|
|
||||||
|
|
||||||
for pattern in &config.exclude_patterns {
|
|
||||||
override_builder
|
|
||||||
.add(pattern)
|
|
||||||
.map_err(|e| anyhow!("Malformed exclude pattern: {}", e))?;
|
|
||||||
}
|
|
||||||
let overrides = override_builder
|
|
||||||
.build()
|
|
||||||
.map_err(|_| anyhow!("Mismatch in exclude patterns"))?;
|
|
||||||
|
|
||||||
let mut walker = WalkBuilder::new(first_path);
|
|
||||||
walker
|
|
||||||
.hidden(config.ignore_hidden)
|
|
||||||
.ignore(config.read_fdignore)
|
|
||||||
.parents(config.read_parent_ignore && (config.read_fdignore || config.read_vcsignore))
|
|
||||||
.git_ignore(config.read_vcsignore)
|
|
||||||
.git_global(config.read_vcsignore)
|
|
||||||
.git_exclude(config.read_vcsignore)
|
|
||||||
.overrides(overrides)
|
|
||||||
.follow_links(config.follow_links)
|
|
||||||
// No need to check for supported platforms, option is unavailable on unsupported ones
|
|
||||||
.same_file_system(config.one_file_system)
|
|
||||||
.max_depth(config.max_depth);
|
|
||||||
|
|
||||||
if config.read_fdignore {
|
|
||||||
walker.add_custom_ignore_filename(".fdignore");
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.read_global_ignore {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let config_dir_op = std::env::var_os("XDG_CONFIG_HOME")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.filter(|p| p.is_absolute())
|
|
||||||
.or_else(|| dirs_next::home_dir().map(|d| d.join(".config")));
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
let config_dir_op = dirs_next::config_dir();
|
|
||||||
|
|
||||||
if let Some(global_ignore_file) = config_dir_op
|
|
||||||
.map(|p| p.join("fd").join("ignore"))
|
|
||||||
.filter(|p| p.is_file())
|
|
||||||
{
|
|
||||||
let result = walker.add_ignore(global_ignore_file);
|
|
||||||
match result {
|
|
||||||
Some(ignore::Error::Partial(_)) => (),
|
|
||||||
Some(err) => {
|
|
||||||
print_error(format!("Malformed pattern in global ignore file. {}.", err));
|
|
||||||
}
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ignore_file in &config.ignore_files {
|
|
||||||
let result = walker.add_ignore(ignore_file);
|
|
||||||
match result {
|
|
||||||
Some(ignore::Error::Partial(_)) => (),
|
|
||||||
Some(err) => {
|
|
||||||
print_error(format!("Malformed pattern in custom ignore file. {}.", err));
|
|
||||||
}
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for path in &paths[1..] {
|
|
||||||
walker.add(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
let parallel_walker = walker.threads(config.threads).build_parallel();
|
|
||||||
|
|
||||||
// Flag for cleanly shutting down the parallel walk
|
|
||||||
let quit_flag = Arc::new(AtomicBool::new(false));
|
|
||||||
// Flag specifically for quitting due to ^C
|
|
||||||
let interrupt_flag = Arc::new(AtomicBool::new(false));
|
|
||||||
|
|
||||||
if config.ls_colors.is_some() && config.is_printing() {
|
|
||||||
let quit_flag = Arc::clone(&quit_flag);
|
|
||||||
let interrupt_flag = Arc::clone(&interrupt_flag);
|
|
||||||
|
|
||||||
ctrlc::set_handler(move || {
|
|
||||||
quit_flag.store(true, Ordering::Relaxed);
|
|
||||||
|
|
||||||
if interrupt_flag.fetch_or(true, Ordering::Relaxed) {
|
|
||||||
// Ctrl-C has been pressed twice, exit NOW
|
|
||||||
ExitCode::KilledBySigint.exit();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn the thread that receives all results through the channel.
|
|
||||||
let receiver_thread = spawn_receiver(&config, &quit_flag, &interrupt_flag, rx);
|
|
||||||
|
|
||||||
// Spawn the sender threads.
|
|
||||||
spawn_senders(&config, &quit_flag, pattern, parallel_walker, tx);
|
|
||||||
|
|
||||||
// Wait for the receiver thread to print out all results.
|
|
||||||
let exit_code = receiver_thread.join().unwrap();
|
|
||||||
|
|
||||||
if interrupt_flag.load(Ordering::Relaxed) {
|
|
||||||
Ok(ExitCode::KilledBySigint)
|
|
||||||
} else {
|
|
||||||
Ok(exit_code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapper for the receiver thread's buffering behavior.
|
/// Wrapper for the receiver thread's buffering behavior.
|
||||||
struct ReceiverBuffer<W> {
|
struct ReceiverBuffer<'a, W> {
|
||||||
/// The configuration.
|
/// The configuration.
|
||||||
config: Arc<Config>,
|
config: &'a Config,
|
||||||
/// For shutting down the senders.
|
/// For shutting down the senders.
|
||||||
quit_flag: Arc<AtomicBool>,
|
quit_flag: &'a AtomicBool,
|
||||||
/// The ^C notifier.
|
/// The ^C notifier.
|
||||||
interrupt_flag: Arc<AtomicBool>,
|
interrupt_flag: &'a AtomicBool,
|
||||||
/// Receiver for worker results.
|
/// Receiver for worker results.
|
||||||
rx: Receiver<WorkerResult>,
|
rx: Receiver<Batch>,
|
||||||
/// Standard output.
|
/// Standard output.
|
||||||
stdout: W,
|
stdout: W,
|
||||||
/// The current buffer mode.
|
/// The current buffer mode.
|
||||||
|
@ -187,15 +148,12 @@ struct ReceiverBuffer<W> {
|
||||||
num_results: usize,
|
num_results: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: Write> ReceiverBuffer<W> {
|
impl<'a, W: Write> ReceiverBuffer<'a, W> {
|
||||||
/// Create a new receiver buffer.
|
/// Create a new receiver buffer.
|
||||||
fn new(
|
fn new(state: &'a WorkerState, rx: Receiver<Batch>, stdout: W) -> Self {
|
||||||
config: Arc<Config>,
|
let config = &state.config;
|
||||||
quit_flag: Arc<AtomicBool>,
|
let quit_flag = state.quit_flag.as_ref();
|
||||||
interrupt_flag: Arc<AtomicBool>,
|
let interrupt_flag = state.interrupt_flag.as_ref();
|
||||||
rx: Receiver<WorkerResult>,
|
|
||||||
stdout: W,
|
|
||||||
) -> Self {
|
|
||||||
let max_buffer_time = config.max_buffer_time.unwrap_or(DEFAULT_MAX_BUFFER_TIME);
|
let max_buffer_time = config.max_buffer_time.unwrap_or(DEFAULT_MAX_BUFFER_TIME);
|
||||||
let deadline = Instant::now() + max_buffer_time;
|
let deadline = Instant::now() + max_buffer_time;
|
||||||
|
|
||||||
|
@ -223,7 +181,7 @@ impl<W: Write> ReceiverBuffer<W> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Receive the next worker result.
|
/// Receive the next worker result.
|
||||||
fn recv(&self) -> Result<WorkerResult, RecvTimeoutError> {
|
fn recv(&self) -> Result<Batch, RecvTimeoutError> {
|
||||||
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
|
||||||
|
@ -239,7 +197,10 @@ impl<W: Write> ReceiverBuffer<W> {
|
||||||
/// Wait for a result or state change.
|
/// Wait for a result or state change.
|
||||||
fn poll(&mut self) -> Result<(), ExitCode> {
|
fn poll(&mut self) -> Result<(), ExitCode> {
|
||||||
match self.recv() {
|
match self.recv() {
|
||||||
Ok(WorkerResult::Entry(dir_entry)) => {
|
Ok(batch) => {
|
||||||
|
for result in batch {
|
||||||
|
match result {
|
||||||
|
WorkerResult::Entry(dir_entry) => {
|
||||||
if self.config.quiet {
|
if self.config.quiet {
|
||||||
return Err(ExitCode::HasResults(true));
|
return Err(ExitCode::HasResults(true));
|
||||||
}
|
}
|
||||||
|
@ -253,7 +214,6 @@ impl<W: Write> ReceiverBuffer<W> {
|
||||||
}
|
}
|
||||||
ReceiverMode::Streaming => {
|
ReceiverMode::Streaming => {
|
||||||
self.print(&dir_entry)?;
|
self.print(&dir_entry)?;
|
||||||
self.flush()?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,11 +224,19 @@ impl<W: Write> ReceiverBuffer<W> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(WorkerResult::Error(err)) => {
|
WorkerResult::Error(err) => {
|
||||||
if self.config.show_filesystem_errors {
|
if self.config.show_filesystem_errors {
|
||||||
print_error(err.to_string());
|
print_error(err.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have another batch ready, flush before waiting
|
||||||
|
if self.mode == ReceiverMode::Streaming && self.rx.is_empty() {
|
||||||
|
self.flush()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(RecvTimeoutError::Timeout) => {
|
Err(RecvTimeoutError::Timeout) => {
|
||||||
self.stream()?;
|
self.stream()?;
|
||||||
}
|
}
|
||||||
|
@ -282,7 +250,7 @@ impl<W: Write> ReceiverBuffer<W> {
|
||||||
|
|
||||||
/// Output a path.
|
/// Output a path.
|
||||||
fn print(&mut self, entry: &DirEntry) -> Result<(), ExitCode> {
|
fn print(&mut self, entry: &DirEntry) -> Result<(), ExitCode> {
|
||||||
output::print_entry(&mut self.stdout, entry, &self.config);
|
output::print_entry(&mut self.stdout, entry, self.config);
|
||||||
|
|
||||||
if self.interrupt_flag.load(Ordering::Relaxed) {
|
if self.interrupt_flag.load(Ordering::Relaxed) {
|
||||||
// Ignore any errors on flush, because we're about to exit anyway
|
// Ignore any errors on flush, because we're about to exit anyway
|
||||||
|
@ -321,7 +289,7 @@ impl<W: Write> ReceiverBuffer<W> {
|
||||||
|
|
||||||
/// Flush stdout if necessary.
|
/// Flush stdout if necessary.
|
||||||
fn flush(&mut self) -> Result<(), ExitCode> {
|
fn flush(&mut self) -> Result<(), ExitCode> {
|
||||||
if self.config.interactive_terminal && self.stdout.flush().is_err() {
|
if self.stdout.flush().is_err() {
|
||||||
// Probably a broken pipe. Exit gracefully.
|
// Probably a broken pipe. Exit gracefully.
|
||||||
return Err(ExitCode::GeneralError);
|
return Err(ExitCode::GeneralError);
|
||||||
}
|
}
|
||||||
|
@ -329,79 +297,173 @@ impl<W: Write> ReceiverBuffer<W> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_receiver(
|
/// State shared by the sender and receiver threads.
|
||||||
config: &Arc<Config>,
|
struct WorkerState {
|
||||||
quit_flag: &Arc<AtomicBool>,
|
/// The search patterns.
|
||||||
interrupt_flag: &Arc<AtomicBool>,
|
patterns: Vec<Regex>,
|
||||||
rx: Receiver<WorkerResult>,
|
/// The command line configuration.
|
||||||
) -> thread::JoinHandle<ExitCode> {
|
config: Config,
|
||||||
let config = Arc::clone(config);
|
/// Flag for cleanly shutting down the parallel walk
|
||||||
let quit_flag = Arc::clone(quit_flag);
|
quit_flag: Arc<AtomicBool>,
|
||||||
let interrupt_flag = Arc::clone(interrupt_flag);
|
/// Flag specifically for quitting due to ^C
|
||||||
|
interrupt_flag: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkerState {
|
||||||
|
fn new(patterns: Vec<Regex>, config: Config) -> Self {
|
||||||
|
let quit_flag = Arc::new(AtomicBool::new(false));
|
||||||
|
let interrupt_flag = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
patterns,
|
||||||
|
config,
|
||||||
|
quit_flag,
|
||||||
|
interrupt_flag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_overrides(&self, paths: &[PathBuf]) -> Result<Override> {
|
||||||
|
let first_path = &paths[0];
|
||||||
|
let config = &self.config;
|
||||||
|
|
||||||
|
let mut builder = OverrideBuilder::new(first_path);
|
||||||
|
|
||||||
|
for pattern in &config.exclude_patterns {
|
||||||
|
builder
|
||||||
|
.add(pattern)
|
||||||
|
.map_err(|e| anyhow!("Malformed exclude pattern: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.build()
|
||||||
|
.map_err(|_| anyhow!("Mismatch in exclude patterns"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_walker(&self, paths: &[PathBuf]) -> Result<WalkParallel> {
|
||||||
|
let first_path = &paths[0];
|
||||||
|
let config = &self.config;
|
||||||
|
let overrides = self.build_overrides(paths)?;
|
||||||
|
|
||||||
|
let mut builder = WalkBuilder::new(first_path);
|
||||||
|
builder
|
||||||
|
.hidden(config.ignore_hidden)
|
||||||
|
.ignore(config.read_fdignore)
|
||||||
|
.parents(config.read_parent_ignore && (config.read_fdignore || config.read_vcsignore))
|
||||||
|
.git_ignore(config.read_vcsignore)
|
||||||
|
.git_global(config.read_vcsignore)
|
||||||
|
.git_exclude(config.read_vcsignore)
|
||||||
|
.require_git(config.require_git_to_read_vcsignore)
|
||||||
|
.overrides(overrides)
|
||||||
|
.follow_links(config.follow_links)
|
||||||
|
// No need to check for supported platforms, option is unavailable on unsupported ones
|
||||||
|
.same_file_system(config.one_file_system)
|
||||||
|
.max_depth(config.max_depth);
|
||||||
|
|
||||||
|
if config.read_fdignore {
|
||||||
|
builder.add_custom_ignore_filename(".fdignore");
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.read_global_ignore {
|
||||||
|
if let Ok(basedirs) = etcetera::choose_base_strategy() {
|
||||||
|
let global_ignore_file = basedirs.config_dir().join("fd").join("ignore");
|
||||||
|
if global_ignore_file.is_file() {
|
||||||
|
let result = builder.add_ignore(global_ignore_file);
|
||||||
|
match result {
|
||||||
|
Some(ignore::Error::Partial(_)) => (),
|
||||||
|
Some(err) => {
|
||||||
|
print_error(format!(
|
||||||
|
"Malformed pattern in global ignore file. {}.",
|
||||||
|
err
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ignore_file in &config.ignore_files {
|
||||||
|
let result = builder.add_ignore(ignore_file);
|
||||||
|
match result {
|
||||||
|
Some(ignore::Error::Partial(_)) => (),
|
||||||
|
Some(err) => {
|
||||||
|
print_error(format!("Malformed pattern in custom ignore file. {}.", err));
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for path in &paths[1..] {
|
||||||
|
builder.add(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let walker = builder.threads(config.threads).build_parallel();
|
||||||
|
Ok(walker)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the receiver work, either on this thread or a pool of background
|
||||||
|
/// threads (for --exec).
|
||||||
|
fn receive(&self, rx: Receiver<Batch>) -> ExitCode {
|
||||||
|
let config = &self.config;
|
||||||
|
|
||||||
let threads = config.threads;
|
|
||||||
thread::spawn(move || {
|
|
||||||
// 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(rx, cmd, &config)
|
exec::batch(rx.into_iter().flatten(), cmd, config)
|
||||||
} else {
|
} else {
|
||||||
let out_perm = Arc::new(Mutex::new(()));
|
let out_perm = Mutex::new(());
|
||||||
|
|
||||||
// Each spawned job will store it's thread handle in here.
|
thread::scope(|scope| {
|
||||||
|
// Each spawned job will store its thread handle in here.
|
||||||
|
let threads = config.threads;
|
||||||
let mut handles = Vec::with_capacity(threads);
|
let mut handles = Vec::with_capacity(threads);
|
||||||
for _ in 0..threads {
|
for _ in 0..threads {
|
||||||
let config = Arc::clone(&config);
|
|
||||||
let rx = rx.clone();
|
let rx = rx.clone();
|
||||||
let cmd = Arc::clone(cmd);
|
|
||||||
let out_perm = Arc::clone(&out_perm);
|
|
||||||
|
|
||||||
// Spawn a job thread that will listen for and execute inputs.
|
// Spawn a job thread that will listen for and execute inputs.
|
||||||
let handle = thread::spawn(move || exec::job(rx, cmd, out_perm, &config));
|
let handle = scope
|
||||||
|
.spawn(|| exec::job(rx.into_iter().flatten(), cmd, &out_perm, config));
|
||||||
|
|
||||||
// Push the handle of the spawned thread into the vector for later joining.
|
// Push the handle of the spawned thread into the vector for later joining.
|
||||||
handles.push(handle);
|
handles.push(handle);
|
||||||
}
|
}
|
||||||
|
let exit_codes = handles.into_iter().map(|handle| handle.join().unwrap());
|
||||||
let exit_codes = handles
|
|
||||||
.into_iter()
|
|
||||||
.map(|handle| handle.join().unwrap())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
merge_exitcodes(exit_codes)
|
merge_exitcodes(exit_codes)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let stdout = io::stdout();
|
|
||||||
let stdout = stdout.lock();
|
|
||||||
let stdout = io::BufWriter::new(stdout);
|
|
||||||
|
|
||||||
let mut rxbuffer = ReceiverBuffer::new(config, quit_flag, interrupt_flag, rx, stdout);
|
|
||||||
rxbuffer.process()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let stdout = io::stdout().lock();
|
||||||
|
let stdout = io::BufWriter::new(stdout);
|
||||||
|
|
||||||
fn spawn_senders(
|
ReceiverBuffer::new(self, rx, stdout).process()
|
||||||
config: &Arc<Config>,
|
}
|
||||||
quit_flag: &Arc<AtomicBool>,
|
|
||||||
pattern: Arc<Regex>,
|
|
||||||
parallel_walker: ignore::WalkParallel,
|
|
||||||
tx: Sender<WorkerResult>,
|
|
||||||
) {
|
|
||||||
parallel_walker.run(|| {
|
|
||||||
let config = Arc::clone(config);
|
|
||||||
let pattern = Arc::clone(&pattern);
|
|
||||||
let tx_thread = tx.clone();
|
|
||||||
let quit_flag = Arc::clone(quit_flag);
|
|
||||||
|
|
||||||
Box::new(move |entry_o| {
|
|
||||||
if quit_flag.load(Ordering::Relaxed) {
|
|
||||||
return ignore::WalkState::Quit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry = match entry_o {
|
/// Spawn the sender threads.
|
||||||
|
fn spawn_senders(&self, walker: WalkParallel, tx: Sender<Batch>) {
|
||||||
|
walker.run(|| {
|
||||||
|
let patterns = &self.patterns;
|
||||||
|
let config = &self.config;
|
||||||
|
let quit_flag = self.quit_flag.as_ref();
|
||||||
|
|
||||||
|
let mut limit = 0x100;
|
||||||
|
if let Some(cmd) = &config.command {
|
||||||
|
if !cmd.in_batch_mode() && config.threads > 1 {
|
||||||
|
// Evenly distribute work between multiple receivers
|
||||||
|
limit = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut tx = BatchSender::new(tx.clone(), limit);
|
||||||
|
|
||||||
|
Box::new(move |entry| {
|
||||||
|
if quit_flag.load(Ordering::Relaxed) {
|
||||||
|
return WalkState::Quit;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = match entry {
|
||||||
Ok(ref e) if e.depth() == 0 => {
|
Ok(ref e) if e.depth() == 0 => {
|
||||||
// Skip the root directory entry.
|
// Skip the root directory entry.
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
Ok(e) => DirEntry::normal(e),
|
Ok(e) => DirEntry::normal(e),
|
||||||
Err(ignore::Error::WithPath {
|
Err(ignore::Error::WithPath {
|
||||||
|
@ -418,26 +480,26 @@ fn spawn_senders(
|
||||||
DirEntry::broken_symlink(path)
|
DirEntry::broken_symlink(path)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return match tx_thread.send(WorkerResult::Error(ignore::Error::WithPath {
|
return match tx.send(WorkerResult::Error(ignore::Error::WithPath {
|
||||||
path,
|
path,
|
||||||
err: inner_err,
|
err: inner_err,
|
||||||
})) {
|
})) {
|
||||||
Ok(_) => ignore::WalkState::Continue,
|
Ok(_) => WalkState::Continue,
|
||||||
Err(_) => ignore::WalkState::Quit,
|
Err(_) => WalkState::Quit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return match tx_thread.send(WorkerResult::Error(err)) {
|
return match tx.send(WorkerResult::Error(err)) {
|
||||||
Ok(_) => ignore::WalkState::Continue,
|
Ok(_) => WalkState::Continue,
|
||||||
Err(_) => ignore::WalkState::Quit,
|
Err(_) => WalkState::Quit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(min_depth) = config.min_depth {
|
if let Some(min_depth) = config.min_depth {
|
||||||
if entry.depth().map_or(true, |d| d < min_depth) {
|
if entry.depth().map_or(true, |d| d < min_depth) {
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,25 +521,28 @@ fn spawn_senders(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if !pattern.is_match(&filesystem::osstr_to_bytes(search_str.as_ref())) {
|
if !patterns
|
||||||
return ignore::WalkState::Continue;
|
.iter()
|
||||||
|
.all(|pat| pat.is_match(&filesystem::osstr_to_bytes(search_str.as_ref())))
|
||||||
|
{
|
||||||
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out unwanted extensions.
|
// Filter out unwanted extensions.
|
||||||
if let Some(ref exts_regex) = config.extensions {
|
if let Some(ref exts_regex) = config.extensions {
|
||||||
if let Some(path_str) = entry_path.file_name() {
|
if let Some(path_str) = entry_path.file_name() {
|
||||||
if !exts_regex.is_match(&filesystem::osstr_to_bytes(path_str)) {
|
if !exts_regex.is_match(&filesystem::osstr_to_bytes(path_str)) {
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out unwanted file types.
|
// Filter out unwanted file types.
|
||||||
if let Some(ref file_types) = config.file_types {
|
if let Some(ref file_types) = config.file_types {
|
||||||
if file_types.should_ignore(&entry) {
|
if file_types.should_ignore(&entry) {
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -486,10 +551,10 @@ fn spawn_senders(
|
||||||
if let Some(ref owner_constraint) = config.owner_constraint {
|
if let Some(ref owner_constraint) = config.owner_constraint {
|
||||||
if let Some(metadata) = entry.metadata() {
|
if let Some(metadata) = entry.metadata() {
|
||||||
if !owner_constraint.matches(metadata) {
|
if !owner_constraint.matches(metadata) {
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -504,13 +569,13 @@ fn spawn_senders(
|
||||||
.iter()
|
.iter()
|
||||||
.any(|sc| !sc.is_within(file_size))
|
.any(|sc| !sc.is_within(file_size))
|
||||||
{
|
{
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -526,7 +591,7 @@ fn spawn_senders(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !matched {
|
if !matched {
|
||||||
return ignore::WalkState::Continue;
|
return WalkState::Continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -537,18 +602,67 @@ fn spawn_senders(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let send_result = tx_thread.send(WorkerResult::Entry(entry));
|
let send_result = tx.send(WorkerResult::Entry(entry));
|
||||||
|
|
||||||
if send_result.is_err() {
|
if send_result.is_err() {
|
||||||
return ignore::WalkState::Quit;
|
return WalkState::Quit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply pruning.
|
// Apply pruning.
|
||||||
if config.prune {
|
if config.prune {
|
||||||
return ignore::WalkState::Skip;
|
return WalkState::Skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
ignore::WalkState::Continue
|
WalkState::Continue
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform the recursive scan.
|
||||||
|
fn scan(&self, paths: &[PathBuf]) -> Result<ExitCode> {
|
||||||
|
let config = &self.config;
|
||||||
|
let walker = self.build_walker(paths)?;
|
||||||
|
|
||||||
|
if config.ls_colors.is_some() && config.is_printing() {
|
||||||
|
let quit_flag = Arc::clone(&self.quit_flag);
|
||||||
|
let interrupt_flag = Arc::clone(&self.interrupt_flag);
|
||||||
|
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
quit_flag.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
|
if interrupt_flag.fetch_or(true, Ordering::Relaxed) {
|
||||||
|
// Ctrl-C has been pressed twice, exit NOW
|
||||||
|
ExitCode::KilledBySigint.exit();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = bounded(2 * config.threads);
|
||||||
|
|
||||||
|
let exit_code = thread::scope(|scope| {
|
||||||
|
// Spawn the receiver thread(s)
|
||||||
|
let receiver = scope.spawn(|| self.receive(rx));
|
||||||
|
|
||||||
|
// Spawn the sender threads.
|
||||||
|
self.spawn_senders(walker, tx);
|
||||||
|
|
||||||
|
receiver.join().unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.interrupt_flag.load(Ordering::Relaxed) {
|
||||||
|
Ok(ExitCode::KilledBySigint)
|
||||||
|
} else {
|
||||||
|
Ok(exit_code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively scan the given search path for files / pathnames matching the patterns.
|
||||||
|
///
|
||||||
|
/// If the `--exec` argument was supplied, this will create a thread pool for executing
|
||||||
|
/// jobs in parallel from a given command line and the discovered paths. Otherwise, each
|
||||||
|
/// path will simply be written to standard output.
|
||||||
|
pub fn scan(paths: &[PathBuf], patterns: Vec<Regex>, config: Config) -> Result<ExitCode> {
|
||||||
|
WorkerState::new(patterns, config).scan(paths)
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use std::os::windows;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use tempdir::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
/// Environment for the integration tests.
|
/// Environment for the integration tests.
|
||||||
pub struct TestEnv {
|
pub struct TestEnv {
|
||||||
|
@ -20,6 +20,9 @@ pub struct TestEnv {
|
||||||
|
|
||||||
/// Normalize each line by sorting the whitespace-separated words
|
/// Normalize each line by sorting the whitespace-separated words
|
||||||
normalize_line: bool,
|
normalize_line: bool,
|
||||||
|
|
||||||
|
/// Temporary directory for storing test config (global ignore file)
|
||||||
|
config_dir: Option<TempDir>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create the working directory and the test files.
|
/// Create the working directory and the test files.
|
||||||
|
@ -27,7 +30,7 @@ fn create_working_directory(
|
||||||
directories: &[&'static str],
|
directories: &[&'static str],
|
||||||
files: &[&'static str],
|
files: &[&'static str],
|
||||||
) -> Result<TempDir, io::Error> {
|
) -> Result<TempDir, io::Error> {
|
||||||
let temp_dir = TempDir::new("fd-tests")?;
|
let temp_dir = tempfile::Builder::new().prefix("fd-tests").tempdir()?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let root = temp_dir.path();
|
let root = temp_dir.path();
|
||||||
|
@ -59,6 +62,16 @@ fn create_working_directory(
|
||||||
Ok(temp_dir)
|
Ok(temp_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_config_directory_with_global_ignore(ignore_file_content: &str) -> io::Result<TempDir> {
|
||||||
|
let config_dir = tempfile::Builder::new().prefix("fd-config").tempdir()?;
|
||||||
|
let fd_dir = config_dir.path().join("fd");
|
||||||
|
fs::create_dir(&fd_dir)?;
|
||||||
|
let mut ignore_file = fs::File::create(fd_dir.join("ignore"))?;
|
||||||
|
ignore_file.write_all(ignore_file_content.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(config_dir)
|
||||||
|
}
|
||||||
|
|
||||||
/// Find the *fd* executable.
|
/// Find the *fd* executable.
|
||||||
fn find_fd_exe() -> PathBuf {
|
fn find_fd_exe() -> PathBuf {
|
||||||
// Tests exe is in target/debug/deps, the *fd* exe is in target/debug
|
// Tests exe is in target/debug/deps, the *fd* exe is in target/debug
|
||||||
|
@ -116,7 +129,7 @@ fn normalize_output(s: &str, trim_start: bool, normalize_line: bool) -> String {
|
||||||
.lines()
|
.lines()
|
||||||
.map(|line| {
|
.map(|line| {
|
||||||
let line = if trim_start { line.trim_start() } else { line };
|
let line = if trim_start { line.trim_start() } else { line };
|
||||||
let line = line.replace('/', &std::path::MAIN_SEPARATOR.to_string());
|
let line = line.replace('/', std::path::MAIN_SEPARATOR_STR);
|
||||||
if normalize_line {
|
if normalize_line {
|
||||||
let mut words: Vec<_> = line.split_whitespace().collect();
|
let mut words: Vec<_> = line.split_whitespace().collect();
|
||||||
words.sort_unstable();
|
words.sort_unstable();
|
||||||
|
@ -150,6 +163,7 @@ impl TestEnv {
|
||||||
temp_dir,
|
temp_dir,
|
||||||
fd_exe,
|
fd_exe,
|
||||||
normalize_line: false,
|
normalize_line: false,
|
||||||
|
config_dir: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,6 +172,16 @@ impl TestEnv {
|
||||||
temp_dir: self.temp_dir,
|
temp_dir: self.temp_dir,
|
||||||
fd_exe: self.fd_exe,
|
fd_exe: self.fd_exe,
|
||||||
normalize_line: normalize,
|
normalize_line: normalize,
|
||||||
|
config_dir: self.config_dir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn global_ignore_file(self, content: &str) -> TestEnv {
|
||||||
|
let config_dir =
|
||||||
|
create_config_directory_with_global_ignore(content).expect("config directory");
|
||||||
|
TestEnv {
|
||||||
|
config_dir: Some(config_dir),
|
||||||
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +193,9 @@ impl TestEnv {
|
||||||
let root = self.test_root();
|
let root = self.test_root();
|
||||||
let broken_symlink_link = root.join(link_path);
|
let broken_symlink_link = root.join(link_path);
|
||||||
{
|
{
|
||||||
let temp_target_dir = TempDir::new("fd-tests-broken-symlink")?;
|
let temp_target_dir = tempfile::Builder::new()
|
||||||
|
.prefix("fd-tests-broken-symlink")
|
||||||
|
.tempdir()?;
|
||||||
let broken_symlink_target = temp_target_dir.path().join("broken_symlink_target");
|
let broken_symlink_target = temp_target_dir.path().join("broken_symlink_target");
|
||||||
fs::File::create(&broken_symlink_target)?;
|
fs::File::create(&broken_symlink_target)?;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
@ -204,13 +230,8 @@ impl TestEnv {
|
||||||
path: P,
|
path: P,
|
||||||
args: &[&str],
|
args: &[&str],
|
||||||
) -> process::Output {
|
) -> process::Output {
|
||||||
// Setup *fd* command.
|
|
||||||
let mut cmd = process::Command::new(&self.fd_exe);
|
|
||||||
cmd.current_dir(self.temp_dir.path().join(path));
|
|
||||||
cmd.arg("--no-global-ignore-file").args(args);
|
|
||||||
|
|
||||||
// Run *fd*.
|
// Run *fd*.
|
||||||
let output = cmd.output().expect("fd output");
|
let output = self.run_command(path.as_ref(), args);
|
||||||
|
|
||||||
// Check for exit status.
|
// Check for exit status.
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
|
@ -286,6 +307,21 @@ impl TestEnv {
|
||||||
self.assert_error_subdirectory(".", args, Some(expected))
|
self.assert_error_subdirectory(".", args, Some(expected))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_command(&self, path: &Path, args: &[&str]) -> process::Output {
|
||||||
|
// Setup *fd* command.
|
||||||
|
let mut cmd = process::Command::new(&self.fd_exe);
|
||||||
|
cmd.current_dir(self.temp_dir.path().join(path));
|
||||||
|
if let Some(config_dir) = &self.config_dir {
|
||||||
|
cmd.env("XDG_CONFIG_HOME", config_dir.path());
|
||||||
|
} else {
|
||||||
|
cmd.arg("--no-global-ignore-file");
|
||||||
|
}
|
||||||
|
cmd.args(args);
|
||||||
|
|
||||||
|
// Run *fd*.
|
||||||
|
cmd.output().expect("fd output")
|
||||||
|
}
|
||||||
|
|
||||||
/// Assert that calling *fd* in the specified path under the root working directory,
|
/// Assert that calling *fd* in the specified path under the root working directory,
|
||||||
/// and with the specified arguments produces an error with the expected message.
|
/// and with the specified arguments produces an error with the expected message.
|
||||||
fn assert_error_subdirectory<P: AsRef<Path>>(
|
fn assert_error_subdirectory<P: AsRef<Path>>(
|
||||||
|
@ -294,13 +330,7 @@ impl TestEnv {
|
||||||
args: &[&str],
|
args: &[&str],
|
||||||
expected: Option<&str>,
|
expected: Option<&str>,
|
||||||
) -> process::ExitStatus {
|
) -> process::ExitStatus {
|
||||||
// Setup *fd* command.
|
let output = self.run_command(path.as_ref(), args);
|
||||||
let mut cmd = process::Command::new(&self.fd_exe);
|
|
||||||
cmd.current_dir(self.temp_dir.path().join(path));
|
|
||||||
cmd.arg("--no-global-ignore-file").args(args);
|
|
||||||
|
|
||||||
// Run *fd*.
|
|
||||||
let output = cmd.output().expect("fd output");
|
|
||||||
|
|
||||||
if let Some(expected) = expected {
|
if let Some(expected) = expected {
|
||||||
// Normalize both expected and actual output.
|
// Normalize both expected and actual output.
|
||||||
|
|
478
tests/tests.rs
478
tests/tests.rs
|
@ -1,5 +1,7 @@
|
||||||
mod testenv;
|
mod testenv;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use nix::unistd::{Gid, Group, Uid, User};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -76,6 +78,217 @@ fn test_simple() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static AND_EXTRA_FILES: &[&str] = &[
|
||||||
|
"a.foo",
|
||||||
|
"one/b.foo",
|
||||||
|
"one/two/c.foo",
|
||||||
|
"one/two/C.Foo2",
|
||||||
|
"one/two/three/baz-quux",
|
||||||
|
"one/two/three/Baz-Quux2",
|
||||||
|
"one/two/three/d.foo",
|
||||||
|
"fdignored.foo",
|
||||||
|
"gitignored.foo",
|
||||||
|
".hidden.foo",
|
||||||
|
"A-B.jpg",
|
||||||
|
"A-C.png",
|
||||||
|
"B-A.png",
|
||||||
|
"B-C.png",
|
||||||
|
"C-A.jpg",
|
||||||
|
"C-B.png",
|
||||||
|
"e1 e2",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// AND test
|
||||||
|
#[test]
|
||||||
|
fn test_and_basic() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--and", "c"],
|
||||||
|
"one/two/C.Foo2
|
||||||
|
one/two/c.foo
|
||||||
|
one/two/three/directory_foo/",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["f", "--and", "[ad]", "--and", "[_]"],
|
||||||
|
"one/two/three/directory_foo/",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["f", "--and", "[ad]", "--and", "[.]"],
|
||||||
|
"a.foo
|
||||||
|
one/two/three/d.foo",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(&["Foo", "--and", "C"], "one/two/C.Foo2");
|
||||||
|
|
||||||
|
te.assert_output(&["foo", "--and", "asdasdasdsadasd"], "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_empty_pattern() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
te.assert_output(&["Foo", "--and", "2", "--and", ""], "one/two/C.Foo2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_bad_pattern() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
|
||||||
|
te.assert_failure(&["Foo", "--and", "2", "--and", "[", "--and", "C"]);
|
||||||
|
te.assert_failure(&["Foo", "--and", "[", "--and", "2", "--and", "C"]);
|
||||||
|
te.assert_failure(&["Foo", "--and", "2", "--and", "C", "--and", "["]);
|
||||||
|
te.assert_failure(&["[", "--and", "2", "--and", "C", "--and", "Foo"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_pattern_starts_with_dash() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["baz", "--and", "quux"],
|
||||||
|
"one/two/three/Baz-Quux2
|
||||||
|
one/two/three/baz-quux",
|
||||||
|
);
|
||||||
|
te.assert_output(
|
||||||
|
&["baz", "--and", "-"],
|
||||||
|
"one/two/three/Baz-Quux2
|
||||||
|
one/two/three/baz-quux",
|
||||||
|
);
|
||||||
|
te.assert_output(
|
||||||
|
&["Quu", "--and", "x", "--and", "-"],
|
||||||
|
"one/two/three/Baz-Quux2",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_plus_extension() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&[
|
||||||
|
"A",
|
||||||
|
"--and",
|
||||||
|
"B",
|
||||||
|
"--extension",
|
||||||
|
"jpg",
|
||||||
|
"--extension",
|
||||||
|
"png",
|
||||||
|
],
|
||||||
|
"A-B.jpg
|
||||||
|
B-A.png",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&[
|
||||||
|
"A",
|
||||||
|
"--extension",
|
||||||
|
"jpg",
|
||||||
|
"--and",
|
||||||
|
"B",
|
||||||
|
"--extension",
|
||||||
|
"png",
|
||||||
|
],
|
||||||
|
"A-B.jpg
|
||||||
|
B-A.png",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_plus_type() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["c", "--type", "d", "--and", "foo"],
|
||||||
|
"one/two/three/directory_foo/",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["c", "--type", "f", "--and", "foo"],
|
||||||
|
"one/two/C.Foo2
|
||||||
|
one/two/c.foo",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_plus_glob() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
|
||||||
|
te.assert_output(&["*foo", "--glob", "--and", "c*"], "one/two/c.foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_plus_fixed_strings() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--fixed-strings", "--and", "c", "--and", "."],
|
||||||
|
"one/two/c.foo
|
||||||
|
one/two/C.Foo2",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--fixed-strings", "--and", "[c]", "--and", "."],
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["Foo", "--fixed-strings", "--and", "C", "--and", "."],
|
||||||
|
"one/two/C.Foo2",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_plus_ignore_case() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["Foo", "--ignore-case", "--and", "C", "--and", "[.]"],
|
||||||
|
"one/two/C.Foo2
|
||||||
|
one/two/c.foo",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_plus_case_sensitive() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--case-sensitive", "--and", "c", "--and", "[.]"],
|
||||||
|
"one/two/c.foo",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_plus_full_path() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&[
|
||||||
|
"three",
|
||||||
|
"--full-path",
|
||||||
|
"--and",
|
||||||
|
"_foo",
|
||||||
|
"--and",
|
||||||
|
r"[/\\]dir",
|
||||||
|
],
|
||||||
|
"one/two/three/directory_foo/",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&[
|
||||||
|
"three",
|
||||||
|
"--full-path",
|
||||||
|
"--and",
|
||||||
|
r"[/\\]two",
|
||||||
|
"--and",
|
||||||
|
r"[/\\]dir",
|
||||||
|
],
|
||||||
|
"one/two/three/directory_foo/",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Test each pattern type with an empty pattern.
|
/// Test each pattern type with an empty pattern.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_empty_pattern() {
|
fn test_empty_pattern() {
|
||||||
|
@ -597,6 +810,62 @@ fn test_custom_ignore_precedence() {
|
||||||
te.assert_output(&["--no-ignore", "foo"], "inner/foo");
|
te.assert_output(&["--no-ignore", "foo"], "inner/foo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Don't require git to respect gitignore (--no-require-git)
|
||||||
|
#[test]
|
||||||
|
fn test_respect_ignore_files() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||||
|
|
||||||
|
// Not in a git repo anymore
|
||||||
|
fs::remove_dir(te.test_root().join(".git")).unwrap();
|
||||||
|
|
||||||
|
// don't respect gitignore because we're not in a git repo
|
||||||
|
te.assert_output(
|
||||||
|
&["foo"],
|
||||||
|
"a.foo
|
||||||
|
gitignored.foo
|
||||||
|
one/b.foo
|
||||||
|
one/two/c.foo
|
||||||
|
one/two/C.Foo2
|
||||||
|
one/two/three/d.foo
|
||||||
|
one/two/three/directory_foo/",
|
||||||
|
);
|
||||||
|
|
||||||
|
// respect gitignore because we set `--no-require-git`
|
||||||
|
te.assert_output(
|
||||||
|
&["--no-require-git", "foo"],
|
||||||
|
"a.foo
|
||||||
|
one/b.foo
|
||||||
|
one/two/c.foo
|
||||||
|
one/two/C.Foo2
|
||||||
|
one/two/three/d.foo
|
||||||
|
one/two/three/directory_foo/",
|
||||||
|
);
|
||||||
|
|
||||||
|
// make sure overriding works
|
||||||
|
te.assert_output(
|
||||||
|
&["--no-require-git", "--require-git", "foo"],
|
||||||
|
"a.foo
|
||||||
|
gitignored.foo
|
||||||
|
one/b.foo
|
||||||
|
one/two/c.foo
|
||||||
|
one/two/C.Foo2
|
||||||
|
one/two/three/d.foo
|
||||||
|
one/two/three/directory_foo/",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["--no-require-git", "--no-ignore", "foo"],
|
||||||
|
"a.foo
|
||||||
|
gitignored.foo
|
||||||
|
fdignored.foo
|
||||||
|
one/b.foo
|
||||||
|
one/two/c.foo
|
||||||
|
one/two/C.Foo2
|
||||||
|
one/two/three/d.foo
|
||||||
|
one/two/three/directory_foo/",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// VCS ignored files (--no-ignore-vcs)
|
/// VCS ignored files (--no-ignore-vcs)
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_ignore_vcs() {
|
fn test_no_ignore_vcs() {
|
||||||
|
@ -668,6 +937,47 @@ fn test_no_ignore_aliases() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[test]
|
||||||
|
fn test_global_ignore() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES).global_ignore_file("one");
|
||||||
|
te.assert_output(
|
||||||
|
&[],
|
||||||
|
"a.foo
|
||||||
|
e1 e2
|
||||||
|
symlink",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[test_case("--unrestricted", ".hidden.foo
|
||||||
|
a.foo
|
||||||
|
fdignored.foo
|
||||||
|
gitignored.foo
|
||||||
|
one/b.foo
|
||||||
|
one/two/c.foo
|
||||||
|
one/two/C.Foo2
|
||||||
|
one/two/three/d.foo
|
||||||
|
one/two/three/directory_foo/"; "unrestricted")]
|
||||||
|
#[test_case("--no-ignore", "a.foo
|
||||||
|
fdignored.foo
|
||||||
|
gitignored.foo
|
||||||
|
one/b.foo
|
||||||
|
one/two/c.foo
|
||||||
|
one/two/C.Foo2
|
||||||
|
one/two/three/d.foo
|
||||||
|
one/two/three/directory_foo/"; "no-ignore")]
|
||||||
|
#[test_case("--no-global-ignore-file", "a.foo
|
||||||
|
one/b.foo
|
||||||
|
one/two/c.foo
|
||||||
|
one/two/C.Foo2
|
||||||
|
one/two/three/d.foo
|
||||||
|
one/two/three/directory_foo/"; "no-global-ignore-file")]
|
||||||
|
fn test_no_global_ignore(flag: &str, expected_output: &str) {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES).global_ignore_file("one");
|
||||||
|
te.assert_output(&[flag, "foo"], expected_output);
|
||||||
|
}
|
||||||
|
|
||||||
/// Symlinks (--follow)
|
/// Symlinks (--follow)
|
||||||
#[test]
|
#[test]
|
||||||
fn test_follow() {
|
fn test_follow() {
|
||||||
|
@ -991,15 +1301,31 @@ fn test_type() {
|
||||||
fn test_type_executable() {
|
fn test_type_executable() {
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
|
||||||
|
// This test assumes the current user isn't root
|
||||||
|
// (otherwise if the executable bit is set for any level, it is executable for the current
|
||||||
|
// user)
|
||||||
|
if Uid::current().is_root() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||||
|
|
||||||
fs::OpenOptions::new()
|
fs::OpenOptions::new()
|
||||||
.create(true)
|
.create_new(true)
|
||||||
|
.truncate(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.mode(0o777)
|
.mode(0o777)
|
||||||
.open(te.test_root().join("executable-file.sh"))
|
.open(te.test_root().join("executable-file.sh"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.write(true)
|
||||||
|
.mode(0o645)
|
||||||
|
.open(te.test_root().join("not-user-executable-file.sh"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
te.assert_output(&["--type", "executable"], "executable-file.sh");
|
te.assert_output(&["--type", "executable"], "executable-file.sh");
|
||||||
|
|
||||||
te.assert_output(
|
te.assert_output(
|
||||||
|
@ -1298,6 +1624,66 @@ fn test_excludes() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["--format", "path={}", "--path-separator=/"],
|
||||||
|
"path=a.foo
|
||||||
|
path=e1 e2
|
||||||
|
path=one
|
||||||
|
path=one/b.foo
|
||||||
|
path=one/two
|
||||||
|
path=one/two/C.Foo2
|
||||||
|
path=one/two/c.foo
|
||||||
|
path=one/two/three
|
||||||
|
path=one/two/three/d.foo
|
||||||
|
path=one/two/three/directory_foo
|
||||||
|
path=symlink",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--format", "noExt={.}", "--path-separator=/"],
|
||||||
|
"noExt=a
|
||||||
|
noExt=one/b
|
||||||
|
noExt=one/two/C
|
||||||
|
noExt=one/two/c
|
||||||
|
noExt=one/two/three/d
|
||||||
|
noExt=one/two/three/directory_foo",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--format", "basename={/}", "--path-separator=/"],
|
||||||
|
"basename=a.foo
|
||||||
|
basename=b.foo
|
||||||
|
basename=C.Foo2
|
||||||
|
basename=c.foo
|
||||||
|
basename=d.foo
|
||||||
|
basename=directory_foo",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--format", "name={/.}", "--path-separator=/"],
|
||||||
|
"name=a
|
||||||
|
name=b
|
||||||
|
name=C
|
||||||
|
name=c
|
||||||
|
name=d
|
||||||
|
name=directory_foo",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--format", "parent={//}", "--path-separator=/"],
|
||||||
|
"parent=.
|
||||||
|
parent=one
|
||||||
|
parent=one/two
|
||||||
|
parent=one/two
|
||||||
|
parent=one/two/three
|
||||||
|
parent=one/two/three",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Shell script execution (--exec)
|
/// Shell script execution (--exec)
|
||||||
#[test]
|
#[test]
|
||||||
fn test_exec() {
|
fn test_exec() {
|
||||||
|
@ -1483,18 +1869,26 @@ fn test_exec_batch() {
|
||||||
\n\
|
\n\
|
||||||
Usage: fd [OPTIONS] [pattern] [path]...\n\
|
Usage: fd [OPTIONS] [pattern] [path]...\n\
|
||||||
\n\
|
\n\
|
||||||
For more information try '--help'\n\
|
For more information, try '--help'.\n\
|
||||||
",
|
",
|
||||||
);
|
);
|
||||||
|
|
||||||
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-batch <cmd>...' cannot be used with '--exec <cmd>...'",
|
"error: the argument '--exec-batch <cmd>...' cannot be used with '--exec <cmd>...'\n\
|
||||||
|
\n\
|
||||||
|
Usage: fd --exec-batch <cmd>... <pattern> [path]...\n\
|
||||||
|
\n\
|
||||||
|
For more information, try '--help'.\n\
|
||||||
|
",
|
||||||
);
|
);
|
||||||
|
|
||||||
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: a value is required for '--exec-batch <cmd>...' but none was supplied\n\
|
||||||
|
\n\
|
||||||
|
For more information, try '--help'.\n\
|
||||||
|
",
|
||||||
);
|
);
|
||||||
|
|
||||||
te.assert_failure_with_error(
|
te.assert_failure_with_error(
|
||||||
|
@ -1503,7 +1897,7 @@ fn test_exec_batch() {
|
||||||
\n\
|
\n\
|
||||||
Usage: fd [OPTIONS] [pattern] [path]...\n\
|
Usage: fd [OPTIONS] [pattern] [path]...\n\
|
||||||
\n\
|
\n\
|
||||||
For more information try '--help'\n\
|
For more information, try '--help'.\n\
|
||||||
",
|
",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1931,10 +2325,10 @@ fn test_owner_ignore_all() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_owner_current_user() {
|
fn test_owner_current_user() {
|
||||||
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||||
let uid = users::get_current_uid();
|
let uid = Uid::current();
|
||||||
te.assert_output(&["--owner", &uid.to_string(), "a.foo"], "a.foo");
|
te.assert_output(&["--owner", &uid.to_string(), "a.foo"], "a.foo");
|
||||||
if let Some(username) = users::get_current_username().map(|u| u.into_string().unwrap()) {
|
if let Ok(Some(user)) = User::from_uid(uid) {
|
||||||
te.assert_output(&["--owner", &username, "a.foo"], "a.foo");
|
te.assert_output(&["--owner", &user.name, "a.foo"], "a.foo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1942,10 +2336,10 @@ fn test_owner_current_user() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_owner_current_group() {
|
fn test_owner_current_group() {
|
||||||
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||||
let gid = users::get_current_gid();
|
let gid = Gid::current();
|
||||||
te.assert_output(&["--owner", &format!(":{}", gid), "a.foo"], "a.foo");
|
te.assert_output(&["--owner", &format!(":{}", gid), "a.foo"], "a.foo");
|
||||||
if let Some(groupname) = users::get_current_groupname().map(|u| u.into_string().unwrap()) {
|
if let Ok(Some(group)) = Group::from_gid(gid) {
|
||||||
te.assert_output(&["--owner", &format!(":{}", groupname), "a.foo"], "a.foo");
|
te.assert_output(&["--owner", &format!(":{}", group.name), "a.foo"], "a.foo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1953,7 +2347,7 @@ fn test_owner_current_group() {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_owner_root() {
|
fn test_owner_root() {
|
||||||
// This test assumes the current user isn't root
|
// This test assumes the current user isn't root
|
||||||
if users::get_current_uid() == 0 || users::get_current_gid() == 0 {
|
if Uid::current().is_root() || Gid::current() == Gid::from_raw(0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||||
|
@ -2054,6 +2448,11 @@ fn test_max_results() {
|
||||||
};
|
};
|
||||||
assert_just_one_result_with_option("--max-results=1");
|
assert_just_one_result_with_option("--max-results=1");
|
||||||
assert_just_one_result_with_option("-1");
|
assert_just_one_result_with_option("-1");
|
||||||
|
|
||||||
|
// check that --max-results & -1 conflic with --exec
|
||||||
|
te.assert_failure(&["thing", "--max-results=0", "--exec=cat"]);
|
||||||
|
te.assert_failure(&["thing", "-1", "--exec=cat"]);
|
||||||
|
te.assert_failure(&["thing", "--max-results=1", "-1", "--exec=cat"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filenames with non-utf8 paths are passed to the executed program unchanged
|
/// Filenames with non-utf8 paths are passed to the executed program unchanged
|
||||||
|
@ -2140,6 +2539,7 @@ fn test_number_parsing_errors() {
|
||||||
#[test_case("--hidden", &["--no-hidden"] ; "hidden")]
|
#[test_case("--hidden", &["--no-hidden"] ; "hidden")]
|
||||||
#[test_case("--no-ignore", &["--ignore"] ; "no-ignore")]
|
#[test_case("--no-ignore", &["--ignore"] ; "no-ignore")]
|
||||||
#[test_case("--no-ignore-vcs", &["--ignore-vcs"] ; "no-ignore-vcs")]
|
#[test_case("--no-ignore-vcs", &["--ignore-vcs"] ; "no-ignore-vcs")]
|
||||||
|
#[test_case("--no-require-git", &["--require-git"] ; "no-require-git")]
|
||||||
#[test_case("--follow", &["--no-follow"] ; "follow")]
|
#[test_case("--follow", &["--no-follow"] ; "follow")]
|
||||||
#[test_case("--absolute-path", &["--relative-path"] ; "absolute-path")]
|
#[test_case("--absolute-path", &["--relative-path"] ; "absolute-path")]
|
||||||
#[test_case("-u", &["--ignore", "--no-hidden"] ; "u")]
|
#[test_case("-u", &["--ignore", "--no-hidden"] ; "u")]
|
||||||
|
@ -2218,3 +2618,57 @@ fn test_invalid_cwd() {
|
||||||
panic!("{:?}", output);
|
panic!("{:?}", output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test behavior of .git directory with various flags
|
||||||
|
#[test]
|
||||||
|
fn test_git_dir() {
|
||||||
|
let te = TestEnv::new(
|
||||||
|
&[".git/one", "other_dir/.git", "nested/dir/.git"],
|
||||||
|
&[
|
||||||
|
".git/one/foo.a",
|
||||||
|
".git/.foo",
|
||||||
|
".git/a.foo",
|
||||||
|
"other_dir/.git/foo1",
|
||||||
|
"nested/dir/.git/foo2",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["--hidden", "foo"],
|
||||||
|
".git/one/foo.a
|
||||||
|
.git/.foo
|
||||||
|
.git/a.foo
|
||||||
|
other_dir/.git/foo1
|
||||||
|
nested/dir/.git/foo2",
|
||||||
|
);
|
||||||
|
te.assert_output(&["--no-ignore", "foo"], "");
|
||||||
|
te.assert_output(
|
||||||
|
&["--hidden", "--no-ignore", "foo"],
|
||||||
|
".git/one/foo.a
|
||||||
|
.git/.foo
|
||||||
|
.git/a.foo
|
||||||
|
other_dir/.git/foo1
|
||||||
|
nested/dir/.git/foo2",
|
||||||
|
);
|
||||||
|
te.assert_output(
|
||||||
|
&["--hidden", "--no-ignore-vcs", "foo"],
|
||||||
|
".git/one/foo.a
|
||||||
|
.git/.foo
|
||||||
|
.git/a.foo
|
||||||
|
other_dir/.git/foo1
|
||||||
|
nested/dir/.git/foo2",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gitignore_parent() {
|
||||||
|
let te = TestEnv::new(&["sub"], &[".abc", "sub/.abc"]);
|
||||||
|
|
||||||
|
fs::File::create(te.test_root().join(".gitignore"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(b".abc\n")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
te.assert_output_subdirectory("sub", &["--hidden"], "");
|
||||||
|
te.assert_output_subdirectory("sub", &["--hidden", "--search-path", "."], "");
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue