mirror of
https://github.com/sharkdp/fd.git
synced 2026-09-01 04:28:07 +08:00
Compare commits
263 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 563c5f3074 | |||
| 0d680b48e8 | |||
| ec81815c4c | |||
| abf4e470ed | |||
| 5c0c86c2d5 | |||
| 3651f86162 | |||
| 16b46710ee | |||
| edafedbbc7 | |||
| 5b29a4c8b3 | |||
| 5f606ad6e2 | |||
| 202615ccf7 | |||
| c644330a52 | |||
| 34acf31e0f | |||
| 7cfd25cbb6 | |||
| 39a616d6c4 | |||
| db6eb2959c | |||
| 5e8ae1d337 | |||
| e3ce16a7b4 | |||
| 045fd2d689 | |||
| 029ce455fd | |||
| e5b3269d1d | |||
| 2b0f609a48 | |||
| f370301a5b | |||
| f36c7d0be2 | |||
| e0363fc9f8 | |||
| 9859d5117c | |||
| 803d1fd38b | |||
| bbac76747b | |||
| 7d57f61dfb | |||
| c670a0b366 | |||
| a4fdad6ff7 | |||
| da7ccc21d0 | |||
| 595c1af4b6 | |||
| 69e3f1a6e6 | |||
| df550cce26 | |||
| cc7472d4db | |||
| 0c2b432fd8 | |||
| 5803cdb401 | |||
| fe51550687 | |||
| 54fca540e8 | |||
| 3a56c32a1c | |||
| 34d40443a4 | |||
| 3bbe11169a | |||
| d0e2c3d33b | |||
| 72a58676c6 | |||
| 4e6a47fee5 | |||
| 29bc3345f1 | |||
| 9c5b66b6b4 | |||
| 564a61588d | |||
| 8c177567a7 | |||
| 2a66fbab28 | |||
| 86d6e2d45f | |||
| 62e5b11d8c | |||
| 4327c36280 | |||
| 31f8eda117 | |||
| 727a976d6b | |||
| 0f69a1af62 | |||
| 5ab7a9b56d | |||
| 1cc4cec3cb | |||
| 64b326da79 | |||
| ce39d34483 | |||
| f86a460e48 | |||
| 1785baa372 | |||
| 331daf10b8 | |||
| 4d957d3a63 | |||
| 4104411a1b | |||
| 915483bcd0 | |||
| f272180939 | |||
| 285ff99ce0 | |||
| ff434bb8a0 | |||
| 54015f3fbe | |||
| e11180b323 | |||
| 9547622726 | |||
| 7a908a474c | |||
| 71deea6671 | |||
| bb676df284 | |||
| b517934b42 | |||
| 06483d1df5 | |||
| 8c93b19883 | |||
| 4505da8771 | |||
| 22b73a0253 | |||
| 202e32953c | |||
| 87f230a66e | |||
| 4bdee80afa | |||
| 3d0d1865c8 | |||
| 0d99badc1b | |||
| 3b7b95d081 | |||
| bd9882d797 | |||
| 1fed60d5c1 | |||
| f84c1d67c0 | |||
| e50cb817e1 | |||
| 8207b3ebae | |||
| aa81800ffa | |||
| 5de01fdd7b | |||
| 85764e28ef | |||
| a4b3bd29aa | |||
| 27c6b50919 | |||
| 8e0bd33af7 | |||
| b674b9b820 | |||
| 96ac5fa0a0 | |||
| 9b1017a4d5 | |||
| b191368713 | |||
| e3d4f57e75 | |||
| 1ce6906808 | |||
| 9cf415ccc7 | |||
| e9fe337921 | |||
| 8958a7bd74 | |||
| c26c45dba3 | |||
| 7decd53c70 | |||
| d5253c5372 | |||
| 3e7691ee6e | |||
| 385131765d | |||
| adbae9cf54 | |||
| b862c11550 | |||
| e7b93e6c96 | |||
| 2ea5d2a90e | |||
| 80a73fda49 | |||
| db9d2fa299 | |||
| 830fed4f82 | |||
| e3b7dcbb7e | |||
| 55d2e78cc1 | |||
| 9f0fea6e21 | |||
| 3480df88ef | |||
| cfced9787e | |||
| 9cefd79ef4 | |||
| d5525e171b | |||
| 724275f3ef | |||
| 79d1435c68 | |||
| 5147fd6aec | |||
| f55708e31d | |||
| b6b16ce959 | |||
| e65104e762 | |||
| da65108efc | |||
| c74fefabb6 | |||
| 45d6fbb9e2 | |||
| 4e9672250b | |||
| f8270a6a44 | |||
| 7042dff969 | |||
| f477c4f2c9 | |||
| b1f7aef00b | |||
| d8d2c37ec0 | |||
| 609f1adf90 | |||
| bd649e2fd7 | |||
| be815c261a | |||
| d8a808c0e3 | |||
| c92290c3d7 | |||
| 0649c2b379 | |||
| 755970deba | |||
| f897b82d76 | |||
| dbbabed606 | |||
| ac178e20af | |||
| 29936f0fba | |||
| bfc16a1dee | |||
| 289a68bac3 | |||
| fcaff0f385 | |||
| 36163f9c3a | |||
| d90ec1758e | |||
| ea22cbd712 | |||
| d44badc190 | |||
| 6becb66185 | |||
| 1a1f057e5d | |||
| 10a269bd3f | |||
| 90d3381814 | |||
| b1f83a0bb0 | |||
| 3bc70925a9 | |||
| f287f08b9f | |||
| 0e4488e9dc | |||
| d7d63eddbe | |||
| 8acd7722f0 | |||
| 92fab6e058 | |||
| a0ee0856db | |||
| b8df500a70 | |||
| cd96ca071d | |||
| 216472ff9f | |||
| 3680d10e5c | |||
| abe3b9cd78 | |||
| 7aad6c9edf | |||
| ddd3aae249 | |||
| 6d3bb68faf | |||
| 21d50dae8c | |||
| 9279b1f0af | |||
| 6647085015 | |||
| 6af8f092ee | |||
| c4094c7a05 | |||
| 6d58df5f0c | |||
| ffecccf209 | |||
| 31f2839751 | |||
| e10a4eab2b | |||
| 8eb047945e | |||
| 1031325cca | |||
| 9fc2167cf9 | |||
| ae1de4de24 | |||
| 7e5d14b733 | |||
| 85cbea8dcb | |||
| bc6782624e | |||
| cf6ff87c7d | |||
| 3cd73d7927 | |||
| 7794c4aae5 | |||
| 8c7a84ea30 | |||
| e262ade74e | |||
| 11069e284a | |||
| 6e2e86decb | |||
| 15d3b63ccc | |||
| 453577651e | |||
| 39c07b7b4c | |||
| 5910285db0 | |||
| 68fe31da3f | |||
| f875ea9a52 | |||
| 138919907b | |||
| b8744626e7 | |||
| b08d78f6fc | |||
| 4efc05ef27 | |||
| 0788c43c3f | |||
| 3b2fd158b5 | |||
| c38dbacbd0 | |||
| 728b3200c0 | |||
| 7f74cd9e56 | |||
| 6ae8da6a39 | |||
| f699c8bb6a | |||
| ffde94c10e | |||
| b0a8848f68 | |||
| d651a595d4 | |||
| 969316cc0e | |||
| 5b46867507 | |||
| e117a373a7 | |||
| a4aed14337 | |||
| 9cde3c12a2 | |||
| 906e7a933e | |||
| 077d28d13a | |||
| b55bb1e9be | |||
| 7a6cc92d6d | |||
| b694c6e673 | |||
| 17895538a0 | |||
| 72ff1f9a87 | |||
| ef3194a510 | |||
| 8773402246 | |||
| ff3fc81db4 | |||
| 0dc3342c33 | |||
| c66fc812ac | |||
| 14ed023875 | |||
| 58284b8dbe | |||
| 60889d0b99 | |||
| 7e19bad0a4 | |||
| 4b1d73d39d | |||
| 03e19a1ad2 | |||
| 8fb9499c20 | |||
| 38fb6a5958 | |||
| 49cd62d65e | |||
| 24bb5216bb | |||
| 7f8760fd1f | |||
| 3cb6b9d93a | |||
| c591106b86 | |||
| 9f096737db | |||
| 1bda165b25 | |||
| f48372624d | |||
| 5cd15536b6 | |||
| aeb4a5fdad | |||
| 9529f30129 | |||
| 266311ca33 | |||
| 954a3900b9 | |||
| 07343b5baf | |||
| a03ed8b300 | |||
| 13a93e5cbe |
+34
-121
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
ensure_cargo_fmt:
|
||||
name: Ensure 'cargo fmt' has been run
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -44,9 +44,19 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cargo fmt -- --check
|
||||
|
||||
lint_check:
|
||||
name: Ensure 'cargo clippy' has no warnings
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: actions/checkout@v4
|
||||
- run: cargo clippy --all-targets --all-features -- -Dwarnings
|
||||
|
||||
min_version:
|
||||
name: Minimum supported rust version
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: crate_metadata
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
@@ -70,17 +80,19 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
|
||||
- { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
||||
- { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||
- { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
- { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
- { target: x86_64-apple-darwin , os: macos-12 }
|
||||
- { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||
- { target: x86_64-pc-windows-msvc , os: windows-2019 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
- { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
- { target: aarch64-unknown-linux-gnu , os: ubuntu-22.04, use-cross: true }
|
||||
- { target: aarch64-unknown-linux-musl , os: ubuntu-22.04, use-cross: true }
|
||||
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-22.04, use-cross: true }
|
||||
- { target: arm-unknown-linux-musleabihf, os: ubuntu-22.04, use-cross: true }
|
||||
- { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||
- { target: i686-unknown-linux-gnu , os: ubuntu-22.04, use-cross: true }
|
||||
- { target: i686-unknown-linux-musl , os: ubuntu-22.04, use-cross: true }
|
||||
- { target: x86_64-apple-darwin , os: macos-13 }
|
||||
- { target: aarch64-apple-darwin , os: macos-14 }
|
||||
- { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||
- { target: x86_64-pc-windows-msvc , os: windows-2022 }
|
||||
- { 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:
|
||||
@@ -99,6 +111,9 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.job.target }}
|
||||
# On windows, for now build with 1.77.2, so that it works on windows 7.
|
||||
# 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
|
||||
@@ -201,113 +216,11 @@ jobs:
|
||||
id: debian-package
|
||||
shell: bash
|
||||
if: startsWith(matrix.job.os, 'ubuntu')
|
||||
run: |
|
||||
COPYRIGHT_YEARS="2018 - "$(date "+%Y")
|
||||
DPKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/debian-package"
|
||||
DPKG_DIR="${DPKG_STAGING}/dpkg"
|
||||
mkdir -p "${DPKG_DIR}"
|
||||
|
||||
DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}
|
||||
DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl
|
||||
case ${{ matrix.job.target }} in *-musl*) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac;
|
||||
DPKG_VERSION=${{ needs.crate_metadata.outputs.version }}
|
||||
|
||||
unset DPKG_ARCH
|
||||
case ${{ matrix.job.target }} in
|
||||
aarch64-*-linux-*) DPKG_ARCH=arm64 ;;
|
||||
arm-*-linux-*hf) DPKG_ARCH=armhf ;;
|
||||
i686-*-linux-*) DPKG_ARCH=i686 ;;
|
||||
x86_64-*-linux-*) DPKG_ARCH=amd64 ;;
|
||||
*) DPKG_ARCH=notset ;;
|
||||
esac;
|
||||
|
||||
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
|
||||
echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Binary
|
||||
install -Dm755 "${{ steps.bin.outputs.BIN_PATH }}" "${DPKG_DIR}/usr/bin/${{ steps.bin.outputs.BIN_NAME }}"
|
||||
|
||||
# Man page
|
||||
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/${{ needs.crate_metadata.outputs.name }}.1"
|
||||
|
||||
# Autocompletion files
|
||||
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/${{ needs.crate_metadata.outputs.name }}.fish"
|
||||
install -Dm644 'autocomplete/_fd' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ needs.crate_metadata.outputs.name }}"
|
||||
|
||||
# README and LICENSE
|
||||
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
|
||||
install -Dm644 "LICENSE-MIT" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-MIT"
|
||||
install -Dm644 "LICENSE-APACHE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-APACHE"
|
||||
install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
|
||||
gzip -n --best "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
|
||||
|
||||
cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" <<EOF
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: ${{ needs.crate_metadata.outputs.name }}
|
||||
Source: ${{ needs.crate_metadata.outputs.homepage }}
|
||||
|
||||
Files: *
|
||||
Copyright: ${{ needs.crate_metadata.outputs.maintainer }}
|
||||
Copyright: $COPYRIGHT_YEARS ${{ needs.crate_metadata.outputs.maintainer }}
|
||||
License: Apache-2.0 or MIT
|
||||
|
||||
License: Apache-2.0
|
||||
On Debian systems, the complete text of the Apache-2.0 can be found in the
|
||||
file /usr/share/common-licenses/Apache-2.0.
|
||||
|
||||
License: MIT
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
EOF
|
||||
chmod 644 "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright"
|
||||
|
||||
# control file
|
||||
mkdir -p "${DPKG_DIR}/DEBIAN"
|
||||
cat > "${DPKG_DIR}/DEBIAN/control" <<EOF
|
||||
Package: ${DPKG_BASENAME}
|
||||
Version: ${DPKG_VERSION}
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Maintainer: ${{ needs.crate_metadata.outputs.maintainer }}
|
||||
Homepage: ${{ needs.crate_metadata.outputs.homepage }}
|
||||
Architecture: ${DPKG_ARCH}
|
||||
Provides: ${{ needs.crate_metadata.outputs.name }}
|
||||
Conflicts: ${DPKG_CONFLICTS}
|
||||
Description: simple, fast and user-friendly alternative to find
|
||||
fd is a program to find entries in your filesystem.
|
||||
It is a simple, fast and user-friendly alternative to find.
|
||||
While it does not aim to support all of finds powerful functionality, it provides
|
||||
sensible (opinionated) defaults for a majority of use cases.
|
||||
EOF
|
||||
|
||||
DPKG_PATH="${DPKG_STAGING}/${DPKG_NAME}"
|
||||
echo "DPKG_PATH=${DPKG_PATH}" >> $GITHUB_OUTPUT
|
||||
|
||||
# build dpkg
|
||||
fakeroot dpkg-deb --build "${DPKG_DIR}" "${DPKG_PATH}"
|
||||
run: bash scripts/create-deb.sh
|
||||
env:
|
||||
TARGET: ${{ matrix.job.target }}
|
||||
DPKG_VERSION: ${{ needs.crate_metadata.version }}
|
||||
BIN_PATH: ${{ steps.bin.outputs.BIN_PATH }}
|
||||
|
||||
- name: "Artifact upload: tarball"
|
||||
uses: actions/upload-artifact@master
|
||||
@@ -330,7 +243,7 @@ jobs:
|
||||
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Publish archives and packages
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: steps.is-release.outputs.IS_RELEASE
|
||||
with:
|
||||
files: |
|
||||
|
||||
+80
-1
@@ -1,9 +1,88 @@
|
||||
# Upcoming release
|
||||
|
||||
## Features
|
||||
|
||||
- Add a hidden `--mindepth` alias for `--min-depth`. (#1617)
|
||||
|
||||
|
||||
## Bugfixes
|
||||
|
||||
|
||||
## Changes
|
||||
|
||||
- Replace `humantime` crate and `chrono` crate with `jiff` crate, see #1690 (@sorairolake). This has some small changes to the
|
||||
way dates given to options such `--changed-within` and `--changed-before` including:
|
||||
- 'M' no longer means "month", as that could be confusing with minutes. Use "mo", "mos", "month" or "months" instead.
|
||||
- month and year now account for variability in the calander rather than being a hard-coded number of seconds. That is probably
|
||||
what you would expect, but it is a slight change in behavior.
|
||||
|
||||
## Other
|
||||
|
||||
|
||||
# 10.2.0
|
||||
|
||||
## Features
|
||||
|
||||
- Add --hyperlink option to add OSC 8 hyperlinks to output
|
||||
|
||||
|
||||
## Bugfixes
|
||||
|
||||
|
||||
## Changes
|
||||
|
||||
- Build windows releases with rust 1.77 so windows 7 is still supported
|
||||
- Deb packages now include symlink for fdfind to be more consistent with official packages
|
||||
|
||||
|
||||
## Other
|
||||
|
||||
# 10.1.0
|
||||
|
||||
## 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, #13620) - @tavianator.
|
||||
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).
|
||||
|
||||
|
||||
Generated
+275
-359
File diff suppressed because it is too large
Load Diff
+15
-19
@@ -5,6 +5,7 @@ categories = ["command-line-utilities"]
|
||||
description = "fd is a simple, fast and user-friendly alternative to find."
|
||||
exclude = ["/benchmarks/*"]
|
||||
homepage = "https://github.com/sharkdp/fd"
|
||||
documentation = "https://docs.rs/fd-find"
|
||||
keywords = [
|
||||
"search",
|
||||
"find",
|
||||
@@ -16,9 +17,9 @@ license = "MIT OR Apache-2.0"
|
||||
name = "fd-find"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/sharkdp/fd"
|
||||
version = "9.0.0"
|
||||
version = "10.2.0"
|
||||
edition= "2021"
|
||||
rust-version = "1.70.0"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
[badges.appveyor]
|
||||
repository = "sharkdp/fd"
|
||||
@@ -34,38 +35,33 @@ path = "src/main.rs"
|
||||
version_check = "0.9"
|
||||
|
||||
[dependencies]
|
||||
aho-corasick = "1.0"
|
||||
nu-ansi-term = "0.49"
|
||||
aho-corasick = "1.1"
|
||||
nu-ansi-term = "0.50"
|
||||
argmax = "0.3.1"
|
||||
ignore = "0.4.21"
|
||||
regex = "1.9.6"
|
||||
ignore = "0.4.23"
|
||||
regex = "1.11.1"
|
||||
regex-syntax = "0.8"
|
||||
ctrlc = "3.2"
|
||||
humantime = "2.1"
|
||||
globset = "0.4"
|
||||
anyhow = "1.0"
|
||||
etcetera = "0.8"
|
||||
etcetera = "0.10"
|
||||
normpath = "1.1.1"
|
||||
crossbeam-channel = "0.5.8"
|
||||
clap_complete = {version = "4.4.4", optional = true}
|
||||
crossbeam-channel = "0.5.15"
|
||||
clap_complete = {version = "4.5.47", optional = true}
|
||||
faccess = "0.2.4"
|
||||
jiff = "0.2.5"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.4.10"
|
||||
version = "4.5.35"
|
||||
features = ["suggestions", "color", "wrap_help", "cargo", "derive"]
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.31"
|
||||
default-features = false
|
||||
features = ["std", "clock"]
|
||||
|
||||
[dependencies.lscolors]
|
||||
version = "0.16"
|
||||
version = "0.20"
|
||||
default-features = false
|
||||
features = ["nu-ansi-term"]
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { version = "0.27.1", default-features = false, features = ["signal", "user"] }
|
||||
nix = { version = "0.29.0", default-features = false, features = ["signal", "user", "hostname"] }
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
||||
libc = "0.2"
|
||||
@@ -78,7 +74,7 @@ jemallocator = {version = "0.5.4", optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
diff = "0.1"
|
||||
tempfile = "3.8"
|
||||
tempfile = "3.19"
|
||||
filetime = "0.2"
|
||||
test-case = "3.3"
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -6,7 +6,7 @@ datadir=$(prefix)/share
|
||||
exe_name=fd
|
||||
|
||||
$(EXE): Cargo.toml src/**/*.rs
|
||||
cargo build --profile $(PROFILE)
|
||||
cargo build --profile $(PROFILE) --locked
|
||||
|
||||
.PHONY: completions
|
||||
completions: autocomplete/fd.bash autocomplete/fd.fish autocomplete/fd.ps1 autocomplete/_fd
|
||||
|
||||
@@ -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
|
||||
(opinionated) defaults for a majority of use cases.
|
||||
|
||||
Quick links:
|
||||
* [How to use](#how-to-use)
|
||||
* [Installation](#installation)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
[Installation](#installation) • [How to use](#how-to-use) • [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -63,7 +60,7 @@ X11/xinit/xinitrc
|
||||
X11/xinit/xserverrc
|
||||
```
|
||||
|
||||
The regular expression syntax used by `fd` is [documented here](https://docs.rs/regex/1.0.0/regex/#syntax).
|
||||
The regular expression syntax used by `fd` is [documented here](https://docs.rs/regex/latest/regex/#syntax).
|
||||
|
||||
### Specifying the root directory
|
||||
|
||||
@@ -261,12 +258,17 @@ To make exclude-patterns like these permanent, you can create a `.fdignore` file
|
||||
/mnt/external-drive
|
||||
*.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.
|
||||
This is usually located in `~/.config/fd/ignore` in macOS or Linux, and `%APPDATA%\fd\ignore` in
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
@@ -313,7 +316,7 @@ Options:
|
||||
-p, --full-path Search full abs. path (default: filename only)
|
||||
-d, --max-depth <depth> Set maximum search depth (default: none)
|
||||
-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), char-device
|
||||
(c), block-device (b)
|
||||
-e, --extension <ext> Filter by file extension
|
||||
@@ -321,10 +324,13 @@ Options:
|
||||
--changed-within <date|dur> Filter by file modification time (newer than)
|
||||
--changed-before <date|dur> Filter by file modification time (older than)
|
||||
-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-batch <cmd>... Execute a command with all search results at once
|
||||
-c, --color <when> When to use colors [default: auto] [possible values: auto,
|
||||
always, never]
|
||||
--hyperlink[=<when>] Add hyperlinks to output paths [default: never] [possible
|
||||
values: auto, always, never]
|
||||
-h, --help Print help (see more with '--help')
|
||||
-V, --version Print version
|
||||
```
|
||||
@@ -351,7 +357,7 @@ Benchmark 2: find ~ -iname '*[0-9].jpg'
|
||||
```
|
||||
|
||||
Now let's try the same for `fd`. Note that `fd` performs a regular expression
|
||||
search by defautl. The options `-u`/`--unrestricted` option is needed here for
|
||||
search by default. The options `-u`/`--unrestricted` option is needed here for
|
||||
a fair comparison. Otherwise `fd` does not have to traverse hidden folders and
|
||||
ignored paths (see below):
|
||||
```
|
||||
@@ -373,6 +379,20 @@ also used in [ripgrep](https://github.com/BurntSushi/ripgrep) (check it out!).
|
||||
|
||||
## 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 …
|
||||
```
|
||||
|
||||
Also remember that by default, `fd` only searches based on the filename and
|
||||
doesn't compare the pattern to the full path. If you want to search based on the
|
||||
full path (similar to the `-path` option of `find`) you need to use the `--full-path`
|
||||
(or `-p`) option.
|
||||
|
||||
### Colorized output
|
||||
|
||||
`fd` can colorize files by extension, just like `ls`. In order for this to work, the environment
|
||||
@@ -386,15 +406,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` 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 …
|
||||
```
|
||||
|
||||
### `fd` doesn't seem to interpret my regex pattern correctly
|
||||
|
||||
A lot of special regex characters (like `[]`, `^`, `$`, ..) are also special characters in your
|
||||
@@ -517,7 +528,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
|
||||
[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.
|
||||
It is recommended that after installation, you add a link to `fd` by executing command
|
||||
@@ -527,21 +538,25 @@ 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
|
||||
[release page](https://github.com/sharkdp/fd/releases) and install it via:
|
||||
``` bash
|
||||
sudo dpkg -i fd_9.0.0_amd64.deb # adapt version number and architecture
|
||||
dpkg -i fd_9.0.0_amd64.deb # adapt version number and architecture
|
||||
```
|
||||
|
||||
Note that the .deb packages on the release page for this project still name the executable `fd`.
|
||||
|
||||
### On Debian
|
||||
|
||||
If you run Debian Buster or newer, you can install the
|
||||
[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.
|
||||
It is recommended that after installation, you add a link to `fd` by executing command
|
||||
`ln -s $(which fdfind) ~/.local/bin/fd`, in order to use `fd` in the same way as in this documentation.
|
||||
Make sure that `$HOME/.local/bin` is in your `$PATH`.
|
||||
|
||||
Note that the .deb packages on the release page for this project still name the executable `fd`.
|
||||
|
||||
### On Fedora
|
||||
|
||||
Starting with Fedora 28, you can install `fd` from the official package sources:
|
||||
@@ -559,10 +574,12 @@ apk add fd
|
||||
|
||||
### On Arch Linux
|
||||
|
||||
You can install [the fd package](https://www.archlinux.org/packages/community/x86_64/fd/) from the official repos:
|
||||
You can install [the fd package](https://www.archlinux.org/packages/extra/x86_64/fd/) from the official repos:
|
||||
```
|
||||
pacman -S fd
|
||||
```
|
||||
You can also install fd [from the AUR](https://aur.archlinux.org/packages/fd-git).
|
||||
|
||||
### On Gentoo Linux
|
||||
|
||||
You can use [the fd ebuild](https://packages.gentoo.org/packages/sys-apps/fd) from the official repo:
|
||||
@@ -584,6 +601,20 @@ You can install `fd` via xbps-install:
|
||||
xbps-install -S fd
|
||||
```
|
||||
|
||||
### On ALT Linux
|
||||
|
||||
You can install [the fd package](https://packages.altlinux.org/en/sisyphus/srpms/fd/) from the official repo:
|
||||
```
|
||||
apt-get install 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.
|
||||
@@ -604,7 +635,7 @@ brew install fd
|
||||
|
||||
… or with MacPorts:
|
||||
```
|
||||
sudo port install fd
|
||||
port install fd
|
||||
```
|
||||
|
||||
### On Windows
|
||||
@@ -640,6 +671,13 @@ You can use the [Nix package manager](https://nixos.org/nix/) to install `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
|
||||
|
||||
You can install [the fd-find package](https://www.freshports.org/sysutils/fd) from the official repo:
|
||||
@@ -649,7 +687,7 @@ pkg install fd-find
|
||||
|
||||
### 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
|
||||
@@ -661,7 +699,7 @@ With Rust's package manager [cargo](https://github.com/rust-lang/cargo), you can
|
||||
```
|
||||
cargo install fd-find
|
||||
```
|
||||
Note that rust version *1.70.0* or later is required.
|
||||
Note that rust version *1.77.2* or later is required.
|
||||
|
||||
`make` is also needed for the build.
|
||||
|
||||
@@ -692,8 +730,6 @@ cargo install --path .
|
||||
|
||||
## 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.
|
||||
|
||||
See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details.
|
||||
|
||||
@@ -5,7 +5,7 @@ fn main() {
|
||||
Some(true) => {}
|
||||
// rustc version too small or can't figure it out
|
||||
_ => {
|
||||
eprintln!("'fd' requires rustc >= {}", min_version);
|
||||
eprintln!("'fd' requires rustc >= {min_version}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,8 @@ _fd() {
|
||||
always\:"always use colorized output"
|
||||
))'
|
||||
|
||||
'--hyperlink=-[add hyperlinks to output paths]::when:(auto never always)'
|
||||
|
||||
+ '(threads)'
|
||||
{-j+,--threads=}'[set the number of threads for searching and executing]:number of threads'
|
||||
|
||||
@@ -162,7 +164,7 @@ _fd() {
|
||||
$no'(*)*--search-path=[set search path (instead of positional <path> arguments)]:directory:_files -/'
|
||||
|
||||
+ strip-cwd-prefix
|
||||
$no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix[Strip ./ prefix when output is redirected]'
|
||||
$no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix=-[When to strip ./]::when:(always never auto)'
|
||||
|
||||
+ and
|
||||
'--and=[additional required search path]:pattern'
|
||||
|
||||
@@ -33,16 +33,14 @@ with the '\-\-glob' option.
|
||||
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. For convenenience, '.git' is treated as if it
|
||||
was always included in gitignore rules. These files can be included with options such as
|
||||
'\-\-hidden' and '\-\-no\-ignore'.
|
||||
or ignore rules in .ignore or .fdignore files.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-H, \-\-hidden
|
||||
Include hidden files and directories in the search results
|
||||
(default: hidden files and directories are skipped). The flag can be overridden with '--no-hidden'.
|
||||
.IP
|
||||
Ignored files and .git/ are still excluded unless \-\-no\-ignore or \-\-no\-ignore\-vcs
|
||||
Ignored files are still excluded unless \-\-no\-ignore or \-\-no\-ignore\-vcs
|
||||
is also used.
|
||||
.TP
|
||||
.B \-I, \-\-no\-ignore
|
||||
@@ -79,7 +77,6 @@ and the global gitignore configuration
|
||||
.RI ( core.excludesFile
|
||||
git setting, which defaults to
|
||||
.IR $HOME/.config/git/ignore ).
|
||||
The pattern ".git/" is automatically added to the list of VCS ignore rules.
|
||||
The flag can be overridden with '--ignore-vcs'.
|
||||
.TP
|
||||
.B \-\-no\-require\-git
|
||||
@@ -159,9 +156,20 @@ can be used as an alias.
|
||||
Enable the display of filesystem errors for situations such as insufficient
|
||||
permissions or dead symlinks.
|
||||
.TP
|
||||
.B \-\-strip-cwd-prefix
|
||||
By default, relative paths are prefixed with './' when the output goes to a non interactive terminal
|
||||
(TTY). Use this flag to disable this behaviour.
|
||||
.B \-\-strip-cwd-prefix [when]
|
||||
By default, relative paths are prefixed with './' when -x/--exec,
|
||||
-X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
||||
path starting with '-' being treated as a command line option. Use
|
||||
this flag to 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
|
||||
.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).
|
||||
@@ -191,7 +199,7 @@ Filter search by type:
|
||||
.RS
|
||||
.IP "f, file"
|
||||
regular files
|
||||
.IP "d, directory"
|
||||
.IP "d, dir, directory"
|
||||
directories
|
||||
.IP "l, symlink"
|
||||
symbolic links
|
||||
@@ -268,6 +276,24 @@ Do not colorize output.
|
||||
Always colorize output.
|
||||
.RE
|
||||
.TP
|
||||
.B "\-\-hyperlink
|
||||
Specify whether the output should use terminal escape codes to indicate a hyperlink to a
|
||||
file url pointing to the path.
|
||||
|
||||
The value can be auto, always, or never.
|
||||
|
||||
Currently, the default is "never", and if the option is used without an argument "auto" is
|
||||
used. In the future this may be changed to "auto" and "always".
|
||||
.RS
|
||||
.IP auto
|
||||
Only output hyperlinks if color is also enabled, as a proxy for whether terminal escape
|
||||
codes are acceptable.
|
||||
.IP never
|
||||
Never output hyperlink escapes.
|
||||
.IP always
|
||||
Always output hyperlink escapes, regardless of color settings.
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-j, \-\-threads " num
|
||||
Set number of threads to use for searching & executing (default: number of available CPU cores).
|
||||
.TP
|
||||
@@ -310,10 +336,11 @@ tebibytes
|
||||
.TP
|
||||
.BI "\-\-changed-within " date|duration
|
||||
Filter results based on the file modification time.
|
||||
Files with modification times greater than the argument will be returned.
|
||||
Files with modification timestamps greater than the argument will be returned.
|
||||
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
|
||||
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR).
|
||||
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), or as the prefix '@'
|
||||
followed by the number of seconds since the Unix epoch (@[0-9]+).
|
||||
\fB\-\-change-newer-than\fR,
|
||||
.B --newer
|
||||
or
|
||||
@@ -324,13 +351,15 @@ Examples:
|
||||
\-\-changed-within 2weeks
|
||||
\-\-change-newer-than "2018-10-27 10:00:00"
|
||||
\-\-newer 2018-10-27
|
||||
\-\-changed-after @1704067200
|
||||
.TP
|
||||
.BI "\-\-changed-before " date|duration
|
||||
Filter results based on the file modification time.
|
||||
Files with modification times less than the argument will be returned.
|
||||
Files with modification timestamps less than the argument will be returned.
|
||||
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
|
||||
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR).
|
||||
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), or as the prefix '@'
|
||||
followed by the number of seconds since the Unix epoch (@[0-9]+).
|
||||
.B --change-older-than
|
||||
or
|
||||
.B --older
|
||||
@@ -339,6 +368,7 @@ can be used as aliases.
|
||||
Examples:
|
||||
\-\-changed-before "2018-10-27 10:00:00"
|
||||
\-\-change-older-than 2weeks
|
||||
\-\-older @1704067200
|
||||
.TP
|
||||
.BI "-o, \-\-owner " [user][:group]
|
||||
Filter files by their user and/or group. Format: [(user|uid)][:(group|gid)]. Either side
|
||||
@@ -363,6 +393,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
|
||||
\'fd [FLAGS/OPTIONS] \-\-search\-path PATH \-\-search\-path PATH2 [PATTERN]\'
|
||||
.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
|
||||
.RS
|
||||
Execute
|
||||
@@ -383,29 +437,12 @@ If parallelism is enabled, the order commands will be executed in is non-determi
|
||||
--threads=1, the order is determined by the operating system and may not be what you expect. Thus, it is
|
||||
recommended that you don't rely on any ordering of the results.
|
||||
|
||||
The following placeholders are substituted before the command is executed:
|
||||
.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)
|
||||
.RE
|
||||
Before executing the command, any placeholder patterns in the command are replaced with the
|
||||
corresponding values for the current file. The same placeholders are used as in the "\-\-format"
|
||||
option.
|
||||
|
||||
If no placeholder is present, an implicit "{}" at the end is assumed.
|
||||
|
||||
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.
|
||||
|
||||
Examples:
|
||||
|
||||
- find all *.zip files and unzip them:
|
||||
@@ -429,19 +466,9 @@ once, with all search results as arguments.
|
||||
|
||||
The order of the arguments is non-deterministic and should not be relied upon.
|
||||
|
||||
One of the following placeholders is substituted before the command is executed:
|
||||
.RS
|
||||
.IP {}
|
||||
path (of all search results)
|
||||
.IP {/}
|
||||
basename
|
||||
.IP {//}
|
||||
parent directory
|
||||
.IP {.}
|
||||
path without file extension
|
||||
.IP {/.}
|
||||
basename without file extension
|
||||
.RE
|
||||
This uses the same placeholders as "\-\-format" and "\-\-exec", but instead of expanding
|
||||
once per command invocation each argument containing a placeholder is expanding for every
|
||||
file in a batch and passed as separate arguments.
|
||||
|
||||
If no placeholder is present, an implicit "{}" at the end is assumed.
|
||||
|
||||
@@ -490,6 +517,17 @@ is set, use
|
||||
.IR $XDG_CONFIG_HOME/fd/ignore .
|
||||
Otherwise, use
|
||||
.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
|
||||
.TP
|
||||
.RI "Find files and directories that match the pattern '" needle "':"
|
||||
@@ -503,6 +541,16 @@ $ fd -e py
|
||||
.TP
|
||||
.RI "Open all search results with 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
|
||||
Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues
|
||||
.SH SEE ALSO
|
||||
|
||||
Vendored
+12
@@ -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.
|
||||
+5
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.2 KiB |
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
COPYRIGHT_YEARS="2018 - "$(date "+%Y")
|
||||
MAINTAINER="David Peter <mail@david-peter.de>"
|
||||
REPO="https://github.com/sharkdp/fd"
|
||||
DPKG_STAGING="${CICD_INTERMEDIATES_DIR:-.}/debian-package"
|
||||
DPKG_DIR="${DPKG_STAGING}/dpkg"
|
||||
mkdir -p "${DPKG_DIR}"
|
||||
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
TARGET="$(rustc -vV | sed -n 's|host: \(.*\)|\1|p')"
|
||||
fi
|
||||
|
||||
case "$TARGET" in
|
||||
*-musl*)
|
||||
DPKG_BASENAME=fd-musl
|
||||
DPKG_CONFLICTS="fd, fd-find"
|
||||
;;
|
||||
*)
|
||||
DPKG_BASENAME=fd
|
||||
DPKG_CONFLICTS="fd-musl, fd-find"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z "$DPKG_VERSION" ]]; then
|
||||
DPKG_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r .packages[0].version)
|
||||
fi
|
||||
|
||||
unset DPKG_ARCH
|
||||
case "${TARGET}" in
|
||||
aarch64-*-linux-*) DPKG_ARCH=arm64 ;;
|
||||
arm-*-linux-*hf) DPKG_ARCH=armhf ;;
|
||||
i686-*-linux-*) DPKG_ARCH=i686 ;;
|
||||
x86_64-*-linux-*) DPKG_ARCH=amd64 ;;
|
||||
*) DPKG_ARCH=notset ;;
|
||||
esac;
|
||||
|
||||
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
|
||||
|
||||
BIN_PATH=${BIN_PATH:-target/${TARGET}/release/fd}
|
||||
|
||||
# Binary
|
||||
install -Dm755 "${BIN_PATH}" "${DPKG_DIR}/usr/bin/fd"
|
||||
|
||||
# Man page
|
||||
install -Dm644 'doc/fd.1' "${DPKG_DIR}/usr/share/man/man1/fd.1"
|
||||
gzip -n --best "${DPKG_DIR}/usr/share/man/man1/fd.1"
|
||||
|
||||
# Autocompletion files
|
||||
install -Dm644 'autocomplete/fd.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/fd"
|
||||
install -Dm644 'autocomplete/fd.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/fd.fish"
|
||||
install -Dm644 'autocomplete/_fd' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_fd"
|
||||
|
||||
# README and LICENSE
|
||||
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
|
||||
install -Dm644 "LICENSE-MIT" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-MIT"
|
||||
install -Dm644 "LICENSE-APACHE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-APACHE"
|
||||
install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
|
||||
gzip -n --best "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
|
||||
|
||||
# Create symlinks so fdfind can be used as well:
|
||||
ln -s "/usr/bin/fd" "${DPKG_DIR}/usr/bin/fdfind"
|
||||
ln -s './fd.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/fdfind"
|
||||
ln -s './fd.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/fdfind.fish"
|
||||
ln -s './_fd' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_fdfind"
|
||||
|
||||
cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" <<EOF
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: fd
|
||||
Source: ${REPO}
|
||||
|
||||
Files: *
|
||||
Copyright: ${MAINTAINER}
|
||||
Copyright: $COPYRIGHT_YEARS ${MAINTAINER}
|
||||
License: Apache-2.0 or MIT
|
||||
|
||||
License: Apache-2.0
|
||||
On Debian systems, the complete text of the Apache-2.0 can be found in the
|
||||
file /usr/share/common-licenses/Apache-2.0.
|
||||
|
||||
License: MIT
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
EOF
|
||||
chmod 644 "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright"
|
||||
|
||||
# control file
|
||||
mkdir -p "${DPKG_DIR}/DEBIAN"
|
||||
cat > "${DPKG_DIR}/DEBIAN/control" <<EOF
|
||||
Package: ${DPKG_BASENAME}
|
||||
Version: ${DPKG_VERSION}
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Maintainer: ${MAINTAINER}
|
||||
Homepage: ${REPO}
|
||||
Architecture: ${DPKG_ARCH}
|
||||
Provides: fd
|
||||
Conflicts: ${DPKG_CONFLICTS}
|
||||
Description: simple, fast and user-friendly alternative to find
|
||||
fd is a program to find entries in your filesystem.
|
||||
It is a simple, fast and user-friendly alternative to find.
|
||||
While it does not aim to support all of finds powerful functionality, it provides
|
||||
sensible (opinionated) defaults for a majority of use cases.
|
||||
EOF
|
||||
|
||||
DPKG_PATH="${DPKG_STAGING}/${DPKG_NAME}"
|
||||
|
||||
if [[ -n $GITHUB_OUTPUT ]]; then
|
||||
echo "DPKG_NAME=${DPKG_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "DPKG_PATH=${DPKG_PATH}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# build dpkg
|
||||
fakeroot dpkg-deb --build "${DPKG_DIR}" "${DPKG_PATH}"
|
||||
Executable
+22
@@ -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
|
||||
|
||||
+76
-21
@@ -49,8 +49,7 @@ pub struct Opts {
|
||||
no_hidden: (),
|
||||
|
||||
/// Show search results from files and directories that would otherwise be
|
||||
/// ignored by '.gitignore', '.ignore', '.fdignore', the global ignore file,
|
||||
/// or the default rule that excludes .git/.
|
||||
/// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file,
|
||||
/// The flag can be overridden with --ignore.
|
||||
#[arg(
|
||||
long,
|
||||
@@ -64,7 +63,7 @@ pub struct Opts {
|
||||
#[arg(long, overrides_with = "no_ignore", hide = true, action = ArgAction::SetTrue)]
|
||||
ignore: (),
|
||||
|
||||
///Show search results from '.git/' folders and files and directories that
|
||||
///Show search results from files and directories that
|
||||
///would otherwise be ignored by '.gitignore' files.
|
||||
///The flag can be overridden with --ignore-vcs.
|
||||
#[arg(
|
||||
@@ -276,6 +275,7 @@ pub struct Opts {
|
||||
long,
|
||||
value_name = "depth",
|
||||
hide_short_help = true,
|
||||
alias("mindepth"),
|
||||
help = "Only show search results starting at the given depth.",
|
||||
long_help
|
||||
)]
|
||||
@@ -314,7 +314,7 @@ pub struct Opts {
|
||||
|
||||
/// Filter the search by type:
|
||||
/// {n} 'f' or 'file': regular files
|
||||
/// {n} 'd' or 'directory': directories
|
||||
/// {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)
|
||||
@@ -352,7 +352,7 @@ pub struct Opts {
|
||||
value_name = "filetype",
|
||||
hide_possible_values = true,
|
||||
value_enum,
|
||||
help = "Filter by type: file (f), directory (d), symlink (l), \
|
||||
help = "Filter by type: file (f), directory (d/dir), symlink (l), \
|
||||
executable (x), empty (e), socket (s), pipe (p), \
|
||||
char-device (c), block-device (b)",
|
||||
long_help
|
||||
@@ -399,7 +399,7 @@ pub struct Opts {
|
||||
|
||||
/// 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 as a duration (10h, 1d, 35min).
|
||||
/// 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.
|
||||
///
|
||||
@@ -421,7 +421,7 @@ pub struct Opts {
|
||||
|
||||
/// 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 as a duration (10h, 1d, 35min).
|
||||
/// 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:
|
||||
@@ -453,6 +453,20 @@ pub struct Opts {
|
||||
)]
|
||||
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)]
|
||||
pub exec: Exec,
|
||||
|
||||
@@ -496,6 +510,24 @@ pub struct Opts {
|
||||
)]
|
||||
pub color: ColorWhen,
|
||||
|
||||
/// Add a terminal hyperlink to a file:// url for each path in the output.
|
||||
///
|
||||
/// Auto mode is used if no argument is given to this option.
|
||||
///
|
||||
/// This doesn't do anything for --exec and --exec-batch.
|
||||
#[arg(
|
||||
long,
|
||||
alias = "hyper",
|
||||
value_name = "when",
|
||||
require_equals = true,
|
||||
value_enum,
|
||||
default_value_t = HyperlinkWhen::Never,
|
||||
default_missing_value = "auto",
|
||||
num_args = 0..=1,
|
||||
help = "Add hyperlinks to output paths"
|
||||
)]
|
||||
pub hyperlink: HyperlinkWhen,
|
||||
|
||||
/// Set number of threads to use for searching & executing (default: number
|
||||
/// of available CPU cores)
|
||||
#[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = str::parse::<NonZeroUsize>)]
|
||||
@@ -618,9 +650,10 @@ pub struct Opts {
|
||||
/// By default, relative paths are prefixed with './' when -x/--exec,
|
||||
/// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
||||
/// path starting with '-' being treated as a command line option. Use
|
||||
/// this flag to disable this behaviour.
|
||||
#[arg(long, conflicts_with_all(&["path", "search_path"]), hide_short_help = true, long_help)]
|
||||
pub strip_cwd_prefix: bool,
|
||||
/// this flag to change this behavior. If this flag is used without a value,
|
||||
/// it is equivalent to passing "always".
|
||||
#[arg(long, conflicts_with_all(&["path", "search_path"]), value_name = "when", hide_short_help = true, require_equals = true, long_help)]
|
||||
strip_cwd_prefix: Option<Option<StripCwdWhen>>,
|
||||
|
||||
/// 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
|
||||
@@ -643,7 +676,7 @@ impl Opts {
|
||||
} else if !self.search_path.is_empty() {
|
||||
&self.search_path
|
||||
} else {
|
||||
let current_directory = Path::new(".");
|
||||
let current_directory = Path::new("./");
|
||||
ensure_current_directory_exists(current_directory)?;
|
||||
return Ok(vec![self.normalize_path(current_directory)]);
|
||||
};
|
||||
@@ -666,6 +699,9 @@ impl Opts {
|
||||
fn normalize_path(&self, path: &Path) -> PathBuf {
|
||||
if self.absolute_path {
|
||||
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 {
|
||||
path.to_path_buf()
|
||||
}
|
||||
@@ -698,6 +734,16 @@ impl Opts {
|
||||
.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")]
|
||||
pub fn gen_completions(&self) -> anyhow::Result<Option<Shell>> {
|
||||
self.gen_completions
|
||||
@@ -729,7 +775,7 @@ fn default_num_threads() -> NonZeroUsize {
|
||||
pub enum FileType {
|
||||
#[value(alias = "f")]
|
||||
File,
|
||||
#[value(alias = "d")]
|
||||
#[value(alias = "d", alias = "dir")]
|
||||
Directory,
|
||||
#[value(alias = "l")]
|
||||
Symlink,
|
||||
@@ -758,15 +804,24 @@ pub enum ColorWhen {
|
||||
Never,
|
||||
}
|
||||
|
||||
impl ColorWhen {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
use ColorWhen::*;
|
||||
match *self {
|
||||
Auto => "auto",
|
||||
Never => "never",
|
||||
Always => "always",
|
||||
}
|
||||
}
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
pub enum StripCwdWhen {
|
||||
/// Use the default behavior
|
||||
Auto,
|
||||
/// Always strip the ./ at the beginning of paths
|
||||
Always,
|
||||
/// Never strip the ./
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
pub enum HyperlinkWhen {
|
||||
/// Use hyperlinks only if color is enabled
|
||||
Auto,
|
||||
/// Always use hyperlinks when printing file paths
|
||||
Always,
|
||||
/// Never use hyperlinks
|
||||
Never,
|
||||
}
|
||||
|
||||
// there isn't a derive api for getting grouped values yet,
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::filetypes::FileTypes;
|
||||
#[cfg(unix)]
|
||||
use crate::filter::OwnerFilter;
|
||||
use crate::filter::{SizeFilter, TimeFilter};
|
||||
use crate::fmt::FormatTemplate;
|
||||
|
||||
/// Configuration options for *fd*.
|
||||
pub struct Config {
|
||||
@@ -74,6 +75,7 @@ pub struct Config {
|
||||
pub ls_colors: Option<LsColors>,
|
||||
|
||||
/// Whether or not we are writing to an interactive terminal
|
||||
#[cfg_attr(not(unix), allow(unused))]
|
||||
pub interactive_terminal: bool,
|
||||
|
||||
/// The type of file to search for. If set to `None`, all file types are displayed. If
|
||||
@@ -85,6 +87,9 @@ pub struct Config {
|
||||
/// The value (if present) will be a lowercase string without leading dots.
|
||||
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.
|
||||
pub command: Option<Arc<CommandSet>>,
|
||||
|
||||
@@ -122,6 +127,9 @@ pub struct Config {
|
||||
|
||||
/// Whether or not to strip the './' prefix for search results
|
||||
pub strip_cwd_prefix: bool,
|
||||
|
||||
/// Whether or not to use hyperlinks on paths
|
||||
pub hyperlink: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
+2
-2
@@ -113,7 +113,7 @@ impl Eq for DirEntry {}
|
||||
impl PartialOrd for DirEntry {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.path().partial_cmp(other.path())
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ impl Colorable for DirEntry {
|
||||
// but we want it for all component types, so we open code it.
|
||||
// Copied from LsColors::style_for_path_with_metadata().
|
||||
path.components()
|
||||
.last()
|
||||
.next_back()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| path.as_os_str())
|
||||
}
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ pub fn handle_cmd_error(cmd: Option<&Command>, err: io::Error) -> ExitCode {
|
||||
ExitCode::GeneralError
|
||||
}
|
||||
(_, err) => {
|
||||
print_error(format!("Problem while executing command: {}", err));
|
||||
print_error(format!("Problem while executing command: {err}"));
|
||||
ExitCode::GeneralError
|
||||
}
|
||||
}
|
||||
|
||||
+25
-134
@@ -1,13 +1,10 @@
|
||||
mod command;
|
||||
mod input;
|
||||
mod job;
|
||||
mod token;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::iter;
|
||||
use std::path::{Component, Path, PathBuf, Prefix};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::sync::Mutex;
|
||||
|
||||
@@ -15,11 +12,10 @@ use anyhow::{bail, Result};
|
||||
use argmax::Command;
|
||||
|
||||
use crate::exit_codes::{merge_exitcodes, ExitCode};
|
||||
use crate::fmt::{FormatTemplate, Token};
|
||||
|
||||
use self::command::{execute_commands, handle_cmd_error};
|
||||
use self::input::{basename, dirname, remove_extension};
|
||||
pub use self::job::{batch, job};
|
||||
use self::token::{tokenize, Token};
|
||||
|
||||
/// Execution mode of the command
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -131,7 +127,7 @@ impl CommandSet {
|
||||
#[derive(Debug)]
|
||||
struct CommandBuilder {
|
||||
pre_args: Vec<OsString>,
|
||||
path_arg: ArgumentTemplate,
|
||||
path_arg: FormatTemplate,
|
||||
post_args: Vec<OsString>,
|
||||
cmd: Command,
|
||||
count: usize,
|
||||
@@ -220,7 +216,7 @@ impl CommandBuilder {
|
||||
/// `generate_and_execute()` method will be used to generate a command and execute it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct CommandTemplate {
|
||||
args: Vec<ArgumentTemplate>,
|
||||
args: Vec<FormatTemplate>,
|
||||
}
|
||||
|
||||
impl CommandTemplate {
|
||||
@@ -235,7 +231,7 @@ impl CommandTemplate {
|
||||
for arg in input {
|
||||
let arg = arg.as_ref();
|
||||
|
||||
let tmpl = tokenize(arg);
|
||||
let tmpl = FormatTemplate::parse(arg);
|
||||
has_placeholder |= tmpl.has_tokens();
|
||||
args.push(tmpl);
|
||||
}
|
||||
@@ -251,7 +247,7 @@ impl CommandTemplate {
|
||||
|
||||
// If a placeholder token was not supplied, append one at the end of the command.
|
||||
if !has_placeholder {
|
||||
args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder]));
|
||||
args.push(FormatTemplate::Tokens(vec![Token::Placeholder]));
|
||||
}
|
||||
|
||||
Ok(CommandTemplate { args })
|
||||
@@ -274,111 +270,6 @@ 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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -398,9 +289,9 @@ mod tests {
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Text("${SHELL}:".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Text("${SHELL}:".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Placeholder]),
|
||||
]
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
@@ -415,8 +306,8 @@ mod tests {
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
@@ -431,8 +322,8 @@ mod tests {
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Basename]),
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Basename]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
@@ -447,8 +338,8 @@ mod tests {
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Parent]),
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Parent]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
@@ -463,8 +354,8 @@ mod tests {
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]),
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::BasenameNoExt]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
@@ -494,9 +385,9 @@ mod tests {
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("cp".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
|
||||
ArgumentTemplate::Tokens(vec![
|
||||
FormatTemplate::Text("cp".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Placeholder]),
|
||||
FormatTemplate::Tokens(vec![
|
||||
Token::BasenameNoExt,
|
||||
Token::Text(".ext".into())
|
||||
]),
|
||||
@@ -514,8 +405,8 @@ mod tests {
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::Batch,
|
||||
@@ -540,7 +431,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn generate_custom_path_separator() {
|
||||
let arg = ArgumentTemplate::Tokens(vec![Token::Placeholder]);
|
||||
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||
macro_rules! check {
|
||||
($input:expr, $expected:expr) => {
|
||||
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
||||
@@ -555,7 +446,7 @@ mod tests {
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn generate_custom_path_separator_windows() {
|
||||
let arg = ArgumentTemplate::Tokens(vec![Token::Placeholder]);
|
||||
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||
macro_rules! check {
|
||||
($input:expr, $expected:expr) => {
|
||||
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
use aho_corasick::AhoCorasick;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::ArgumentTemplate;
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
|
||||
static PLACEHOLDERS: OnceLock<AhoCorasick> = OnceLock::new();
|
||||
|
||||
pub(super) fn tokenize(input: &str) -> ArgumentTemplate {
|
||||
// NOTE: we assume that { and } have the same length
|
||||
const BRACE_LEN: usize = '{'.len_utf8();
|
||||
let mut tokens = Vec::new();
|
||||
let mut remaining = input;
|
||||
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 ArgumentTemplate::Text(buf);
|
||||
}
|
||||
// Add final text segment
|
||||
if !buf.is_empty() {
|
||||
tokens.push(Token::Text(buf));
|
||||
}
|
||||
debug_assert!(!tokens.is_empty());
|
||||
ArgumentTemplate::Tokens(tokens)
|
||||
}
|
||||
|
||||
fn token_from_pattern_id(id: u32) -> Token {
|
||||
use Token::*;
|
||||
match id {
|
||||
2 => Placeholder,
|
||||
3 => Basename,
|
||||
4 => Parent,
|
||||
5 => NoExt,
|
||||
6 => BasenameNoExt,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -128,13 +128,11 @@ pub fn strip_current_dir(path: &Path) -> &Path {
|
||||
pub fn default_path_separator() -> Option<String> {
|
||||
if cfg!(windows) {
|
||||
let msystem = env::var("MSYSTEM").ok()?;
|
||||
match msystem.as_str() {
|
||||
"MINGW64" | "MINGW32" | "MSYS" => Some("/".to_owned()),
|
||||
_ => None,
|
||||
if !msystem.is_empty() {
|
||||
return Some("/".to_owned());
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+121
-78
@@ -1,6 +1,6 @@
|
||||
use chrono::{DateTime, Local, NaiveDate, NaiveDateTime};
|
||||
use jiff::{civil::DateTime, tz::TimeZone, Span, Timestamp, Zoned};
|
||||
|
||||
use std::time::SystemTime;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Filter based on time ranges.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
@@ -9,38 +9,49 @@ pub enum TimeFilter {
|
||||
After(SystemTime),
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn now() -> Zoned {
|
||||
Zoned::now()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
static TESTTIME: std::cell::RefCell<Option<Zoned>> = None.into();
|
||||
}
|
||||
|
||||
/// This allows us to set a specific time when running tests
|
||||
#[cfg(test)]
|
||||
fn now() -> Zoned {
|
||||
TESTTIME.with_borrow(|reftime| reftime.as_ref().cloned().unwrap_or_else(Zoned::now))
|
||||
}
|
||||
|
||||
impl TimeFilter {
|
||||
fn from_str(ref_time: &SystemTime, s: &str) -> Option<SystemTime> {
|
||||
humantime::parse_duration(s)
|
||||
.map(|duration| *ref_time - duration)
|
||||
.ok()
|
||||
.or_else(|| {
|
||||
DateTime::parse_from_rfc3339(s)
|
||||
.map(|dt| dt.into())
|
||||
.ok()
|
||||
.or_else(|| {
|
||||
NaiveDate::parse_from_str(s, "%F")
|
||||
.ok()?
|
||||
.and_hms_opt(0, 0, 0)?
|
||||
.and_local_timezone(Local)
|
||||
.latest()
|
||||
})
|
||||
.or_else(|| {
|
||||
NaiveDateTime::parse_from_str(s, "%F %T")
|
||||
.ok()?
|
||||
.and_local_timezone(Local)
|
||||
.latest()
|
||||
})
|
||||
.map(|dt| dt.into())
|
||||
})
|
||||
fn from_str(s: &str) -> Option<SystemTime> {
|
||||
if let Ok(span) = s.parse::<Span>() {
|
||||
let datetime = now().checked_sub(span).ok()?;
|
||||
Some(datetime.into())
|
||||
} else if let Ok(timestamp) = s.parse::<Timestamp>() {
|
||||
Some(timestamp.into())
|
||||
} else if let Ok(datetime) = s.parse::<DateTime>() {
|
||||
Some(
|
||||
TimeZone::system()
|
||||
.to_ambiguous_zoned(datetime)
|
||||
.later()
|
||||
.ok()?
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
let timestamp_secs: u64 = s.strip_prefix('@')?.parse().ok()?;
|
||||
Some(UNIX_EPOCH + Duration::from_secs(timestamp_secs))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn before(ref_time: &SystemTime, s: &str) -> Option<TimeFilter> {
|
||||
TimeFilter::from_str(ref_time, s).map(TimeFilter::Before)
|
||||
pub fn before(s: &str) -> Option<TimeFilter> {
|
||||
TimeFilter::from_str(s).map(TimeFilter::Before)
|
||||
}
|
||||
|
||||
pub fn after(ref_time: &SystemTime, s: &str) -> Option<TimeFilter> {
|
||||
TimeFilter::from_str(ref_time, s).map(TimeFilter::After)
|
||||
pub fn after(s: &str) -> Option<TimeFilter> {
|
||||
TimeFilter::from_str(s).map(TimeFilter::After)
|
||||
}
|
||||
|
||||
pub fn applies_to(&self, t: &SystemTime) -> bool {
|
||||
@@ -56,84 +67,116 @@ mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
struct TestTime(SystemTime);
|
||||
|
||||
impl TestTime {
|
||||
fn new(time: Zoned) -> Self {
|
||||
TESTTIME.with_borrow_mut(|t| *t = Some(time.clone()));
|
||||
TestTime(time.into())
|
||||
}
|
||||
|
||||
fn set(&mut self, time: Zoned) {
|
||||
TESTTIME.with_borrow_mut(|t| *t = Some(time.clone()));
|
||||
self.0 = time.into();
|
||||
}
|
||||
|
||||
fn timestamp(&self) -> SystemTime {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestTime {
|
||||
fn drop(&mut self) {
|
||||
// Stop using manually set times
|
||||
TESTTIME.with_borrow_mut(|t| *t = None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_time_filter_applicable() {
|
||||
let ref_time = NaiveDateTime::parse_from_str("2010-10-10 10:10:10", "%F %T")
|
||||
.unwrap()
|
||||
.and_local_timezone(Local)
|
||||
.latest()
|
||||
.unwrap()
|
||||
.into();
|
||||
let local_tz = TimeZone::system();
|
||||
let mut test_time = TestTime::new(
|
||||
local_tz
|
||||
.to_ambiguous_zoned("2010-10-10 10:10:10".parse::<DateTime>().unwrap())
|
||||
.later()
|
||||
.unwrap(),
|
||||
);
|
||||
let mut ref_time = test_time.timestamp();
|
||||
|
||||
assert!(TimeFilter::after(&ref_time, "1min")
|
||||
.unwrap()
|
||||
.applies_to(&ref_time));
|
||||
assert!(!TimeFilter::before(&ref_time, "1min")
|
||||
.unwrap()
|
||||
.applies_to(&ref_time));
|
||||
assert!(TimeFilter::after("1min").unwrap().applies_to(&ref_time));
|
||||
assert!(!TimeFilter::before("1min").unwrap().applies_to(&ref_time));
|
||||
|
||||
let t1m_ago = ref_time - Duration::from_secs(60);
|
||||
assert!(!TimeFilter::after(&ref_time, "30sec")
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
assert!(TimeFilter::after(&ref_time, "2min")
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
assert!(!TimeFilter::after("30sec").unwrap().applies_to(&t1m_ago));
|
||||
assert!(TimeFilter::after("2min").unwrap().applies_to(&t1m_ago));
|
||||
|
||||
assert!(TimeFilter::before(&ref_time, "30sec")
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
assert!(!TimeFilter::before(&ref_time, "2min")
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
assert!(TimeFilter::before("30sec").unwrap().applies_to(&t1m_ago));
|
||||
assert!(!TimeFilter::before("2min").unwrap().applies_to(&t1m_ago));
|
||||
|
||||
let t10s_before = "2010-10-10 10:10:00";
|
||||
assert!(!TimeFilter::before(&ref_time, t10s_before)
|
||||
assert!(!TimeFilter::before(t10s_before)
|
||||
.unwrap()
|
||||
.applies_to(&ref_time));
|
||||
assert!(TimeFilter::before(&ref_time, t10s_before)
|
||||
assert!(TimeFilter::before(t10s_before)
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
|
||||
assert!(TimeFilter::after(&ref_time, t10s_before)
|
||||
assert!(TimeFilter::after(t10s_before)
|
||||
.unwrap()
|
||||
.applies_to(&ref_time));
|
||||
assert!(!TimeFilter::after(&ref_time, t10s_before)
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
assert!(!TimeFilter::after(t10s_before).unwrap().applies_to(&t1m_ago));
|
||||
|
||||
let same_day = "2010-10-10";
|
||||
assert!(!TimeFilter::before(&ref_time, same_day)
|
||||
.unwrap()
|
||||
.applies_to(&ref_time));
|
||||
assert!(!TimeFilter::before(&ref_time, same_day)
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
assert!(!TimeFilter::before(same_day).unwrap().applies_to(&ref_time));
|
||||
assert!(!TimeFilter::before(same_day).unwrap().applies_to(&t1m_ago));
|
||||
|
||||
assert!(TimeFilter::after(&ref_time, same_day)
|
||||
.unwrap()
|
||||
.applies_to(&ref_time));
|
||||
assert!(TimeFilter::after(&ref_time, same_day)
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
assert!(TimeFilter::after(same_day).unwrap().applies_to(&ref_time));
|
||||
assert!(TimeFilter::after(same_day).unwrap().applies_to(&t1m_ago));
|
||||
|
||||
let ref_time = DateTime::parse_from_rfc3339("2010-10-10T10:10:10+00:00")
|
||||
.unwrap()
|
||||
.into();
|
||||
test_time.set(
|
||||
"2010-10-10T10:10:10+00:00"
|
||||
.parse::<Timestamp>()
|
||||
.unwrap()
|
||||
.to_zoned(local_tz.clone()),
|
||||
);
|
||||
ref_time = test_time.timestamp();
|
||||
let t1m_ago = ref_time - Duration::from_secs(60);
|
||||
let t10s_before = "2010-10-10T10:10:00+00:00";
|
||||
assert!(!TimeFilter::before(&ref_time, t10s_before)
|
||||
assert!(!TimeFilter::before(t10s_before)
|
||||
.unwrap()
|
||||
.applies_to(&ref_time));
|
||||
assert!(TimeFilter::before(&ref_time, t10s_before)
|
||||
assert!(TimeFilter::before(t10s_before)
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
|
||||
assert!(TimeFilter::after(&ref_time, t10s_before)
|
||||
assert!(TimeFilter::after(t10s_before)
|
||||
.unwrap()
|
||||
.applies_to(&ref_time));
|
||||
assert!(!TimeFilter::after(&ref_time, t10s_before)
|
||||
assert!(!TimeFilter::after(t10s_before).unwrap().applies_to(&t1m_ago));
|
||||
|
||||
let ref_timestamp = 1707723412u64; // Mon Feb 12 07:36:52 UTC 2024
|
||||
test_time.set(
|
||||
"2024-02-12T07:36:52+00:00"
|
||||
.parse::<Timestamp>()
|
||||
.unwrap()
|
||||
.to_zoned(local_tz),
|
||||
);
|
||||
ref_time = test_time.timestamp();
|
||||
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_timestamp.to_string()).is_none());
|
||||
assert!(TimeFilter::before(&format!("@{ref_timestamp}"))
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
assert!(!TimeFilter::before(&format!("@{ref_timestamp}"))
|
||||
.unwrap()
|
||||
.applies_to(&t1s_later));
|
||||
assert!(!TimeFilter::after(&format!("@{ref_timestamp}"))
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
assert!(TimeFilter::after(&format!("@{ref_timestamp}"))
|
||||
.unwrap()
|
||||
.applies_to(&t1s_later));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,10 @@ pub fn dirname(path: &Path) -> OsString {
|
||||
#[cfg(test)]
|
||||
mod path_tests {
|
||||
use super::*;
|
||||
use std::path::MAIN_SEPARATOR;
|
||||
use std::path::MAIN_SEPARATOR_STR;
|
||||
|
||||
fn correct(input: &str) -> String {
|
||||
input.replace('/', &MAIN_SEPARATOR.to_string())
|
||||
input.replace('/', MAIN_SEPARATOR_STR)
|
||||
}
|
||||
|
||||
macro_rules! func_tests {
|
||||
+281
@@ -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 }"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use crate::filesystem::absolute_path;
|
||||
use std::fmt::{self, Formatter, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub(crate) struct PathUrl(PathBuf);
|
||||
|
||||
impl PathUrl {
|
||||
pub(crate) fn new(path: &Path) -> Option<PathUrl> {
|
||||
Some(PathUrl(absolute_path(path).ok()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PathUrl {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "file://{}", host())?;
|
||||
let bytes = self.0.as_os_str().as_encoded_bytes();
|
||||
for &byte in bytes.iter() {
|
||||
encode(f, byte)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(f: &mut Formatter, byte: u8) -> fmt::Result {
|
||||
// NOTE:
|
||||
// Most terminals can handle non-ascii unicode characters in a file url fine. But on some OSes (notably
|
||||
// windows), the encoded bytes of the path may not be valid UTF-8. Since we don't know if a
|
||||
// byte >= 128 is part of a valid UTF-8 encoding or not, we just percent encode any non-ascii
|
||||
// byte.
|
||||
// Percent encoding these bytes is probably safer anyway.
|
||||
match byte {
|
||||
b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'/' | b':' | b'-' | b'.' | b'_' | b'~' => {
|
||||
f.write_char(byte.into())
|
||||
}
|
||||
#[cfg(windows)]
|
||||
b'\\' => f.write_char('/'),
|
||||
_ => {
|
||||
write!(f, "%{byte:02X}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn host() -> &'static str {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static HOSTNAME: OnceLock<String> = OnceLock::new();
|
||||
|
||||
HOSTNAME
|
||||
.get_or_init(|| {
|
||||
nix::unistd::gethostname()
|
||||
.ok()
|
||||
.and_then(|h| h.into_string().ok())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
const fn host() -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
// This allows us to test the encoding without having to worry about the host, or absolute path
|
||||
struct Encoded(&'static str);
|
||||
|
||||
impl fmt::Display for Encoded {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
for byte in self.0.bytes() {
|
||||
encode(f, byte)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_encoding() {
|
||||
assert_eq!(
|
||||
Encoded("$*\x1bßé/∫😃\x07").to_string(),
|
||||
"%24%2A%1B%C3%9F%C3%A9/%E2%88%AB%F0%9F%98%83%07",
|
||||
);
|
||||
}
|
||||
}
|
||||
+24
-11
@@ -7,6 +7,8 @@ mod exit_codes;
|
||||
mod filesystem;
|
||||
mod filetypes;
|
||||
mod filter;
|
||||
mod fmt;
|
||||
mod hyperlink;
|
||||
mod output;
|
||||
mod regex_helper;
|
||||
mod walk;
|
||||
@@ -15,7 +17,6 @@ use std::env;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use clap::{CommandFactory, Parser};
|
||||
@@ -23,7 +24,7 @@ use globset::GlobBuilder;
|
||||
use lscolors::LsColors;
|
||||
use regex::bytes::{Regex, RegexBuilder, RegexSetBuilder};
|
||||
|
||||
use crate::cli::{ColorWhen, Opts};
|
||||
use crate::cli::{ColorWhen, HyperlinkWhen, Opts};
|
||||
use crate::config::Config;
|
||||
use crate::exec::CommandSet;
|
||||
use crate::exit_codes::ExitCode;
|
||||
@@ -61,7 +62,7 @@ fn main() {
|
||||
exit_code.exit();
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("[fd error]: {:#}", err);
|
||||
eprintln!("[fd error]: {err:#}");
|
||||
ExitCode::GeneralError.exit();
|
||||
}
|
||||
}
|
||||
@@ -233,6 +234,11 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let hyperlink = match opts.hyperlink {
|
||||
HyperlinkWhen::Always => true,
|
||||
HyperlinkWhen::Never => false,
|
||||
HyperlinkWhen::Auto => colored_output,
|
||||
};
|
||||
let command = extract_command(&mut opts, colored_output)?;
|
||||
let has_command = command.is_some();
|
||||
|
||||
@@ -257,6 +263,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
|
||||
threads: opts.threads().get(),
|
||||
max_buffer_time: opts.max_buffer_time,
|
||||
ls_colors,
|
||||
hyperlink,
|
||||
interactive_terminal,
|
||||
file_types: opts.filetype.as_ref().map(|values| {
|
||||
use crate::cli::FileType::*;
|
||||
@@ -299,6 +306,10 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
|
||||
.build()
|
||||
})
|
||||
.transpose()?,
|
||||
format: opts
|
||||
.format
|
||||
.as_deref()
|
||||
.map(crate::fmt::FormatTemplate::parse),
|
||||
command: command.map(Arc::new),
|
||||
batch_size: opts.batch_size,
|
||||
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
|
||||
@@ -311,8 +322,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
|
||||
path_separator,
|
||||
actual_path_separator,
|
||||
max_results: opts.max_results(),
|
||||
strip_cwd_prefix: (opts.no_search_paths()
|
||||
&& (opts.strip_cwd_prefix || !(opts.null_separator || has_command))),
|
||||
strip_cwd_prefix: opts.strip_cwd_prefix(|| !(opts.null_separator || has_command)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -325,18 +335,22 @@ fn extract_command(opts: &mut Opts, colored_output: bool) -> Result<Option<Comma
|
||||
if !opts.list_details {
|
||||
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());
|
||||
Some(res)
|
||||
})
|
||||
.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)]
|
||||
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
|
||||
// platforms (like BusyBox).
|
||||
vec![
|
||||
@@ -414,10 +428,9 @@ fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&st
|
||||
}
|
||||
|
||||
fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
|
||||
let now = time::SystemTime::now();
|
||||
let mut time_constraints: Vec<TimeFilter> = Vec::new();
|
||||
if let Some(ref t) = opts.changed_within {
|
||||
if let Some(f) = TimeFilter::after(&now, t) {
|
||||
if let Some(f) = TimeFilter::after(t) {
|
||||
time_constraints.push(f);
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
@@ -427,7 +440,7 @@ fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
|
||||
}
|
||||
}
|
||||
if let Some(ref t) = opts.changed_before {
|
||||
if let Some(f) = TimeFilter::before(&now, t) {
|
||||
if let Some(f) = TimeFilter::before(t) {
|
||||
time_constraints.push(f);
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
|
||||
+43
-28
@@ -5,29 +5,39 @@ use lscolors::{Indicator, LsColors, Style};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::dir_entry::DirEntry;
|
||||
use crate::error::print_error;
|
||||
use crate::exit_codes::ExitCode;
|
||||
use crate::fmt::FormatTemplate;
|
||||
use crate::hyperlink::PathUrl;
|
||||
|
||||
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
|
||||
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
|
||||
}
|
||||
|
||||
// TODO: this function is performance critical and can probably be optimized
|
||||
pub fn print_entry<W: Write>(stdout: &mut W, entry: &DirEntry, config: &Config) {
|
||||
let r = if let Some(ref ls_colors) = config.ls_colors {
|
||||
print_entry_colorized(stdout, entry, config, ls_colors)
|
||||
pub fn print_entry<W: Write>(stdout: &mut W, entry: &DirEntry, config: &Config) -> io::Result<()> {
|
||||
let mut has_hyperlink = false;
|
||||
if config.hyperlink {
|
||||
if let Some(url) = PathUrl::new(entry.path()) {
|
||||
write!(stdout, "\x1B]8;;{url}\x1B\\")?;
|
||||
has_hyperlink = true;
|
||||
}
|
||||
}
|
||||
|
||||
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)?;
|
||||
} else {
|
||||
print_entry_uncolorized(stdout, entry, config)
|
||||
print_entry_uncolorized(stdout, entry, config)?;
|
||||
};
|
||||
|
||||
if let Err(e) = r {
|
||||
if e.kind() == ::std::io::ErrorKind::BrokenPipe {
|
||||
// Exit gracefully in case of a broken pipe (e.g. 'fd ... | head -n 3').
|
||||
ExitCode::Success.exit();
|
||||
} else {
|
||||
print_error(format!("Could not write to output: {}", e));
|
||||
ExitCode::GeneralError.exit();
|
||||
}
|
||||
if has_hyperlink {
|
||||
write!(stdout, "\x1B]8;;\x1B\\")?;
|
||||
}
|
||||
|
||||
if config.null_separator {
|
||||
write!(stdout, "\0")
|
||||
} else {
|
||||
writeln!(stdout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +51,7 @@ fn print_trailing_slash<W: Write>(
|
||||
config: &Config,
|
||||
style: Option<&Style>,
|
||||
) -> io::Result<()> {
|
||||
if entry.file_type().map_or(false, |ft| ft.is_dir()) {
|
||||
if entry.file_type().is_some_and(|ft| ft.is_dir()) {
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
@@ -54,6 +64,21 @@ fn print_trailing_slash<W: Write>(
|
||||
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 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())
|
||||
}
|
||||
|
||||
// TODO: this function is performance critical and can probably be optimized
|
||||
fn print_entry_colorized<W: Write>(
|
||||
stdout: &mut W,
|
||||
@@ -103,12 +128,6 @@ fn print_entry_colorized<W: Write>(
|
||||
ls_colors.style_for_indicator(Indicator::Directory),
|
||||
)?;
|
||||
|
||||
if config.null_separator {
|
||||
write!(stdout, "\0")?;
|
||||
} else {
|
||||
writeln!(stdout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -118,16 +137,14 @@ fn print_entry_uncolorized_base<W: Write>(
|
||||
entry: &DirEntry,
|
||||
config: &Config,
|
||||
) -> io::Result<()> {
|
||||
let separator = if config.null_separator { "\0" } else { "\n" };
|
||||
let path = entry.stripped_path(config);
|
||||
|
||||
let mut path_string = path.to_string_lossy();
|
||||
if let Some(ref separator) = config.path_separator {
|
||||
*path_string.to_mut() = replace_path_separator(&path_string, separator);
|
||||
}
|
||||
write!(stdout, "{}", path_string)?;
|
||||
print_trailing_slash(stdout, entry, config, None)?;
|
||||
write!(stdout, "{}", separator)
|
||||
write!(stdout, "{path_string}")?;
|
||||
print_trailing_slash(stdout, entry, config, None)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
@@ -152,9 +169,7 @@ fn print_entry_uncolorized<W: Write>(
|
||||
print_entry_uncolorized_base(stdout, entry, config)
|
||||
} else {
|
||||
// Print path as raw bytes, allowing invalid UTF-8 filenames to be passed to other processes
|
||||
let separator = if config.null_separator { b"\0" } else { b"\n" };
|
||||
stdout.write_all(entry.stripped_path(config).as_os_str().as_bytes())?;
|
||||
print_trailing_slash(stdout, entry, config, None)?;
|
||||
stdout.write_all(separator)
|
||||
print_trailing_slash(stdout, entry, config, None)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ fn hir_has_uppercase_char(hir: &Hir) -> bool {
|
||||
use regex_syntax::hir::*;
|
||||
|
||||
match hir.kind() {
|
||||
HirKind::Literal(Literal(bytes)) => match std::str::from_utf8(&bytes) {
|
||||
HirKind::Literal(Literal(bytes)) => match std::str::from_utf8(bytes) {
|
||||
Ok(s) => s.chars().any(|c| c.is_uppercase()),
|
||||
Err(_) => bytes.iter().any(|b| char::from(*b).is_uppercase()),
|
||||
},
|
||||
@@ -67,7 +67,7 @@ fn hir_matches_strings_with_leading_dot(hir: &Hir) -> bool {
|
||||
|
||||
if let Some(hir) = hirs.next() {
|
||||
match hir.kind() {
|
||||
HirKind::Literal(Literal(bytes)) => bytes.starts_with(&[b'.']),
|
||||
HirKind::Literal(Literal(bytes)) => bytes.starts_with(b"."),
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
|
||||
+12
-14
@@ -12,7 +12,7 @@ use anyhow::{anyhow, Result};
|
||||
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, SendError, Sender};
|
||||
use etcetera::BaseStrategy;
|
||||
use ignore::overrides::{Override, OverrideBuilder};
|
||||
use ignore::{self, WalkBuilder, WalkParallel, WalkState};
|
||||
use ignore::{WalkBuilder, WalkParallel, WalkState};
|
||||
use regex::bytes::Regex;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -250,7 +250,12 @@ impl<'a, W: Write> ReceiverBuffer<'a, W> {
|
||||
|
||||
/// Output a path.
|
||||
fn print(&mut self, entry: &DirEntry) -> Result<(), ExitCode> {
|
||||
output::print_entry(&mut self.stdout, entry, &self.config);
|
||||
if let Err(e) = output::print_entry(&mut self.stdout, entry, self.config) {
|
||||
if e.kind() != ::std::io::ErrorKind::BrokenPipe {
|
||||
print_error(format!("Could not write to output: {e}"));
|
||||
return Err(ExitCode::GeneralError);
|
||||
}
|
||||
}
|
||||
|
||||
if self.interrupt_flag.load(Ordering::Relaxed) {
|
||||
// Ignore any errors on flush, because we're about to exit anyway
|
||||
@@ -334,10 +339,6 @@ impl WorkerState {
|
||||
.map_err(|e| anyhow!("Malformed exclude pattern: {}", e))?;
|
||||
}
|
||||
|
||||
if config.read_vcsignore {
|
||||
builder.add("!.git/").expect("Invalid exclude pattern");
|
||||
}
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map_err(|_| anyhow!("Mismatch in exclude patterns"))
|
||||
@@ -375,10 +376,7 @@ impl WorkerState {
|
||||
match result {
|
||||
Some(ignore::Error::Partial(_)) => (),
|
||||
Some(err) => {
|
||||
print_error(format!(
|
||||
"Malformed pattern in global ignore file. {}.",
|
||||
err
|
||||
));
|
||||
print_error(format!("Malformed pattern in global ignore file. {err}."));
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
@@ -391,7 +389,7 @@ impl WorkerState {
|
||||
match result {
|
||||
Some(ignore::Error::Partial(_)) => (),
|
||||
Some(err) => {
|
||||
print_error(format!("Malformed pattern in custom ignore file. {}.", err));
|
||||
print_error(format!("Malformed pattern in custom ignore file. {err}."));
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
@@ -413,7 +411,7 @@ impl WorkerState {
|
||||
// This will be set to `Some` if the `--exec` argument was supplied.
|
||||
if let Some(ref cmd) = config.command {
|
||||
if cmd.in_batch_mode() {
|
||||
exec::batch(rx.into_iter().flatten(), cmd, &config)
|
||||
exec::batch(rx.into_iter().flatten(), cmd, config)
|
||||
} else {
|
||||
let out_perm = Mutex::new(());
|
||||
|
||||
@@ -426,7 +424,7 @@ impl WorkerState {
|
||||
|
||||
// Spawn a job thread that will listen for and execute inputs.
|
||||
let handle = scope
|
||||
.spawn(|| exec::job(rx.into_iter().flatten(), cmd, &out_perm, &config));
|
||||
.spawn(|| exec::job(rx.into_iter().flatten(), cmd, &out_perm, config));
|
||||
|
||||
// Push the handle of the spawned thread into the vector for later joining.
|
||||
handles.push(handle);
|
||||
@@ -479,7 +477,7 @@ impl WorkerState {
|
||||
&& path
|
||||
.symlink_metadata()
|
||||
.ok()
|
||||
.map_or(false, |m| m.file_type().is_symlink()) =>
|
||||
.is_some_and(|m| m.file_type().is_symlink()) =>
|
||||
{
|
||||
DirEntry::broken_symlink(path)
|
||||
}
|
||||
|
||||
@@ -104,9 +104,9 @@ fn format_output_error(args: &[&str], expected: &str, actual: &str) -> String {
|
||||
let diff_text = diff::lines(expected, actual)
|
||||
.into_iter()
|
||||
.map(|diff| match diff {
|
||||
diff::Result::Left(l) => format!("-{}", l),
|
||||
diff::Result::Both(l, _) => format!(" {}", l),
|
||||
diff::Result::Right(r) => format!("+{}", r),
|
||||
diff::Result::Left(l) => format!("-{l}"),
|
||||
diff::Result::Both(l, _) => format!(" {l}"),
|
||||
diff::Result::Right(r) => format!("+{r}"),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
@@ -129,7 +129,7 @@ fn normalize_output(s: &str, trim_start: bool, normalize_line: bool) -> String {
|
||||
.lines()
|
||||
.map(|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 {
|
||||
let mut words: Vec<_> = line.split_whitespace().collect();
|
||||
words.sort_unstable();
|
||||
@@ -290,7 +290,7 @@ impl TestEnv {
|
||||
pub fn assert_failure_with_error(&self, args: &[&str], expected: &str) {
|
||||
let status = self.assert_error_subdirectory(".", args, Some(expected));
|
||||
if status.success() {
|
||||
panic!("error '{}' did not occur.", expected);
|
||||
panic!("error '{expected}' did not occur.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,9 @@ impl TestEnv {
|
||||
} else {
|
||||
cmd.arg("--no-global-ignore-file");
|
||||
}
|
||||
// Make sure LS_COLORS is unset to ensure consistent
|
||||
// color output
|
||||
cmd.env("LS_COLORS", "");
|
||||
cmd.args(args);
|
||||
|
||||
// Run *fd*.
|
||||
|
||||
+115
-13
@@ -8,6 +8,7 @@ use std::path::Path;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use test_case::test_case;
|
||||
|
||||
use jiff::Timestamp;
|
||||
use normpath::PathExt;
|
||||
use regex::escape;
|
||||
|
||||
@@ -595,10 +596,7 @@ fn test_full_path() {
|
||||
let prefix = escape(&root.to_string_lossy());
|
||||
|
||||
te.assert_output(
|
||||
&[
|
||||
"--full-path",
|
||||
&format!("^{prefix}.*three.*foo$", prefix = prefix),
|
||||
],
|
||||
&["--full-path", &format!("^{prefix}.*three.*foo$")],
|
||||
"one/two/three/d.foo
|
||||
one/two/three/directory_foo/",
|
||||
);
|
||||
@@ -1311,7 +1309,8 @@ fn test_type_executable() {
|
||||
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||
|
||||
fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.create_new(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.mode(0o777)
|
||||
.open(te.test_root().join("executable-file.sh"))
|
||||
@@ -1319,6 +1318,7 @@ fn test_type_executable() {
|
||||
|
||||
fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.mode(0o645)
|
||||
.open(te.test_root().join("not-user-executable-file.sh"))
|
||||
@@ -1516,7 +1516,7 @@ fn test_symlink_as_absolute_root() {
|
||||
let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES);
|
||||
|
||||
te.assert_output(
|
||||
&["", &format!("{abs_path}/symlink", abs_path = abs_path)],
|
||||
&["", &format!("{abs_path}/symlink")],
|
||||
&format!(
|
||||
"{abs_path}/symlink/c.foo
|
||||
{abs_path}/symlink/C.Foo2
|
||||
@@ -1541,7 +1541,7 @@ fn test_symlink_and_full_path() {
|
||||
&[
|
||||
"--absolute-path",
|
||||
"--full-path",
|
||||
&format!("^{prefix}.*three", prefix = prefix),
|
||||
&format!("^{prefix}.*three"),
|
||||
],
|
||||
&format!(
|
||||
"{abs_path}/{expected_path}/three/
|
||||
@@ -1561,8 +1561,8 @@ fn test_symlink_and_full_path_abs_path() {
|
||||
te.assert_output(
|
||||
&[
|
||||
"--full-path",
|
||||
&format!("^{prefix}.*symlink.*three", prefix = prefix),
|
||||
&format!("{abs_path}/symlink", abs_path = abs_path),
|
||||
&format!("^{prefix}.*symlink.*three"),
|
||||
&format!("{abs_path}/symlink"),
|
||||
],
|
||||
&format!(
|
||||
"{abs_path}/symlink/three/
|
||||
@@ -1622,6 +1622,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)
|
||||
#[test]
|
||||
fn test_exec() {
|
||||
@@ -2229,7 +2289,10 @@ fn test_modified_relative() {
|
||||
|
||||
#[cfg(test)]
|
||||
fn change_file_modified<P: AsRef<Path>>(path: P, iso_date: &str) {
|
||||
let st = humantime::parse_rfc3339(iso_date).expect("invalid date");
|
||||
let st = iso_date
|
||||
.parse::<Timestamp>()
|
||||
.map(SystemTime::from)
|
||||
.expect("invalid date");
|
||||
let ft = filetime::FileTime::from_system_time(st);
|
||||
filetime::set_file_times(path, ft, ft).expect("time modification failde");
|
||||
}
|
||||
@@ -2275,7 +2338,7 @@ fn test_owner_current_user() {
|
||||
fn test_owner_current_group() {
|
||||
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||
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 Ok(Some(group)) = Group::from_gid(gid) {
|
||||
te.assert_output(&["--owner", &format!(":{}", group.name), "a.foo"], "a.foo");
|
||||
}
|
||||
@@ -2465,6 +2528,7 @@ fn test_number_parsing_errors() {
|
||||
te.assert_failure(&["--threads=0"]);
|
||||
|
||||
te.assert_failure(&["--min-depth=a"]);
|
||||
te.assert_failure(&["--mindepth=a"]);
|
||||
te.assert_failure(&["--max-depth=a"]);
|
||||
te.assert_failure(&["--maxdepth=a"]);
|
||||
te.assert_failure(&["--exact-depth=a"]);
|
||||
@@ -2553,7 +2617,7 @@ fn test_invalid_cwd() {
|
||||
.unwrap();
|
||||
|
||||
if !output.status.success() {
|
||||
panic!("{:?}", output);
|
||||
panic!("{output:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2571,7 +2635,14 @@ fn test_git_dir() {
|
||||
],
|
||||
);
|
||||
|
||||
te.assert_output(&["--hidden", "foo"], "");
|
||||
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"],
|
||||
@@ -2590,3 +2661,34 @@ fn test_git_dir() {
|
||||
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", "."], "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hyperlink() {
|
||||
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||
|
||||
#[cfg(unix)]
|
||||
let hostname = nix::unistd::gethostname().unwrap().into_string().unwrap();
|
||||
#[cfg(not(unix))]
|
||||
let hostname = "";
|
||||
|
||||
let expected = format!(
|
||||
"\x1b]8;;file://{}{}/a.foo\x1b\\a.foo\x1b]8;;\x1b\\",
|
||||
hostname,
|
||||
get_absolute_root_path(&te),
|
||||
);
|
||||
|
||||
te.assert_output(&["--hyperlink=always", "a.foo"], &expected);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user