mirror of
https://github.com/sharkdp/fd.git
synced 2024-09-27 20:41:30 +02:00
Compare commits
No commits in common. "master" and "v8.3.2" have entirely different histories.
1
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -9,7 +9,6 @@ body:
|
||||
Please check out the [troubleshooting section](https://github.com/sharkdp/fd#troubleshooting) first.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checks
|
||||
options:
|
||||
- label: I have read the troubleshooting section and still think this is a bug.
|
||||
required: true
|
||||
|
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@ -4,7 +4,3 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
347
.github/workflows/CICD.yml
vendored
347
.github/workflows/CICD.yml
vendored
@ -1,8 +1,8 @@
|
||||
name: CICD
|
||||
|
||||
env:
|
||||
MIN_SUPPORTED_RUST_VERSION: "1.54.0"
|
||||
CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||
MSRV_FEATURES: "--all-features"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@ -14,90 +14,79 @@ on:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
crate_metadata:
|
||||
name: Extract crate metadata
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Extract crate information
|
||||
id: crate_metadata
|
||||
run: |
|
||||
echo "name=fd" | tee -a $GITHUB_OUTPUT
|
||||
cargo metadata --no-deps --format-version 1 | jq -r '"version=" + .packages[0].version' | tee -a $GITHUB_OUTPUT
|
||||
cargo metadata --no-deps --format-version 1 | jq -r '"maintainer=" + .packages[0].authors[0]' | tee -a $GITHUB_OUTPUT
|
||||
cargo metadata --no-deps --format-version 1 | jq -r '"homepage=" + .packages[0].homepage' | tee -a $GITHUB_OUTPUT
|
||||
cargo metadata --no-deps --format-version 1 | jq -r '"msrv=" + .packages[0].rust_version' | tee -a $GITHUB_OUTPUT
|
||||
outputs:
|
||||
name: ${{ steps.crate_metadata.outputs.name }}
|
||||
version: ${{ steps.crate_metadata.outputs.version }}
|
||||
maintainer: ${{ steps.crate_metadata.outputs.maintainer }}
|
||||
homepage: ${{ steps.crate_metadata.outputs.homepage }}
|
||||
msrv: ${{ steps.crate_metadata.outputs.msrv }}
|
||||
|
||||
ensure_cargo_fmt:
|
||||
name: Ensure 'cargo fmt' has been run
|
||||
code_quality:
|
||||
name: Code quality
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
components: rustfmt
|
||||
- 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
|
||||
toolchain: stable
|
||||
default: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
components: clippy, rustfmt
|
||||
- name: Ensure `cargo fmt` has been run
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
components: clippy
|
||||
- uses: actions/checkout@v4
|
||||
- run: cargo clippy --all-targets --all-features -- -Dwarnings
|
||||
command: fmt
|
||||
args: -- --check
|
||||
- name: Ensure MSRV is set in `clippy.toml`
|
||||
run: grep "^msrv = \"${{ env.MIN_SUPPORTED_RUST_VERSION }}\"\$" clippy.toml
|
||||
- name: Run clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --locked --all-targets --all-features
|
||||
|
||||
min_version:
|
||||
name: Minimum supported rust version
|
||||
runs-on: ubuntu-20.04
|
||||
needs: crate_metadata
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust toolchain (v${{ needs.crate_metadata.outputs.msrv }})
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
- name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ needs.crate_metadata.outputs.msrv }}
|
||||
toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }}
|
||||
default: true
|
||||
components: clippy
|
||||
- name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
|
||||
run: cargo clippy --locked --all-targets ${{ env.MSRV_FEATURES }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --locked --all-targets --all-features
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
- name: Run tests
|
||||
run: cargo test --locked ${{ env.MSRV_FEATURES }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --locked
|
||||
|
||||
build:
|
||||
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
||||
name: ${{ matrix.job.os }} (${{ matrix.job.target }})
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
needs: crate_metadata
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { 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-12 }
|
||||
- { 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
|
||||
- { os: ubuntu-20.04, target: arm-unknown-linux-gnueabihf , use-cross: true }
|
||||
- { os: ubuntu-20.04, target: arm-unknown-linux-musleabihf, use-cross: true }
|
||||
- { os: ubuntu-20.04, target: aarch64-unknown-linux-gnu , use-cross: true }
|
||||
- { os: ubuntu-20.04, target: i686-unknown-linux-gnu , use-cross: true }
|
||||
- { os: ubuntu-20.04, target: i686-unknown-linux-musl , use-cross: true }
|
||||
- { os: ubuntu-20.04, target: x86_64-unknown-linux-gnu }
|
||||
- { os: ubuntu-20.04, target: x86_64-unknown-linux-musl , use-cross: true }
|
||||
- { os: macos-10.15 , target: x86_64-apple-darwin }
|
||||
# - { os: windows-2019, target: i686-pc-windows-gnu } ## disabled; error: linker `i686-w64-mingw32-gcc` not found
|
||||
- { os: windows-2019, target: i686-pc-windows-msvc }
|
||||
- { os: windows-2019, target: x86_64-pc-windows-gnu }
|
||||
- { os: windows-2019, target: x86_64-pc-windows-msvc }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install prerequisites
|
||||
shell: bash
|
||||
@ -107,24 +96,21 @@ jobs:
|
||||
aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
|
||||
esac
|
||||
|
||||
- name: Install Rust toolchain
|
||||
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
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cross
|
||||
|
||||
- name: Overwrite build command env variable
|
||||
if: matrix.job.use-cross
|
||||
- name: Extract crate information
|
||||
shell: bash
|
||||
run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
|
||||
run: |
|
||||
echo "PROJECT_NAME=fd" >> $GITHUB_ENV
|
||||
echo "PROJECT_VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV
|
||||
echo "PROJECT_MAINTAINER=$(sed -n 's/^authors = \["\(.*\)"\]/\1/p' Cargo.toml)" >> $GITHUB_ENV
|
||||
echo "PROJECT_HOMEPAGE=$(sed -n 's/^homepage = "\(.*\)"/\1/p' Cargo.toml)" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- name: Show version information (Rust, cargo, GCC)
|
||||
shell: bash
|
||||
@ -137,11 +123,14 @@ jobs:
|
||||
rustc -V
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: $BUILD_CMD build --locked --release --target=${{ matrix.job.target }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.job.use-cross }}
|
||||
command: build
|
||||
args: --locked --release --target=${{ matrix.job.target }}
|
||||
|
||||
- name: Set binary name & path
|
||||
id: bin
|
||||
- name: Strip debug information from executable
|
||||
id: strip
|
||||
shell: bash
|
||||
run: |
|
||||
# Figure out suffix of binary
|
||||
@ -150,13 +139,31 @@ jobs:
|
||||
*-pc-windows-*) EXE_suffix=".exe" ;;
|
||||
esac;
|
||||
|
||||
# Setup paths
|
||||
BIN_NAME="${{ needs.crate_metadata.outputs.name }}${EXE_suffix}"
|
||||
BIN_PATH="target/${{ matrix.job.target }}/release/${BIN_NAME}"
|
||||
# Figure out what strip tool to use if any
|
||||
STRIP="strip"
|
||||
case ${{ matrix.job.target }} in
|
||||
arm-unknown-linux-*) STRIP="arm-linux-gnueabihf-strip" ;;
|
||||
aarch64-unknown-linux-gnu) STRIP="aarch64-linux-gnu-strip" ;;
|
||||
*-pc-windows-msvc) STRIP="" ;;
|
||||
esac;
|
||||
|
||||
# Let subsequent steps know where to find the binary
|
||||
echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT
|
||||
echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT
|
||||
# Setup paths
|
||||
BIN_DIR="${{ env.CICD_INTERMEDIATES_DIR }}/stripped-release-bin/"
|
||||
mkdir -p "${BIN_DIR}"
|
||||
BIN_NAME="${{ env.PROJECT_NAME }}${EXE_suffix}"
|
||||
BIN_PATH="${BIN_DIR}/${BIN_NAME}"
|
||||
|
||||
# Copy the release build binary to the result location
|
||||
cp "target/${{ matrix.job.target }}/release/${BIN_NAME}" "${BIN_DIR}"
|
||||
|
||||
# Also strip if possible
|
||||
if [ -n "${STRIP}" ]; then
|
||||
"${STRIP}" "${BIN_PATH}"
|
||||
fi
|
||||
|
||||
# Let subsequent steps know where to find the (stripped) bin
|
||||
echo ::set-output name=BIN_PATH::${BIN_PATH}
|
||||
echo ::set-output name=BIN_NAME::${BIN_NAME}
|
||||
|
||||
- name: Set testing options
|
||||
id: test-options
|
||||
@ -164,42 +171,44 @@ jobs:
|
||||
run: |
|
||||
# test only library unit tests and binary for arm-type targets
|
||||
unset CARGO_TEST_OPTIONS
|
||||
unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--bin ${{ needs.crate_metadata.outputs.name }}" ;; esac;
|
||||
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
|
||||
unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--bin ${PROJECT_NAME}" ;; esac;
|
||||
echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS}
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: $BUILD_CMD test --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
|
||||
|
||||
- name: Generate completions
|
||||
id: completions
|
||||
shell: bash
|
||||
run: make completions
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.job.use-cross }}
|
||||
command: test
|
||||
args: --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
|
||||
|
||||
- name: Create tarball
|
||||
id: package
|
||||
shell: bash
|
||||
run: |
|
||||
PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac;
|
||||
PKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-v${{ needs.crate_metadata.outputs.version }}-${{ matrix.job.target }}
|
||||
PKG_BASENAME=${PROJECT_NAME}-v${PROJECT_VERSION}-${{ matrix.job.target }}
|
||||
PKG_NAME=${PKG_BASENAME}${PKG_suffix}
|
||||
echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT
|
||||
echo ::set-output name=PKG_NAME::${PKG_NAME}
|
||||
|
||||
PKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/package"
|
||||
ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/"
|
||||
mkdir -p "${ARCHIVE_DIR}"
|
||||
mkdir -p "${ARCHIVE_DIR}/autocomplete"
|
||||
|
||||
# Binary
|
||||
cp "${{ steps.bin.outputs.BIN_PATH }}" "$ARCHIVE_DIR"
|
||||
cp "${{ steps.strip.outputs.BIN_PATH }}" "$ARCHIVE_DIR"
|
||||
|
||||
# Man page
|
||||
cp 'doc/${{ env.PROJECT_NAME }}.1' "$ARCHIVE_DIR"
|
||||
|
||||
# README, LICENSE and CHANGELOG files
|
||||
cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR"
|
||||
|
||||
# Man page
|
||||
cp 'doc/${{ needs.crate_metadata.outputs.name }}.1' "$ARCHIVE_DIR"
|
||||
|
||||
# Autocompletion files
|
||||
cp -r autocomplete "${ARCHIVE_DIR}"
|
||||
cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'${{ env.PROJECT_NAME }}.bash' "$ARCHIVE_DIR/autocomplete/"
|
||||
cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'${{ env.PROJECT_NAME }}.fish' "$ARCHIVE_DIR/autocomplete/"
|
||||
cp 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'_${{ env.PROJECT_NAME }}.ps1' "$ARCHIVE_DIR/autocomplete/"
|
||||
cp 'contrib/completion/_fd' "$ARCHIVE_DIR/autocomplete/"
|
||||
|
||||
# base compressed package
|
||||
pushd "${PKG_STAGING}/" >/dev/null
|
||||
@ -210,17 +219,119 @@ jobs:
|
||||
popd >/dev/null
|
||||
|
||||
# Let subsequent steps know where to find the compressed package
|
||||
echo "PKG_PATH=${PKG_STAGING}/${PKG_NAME}" >> $GITHUB_OUTPUT
|
||||
echo ::set-output name=PKG_PATH::"${PKG_STAGING}/${PKG_NAME}"
|
||||
|
||||
- name: Create Debian package
|
||||
id: debian-package
|
||||
shell: bash
|
||||
if: startsWith(matrix.job.os, 'ubuntu')
|
||||
run: bash scripts/create-deb.sh
|
||||
env:
|
||||
TARGET: ${{ matrix.job.target }}
|
||||
DPKG_VERSION: ${{ needs.crate_metadata.version }}
|
||||
BIN_PATH: ${{ steps.bin.outputs.BIN_PATH }}
|
||||
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=${PROJECT_NAME}
|
||||
DPKG_CONFLICTS=${PROJECT_NAME}-musl
|
||||
case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${PROJECT_NAME}-musl ; DPKG_CONFLICTS=${PROJECT_NAME} ;; esac;
|
||||
DPKG_VERSION=${PROJECT_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 ::set-output name=DPKG_NAME::${DPKG_NAME}
|
||||
|
||||
# Binary
|
||||
install -Dm755 "${{ steps.strip.outputs.BIN_PATH }}" "${DPKG_DIR}/usr/bin/${{ steps.strip.outputs.BIN_NAME }}"
|
||||
|
||||
# Man page
|
||||
install -Dm644 'doc/${{ env.PROJECT_NAME }}.1' "${DPKG_DIR}/usr/share/man/man1/${{ env.PROJECT_NAME }}.1"
|
||||
gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ env.PROJECT_NAME }}.1"
|
||||
|
||||
# Autocompletion files
|
||||
install -Dm644 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'${{ env.PROJECT_NAME }}.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/${{ env.PROJECT_NAME }}"
|
||||
install -Dm644 'target/${{ matrix.job.target }}/release/build/${{ env.PROJECT_NAME }}'*/out/'${{ env.PROJECT_NAME }}.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/${{ env.PROJECT_NAME }}.fish"
|
||||
install -Dm644 'contrib/completion/_fd' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ env.PROJECT_NAME }}"
|
||||
|
||||
# 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: ${{ env.PROJECT_NAME }}
|
||||
Source: ${{ env.PROJECT_HOMEPAGE }}
|
||||
|
||||
Files: *
|
||||
Copyright: ${{ env.PROJECT_MAINTAINER }}
|
||||
Copyright: $COPYRIGHT_YEARS ${{ env.PROJECT_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: ${{ env.PROJECT_MAINTAINER }}
|
||||
Homepage: ${{ env.PROJECT_HOMEPAGE }}
|
||||
Architecture: ${DPKG_ARCH}
|
||||
Provides: ${{ env.PROJECT_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 ::set-output name=DPKG_PATH::${DPKG_PATH}
|
||||
|
||||
# build dpkg
|
||||
fakeroot dpkg-deb --build "${DPKG_DIR}" "${DPKG_PATH}"
|
||||
|
||||
- name: "Artifact upload: tarball"
|
||||
uses: actions/upload-artifact@master
|
||||
@ -240,10 +351,10 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
unset IS_RELEASE ; if [[ $GITHUB_REF =~ ^refs/tags/v[0-9].* ]]; then IS_RELEASE='true' ; fi
|
||||
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
|
||||
echo ::set-output name=IS_RELEASE::${IS_RELEASE}
|
||||
|
||||
- name: Publish archives and packages
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: steps.is-release.outputs.IS_RELEASE
|
||||
with:
|
||||
files: |
|
||||
@ -251,15 +362,3 @@ jobs:
|
||||
${{ steps.debian-package.outputs.DPKG_PATH }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
winget:
|
||||
name: Publish to Winget
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: sharkdp.fd
|
||||
installers-regex: '-pc-windows-msvc\.zip$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
||||
target/
|
||||
/autocomplete/
|
||||
**/*.rs.bk
|
||||
|
208
CHANGELOG.md
208
CHANGELOG.md
@ -1,219 +1,19 @@
|
||||
# 10.2.0
|
||||
# Upcoming release
|
||||
|
||||
## Performance improvements
|
||||
|
||||
|
||||
## 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, #1362) - @tavianator.
|
||||
[Benchmarks results](https://gist.github.com/tavianator/32edbe052f33ef60570cf5456b59de81) show gains
|
||||
of 6-8x for full traversals of smaller directories (100k files) and up to 13x for larger directories (1M files).
|
||||
|
||||
- The default number of threads is now constrained to be at most 64. This should improve startup time on
|
||||
systems with many CPU cores. (#1203, #1410, #1412, #1431) - @tmccombs and @tavianator
|
||||
|
||||
- New flushing behavior when writing output to stdout, providing better performance for TTY and non-TTY
|
||||
use cases, see #1452 and #1313 (@tavianator).
|
||||
|
||||
## Features
|
||||
|
||||
- Support character and block device file types, see #1213 and #1336 (@cgzones)
|
||||
- Breaking: `.git/` is now ignored by default when using `--hidden` / `-H`, use `--no-ignore` / `-I` or
|
||||
`--no-ignore-vcs` to override, see #1387 and #1396 (@skoriop)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix `NO_COLOR` support, see #1421 (@acuteenvy)
|
||||
|
||||
## Other
|
||||
|
||||
- Fixed documentation typos, see #1409 (@marcospb19)
|
||||
|
||||
## Thanks
|
||||
|
||||
Special thanks to @tavianator for his incredible work on performance in the `ignore` crate and `fd` itself.
|
||||
|
||||
|
||||
|
||||
# v8.7.1
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- `-1` properly conflicts with the exec family of options.
|
||||
- `--max-results` overrides `-1`
|
||||
- `--quiet` properly conflicts with the exec family of options. This used to be the case, but broke during the switch to clap-derive
|
||||
- `--changed-within` now accepts a space as well as a "T" as the separator between date and time (due to update of chrono dependency)
|
||||
|
||||
## Other
|
||||
- Many dependencies were updated
|
||||
- Some documentation was updated and fixed
|
||||
|
||||
# v8.7.0
|
||||
|
||||
## Features
|
||||
|
||||
- Add flag --no-require-git to always respect gitignore files, see #1216 (@vegerot)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix logic for when to use global ignore file. There was a bug where the only case where the
|
||||
global ignore file wasn't processed was if `--no-ignore` was passed, but neither `--unrestricted`
|
||||
nor `--no-global-ignore-file` is passed. See #1209
|
||||
|
||||
# v8.6.0
|
||||
|
||||
## Features
|
||||
|
||||
- New `--and <pattern>` option to add additional patterns that must also be matched. See #315
|
||||
and #1139 (@Uthar)
|
||||
- Added `--changed-after` as alias for `--changed-within`, to have a name consistent with `--changed-before`.
|
||||
|
||||
|
||||
## Changes
|
||||
|
||||
- Breaking: On Unix-like systems, `--type executable` now additionally checks if
|
||||
the file is executable by the current user, see #1106 and #1169 (@ptipiak)
|
||||
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Use fd instead of fd.exe for Powershell completions (when completions are generated on windows)
|
||||
|
||||
|
||||
## Other
|
||||
|
||||
|
||||
# v8.5.3
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix completion generation to not include full path of fd command
|
||||
- Fix build error if completions feature is disabled
|
||||
|
||||
# v8.5.2
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix --owner option value parsing, see #1163 and #1164 (@tmccombs)
|
||||
|
||||
|
||||
# v8.5.1
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix --threads/-j option value parsing, see #1160 and #1162 (@sharkdp)
|
||||
|
||||
|
||||
# v8.5.0
|
||||
|
||||
## Features
|
||||
|
||||
- `--type executable`/`-t` now works on Windows, see #1051 and #1061 (@tavianator)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fixed differences between piped / non-piped output. This changes `fd`s behavior back to what we
|
||||
had before 8.3.0, i.e. there will be no leading `./` prefixes, unless `--exec`/`-x`,
|
||||
`--exec-batch`/`-X`, or `--print0`/`-0` are used. `--strip-cwd-prefix` can be used to strip that
|
||||
prefix in those cases. See #1046, #1115, and #1121 (@tavianator)
|
||||
- `fd` could previously crash with a panic due to a race condition in Rusts standard library
|
||||
(see https://github.com/rust-lang/rust/issues/39364). This has been fixed by switching to a different
|
||||
message passing implementation, see #1060 and #1146 (@tavianator)
|
||||
- `fd`s memory usage will not grow unboundedly on huge directory trees, see #1146 (@tavianator)
|
||||
- fd returns an error when current working directory does not exist while a search path is
|
||||
specified, see #1072 (@vijfhoek)
|
||||
- Improved "command not found" error message, see #1083 and #1109 (@themkat)
|
||||
- Preserve command exit codes when using `--exec-batch`, see #1136 and #1137 (@amesgen)
|
||||
|
||||
## Changes
|
||||
|
||||
- No leading `./` prefix for non-interactive results, see above.
|
||||
- fd now colorizes paths in parallel, significantly improving performance, see #1148 (@tavianator)
|
||||
- fd can now avoid `stat` syscalls even when colorizing paths, as long as the color scheme doesn't
|
||||
require metadata, see #1148 (@tavianator)
|
||||
- The statically linked `musl` versions of `fd` now use `jmalloc`, leading to a significant performance
|
||||
improvement, see #1062 (@tavianator)
|
||||
|
||||
## Other
|
||||
|
||||
- Added link back to GitHub in man page and `--help` text, see #1086 (@scottchiefbaker)
|
||||
- Major update in how `fd` handles command line options internally, see #1067 (@tmccombs)
|
||||
|
||||
# v8.4.0
|
||||
|
||||
## Features
|
||||
|
||||
- Support multiple `--exec <cmd>` instances, see #406 and #960 (@tmccombs)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- "Argument list too long" errors can not appear anymore when using `--exec-batch`/`-X`, as the command invocations are automatically batched at the maximum possible size, even if `--batch-size` is not given. See #410 and #1020 (@tavianator)
|
||||
|
||||
## Changes
|
||||
|
||||
- Directories are now printed with an additional path separator at the end: `foo/bar/`, see #436 and #812 (@yyogo)
|
||||
- The `-u` flag was changed to be equivalent to `-HI` (previously, a single `-u` was only equivalent to `-I`). Additional `-u` flags are still allowed, but ignored. See #840 and #986 (@jacksontheel)
|
||||
|
||||
## Other
|
||||
|
||||
- Added installation instructions for RHEL8, see #989 (@ethsol)
|
||||
|
||||
|
||||
# v8.3.2
|
||||
|
||||
|
@ -13,11 +13,11 @@ give us the chance to discuss any potential changes first.
|
||||
## Add an entry to the changelog
|
||||
|
||||
If your contribution changes the behavior of `fd` (as opposed to a typo-fix
|
||||
in the documentation), please update the [`CHANGELOG.md`](CHANGELOG.md#upcoming-release) file
|
||||
in the documentation), please update the [`CHANGELOG.md`](CHANGELOG.md) file
|
||||
and describe your changes. This makes the release process much easier and
|
||||
therefore helps to get your changes into a new `fd` release faster.
|
||||
|
||||
The top of the `CHANGELOG` contains an *"Upcoming release"* section with a few
|
||||
The top of the `CHANGELOG` contains a *"unreleased"* section with a few
|
||||
subsections (Features, Bugfixes, …). Please add your entry to the subsection
|
||||
that best describes your change.
|
||||
|
||||
|
1117
Cargo.lock
generated
1117
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
61
Cargo.toml
61
Cargo.toml
@ -12,13 +12,12 @@ keywords = [
|
||||
"filesystem",
|
||||
"tool",
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
license = "MIT/Apache-2.0"
|
||||
name = "fd-find"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/sharkdp/fd"
|
||||
version = "10.2.0"
|
||||
edition= "2021"
|
||||
rust-version = "1.77.2"
|
||||
version = "8.3.2"
|
||||
edition= "2018"
|
||||
|
||||
[badges.appveyor]
|
||||
repository = "sharkdp/fd"
|
||||
@ -31,41 +30,34 @@ name = "fd"
|
||||
path = "src/main.rs"
|
||||
|
||||
[build-dependencies]
|
||||
clap = "3.0"
|
||||
clap_complete = "3.0"
|
||||
version_check = "0.9"
|
||||
|
||||
[dependencies]
|
||||
aho-corasick = "1.1"
|
||||
nu-ansi-term = "0.50"
|
||||
argmax = "0.3.1"
|
||||
ignore = "0.4.22"
|
||||
regex = "1.10.5"
|
||||
regex-syntax = "0.8"
|
||||
ansi_term = "0.12"
|
||||
atty = "0.2"
|
||||
ignore = "0.4.3"
|
||||
num_cpus = "1.13"
|
||||
regex = "1.5.4"
|
||||
regex-syntax = "0.6"
|
||||
ctrlc = "3.2"
|
||||
humantime = "2.1"
|
||||
lscolors = "0.8"
|
||||
globset = "0.4"
|
||||
anyhow = "1.0"
|
||||
etcetera = "0.8"
|
||||
normpath = "1.1.1"
|
||||
crossbeam-channel = "0.5.13"
|
||||
clap_complete = {version = "4.5.24", optional = true}
|
||||
faccess = "0.2.4"
|
||||
dirs-next = "2.0"
|
||||
normpath = "0.3.2"
|
||||
chrono = "0.4"
|
||||
once_cell = "1.9.0"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.5.13"
|
||||
features = ["suggestions", "color", "wrap_help", "cargo", "derive"]
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.38"
|
||||
default-features = false
|
||||
features = ["std", "clock"]
|
||||
|
||||
[dependencies.lscolors]
|
||||
version = "0.19"
|
||||
default-features = false
|
||||
features = ["nu-ansi-term"]
|
||||
version = "3.0"
|
||||
features = ["suggestions", "color", "wrap_help", "cargo"]
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { version = "0.29.0", default-features = false, features = ["signal", "user", "hostname"] }
|
||||
users = "0.11.0"
|
||||
nix = "0.23.1"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
||||
libc = "0.2"
|
||||
@ -73,22 +65,19 @@ libc = "0.2"
|
||||
# FIXME: Re-enable jemalloc on macOS
|
||||
# jemalloc is currently disabled on macOS due to a bug in jemalloc in combination with macOS
|
||||
# Catalina. See https://github.com/sharkdp/fd/issues/498 for details.
|
||||
[target.'cfg(all(not(windows), not(target_os = "android"), not(target_os = "macos"), not(target_os = "freebsd"), not(target_os = "openbsd"), not(all(target_env = "musl", target_pointer_width = "32")), not(target_arch = "riscv64")))'.dependencies]
|
||||
jemallocator = {version = "0.5.4", optional = true}
|
||||
[target.'cfg(all(not(windows), not(target_os = "android"), not(target_os = "macos"), not(target_os = "freebsd"), not(target_env = "musl"), not(target_arch = "riscv64")))'.dependencies]
|
||||
jemallocator = {version = "0.3.0", optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
diff = "0.1"
|
||||
tempfile = "3.10"
|
||||
tempdir = "0.3"
|
||||
filetime = "0.2"
|
||||
test-case = "3.3"
|
||||
test-case = "1.2"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
|
||||
[features]
|
||||
use-jemalloc = ["jemallocator"]
|
||||
completions = ["clap_complete"]
|
||||
base = ["use-jemalloc"]
|
||||
default = ["use-jemalloc", "completions"]
|
||||
default = ["use-jemalloc"]
|
||||
|
@ -1,6 +0,0 @@
|
||||
# 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"]
|
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-present The fd developers
|
||||
Copyright (c) 2017-2021 The fd developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
37
Makefile
37
Makefile
@ -1,37 +0,0 @@
|
||||
PROFILE=release
|
||||
EXE=target/$(PROFILE)/fd
|
||||
prefix=/usr/local
|
||||
bindir=$(prefix)/bin
|
||||
datadir=$(prefix)/share
|
||||
exe_name=fd
|
||||
|
||||
$(EXE): Cargo.toml src/**/*.rs
|
||||
cargo build --profile $(PROFILE) --locked
|
||||
|
||||
.PHONY: completions
|
||||
completions: autocomplete/fd.bash autocomplete/fd.fish autocomplete/fd.ps1 autocomplete/_fd
|
||||
|
||||
comp_dir=@mkdir -p autocomplete
|
||||
|
||||
autocomplete/fd.bash: $(EXE)
|
||||
$(comp_dir)
|
||||
$(EXE) --gen-completions bash > $@
|
||||
|
||||
autocomplete/fd.fish: $(EXE)
|
||||
$(comp_dir)
|
||||
$(EXE) --gen-completions fish > $@
|
||||
|
||||
autocomplete/fd.ps1: $(EXE)
|
||||
$(comp_dir)
|
||||
$(EXE) --gen-completions powershell > $@
|
||||
|
||||
autocomplete/_fd: contrib/completion/_fd
|
||||
$(comp_dir)
|
||||
cp $< $@
|
||||
|
||||
install: $(EXE) completions
|
||||
install -Dm755 $(EXE) $(DESTDIR)$(bindir)/fd
|
||||
install -Dm644 autocomplete/fd.bash $(DESTDIR)/$(datadir)/bash-completion/completions/$(exe_name)
|
||||
install -Dm644 autocomplete/fd.fish $(DESTDIR)/$(datadir)/fish/vendor_completions.d/$(exe_name).fish
|
||||
install -Dm644 autocomplete/_fd $(DESTDIR)/$(datadir)/zsh/site-functions/_$(exe_name)
|
||||
install -Dm644 doc/fd.1 $(DESTDIR)/$(datadir)/man/man1/$(exe_name).1
|
266
README.md
266
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
[![CICD](https://github.com/sharkdp/fd/actions/workflows/CICD.yml/badge.svg)](https://github.com/sharkdp/fd/actions/workflows/CICD.yml)
|
||||
[![Version info](https://img.shields.io/crates/v/fd-find.svg)](https://crates.io/crates/fd-find)
|
||||
[[中文](https://github.com/cha0ran/fd-zh)]
|
||||
[[中文](https://github.com/chinanf-boy/fd-zh)]
|
||||
[[한국어](https://github.com/spearkkk/fd-kor)]
|
||||
|
||||
`fd` is a program to find entries in your filesystem.
|
||||
@ -10,14 +10,17 @@ 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.
|
||||
|
||||
[Installation](#installation) • [How to use](#how-to-use) • [Troubleshooting](#troubleshooting)
|
||||
Quick links:
|
||||
* [How to use](#how-to-use)
|
||||
* [Installation](#installation)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Features
|
||||
|
||||
* Intuitive syntax: `fd PATTERN` instead of `find -iname '*PATTERN*'`.
|
||||
* Regular expression (default) and glob-based patterns.
|
||||
* [Very fast](#benchmark) due to parallelized directory traversal.
|
||||
* Uses colors to highlight different file types (same as `ls`).
|
||||
* Uses colors to highlight different file types (same as *ls*).
|
||||
* Supports [parallel command execution](#command-execution)
|
||||
* Smart case: the search is case-insensitive by default. It switches to
|
||||
case-sensitive if the pattern contains an uppercase
|
||||
@ -60,7 +63,7 @@ X11/xinit/xinitrc
|
||||
X11/xinit/xserverrc
|
||||
```
|
||||
|
||||
The regular expression syntax used by `fd` is [documented here](https://docs.rs/regex/latest/regex/#syntax).
|
||||
The regular expression syntax used by `fd` is [documented here](https://docs.rs/regex/1.0.0/regex/#syntax).
|
||||
|
||||
### Specifying the root directory
|
||||
|
||||
@ -140,7 +143,7 @@ target/debug/deps/libnum_cpus-f5ce7ef99006aa05.rlib
|
||||
```
|
||||
|
||||
To really search *all* files and directories, simply combine the hidden and ignore features to show
|
||||
everything (`-HI`) or use `-u`/`--unrestricted`.
|
||||
everything (`-HI`).
|
||||
|
||||
### Matching the full path
|
||||
By default, *fd* only matches the filename of each file. However, using the `--full-path` or `-p` option,
|
||||
@ -203,13 +206,6 @@ fd -e jpg -x convert {} {.}.png
|
||||
Here, `{}` is a placeholder for the search result. `{.}` is the same, without the file extension.
|
||||
See below for more details on the placeholder syntax.
|
||||
|
||||
The terminal output of commands run from parallel threads using `-x` will not be interlaced or garbled,
|
||||
so `fd -x` can be used to rudimentarily parallelize a task run over many files.
|
||||
An example of this is calculating the checksum of each individual file within a directory.
|
||||
```
|
||||
fd -tf -x md5sum > file_checksums.txt
|
||||
```
|
||||
|
||||
#### Placeholder syntax
|
||||
|
||||
The `-x` and `-X` options take a *command template* as a series of arguments (instead of a single string).
|
||||
@ -258,17 +254,12 @@ 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.
|
||||
@ -286,8 +277,7 @@ 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.
|
||||
@ -298,96 +288,101 @@ This is the output of `fd -h`. To see the full set of command-line options, use
|
||||
also includes a much more detailed help text.
|
||||
|
||||
```
|
||||
Usage: fd [OPTIONS] [pattern] [path]...
|
||||
USAGE:
|
||||
fd [FLAGS/OPTIONS] [<pattern>] [<path>...]
|
||||
|
||||
Arguments:
|
||||
[pattern] the search pattern (a regular expression, unless '--glob' is used; optional)
|
||||
[path]... the root directories for the filesystem search (optional)
|
||||
FLAGS:
|
||||
-H, --hidden Search hidden files and directories
|
||||
-I, --no-ignore Do not respect .(git|fd)ignore files
|
||||
-s, --case-sensitive Case-sensitive search (default: smart case)
|
||||
-i, --ignore-case Case-insensitive search (default: smart case)
|
||||
-g, --glob Glob-based search (default: regular expression)
|
||||
-a, --absolute-path Show absolute instead of relative paths
|
||||
-l, --list-details Use a long listing format with file metadata
|
||||
-L, --follow Follow symbolic links
|
||||
-p, --full-path Search full abs. path (default: filename only)
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
|
||||
Options:
|
||||
-H, --hidden Search hidden files and directories
|
||||
-I, --no-ignore Do not respect .(git|fd)ignore files
|
||||
-s, --case-sensitive Case-sensitive search (default: smart case)
|
||||
-i, --ignore-case Case-insensitive search (default: smart case)
|
||||
-g, --glob Glob-based search (default: regular expression)
|
||||
-a, --absolute-path Show absolute instead of relative paths
|
||||
-l, --list-details Use a long listing format with file metadata
|
||||
-L, --follow Follow symbolic links
|
||||
-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/dir), symlink (l),
|
||||
executable (x), empty (e), socket (s), pipe (p), char-device
|
||||
(c), block-device (b)
|
||||
-e, --extension <ext> Filter by file extension
|
||||
-S, --size <size> Limit results based on the size of files
|
||||
--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
|
||||
OPTIONS:
|
||||
-d, --max-depth <depth> Set maximum search depth (default: none)
|
||||
-t, --type <filetype>... Filter by type: file (f), directory (d), symlink (l),
|
||||
executable (x), empty (e), socket (s), pipe (p)
|
||||
-e, --extension <ext>... Filter by file extension
|
||||
-x, --exec <cmd> Execute a command for each search result
|
||||
-X, --exec-batch <cmd> Execute a command with all search results at once
|
||||
-E, --exclude <pattern>... Exclude entries that match the given glob pattern
|
||||
-c, --color <when> When to use colors: never, *auto*, always
|
||||
-S, --size <size>... Limit results based on the size of files
|
||||
--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
|
||||
|
||||
ARGS:
|
||||
<pattern> the search pattern (a regular expression, unless '--glob' is used; optional)
|
||||
<path>... the root directory for the filesystem search (optional)
|
||||
```
|
||||
|
||||
## Benchmark
|
||||
|
||||
Let's search my home folder for files that end in `[0-9].jpg`. It contains ~750.000
|
||||
subdirectories and about a 4 million files. For averaging and statistical analysis, I'm using
|
||||
Let's search my home folder for files that end in `[0-9].jpg`. It contains ~190.000
|
||||
subdirectories and about a million files. For averaging and statistical analysis, I'm using
|
||||
[hyperfine](https://github.com/sharkdp/hyperfine). The following benchmarks are performed
|
||||
with a "warm"/pre-filled disk-cache (results for a "cold" disk-cache show the same trends).
|
||||
|
||||
Let's start with `find`:
|
||||
```
|
||||
Benchmark 1: find ~ -iregex '.*[0-9]\.jpg$'
|
||||
Time (mean ± σ): 19.922 s ± 0.109 s
|
||||
Range (min … max): 19.765 s … 20.065 s
|
||||
Benchmark #1: find ~ -iregex '.*[0-9]\.jpg$'
|
||||
|
||||
Time (mean ± σ): 7.236 s ± 0.090 s
|
||||
|
||||
Range (min … max): 7.133 s … 7.385 s
|
||||
```
|
||||
|
||||
`find` is much faster if it does not need to perform a regular-expression search:
|
||||
```
|
||||
Benchmark 2: find ~ -iname '*[0-9].jpg'
|
||||
Time (mean ± σ): 11.226 s ± 0.104 s
|
||||
Range (min … max): 11.119 s … 11.466 s
|
||||
Benchmark #2: find ~ -iname '*[0-9].jpg'
|
||||
|
||||
Time (mean ± σ): 3.914 s ± 0.027 s
|
||||
|
||||
Range (min … max): 3.876 s … 3.964 s
|
||||
```
|
||||
|
||||
Now let's try the same for `fd`. Note that `fd` performs a regular expression
|
||||
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):
|
||||
Now let's try the same for `fd`. Note that `fd` *always* performs a regular expression
|
||||
search. The options `--hidden` and `--no-ignore` are needed for a fair comparison,
|
||||
otherwise `fd` does not have to traverse hidden folders and ignored paths (see below):
|
||||
```
|
||||
Benchmark 3: fd -u '[0-9]\.jpg$' ~
|
||||
Time (mean ± σ): 854.8 ms ± 10.0 ms
|
||||
Range (min … max): 839.2 ms … 868.9 ms
|
||||
```
|
||||
For this particular example, `fd` is approximately **23 times faster** than `find -iregex`
|
||||
and about **13 times faster** than `find -iname`. By the way, both tools found the exact
|
||||
same 546 files :smile:.
|
||||
Benchmark #3: fd -HI '.*[0-9]\.jpg$' ~
|
||||
|
||||
**Note**: This is *one particular* benchmark on *one particular* machine. While we have
|
||||
performed a lot of different tests (and found consistent results), things might
|
||||
be different for you! We encourage everyone to try it out on their own. See
|
||||
Time (mean ± σ): 811.6 ms ± 26.9 ms
|
||||
|
||||
Range (min … max): 786.0 ms … 870.7 ms
|
||||
```
|
||||
For this particular example, `fd` is approximately nine times faster than `find -iregex`
|
||||
and about five times faster than `find -iname`. By the way, both tools found the exact
|
||||
same 20880 files :smile:.
|
||||
|
||||
Finally, let's run `fd` without `--hidden` and `--no-ignore` (this can lead to different
|
||||
search results, of course). If *fd* does not have to traverse the hidden and git-ignored
|
||||
folders, it is almost an order of magnitude faster:
|
||||
```
|
||||
Benchmark #4: fd '[0-9]\.jpg$' ~
|
||||
|
||||
Time (mean ± σ): 123.7 ms ± 6.0 ms
|
||||
|
||||
Range (min … max): 118.8 ms … 140.0 ms
|
||||
```
|
||||
|
||||
**Note**: This is *one particular* benchmark on *one particular* machine. While I have
|
||||
performed quite a lot of different tests (and found consistent results), things might
|
||||
be different for you! I encourage everyone to try it out on their own. See
|
||||
[this repository](https://github.com/sharkdp/fd-benchmarks) for all necessary scripts.
|
||||
|
||||
Concerning *fd*'s speed, a lot of credit goes to the `regex` and `ignore` crates that are
|
||||
also used in [ripgrep](https://github.com/BurntSushi/ripgrep) (check it out!).
|
||||
Concerning *fd*'s speed, the main credit goes to the `regex` and `ignore` crates that are 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 …
|
||||
```
|
||||
|
||||
### Colorized output
|
||||
|
||||
`fd` can colorize files by extension, just like `ls`. In order for this to work, the environment
|
||||
@ -401,6 +396,15 @@ 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 `-H` and `-I` to disable these two features:
|
||||
``` bash
|
||||
> fd -HI …
|
||||
```
|
||||
|
||||
### `fd` doesn't seem to interpret my regex pattern correctly
|
||||
|
||||
A lot of special regex characters (like `[]`, `^`, `$`, ..) are also special characters in your
|
||||
@ -479,17 +483,16 @@ In emacs, run `M-x find-file-in-project-by-selected` to find matching files. Alt
|
||||
|
||||
### Printing the output as a tree
|
||||
|
||||
To format the output of `fd` as a file-tree you can use the `tree` command with
|
||||
`--fromfile`:
|
||||
To format the output of `fd` similar to the `tree` command, install [`as-tree`] and pipe the output
|
||||
of `fd` to `as-tree`:
|
||||
```bash
|
||||
❯ fd | tree --fromfile
|
||||
fd | as-tree
|
||||
```
|
||||
|
||||
This can be more useful than running `tree` by itself because `tree` does not
|
||||
ignore any files by default, nor does it support as rich a set of options as
|
||||
`fd` does to control what to print:
|
||||
This can be more useful than running `tree` by itself because `tree` does not ignore any files by
|
||||
default, nor does it support as rich a set of options as `fd` does to control what to print:
|
||||
```bash
|
||||
❯ fd --extension rs | tree --fromfile
|
||||
❯ fd --extension rs | as-tree
|
||||
.
|
||||
├── build.rs
|
||||
└── src
|
||||
@ -497,10 +500,9 @@ ignore any files by default, nor does it support as rich a set of options as
|
||||
└── error.rs
|
||||
```
|
||||
|
||||
On bash and similar you can simply create an alias:
|
||||
```bash
|
||||
❯ alias as-tree='tree --fromfile'
|
||||
```
|
||||
For more information about `as-tree`, see [the `as-tree` README][`as-tree`].
|
||||
|
||||
[`as-tree`]: https://github.com/jez/as-tree
|
||||
|
||||
### Using fd with `xargs` or `parallel`
|
||||
|
||||
@ -523,7 +525,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):
|
||||
```
|
||||
apt install fd-find
|
||||
sudo 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
|
||||
@ -533,25 +535,21 @@ 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
|
||||
dpkg -i fd_9.0.0_amd64.deb # adapt version number and architecture
|
||||
sudo dpkg -i fd_8.3.2_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):
|
||||
```
|
||||
apt-get install fd-find
|
||||
sudo 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,6 +557,12 @@ Starting with Fedora 28, you can install `fd` from the official package sources:
|
||||
dnf install fd-find
|
||||
```
|
||||
|
||||
For older versions, you can use this [Fedora copr](https://copr.fedorainfracloud.org/coprs/keefle/fd/) to install `fd`:
|
||||
``` bash
|
||||
dnf copr enable keefle/fd
|
||||
dnf install fd
|
||||
```
|
||||
|
||||
### On Alpine Linux
|
||||
|
||||
You can install [the fd package](https://pkgs.alpinelinux.org/packages?name=fd)
|
||||
@ -573,8 +577,6 @@ You can install [the fd package](https://www.archlinux.org/packages/community/x8
|
||||
```
|
||||
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:
|
||||
@ -596,31 +598,6 @@ 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.
|
||||
|
||||
```bash
|
||||
dnf copr enable tkbcopr/fd
|
||||
dnf install fd
|
||||
```
|
||||
|
||||
A different version using the [slower](https://github.com/sharkdp/fd/pull/481#issuecomment-534494592) malloc [instead of jemalloc](https://bugzilla.redhat.com/show_bug.cgi?id=2216193#c1) is also available from the EPEL8/9 repo as the package `fd-find`.
|
||||
|
||||
### On macOS
|
||||
|
||||
You can install `fd` with [Homebrew](https://formulae.brew.sh/formula/fd):
|
||||
@ -630,7 +607,7 @@ brew install fd
|
||||
|
||||
… or with MacPorts:
|
||||
```
|
||||
port install fd
|
||||
sudo port install fd
|
||||
```
|
||||
|
||||
### On Windows
|
||||
@ -647,18 +624,6 @@ Or via [Chocolatey](https://chocolatey.org):
|
||||
choco install fd
|
||||
```
|
||||
|
||||
Or via [Winget](https://learn.microsoft.com/en-us/windows/package-manager/):
|
||||
```
|
||||
winget install sharkdp.fd
|
||||
```
|
||||
|
||||
### On GuixOS
|
||||
|
||||
You can install [the fd package](https://guix.gnu.org/en/packages/fd-8.1.1/) from the official repo:
|
||||
```
|
||||
guix install fd
|
||||
```
|
||||
|
||||
### On NixOS / via Nix
|
||||
|
||||
You can use the [Nix package manager](https://nixos.org/nix/) to install `fd`:
|
||||
@ -666,13 +631,6 @@ 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:
|
||||
@ -682,7 +640,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
|
||||
@ -694,7 +652,7 @@ With Rust's package manager [cargo](https://github.com/rust-lang/cargo), you can
|
||||
```
|
||||
cargo install fd-find
|
||||
```
|
||||
Note that rust version *1.77.2* or later is required.
|
||||
Note that rust version *1.54.0* or later is required.
|
||||
|
||||
`make` is also needed for the build.
|
||||
|
||||
@ -725,6 +683,8 @@ 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.
|
||||
|
23
build.rs
23
build.rs
@ -1,5 +1,13 @@
|
||||
use std::fs;
|
||||
|
||||
use clap_complete::{generate_to, Shell};
|
||||
use Shell::*;
|
||||
//use clap_complete::shells::Shel{Bash, Fish, PowerShell, Elvish};
|
||||
|
||||
include!("src/app.rs");
|
||||
|
||||
fn main() {
|
||||
let min_version = "1.64";
|
||||
let min_version = "1.54";
|
||||
|
||||
match version_check::is_min_version(min_version) {
|
||||
Some(true) => {}
|
||||
@ -9,4 +17,17 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let var = std::env::var_os("SHELL_COMPLETIONS_DIR").or_else(|| std::env::var_os("OUT_DIR"));
|
||||
let outdir = match var {
|
||||
None => return,
|
||||
Some(outdir) => outdir,
|
||||
};
|
||||
fs::create_dir_all(&outdir).unwrap();
|
||||
|
||||
let mut app = build_app();
|
||||
// NOTE: zsh completions are hand written in contrib/completion/_fd
|
||||
for shell in [Bash, PowerShell, Fish, Elvish] {
|
||||
generate_to(shell, &mut app, "fd", &outdir).unwrap();
|
||||
}
|
||||
}
|
||||
|
1
clippy.toml
Normal file
1
clippy.toml
Normal file
@ -0,0 +1 @@
|
||||
msrv = "1.54.0"
|
@ -26,8 +26,6 @@ _fd() {
|
||||
{l,symlink}'\:"symbolic links"'
|
||||
{e,empty}'\:"empty files or directories"'
|
||||
{x,executable}'\:"executable (files)"'
|
||||
{b,block-device}'\:"block devices"'
|
||||
{c,char-device}'\:"character devices"'
|
||||
{s,socket}'\:"sockets"'
|
||||
{p,pipe}'\:"named pipes (FIFOs)"'
|
||||
)
|
||||
@ -38,7 +36,7 @@ _fd() {
|
||||
# for all of the potential negation options listed below!
|
||||
if
|
||||
# (--[bpsu]* => match all options marked with '$no')
|
||||
[[ $PREFIX$SUFFIX == --[bopsun]* ]] ||
|
||||
[[ $PREFIX$SUFFIX == --[bopsu]* ]] ||
|
||||
zstyle -t ":complete:$curcontext:*" complete-all
|
||||
then
|
||||
no=
|
||||
@ -72,9 +70,6 @@ _fd() {
|
||||
{-g,--glob}'[perform a glob-based search]'
|
||||
{-F,--fixed-strings}'[treat pattern as literal string instead of a regex]'
|
||||
|
||||
+ '(no-require-git)'
|
||||
"$no(no-ignore-full --no-ignore-vcs --no-require-git)--no-require-git[don't require git repo to respect gitignores]"
|
||||
|
||||
+ '(match-full)' # match against full path
|
||||
{-p,--full-path}'[match the pattern against the full path instead of the basename]'
|
||||
|
||||
@ -123,7 +118,6 @@ _fd() {
|
||||
|
||||
+ '(filter-mtime-newer)' # filter by files modified after than
|
||||
'--changed-within=[limit search to files/directories modified within the given date/duration]:date or duration'
|
||||
'--changed-after=[alias for --changed-within]:date/duration'
|
||||
'!--change-newer-than=:date/duration'
|
||||
'!--newer=:date/duration'
|
||||
|
||||
@ -139,8 +133,6 @@ _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'
|
||||
|
||||
@ -154,7 +146,7 @@ _fd() {
|
||||
|
||||
+ '(about)' # about flags
|
||||
'(: * -)'{-h,--help}'[display help message]'
|
||||
'(: * -)'{-V,--version}'[display version information]'
|
||||
'(: * -)'{-v,--version}'[display version information]'
|
||||
|
||||
+ path-sep # set path separator for output
|
||||
$no'(--path-separator)--path-separator=[set the path separator to use when printing file paths]:path separator'
|
||||
@ -164,11 +156,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=-[When to strip ./]::when:(always never auto)'
|
||||
|
||||
+ and
|
||||
'--and=[additional required search path]:pattern'
|
||||
|
||||
$no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix[Strip ./ prefix when output is redirected]'
|
||||
|
||||
+ args # positional arguments
|
||||
'1: :_guard "^-*" pattern'
|
||||
|
194
doc/fd.1
vendored
194
doc/fd.1
vendored
@ -24,24 +24,11 @@ fd \- find entries in the filesystem
|
||||
.B fd
|
||||
is a simple, fast and user-friendly alternative to
|
||||
.BR find (1).
|
||||
.P
|
||||
By default
|
||||
.B fd
|
||||
uses regular expressions for the pattern. However, this can be changed to use simple glob patterns
|
||||
with the '\-\-glob' option.
|
||||
.P
|
||||
By default
|
||||
.B fd
|
||||
will exclude hidden files and directories, as well as any files that match gitignore rules
|
||||
or ignore rules in .ignore or .fdignore files.
|
||||
.SH OPTIONS
|
||||
.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 are still excluded unless \-\-no\-ignore or \-\-no\-ignore\-vcs
|
||||
is also used.
|
||||
.TP
|
||||
.B \-I, \-\-no\-ignore
|
||||
Show search results from files and directories that would otherwise be ignored by
|
||||
@ -66,7 +53,7 @@ The global fd ignore file (usually
|
||||
The flag can be overridden with '--ignore'.
|
||||
.TP
|
||||
.B \-u, \-\-unrestricted
|
||||
Perform an unrestricted search, including ignored and hidden files. This is an alias for '--hidden --no-ignore'.
|
||||
Alias for '--no-ignore'. Can be repeated; '-uu' is an alias for '--no-ignore --hidden'.
|
||||
.TP
|
||||
.B \-\-no\-ignore\-vcs
|
||||
Show search results from files and directories that would otherwise be ignored by gitignore files
|
||||
@ -79,14 +66,6 @@ git setting, which defaults to
|
||||
.IR $HOME/.config/git/ignore ).
|
||||
The flag can be overridden with '--ignore-vcs'.
|
||||
.TP
|
||||
.B \-\-no\-require\-git
|
||||
Do not require a git repository to respect gitignores. By default, fd will only
|
||||
respect global gitignore rules, .gitignore rules and local exclude rules if fd
|
||||
detects that you are searching inside a git repository. This flag allows you to
|
||||
relax this restriction such that fd will respect all git related ignore rules
|
||||
regardless of whether you’re searching in a git repository or not. The flag can
|
||||
be overridden with '--require-git'.
|
||||
.TP
|
||||
.B \-\-no\-ignore\-parent
|
||||
Show search results from files and directories that would otherwise be ignored by gitignore files in
|
||||
parent directories.
|
||||
@ -110,11 +89,6 @@ Perform a regular-expression based search (default). This can be used to overrid
|
||||
Treat the pattern as a literal string instead of a regular expression. Note that this also
|
||||
performs substring comparison. If you want to match on an exact filename, consider using '\-\-glob'.
|
||||
.TP
|
||||
.BI "\-\-and " pattern
|
||||
Add additional required search patterns, all of which must be matched. Multiple additional
|
||||
patterns can be specified. The patterns are regular expressions, unless '\-\-glob'
|
||||
or '\-\-fixed\-strings' is used.
|
||||
.TP
|
||||
.B \-a, \-\-absolute\-path
|
||||
Shows the full path starting from the root as opposed to relative paths.
|
||||
The flag can be overridden with '--relative-path'.
|
||||
@ -156,20 +130,9 @@ 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 [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
|
||||
.B \-\-strip-cwd-prefix
|
||||
By default, relative paths are prefixed with './' when the output goes to a non interactive terminal
|
||||
(TTY). Use this flag to disable this behaviour.
|
||||
.TP
|
||||
.B \-\-one\-file\-system, \-\-mount, \-\-xdev
|
||||
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).
|
||||
@ -199,14 +162,10 @@ Filter search by type:
|
||||
.RS
|
||||
.IP "f, file"
|
||||
regular files
|
||||
.IP "d, dir, directory"
|
||||
.IP "d, directory"
|
||||
directories
|
||||
.IP "l, symlink"
|
||||
symbolic links
|
||||
.IP "b, block-device"
|
||||
block devices
|
||||
.IP "c, char-device"
|
||||
character devices
|
||||
.IP "s, socket"
|
||||
sockets
|
||||
.IP "p, pipe"
|
||||
@ -276,24 +235,6 @@ 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
|
||||
@ -338,37 +279,27 @@ tebibytes
|
||||
Filter results based on the file modification time.
|
||||
Files with modification times greater than the argument will be returned.
|
||||
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
||||
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
|
||||
.B --changed-after
|
||||
can be used as aliases.
|
||||
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).
|
||||
.B --change-newer-than
|
||||
can be used as an alias.
|
||||
|
||||
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.
|
||||
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
||||
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]+).
|
||||
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).
|
||||
.B --change-older-than
|
||||
or
|
||||
.B --older
|
||||
can be used as aliases.
|
||||
can be used as an alias.
|
||||
|
||||
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
|
||||
@ -393,30 +324,6 @@ 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
|
||||
@ -429,17 +336,20 @@ Note that all subsequent positional arguments are considered to be arguments to
|
||||
It is therefore recommended to place the \-x/\-\-exec option last. Alternatively, you can supply
|
||||
a ';' argument to end the argument list and continue with more fd options.
|
||||
Most shells require ';' to be escaped: '\\;'.
|
||||
This option can be specified multiple times, in which case all commands are run for each
|
||||
file found, in the order they are provided. In that case, you must supply a ';' argument for
|
||||
all but the last commands.
|
||||
|
||||
If parallelism is enabled, the order commands will be executed in is non-deterministic. And even with
|
||||
--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.
|
||||
|
||||
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.
|
||||
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
|
||||
.RE
|
||||
|
||||
If no placeholder is present, an implicit "{}" at the end is assumed.
|
||||
|
||||
@ -463,18 +373,22 @@ Examples:
|
||||
Execute
|
||||
.I command
|
||||
once, with all search results as arguments.
|
||||
|
||||
The order of the arguments is non-deterministic and should not be relied upon.
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
If no placeholder is present, an implicit "{}" at the end is assumed.
|
||||
|
||||
Like \-\-exec, this can be used multiple times, in which case each command will be run in
|
||||
the order given.
|
||||
|
||||
Examples:
|
||||
|
||||
- Find all test_*.py files and open them in your favorite editor:
|
||||
@ -491,8 +405,7 @@ Examples:
|
||||
.BI "\-\-batch-size " size
|
||||
Maximum number of arguments to pass to the command given with -X. If the number of results is
|
||||
greater than the given size, the command given with -X is run again with remaining arguments. A
|
||||
batch size of zero means there is no limit (default), but note that batching might still happen
|
||||
due to OS restrictions on the maximum length of command lines.
|
||||
batch size of zero means there is no limit.
|
||||
.SH PATTERN SYNTAX
|
||||
The regular expression syntax used by fd is documented here:
|
||||
|
||||
@ -517,17 +430,6 @@ 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 "':"
|
||||
@ -541,17 +443,11 @@ $ 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
|
||||
.TP
|
||||
.BI "\-\-batch\-size " size
|
||||
Pass at most
|
||||
.I size
|
||||
arguments to each call to the command given with -X.
|
||||
.TP
|
||||
.SH SEE ALSO
|
||||
.BR find (1)
|
||||
|
66
doc/release-checklist.md
vendored
66
doc/release-checklist.md
vendored
@ -1,66 +0,0 @@
|
||||
# Release checklist
|
||||
|
||||
This file can be used as-is, or copied into the GitHub PR description which includes
|
||||
necessary changes for the upcoming release.
|
||||
|
||||
## Version bump
|
||||
|
||||
- [ ] Create a new branch for the required changes for this release.
|
||||
- [ ] Update version in `Cargo.toml`. Run `cargo build` to update `Cargo.lock`.
|
||||
Make sure to `git add` the `Cargo.lock` changes as well.
|
||||
- [ ] Find the current min. supported Rust version by running
|
||||
`grep rust-version Cargo.toml`.
|
||||
- [ ] Update the `fd` version and the min. supported Rust version in `README.md`.
|
||||
- [ ] Update `CHANGELOG.md`. Change the heading of the *"Upcoming release"* section
|
||||
to the version of this release.
|
||||
|
||||
## Pre-release checks and updates
|
||||
|
||||
- [ ] Install the latest version (`cargo install --locked -f --path .`) and make
|
||||
sure that it is available on the `PATH` (`fd --version` should show the
|
||||
new version).
|
||||
- [ ] Review `-h`, `--help`, and the `man` page.
|
||||
- [ ] Run `fd -h` and copy the output to the *"Command-line options"* section in
|
||||
the README
|
||||
- [ ] Push all changes and wait for CI to succeed (before continuing with the
|
||||
next section).
|
||||
- [ ] Optional: manually test the new features and command-line options described
|
||||
in the `CHANGELOG.md`.
|
||||
- [ ] Run `cargo publish --dry-run` to make sure that it will succeed later
|
||||
(after creating the GitHub release).
|
||||
|
||||
## Release
|
||||
|
||||
- [ ] Merge your release branch (should be a fast-forward merge).
|
||||
- [ ] Create a tag and push it: `git tag vX.Y.Z; git push origin tag vX.Y.Z`.
|
||||
This will trigger the deployment via GitHub Actions.
|
||||
REMINDER: If your `origin` is a fork, don't forget to push to e.g. `upstream`
|
||||
instead.
|
||||
- [ ] Go to https://github.com/sharkdp/fd/releases/new to create the new
|
||||
release. Select the new tag and also use it as the release title. For the
|
||||
release notes, copy the corresponding section from `CHANGELOG.md` and
|
||||
possibly add additional remarks for package maintainers.
|
||||
Publish the release.
|
||||
- [ ] Check if the binary deployment works (archives and Debian packages should
|
||||
appear when the CI run *for the Git tag* has finished).
|
||||
- [ ] Publish to crates.io by running `cargo publish` in a *clean* repository.
|
||||
One way to do this is to clone a fresh copy.
|
||||
|
||||
## Post-release
|
||||
|
||||
- [ ] Prepare a new *"Upcoming release"* section at the top of `CHANGELOG.md`.
|
||||
Put this at the top:
|
||||
|
||||
# Upcoming release
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
## Bugfixes
|
||||
|
||||
|
||||
## Changes
|
||||
|
||||
|
||||
## Other
|
||||
|
8
doc/screencast.sh
vendored
8
doc/screencast.sh
vendored
@ -1,8 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Designed to be executed via svg-term from the fd root directory:
|
||||
# svg-term --command="bash doc/screencast.sh" --out doc/screencast.svg --padding=10
|
||||
# Then run this (workaround for #1003):
|
||||
# sed -i '' 's/<text/<text font-size="1.67"/g' doc/screencast.svg
|
||||
set -e
|
||||
set -u
|
||||
|
||||
@ -36,11 +34,9 @@ main() {
|
||||
|
||||
enter "fd app"
|
||||
|
||||
enter "fd fi"
|
||||
enter "fd sh"
|
||||
|
||||
enter "fd fi --type f"
|
||||
|
||||
enter "fd --type d"
|
||||
enter "fd sh --type f"
|
||||
|
||||
enter "fd -e md"
|
||||
|
||||
|
2
doc/screencast.svg
vendored
2
doc/screencast.svg
vendored
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 115 KiB |
12
doc/sponsors.md
vendored
12
doc/sponsors.md
vendored
@ -1,12 +0,0 @@
|
||||
## 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
doc/sponsors/terminal_trove_green.svg
vendored
5
doc/sponsors/terminal_trove_green.svg
vendored
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 7.2 KiB |
@ -1 +0,0 @@
|
||||
# Defaults are used
|
@ -1,134 +0,0 @@
|
||||
#!/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}"
|
@ -1,22 +0,0 @@
|
||||
#!/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
|
||||
|
770
src/app.rs
Normal file
770
src/app.rs
Normal file
@ -0,0 +1,770 @@
|
||||
use clap::{crate_version, App, AppSettings, Arg, ColorChoice};
|
||||
|
||||
pub fn build_app() -> App<'static> {
|
||||
let clap_color_choice = if std::env::var_os("NO_COLOR").is_none() {
|
||||
ColorChoice::Auto
|
||||
} else {
|
||||
ColorChoice::Never
|
||||
};
|
||||
|
||||
let mut app = App::new("fd")
|
||||
.version(crate_version!())
|
||||
.color(clap_color_choice)
|
||||
.setting(AppSettings::DeriveDisplayOrder)
|
||||
.setting(AppSettings::DontCollapseArgsInUsage)
|
||||
.after_help(
|
||||
"Note: `fd -h` prints a short and concise overview while `fd --help` gives all \
|
||||
details.",
|
||||
)
|
||||
.arg(
|
||||
Arg::new("hidden")
|
||||
.long("hidden")
|
||||
.short('H')
|
||||
.overrides_with("hidden")
|
||||
.help("Search hidden files and directories")
|
||||
.long_help(
|
||||
"Include hidden directories and files in the search results (default: \
|
||||
hidden files and directories are skipped). Files and directories are \
|
||||
considered to be hidden if their name starts with a `.` sign (dot). \
|
||||
The flag can be overridden with --no-hidden.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-hidden")
|
||||
.long("no-hidden")
|
||||
.overrides_with("hidden")
|
||||
.hide(true)
|
||||
.long_help(
|
||||
"Overrides --hidden.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-ignore")
|
||||
.long("no-ignore")
|
||||
.short('I')
|
||||
.overrides_with("no-ignore")
|
||||
.help("Do not respect .(git|fd)ignore files")
|
||||
.long_help(
|
||||
"Show search results from files and directories that would otherwise be \
|
||||
ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file. \
|
||||
The flag can be overridden with --ignore.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore")
|
||||
.long("ignore")
|
||||
.overrides_with("no-ignore")
|
||||
.hide(true)
|
||||
.long_help(
|
||||
"Overrides --no-ignore.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-ignore-vcs")
|
||||
.long("no-ignore-vcs")
|
||||
.overrides_with("no-ignore-vcs")
|
||||
.hide_short_help(true)
|
||||
.help("Do not respect .gitignore files")
|
||||
.long_help(
|
||||
"Show search results from files and directories that would otherwise be \
|
||||
ignored by '.gitignore' files. The flag can be overridden with --ignore-vcs.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore-vcs")
|
||||
.long("ignore-vcs")
|
||||
.overrides_with("no-ignore-vcs")
|
||||
.hide(true)
|
||||
.long_help(
|
||||
"Overrides --no-ignore-vcs.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-ignore-parent")
|
||||
.long("no-ignore-parent")
|
||||
.overrides_with("no-ignore-parent")
|
||||
.hide_short_help(true)
|
||||
.help("Do not respect .(git|fd)ignore files in parent directories")
|
||||
.long_help(
|
||||
"Show search results from files and directories that would otherwise be \
|
||||
ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-global-ignore-file")
|
||||
.long("no-global-ignore-file")
|
||||
.hide(true)
|
||||
.help("Do not respect the global ignore file")
|
||||
.long_help("Do not respect the global ignore file."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("rg-alias-hidden-ignore")
|
||||
.short('u')
|
||||
.long("unrestricted")
|
||||
.overrides_with_all(&["ignore", "no-hidden"])
|
||||
.multiple_occurrences(true)
|
||||
.hide_short_help(true)
|
||||
.help("Alias for '--no-ignore', and '--hidden' when given twice")
|
||||
.long_help(
|
||||
"Alias for '--no-ignore'. Can be repeated. '-uu' is an alias for \
|
||||
'--no-ignore --hidden'.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("case-sensitive")
|
||||
.long("case-sensitive")
|
||||
.short('s')
|
||||
.overrides_with_all(&["ignore-case", "case-sensitive"])
|
||||
.help("Case-sensitive search (default: smart case)")
|
||||
.long_help(
|
||||
"Perform a case-sensitive search. By default, fd uses case-insensitive \
|
||||
searches, unless the pattern contains an uppercase character (smart \
|
||||
case).",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore-case")
|
||||
.long("ignore-case")
|
||||
.short('i')
|
||||
.overrides_with_all(&["case-sensitive", "ignore-case"])
|
||||
.help("Case-insensitive search (default: smart case)")
|
||||
.long_help(
|
||||
"Perform a case-insensitive search. By default, fd uses case-insensitive \
|
||||
searches, unless the pattern contains an uppercase character (smart \
|
||||
case).",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("glob")
|
||||
.long("glob")
|
||||
.short('g')
|
||||
.conflicts_with("fixed-strings")
|
||||
.overrides_with("glob")
|
||||
.help("Glob-based search (default: regular expression)")
|
||||
.long_help("Perform a glob-based search instead of a regular expression search."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("regex")
|
||||
.long("regex")
|
||||
.overrides_with_all(&["glob", "regex"])
|
||||
.hide_short_help(true)
|
||||
.help("Regular-expression based search (default)")
|
||||
.long_help(
|
||||
"Perform a regular-expression based search (default). This can be used to \
|
||||
override --glob.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("fixed-strings")
|
||||
.long("fixed-strings")
|
||||
.short('F')
|
||||
.alias("literal")
|
||||
.overrides_with("fixed-strings")
|
||||
.hide_short_help(true)
|
||||
.help("Treat pattern as literal string instead of regex")
|
||||
.long_help(
|
||||
"Treat the pattern as a literal string instead of a regular expression. Note \
|
||||
that this also performs substring comparison. If you want to match on an \
|
||||
exact filename, consider using '--glob'.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("absolute-path")
|
||||
.long("absolute-path")
|
||||
.short('a')
|
||||
.overrides_with("absolute-path")
|
||||
.help("Show absolute instead of relative paths")
|
||||
.long_help(
|
||||
"Shows the full path starting from the root as opposed to relative paths. \
|
||||
The flag can be overridden with --relative-path.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("relative-path")
|
||||
.long("relative-path")
|
||||
.overrides_with("absolute-path")
|
||||
.hide(true)
|
||||
.long_help(
|
||||
"Overrides --absolute-path.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("list-details")
|
||||
.long("list-details")
|
||||
.short('l')
|
||||
.conflicts_with("absolute-path")
|
||||
.help("Use a long listing format with file metadata")
|
||||
.long_help(
|
||||
"Use a detailed listing format like 'ls -l'. This is basically an alias \
|
||||
for '--exec-batch ls -l' with some additional 'ls' options. This can be \
|
||||
used to see more metadata, to show symlink targets and to achieve a \
|
||||
deterministic sort order.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("follow")
|
||||
.long("follow")
|
||||
.short('L')
|
||||
.alias("dereference")
|
||||
.overrides_with("follow")
|
||||
.help("Follow symbolic links")
|
||||
.long_help(
|
||||
"By default, fd does not descend into symlinked directories. Using this \
|
||||
flag, symbolic links are also traversed. \
|
||||
Flag can be overriden with --no-follow.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-follow")
|
||||
.long("no-follow")
|
||||
.overrides_with("follow")
|
||||
.hide(true)
|
||||
.long_help(
|
||||
"Overrides --follow.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("full-path")
|
||||
.long("full-path")
|
||||
.short('p')
|
||||
.overrides_with("full-path")
|
||||
.help("Search full abs. path (default: filename only)")
|
||||
.long_help(
|
||||
"By default, the search pattern is only matched against the filename (or \
|
||||
directory name). Using this flag, the pattern is matched against the full \
|
||||
(absolute) path. Example:\n \
|
||||
fd --glob -p '**/.git/config'",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("null_separator")
|
||||
.long("print0")
|
||||
.short('0')
|
||||
.overrides_with("print0")
|
||||
.conflicts_with("list-details")
|
||||
.hide_short_help(true)
|
||||
.help("Separate results by the null character")
|
||||
.long_help(
|
||||
"Separate search results by the null character (instead of newlines). \
|
||||
Useful for piping results to 'xargs'.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("max-depth")
|
||||
.long("max-depth")
|
||||
.short('d')
|
||||
.takes_value(true)
|
||||
.value_name("depth")
|
||||
.help("Set maximum search depth (default: none)")
|
||||
.long_help(
|
||||
"Limit the directory traversal to a given depth. By default, there is no \
|
||||
limit on the search depth.",
|
||||
),
|
||||
)
|
||||
// support --maxdepth as well, for compatibility with rg
|
||||
.arg(
|
||||
Arg::new("rg-depth")
|
||||
.long("maxdepth")
|
||||
.hide(true)
|
||||
.takes_value(true)
|
||||
.help("Set maximum search depth (default: none)")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("min-depth")
|
||||
.long("min-depth")
|
||||
.takes_value(true)
|
||||
.value_name("depth")
|
||||
.hide_short_help(true)
|
||||
.help("Only show results starting at given depth")
|
||||
.long_help(
|
||||
"Only show search results starting at the given depth. \
|
||||
See also: '--max-depth' and '--exact-depth'",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("exact-depth")
|
||||
.long("exact-depth")
|
||||
.takes_value(true)
|
||||
.value_name("depth")
|
||||
.hide_short_help(true)
|
||||
.conflicts_with_all(&["max-depth", "min-depth"])
|
||||
.help("Only show results at exact given depth")
|
||||
.long_help(
|
||||
"Only show search results at the exact given depth. This is an alias for \
|
||||
'--min-depth <depth> --max-depth <depth>'.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("prune")
|
||||
.long("prune")
|
||||
.conflicts_with_all(&["size", "exact-depth"])
|
||||
.hide_short_help(true)
|
||||
.help("Do not traverse into matching directories")
|
||||
.long_help("Do not traverse into directories that match the search criteria. If \
|
||||
you want to exclude specific directories, use the '--exclude=…' option.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("file-type")
|
||||
.long("type")
|
||||
.short('t')
|
||||
.multiple_occurrences(true)
|
||||
.number_of_values(1)
|
||||
.takes_value(true)
|
||||
.value_name("filetype")
|
||||
.possible_values(&[
|
||||
"f",
|
||||
"file",
|
||||
"d",
|
||||
"directory",
|
||||
"l",
|
||||
"symlink",
|
||||
"x",
|
||||
"executable",
|
||||
"e",
|
||||
"empty",
|
||||
"s",
|
||||
"socket",
|
||||
"p",
|
||||
"pipe",
|
||||
])
|
||||
.hide_possible_values(true)
|
||||
.help(
|
||||
"Filter by type: file (f), directory (d), symlink (l),\nexecutable (x), \
|
||||
empty (e), socket (s), pipe (p)",
|
||||
)
|
||||
.long_help(
|
||||
"Filter the search by type:\n \
|
||||
'f' or 'file': regular files\n \
|
||||
'd' or 'directory': directories\n \
|
||||
'l' or 'symlink': symbolic links\n \
|
||||
's' or 'socket': socket\n \
|
||||
'p' or 'pipe': named pipe (FIFO)\n\n \
|
||||
'x' or 'executable': executables\n \
|
||||
'e' or 'empty': empty files or directories\n\n\
|
||||
This option can be specified more than once to include multiple file types. \
|
||||
Searching for '--type file --type symlink' will show both regular files as \
|
||||
well as symlinks. Note that the 'executable' and 'empty' filters work differently: \
|
||||
'--type executable' implies '--type file' by default. And '--type empty' searches \
|
||||
for empty files and directories, unless either '--type file' or '--type directory' \
|
||||
is specified in addition.\n\n\
|
||||
Examples:\n \
|
||||
- Only search for files:\n \
|
||||
fd --type file …\n \
|
||||
fd -tf …\n \
|
||||
- Find both files and symlinks\n \
|
||||
fd --type file --type symlink …\n \
|
||||
fd -tf -tl …\n \
|
||||
- Find executable files:\n \
|
||||
fd --type executable\n \
|
||||
fd -tx\n \
|
||||
- Find empty files:\n \
|
||||
fd --type empty --type file\n \
|
||||
fd -te -tf\n \
|
||||
- Find empty directories:\n \
|
||||
fd --type empty --type directory\n \
|
||||
fd -te -td"
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("extension")
|
||||
.long("extension")
|
||||
.short('e')
|
||||
.multiple_occurrences(true)
|
||||
.number_of_values(1)
|
||||
.takes_value(true)
|
||||
.value_name("ext")
|
||||
.help("Filter by file extension")
|
||||
.long_help(
|
||||
"(Additionally) filter search results by their file extension. Multiple \
|
||||
allowable file extensions can be specified.\n\
|
||||
If you want to search for files without extension, \
|
||||
you can use the regex '^[^.]+$' as a normal search pattern.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("exec")
|
||||
.long("exec")
|
||||
.short('x')
|
||||
.min_values(1)
|
||||
.allow_hyphen_values(true)
|
||||
.value_terminator(";")
|
||||
.value_name("cmd")
|
||||
.conflicts_with("list-details")
|
||||
.help("Execute a command for each search result")
|
||||
.long_help(
|
||||
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
|
||||
All positional arguments following --exec are considered to be arguments to the command - not to fd. \
|
||||
It is therefore recommended to place the '-x'/'--exec' option last.\n\
|
||||
The following placeholders are substituted before the command is executed:\n \
|
||||
'{}': path (of the current search result)\n \
|
||||
'{/}': basename\n \
|
||||
'{//}': parent directory\n \
|
||||
'{.}': path without file extension\n \
|
||||
'{/.}': basename without file extension\n\n\
|
||||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
||||
Examples:\n\n \
|
||||
- find all *.zip files and unzip them:\n\n \
|
||||
fd -e zip -x unzip\n\n \
|
||||
- find *.h and *.cpp files and run \"clang-format -i ..\" for each of them:\n\n \
|
||||
fd -e h -e cpp -x clang-format -i\n\n \
|
||||
- Convert all *.jpg files to *.png files:\n\n \
|
||||
fd -e jpg -x convert {} {.}.png\
|
||||
",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("exec-batch")
|
||||
.long("exec-batch")
|
||||
.short('X')
|
||||
.min_values(1)
|
||||
.allow_hyphen_values(true)
|
||||
.value_terminator(";")
|
||||
.value_name("cmd")
|
||||
.conflicts_with_all(&["exec", "list-details"])
|
||||
.help("Execute a command with all search results at once")
|
||||
.long_help(
|
||||
"Execute the given command once, with all search results as arguments.\n\
|
||||
One of the following placeholders is substituted before the command is executed:\n \
|
||||
'{}': path (of all search results)\n \
|
||||
'{/}': basename\n \
|
||||
'{//}': parent directory\n \
|
||||
'{.}': path without file extension\n \
|
||||
'{/.}': basename without file extension\n\n\
|
||||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
||||
Examples:\n\n \
|
||||
- Find all test_*.py files and open them in your favorite editor:\n\n \
|
||||
fd -g 'test_*.py' -X vim\n\n \
|
||||
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
|
||||
fd -e rs -X wc -l\
|
||||
"
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("batch-size")
|
||||
.long("batch-size")
|
||||
.takes_value(true)
|
||||
.value_name("size")
|
||||
.hide_short_help(true)
|
||||
.requires("exec-batch")
|
||||
.help("Max number of arguments to run as a batch with -X")
|
||||
.long_help(
|
||||
"Maximum number of arguments to pass to the command given with -X. \
|
||||
If the number of results is greater than the given size, \
|
||||
the command given with -X is run again with remaining arguments. \
|
||||
A batch size of zero means there is no limit.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("exclude")
|
||||
.long("exclude")
|
||||
.short('E')
|
||||
.takes_value(true)
|
||||
.value_name("pattern")
|
||||
.number_of_values(1)
|
||||
.multiple_occurrences(true)
|
||||
.help("Exclude entries that match the given glob pattern")
|
||||
.long_help(
|
||||
"Exclude files/directories that match the given glob pattern. This \
|
||||
overrides any other ignore logic. Multiple exclude patterns can be \
|
||||
specified.\n\n\
|
||||
Examples:\n \
|
||||
--exclude '*.pyc'\n \
|
||||
--exclude node_modules",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore-file")
|
||||
.long("ignore-file")
|
||||
.takes_value(true)
|
||||
.value_name("path")
|
||||
.number_of_values(1)
|
||||
.multiple_occurrences(true)
|
||||
.hide_short_help(true)
|
||||
.help("Add custom ignore-file in '.gitignore' format")
|
||||
.long_help(
|
||||
"Add a custom ignore-file in '.gitignore' format. These files have a low \
|
||||
precedence.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("color")
|
||||
.long("color")
|
||||
.short('c')
|
||||
.takes_value(true)
|
||||
.value_name("when")
|
||||
.possible_values(&["never", "auto", "always"])
|
||||
.hide_possible_values(true)
|
||||
.help("When to use colors: never, *auto*, always")
|
||||
.long_help(
|
||||
"Declare when to use color for the pattern match output:\n \
|
||||
'auto': show colors if the output goes to an interactive console (default)\n \
|
||||
'never': do not use colorized output\n \
|
||||
'always': always use colorized output",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("threads")
|
||||
.long("threads")
|
||||
.short('j')
|
||||
.takes_value(true)
|
||||
.value_name("num")
|
||||
.hide_short_help(true)
|
||||
.help("Set number of threads")
|
||||
.long_help(
|
||||
"Set number of threads to use for searching & executing (default: number \
|
||||
of available CPU cores)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("size")
|
||||
.long("size")
|
||||
.short('S')
|
||||
.takes_value(true)
|
||||
.number_of_values(1)
|
||||
.allow_hyphen_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.help("Limit results based on the size of files")
|
||||
.long_help(
|
||||
"Limit results based on the size of files using the format <+-><NUM><UNIT>.\n \
|
||||
'+': file size must be greater than or equal to this\n \
|
||||
'-': file size must be less than or equal to this\n\
|
||||
If neither '+' nor '-' is specified, file size must be exactly equal to this.\n \
|
||||
'NUM': The numeric size (e.g. 500)\n \
|
||||
'UNIT': The units for NUM. They are not case-sensitive.\n\
|
||||
Allowed unit values:\n \
|
||||
'b': bytes\n \
|
||||
'k': kilobytes (base ten, 10^3 = 1000 bytes)\n \
|
||||
'm': megabytes\n \
|
||||
'g': gigabytes\n \
|
||||
't': terabytes\n \
|
||||
'ki': kibibytes (base two, 2^10 = 1024 bytes)\n \
|
||||
'mi': mebibytes\n \
|
||||
'gi': gibibytes\n \
|
||||
'ti': tebibytes",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("max-buffer-time")
|
||||
.long("max-buffer-time")
|
||||
.takes_value(true)
|
||||
.hide(true)
|
||||
.help("Milliseconds to buffer before streaming search results to console")
|
||||
.long_help(
|
||||
"Amount of time in milliseconds to buffer, before streaming the search \
|
||||
results to the console.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("changed-within")
|
||||
.long("changed-within")
|
||||
.alias("change-newer-than")
|
||||
.alias("newer")
|
||||
.takes_value(true)
|
||||
.value_name("date|dur")
|
||||
.number_of_values(1)
|
||||
.help("Filter by file modification time (newer than)")
|
||||
.long_help(
|
||||
"Filter results based on the file modification time. The argument can be provided \
|
||||
as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min). \
|
||||
If the time is not specified, it defaults to 00:00:00. \
|
||||
'--change-newer-than' or '--newer' can be used as aliases.\n\
|
||||
Examples:\n \
|
||||
--changed-within 2weeks\n \
|
||||
--change-newer-than '2018-10-27 10:00:00'\n \
|
||||
--newer 2018-10-27",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("changed-before")
|
||||
.long("changed-before")
|
||||
.alias("change-older-than")
|
||||
.alias("older")
|
||||
.takes_value(true)
|
||||
.value_name("date|dur")
|
||||
.number_of_values(1)
|
||||
.help("Filter by file modification time (older than)")
|
||||
.long_help(
|
||||
"Filter results based on the file modification time. The argument can be provided \
|
||||
as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min). \
|
||||
'--change-older-than' or '--older' can be used as aliases.\n\
|
||||
Examples:\n \
|
||||
--changed-before '2018-10-27 10:00:00'\n \
|
||||
--change-older-than 2weeks\n \
|
||||
--older 2018-10-27",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("max-results")
|
||||
.long("max-results")
|
||||
.takes_value(true)
|
||||
.value_name("count")
|
||||
// We currently do not support --max-results in combination with
|
||||
// program execution because the results that come up in a --max-results
|
||||
// search are non-deterministic. Users might think that they can run the
|
||||
// same search with `--exec rm` attached and get a reliable removal of
|
||||
// the files they saw in the previous search.
|
||||
.conflicts_with_all(&["exec", "exec-batch", "list-details"])
|
||||
.hide_short_help(true)
|
||||
.help("Limit number of search results")
|
||||
.long_help("Limit the number of search results to 'count' and quit immediately."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("max-one-result")
|
||||
.short('1')
|
||||
.hide_short_help(true)
|
||||
.overrides_with("max-results")
|
||||
.conflicts_with_all(&["exec", "exec-batch", "list-details"])
|
||||
.help("Limit search to a single result")
|
||||
.long_help("Limit the search to a single result and quit immediately. \
|
||||
This is an alias for '--max-results=1'.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("quiet")
|
||||
.long("quiet")
|
||||
.short('q')
|
||||
.alias("has-results")
|
||||
.hide_short_help(true)
|
||||
.conflicts_with_all(&["exec", "exec-batch", "list-details", "max-results"])
|
||||
.help("Print nothing, exit code 0 if match found, 1 otherwise")
|
||||
.long_help(
|
||||
"When the flag is present, the program does not print anything and will \
|
||||
return with an exit code of 0 if there is at least one match. Otherwise, the \
|
||||
exit code will be 1. \
|
||||
'--has-results' can be used as an alias."
|
||||
)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("show-errors")
|
||||
.long("show-errors")
|
||||
.hide_short_help(true)
|
||||
.overrides_with("show-errors")
|
||||
.help("Show filesystem errors")
|
||||
.long_help(
|
||||
"Enable the display of filesystem errors for situations such as \
|
||||
insufficient permissions or dead symlinks.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("base-directory")
|
||||
.long("base-directory")
|
||||
.takes_value(true)
|
||||
.value_name("path")
|
||||
.number_of_values(1)
|
||||
.allow_invalid_utf8(true)
|
||||
.hide_short_help(true)
|
||||
.help("Change current working directory")
|
||||
.long_help(
|
||||
"Change the current working directory of fd to the provided path. This \
|
||||
means that search results will be shown with respect to the given base \
|
||||
path. Note that relative paths which are passed to fd via the positional \
|
||||
<path> argument or the '--search-path' option will also be resolved \
|
||||
relative to this directory.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pattern")
|
||||
.allow_invalid_utf8(true)
|
||||
.help(
|
||||
"the search pattern (a regular expression, unless '--glob' is used; optional)",
|
||||
).long_help(
|
||||
"the search pattern which is either a regular expression (default) or a glob \
|
||||
pattern (if --glob is used). If no pattern has been specified, every entry \
|
||||
is considered a match. If your pattern starts with a dash (-), make sure to \
|
||||
pass '--' first, or it will be considered as a flag (fd -- '-foo').")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("path-separator")
|
||||
.takes_value(true)
|
||||
.value_name("separator")
|
||||
.long("path-separator")
|
||||
.hide_short_help(true)
|
||||
.help("Set path separator when printing file paths")
|
||||
.long_help(
|
||||
"Set the path separator to use when printing file paths. The default is \
|
||||
the OS-specific separator ('/' on Unix, '\\' on Windows).",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("path")
|
||||
.multiple_occurrences(true)
|
||||
.allow_invalid_utf8(true)
|
||||
.help("the root directory for the filesystem search (optional)")
|
||||
.long_help(
|
||||
"The directory where the filesystem search is rooted (optional). If \
|
||||
omitted, search the current working directory.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("search-path")
|
||||
.long("search-path")
|
||||
.takes_value(true)
|
||||
.conflicts_with("path")
|
||||
.multiple_occurrences(true)
|
||||
.hide_short_help(true)
|
||||
.number_of_values(1)
|
||||
.allow_invalid_utf8(true)
|
||||
.help("Provide paths to search as an alternative to the positional <path>")
|
||||
.long_help(
|
||||
"Provide paths to search as an alternative to the positional <path> \
|
||||
argument. Changes the usage to `fd [OPTIONS] --search-path <path> \
|
||||
--search-path <path2> [<pattern>]`",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("strip-cwd-prefix")
|
||||
.long("strip-cwd-prefix")
|
||||
.conflicts_with_all(&["path", "search-path"])
|
||||
.hide_short_help(true)
|
||||
.help("strip './' prefix from non-tty outputs")
|
||||
.long_help(
|
||||
"By default, relative paths are prefixed with './' when the output goes to a non \
|
||||
interactive terminal (TTY). Use this flag to disable this behaviour."
|
||||
)
|
||||
);
|
||||
|
||||
if cfg!(unix) {
|
||||
app = app.arg(
|
||||
Arg::new("owner")
|
||||
.long("owner")
|
||||
.short('o')
|
||||
.takes_value(true)
|
||||
.value_name("user:group")
|
||||
.help("Filter by owning user and/or group")
|
||||
.long_help(
|
||||
"Filter files by their user and/or group. \
|
||||
Format: [(user|uid)][:(group|gid)]. Either side is optional. \
|
||||
Precede either side with a '!' to exclude files instead.\n\
|
||||
Examples:\n \
|
||||
--owner john\n \
|
||||
--owner :students\n \
|
||||
--owner '!john:students'",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Make `--one-file-system` available only on Unix and Windows platforms, as per the
|
||||
// restrictions on the corresponding option in the `ignore` crate.
|
||||
// Provide aliases `mount` and `xdev` for people coming from `find`.
|
||||
if cfg!(any(unix, windows)) {
|
||||
app = app.arg(
|
||||
Arg::new("one-file-system")
|
||||
.long("one-file-system")
|
||||
.aliases(&["mount", "xdev"])
|
||||
.hide_short_help(true)
|
||||
.help("Do not descend into a different file system")
|
||||
.long_help(
|
||||
"By default, fd will traverse the file system tree as far as other options \
|
||||
dictate. With this flag, fd ensures that it does not descend into a \
|
||||
different file system than the one it started in. Comparable to the -mount \
|
||||
or -xdev filters of find(1).",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_app() {
|
||||
build_app().debug_assert()
|
||||
}
|
939
src/cli.rs
939
src/cli.rs
@ -1,939 +0,0 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::{
|
||||
error::ErrorKind, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, Parser,
|
||||
ValueEnum,
|
||||
};
|
||||
#[cfg(feature = "completions")]
|
||||
use clap_complete::Shell;
|
||||
use normpath::PathExt;
|
||||
|
||||
use crate::error::print_error;
|
||||
use crate::exec::CommandSet;
|
||||
use crate::filesystem;
|
||||
#[cfg(unix)]
|
||||
use crate::filter::OwnerFilter;
|
||||
use crate::filter::SizeFilter;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "fd",
|
||||
version,
|
||||
about = "A program to find entries in your filesystem",
|
||||
after_long_help = "Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues",
|
||||
max_term_width = 98,
|
||||
args_override_self = true,
|
||||
group(ArgGroup::new("execs").args(&["exec", "exec_batch", "list_details"]).conflicts_with_all(&[
|
||||
"max_results", "quiet", "max_one_result"])),
|
||||
)]
|
||||
pub struct Opts {
|
||||
/// Include hidden directories and files in the search results (default:
|
||||
/// hidden files and directories are skipped). Files and directories are
|
||||
/// considered to be hidden if their name starts with a `.` sign (dot).
|
||||
/// Any files or directories that are ignored due to the rules described by
|
||||
/// --no-ignore are still ignored unless otherwise specified.
|
||||
/// The flag can be overridden with --no-hidden.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'H',
|
||||
help = "Search hidden files and directories",
|
||||
long_help
|
||||
)]
|
||||
pub hidden: bool,
|
||||
|
||||
/// Overrides --hidden
|
||||
#[arg(long, overrides_with = "hidden", hide = true, action = ArgAction::SetTrue)]
|
||||
no_hidden: (),
|
||||
|
||||
/// Show search results from files and directories that would otherwise be
|
||||
/// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file,
|
||||
/// The flag can be overridden with --ignore.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'I',
|
||||
help = "Do not respect .(git|fd)ignore files",
|
||||
long_help
|
||||
)]
|
||||
pub no_ignore: bool,
|
||||
|
||||
/// Overrides --no-ignore
|
||||
#[arg(long, overrides_with = "no_ignore", hide = true, action = ArgAction::SetTrue)]
|
||||
ignore: (),
|
||||
|
||||
///Show search results from files and directories that
|
||||
///would otherwise be ignored by '.gitignore' files.
|
||||
///The flag can be overridden with --ignore-vcs.
|
||||
#[arg(
|
||||
long,
|
||||
hide_short_help = true,
|
||||
help = "Do not respect .gitignore files",
|
||||
long_help
|
||||
)]
|
||||
pub no_ignore_vcs: bool,
|
||||
|
||||
/// Overrides --no-ignore-vcs
|
||||
#[arg(long, overrides_with = "no_ignore_vcs", hide = true, action = ArgAction::SetTrue)]
|
||||
ignore_vcs: (),
|
||||
|
||||
/// Do not require a git repository to respect gitignores.
|
||||
/// By default, fd will only respect global gitignore rules, .gitignore rules,
|
||||
/// and local exclude rules if fd detects that you are searching inside a
|
||||
/// git repository. This flag allows you to relax this restriction such that
|
||||
/// fd will respect all git related ignore rules regardless of whether you're
|
||||
/// searching in a git repository or not.
|
||||
///
|
||||
///
|
||||
/// This flag can be disabled with --require-git.
|
||||
#[arg(
|
||||
long,
|
||||
overrides_with = "require_git",
|
||||
hide_short_help = true,
|
||||
// same description as ripgrep's flag: ripgrep/crates/core/app.rs
|
||||
long_help
|
||||
)]
|
||||
pub no_require_git: bool,
|
||||
|
||||
/// Overrides --no-require-git
|
||||
#[arg(long, overrides_with = "no_require_git", hide = true, action = ArgAction::SetTrue)]
|
||||
require_git: (),
|
||||
|
||||
/// Show search results from files and directories that would otherwise be
|
||||
/// ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories.
|
||||
#[arg(
|
||||
long,
|
||||
hide_short_help = true,
|
||||
help = "Do not respect .(git|fd)ignore files in parent directories",
|
||||
long_help
|
||||
)]
|
||||
pub no_ignore_parent: bool,
|
||||
|
||||
/// Do not respect the global ignore file
|
||||
#[arg(long, hide = true)]
|
||||
pub no_global_ignore_file: bool,
|
||||
|
||||
/// Perform an unrestricted search, including ignored and hidden files. This is
|
||||
/// an alias for '--no-ignore --hidden'.
|
||||
#[arg(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no_hidden"]), action(ArgAction::Count), hide_short_help = true,
|
||||
help = "Unrestricted search, alias for '--no-ignore --hidden'",
|
||||
long_help,
|
||||
)]
|
||||
rg_alias_hidden_ignore: u8,
|
||||
|
||||
/// Case-sensitive search (default: smart case)
|
||||
#[arg(
|
||||
long,
|
||||
short = 's',
|
||||
overrides_with("ignore_case"),
|
||||
long_help = "Perform a case-sensitive search. By default, fd uses case-insensitive \
|
||||
searches, unless the pattern contains an uppercase character (smart \
|
||||
case)."
|
||||
)]
|
||||
pub case_sensitive: bool,
|
||||
|
||||
/// Perform a case-insensitive search. By default, fd uses case-insensitive
|
||||
/// searches, unless the pattern contains an uppercase character (smart
|
||||
/// case).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'i',
|
||||
overrides_with("case_sensitive"),
|
||||
help = "Case-insensitive search (default: smart case)",
|
||||
long_help
|
||||
)]
|
||||
pub ignore_case: bool,
|
||||
|
||||
/// Perform a glob-based search instead of a regular expression search.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'g',
|
||||
conflicts_with("fixed_strings"),
|
||||
help = "Glob-based search (default: regular expression)",
|
||||
long_help
|
||||
)]
|
||||
pub glob: bool,
|
||||
|
||||
/// Perform a regular-expression based search (default). This can be used to
|
||||
/// override --glob.
|
||||
#[arg(
|
||||
long,
|
||||
overrides_with("glob"),
|
||||
hide_short_help = true,
|
||||
help = "Regular-expression based search (default)",
|
||||
long_help
|
||||
)]
|
||||
pub regex: bool,
|
||||
|
||||
/// Treat the pattern as a literal string instead of a regular expression. Note
|
||||
/// that this also performs substring comparison. If you want to match on an
|
||||
/// exact filename, consider using '--glob'.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'F',
|
||||
alias = "literal",
|
||||
hide_short_help = true,
|
||||
help = "Treat pattern as literal string stead of regex",
|
||||
long_help
|
||||
)]
|
||||
pub fixed_strings: bool,
|
||||
|
||||
/// Add additional required search patterns, all of which must be matched. Multiple
|
||||
/// additional patterns can be specified. The patterns are regular
|
||||
/// expressions, unless '--glob' or '--fixed-strings' is used.
|
||||
#[arg(
|
||||
long = "and",
|
||||
value_name = "pattern",
|
||||
help = "Additional search patterns that need to be matched",
|
||||
long_help,
|
||||
hide_short_help = true,
|
||||
allow_hyphen_values = true
|
||||
)]
|
||||
pub exprs: Option<Vec<String>>,
|
||||
|
||||
/// Shows the full path starting from the root as opposed to relative paths.
|
||||
/// The flag can be overridden with --relative-path.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'a',
|
||||
help = "Show absolute instead of relative paths",
|
||||
long_help
|
||||
)]
|
||||
pub absolute_path: bool,
|
||||
|
||||
/// Overrides --absolute-path
|
||||
#[arg(long, overrides_with = "absolute_path", hide = true, action = ArgAction::SetTrue)]
|
||||
relative_path: (),
|
||||
|
||||
/// Use a detailed listing format like 'ls -l'. This is basically an alias
|
||||
/// for '--exec-batch ls -l' with some additional 'ls' options. This can be
|
||||
/// used to see more metadata, to show symlink targets and to achieve a
|
||||
/// deterministic sort order.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'l',
|
||||
conflicts_with("absolute_path"),
|
||||
help = "Use a long listing format with file metadata",
|
||||
long_help
|
||||
)]
|
||||
pub list_details: bool,
|
||||
|
||||
/// Follow symbolic links
|
||||
#[arg(
|
||||
long,
|
||||
short = 'L',
|
||||
alias = "dereference",
|
||||
long_help = "By default, fd does not descend into symlinked directories. Using this \
|
||||
flag, symbolic links are also traversed. \
|
||||
Flag can be overridden with --no-follow."
|
||||
)]
|
||||
pub follow: bool,
|
||||
|
||||
/// Overrides --follow
|
||||
#[arg(long, overrides_with = "follow", hide = true, action = ArgAction::SetTrue)]
|
||||
no_follow: (),
|
||||
|
||||
/// By default, the search pattern is only matched against the filename (or directory name). Using this flag, the pattern is matched against the full (absolute) path. Example:
|
||||
/// fd --glob -p '**/.git/config'
|
||||
#[arg(
|
||||
long,
|
||||
short = 'p',
|
||||
help = "Search full abs. path (default: filename only)",
|
||||
long_help,
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
pub full_path: bool,
|
||||
|
||||
/// Separate search results by the null character (instead of newlines).
|
||||
/// Useful for piping results to 'xargs'.
|
||||
#[arg(
|
||||
long = "print0",
|
||||
short = '0',
|
||||
conflicts_with("list_details"),
|
||||
hide_short_help = true,
|
||||
help = "Separate search results by the null character",
|
||||
long_help
|
||||
)]
|
||||
pub null_separator: bool,
|
||||
|
||||
/// Limit the directory traversal to a given depth. By default, there is no
|
||||
/// limit on the search depth.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'd',
|
||||
value_name = "depth",
|
||||
alias("maxdepth"),
|
||||
help = "Set maximum search depth (default: none)",
|
||||
long_help
|
||||
)]
|
||||
max_depth: Option<usize>,
|
||||
|
||||
/// Only show search results starting at the given depth.
|
||||
/// See also: '--max-depth' and '--exact-depth'
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "depth",
|
||||
hide_short_help = true,
|
||||
help = "Only show search results starting at the given depth.",
|
||||
long_help
|
||||
)]
|
||||
min_depth: Option<usize>,
|
||||
|
||||
/// Only show search results at the exact given depth. This is an alias for
|
||||
/// '--min-depth <depth> --max-depth <depth>'.
|
||||
#[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]),
|
||||
help = "Only show search results at the exact given depth",
|
||||
long_help,
|
||||
)]
|
||||
exact_depth: Option<usize>,
|
||||
|
||||
/// Exclude files/directories that match the given glob pattern. This
|
||||
/// overrides any other ignore logic. Multiple exclude patterns can be
|
||||
/// specified.
|
||||
///
|
||||
/// Examples:
|
||||
/// {n} --exclude '*.pyc'
|
||||
/// {n} --exclude node_modules
|
||||
#[arg(
|
||||
long,
|
||||
short = 'E',
|
||||
value_name = "pattern",
|
||||
help = "Exclude entries that match the given glob pattern",
|
||||
long_help
|
||||
)]
|
||||
pub exclude: Vec<String>,
|
||||
|
||||
/// Do not traverse into directories that match the search criteria. If
|
||||
/// you want to exclude specific directories, use the '--exclude=…' option.
|
||||
#[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]),
|
||||
long_help,
|
||||
)]
|
||||
pub prune: bool,
|
||||
|
||||
/// Filter the search by type:
|
||||
/// {n} 'f' or 'file': regular files
|
||||
/// {n} 'd' or 'dir' or 'directory': directories
|
||||
/// {n} 'l' or 'symlink': symbolic links
|
||||
/// {n} 's' or 'socket': socket
|
||||
/// {n} 'p' or 'pipe': named pipe (FIFO)
|
||||
/// {n} 'b' or 'block-device': block device
|
||||
/// {n} 'c' or 'char-device': character device
|
||||
/// {n}{n} 'x' or 'executable': executables
|
||||
/// {n} 'e' or 'empty': empty files or directories
|
||||
///
|
||||
/// This option can be specified more than once to include multiple file types.
|
||||
/// Searching for '--type file --type symlink' will show both regular files as
|
||||
/// well as symlinks. Note that the 'executable' and 'empty' filters work differently:
|
||||
/// '--type executable' implies '--type file' by default. And '--type empty' searches
|
||||
/// for empty files and directories, unless either '--type file' or '--type directory'
|
||||
/// is specified in addition.
|
||||
///
|
||||
/// Examples:
|
||||
/// {n} - Only search for files:
|
||||
/// {n} fd --type file …
|
||||
/// {n} fd -tf …
|
||||
/// {n} - Find both files and symlinks
|
||||
/// {n} fd --type file --type symlink …
|
||||
/// {n} fd -tf -tl …
|
||||
/// {n} - Find executable files:
|
||||
/// {n} fd --type executable
|
||||
/// {n} fd -tx
|
||||
/// {n} - Find empty files:
|
||||
/// {n} fd --type empty --type file
|
||||
/// {n} fd -te -tf
|
||||
/// {n} - Find empty directories:
|
||||
/// {n} fd --type empty --type directory
|
||||
/// {n} fd -te -td
|
||||
#[arg(
|
||||
long = "type",
|
||||
short = 't',
|
||||
value_name = "filetype",
|
||||
hide_possible_values = true,
|
||||
value_enum,
|
||||
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
|
||||
)]
|
||||
pub filetype: Option<Vec<FileType>>,
|
||||
|
||||
/// (Additionally) filter search results by their file extension. Multiple
|
||||
/// allowable file extensions can be specified.
|
||||
///
|
||||
/// If you want to search for files without extension,
|
||||
/// you can use the regex '^[^.]+$' as a normal search pattern.
|
||||
#[arg(
|
||||
long = "extension",
|
||||
short = 'e',
|
||||
value_name = "ext",
|
||||
help = "Filter by file extension",
|
||||
long_help
|
||||
)]
|
||||
pub extensions: Option<Vec<String>>,
|
||||
|
||||
/// Limit results based on the size of files using the format <+-><NUM><UNIT>.
|
||||
/// '+': file size must be greater than or equal to this
|
||||
/// '-': file size must be less than or equal to this
|
||||
///
|
||||
/// If neither '+' nor '-' is specified, file size must be exactly equal to this.
|
||||
/// 'NUM': The numeric size (e.g. 500)
|
||||
/// 'UNIT': The units for NUM. They are not case-sensitive.
|
||||
/// Allowed unit values:
|
||||
/// 'b': bytes
|
||||
/// 'k': kilobytes (base ten, 10^3 = 1000 bytes)
|
||||
/// 'm': megabytes
|
||||
/// 'g': gigabytes
|
||||
/// 't': terabytes
|
||||
/// 'ki': kibibytes (base two, 2^10 = 1024 bytes)
|
||||
/// 'mi': mebibytes
|
||||
/// 'gi': gibibytes
|
||||
/// 'ti': tebibytes
|
||||
#[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment, value_name = "size",
|
||||
help = "Limit results based on the size of files",
|
||||
long_help,
|
||||
verbatim_doc_comment,
|
||||
)]
|
||||
pub size: Vec<SizeFilter>,
|
||||
|
||||
/// Filter results based on the file modification time. Files with modification times
|
||||
/// greater than the argument are returned. The argument can be provided
|
||||
/// as a specific point in time (YYYY-MM-DD HH:MM:SS or @timestamp) or as a duration (10h, 1d, 35min).
|
||||
/// If the time is not specified, it defaults to 00:00:00.
|
||||
/// '--change-newer-than', '--newer', or '--changed-after' can be used as aliases.
|
||||
///
|
||||
/// Examples:
|
||||
/// {n} --changed-within 2weeks
|
||||
/// {n} --change-newer-than '2018-10-27 10:00:00'
|
||||
/// {n} --newer 2018-10-27
|
||||
/// {n} --changed-after 1day
|
||||
#[arg(
|
||||
long,
|
||||
alias("change-newer-than"),
|
||||
alias("newer"),
|
||||
alias("changed-after"),
|
||||
value_name = "date|dur",
|
||||
help = "Filter by file modification time (newer than)",
|
||||
long_help
|
||||
)]
|
||||
pub changed_within: Option<String>,
|
||||
|
||||
/// Filter results based on the file modification time. Files with modification times
|
||||
/// less than the argument are returned. The argument can be provided
|
||||
/// as a specific point in time (YYYY-MM-DD HH:MM:SS or @timestamp) or as a duration (10h, 1d, 35min).
|
||||
/// '--change-older-than' or '--older' can be used as aliases.
|
||||
///
|
||||
/// Examples:
|
||||
/// {n} --changed-before '2018-10-27 10:00:00'
|
||||
/// {n} --change-older-than 2weeks
|
||||
/// {n} --older 2018-10-27
|
||||
#[arg(
|
||||
long,
|
||||
alias("change-older-than"),
|
||||
alias("older"),
|
||||
value_name = "date|dur",
|
||||
help = "Filter by file modification time (older than)",
|
||||
long_help
|
||||
)]
|
||||
pub changed_before: Option<String>,
|
||||
|
||||
/// Filter files by their user and/or group.
|
||||
/// Format: [(user|uid)][:(group|gid)]. Either side is optional.
|
||||
/// Precede either side with a '!' to exclude files instead.
|
||||
///
|
||||
/// Examples:
|
||||
/// {n} --owner john
|
||||
/// {n} --owner :students
|
||||
/// {n} --owner '!john:students'
|
||||
#[cfg(unix)]
|
||||
#[arg(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group",
|
||||
help = "Filter by owning user and/or group",
|
||||
long_help,
|
||||
)]
|
||||
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,
|
||||
|
||||
/// Maximum number of arguments to pass to the command given with -X.
|
||||
/// If the number of results is greater than the given size,
|
||||
/// the command given with -X is run again with remaining arguments.
|
||||
/// A batch size of zero means there is no limit (default), but note
|
||||
/// that batching might still happen due to OS restrictions on the
|
||||
/// maximum length of command lines.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "size",
|
||||
hide_short_help = true,
|
||||
requires("exec_batch"),
|
||||
value_parser = value_parser!(usize),
|
||||
default_value_t,
|
||||
help = "Max number of arguments to run as a batch size with -X",
|
||||
long_help,
|
||||
)]
|
||||
pub batch_size: usize,
|
||||
|
||||
/// Add a custom ignore-file in '.gitignore' format. These files have a low precedence.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "path",
|
||||
hide_short_help = true,
|
||||
help = "Add a custom ignore-file in '.gitignore' format",
|
||||
long_help
|
||||
)]
|
||||
pub ignore_file: Vec<PathBuf>,
|
||||
|
||||
/// Declare when to use color for the pattern match output
|
||||
#[arg(
|
||||
long,
|
||||
short = 'c',
|
||||
value_enum,
|
||||
default_value_t = ColorWhen::Auto,
|
||||
value_name = "when",
|
||||
help = "When to use colors",
|
||||
long_help,
|
||||
)]
|
||||
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>)]
|
||||
pub threads: Option<NonZeroUsize>,
|
||||
|
||||
/// Milliseconds to buffer before streaming search results to console
|
||||
///
|
||||
/// Amount of time in milliseconds to buffer, before streaming the search
|
||||
/// results to the console.
|
||||
#[arg(long, hide = true, value_parser = parse_millis)]
|
||||
pub max_buffer_time: Option<Duration>,
|
||||
|
||||
///Limit the number of search results to 'count' and quit immediately.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "count",
|
||||
hide_short_help = true,
|
||||
overrides_with("max_one_result"),
|
||||
help = "Limit the number of search results",
|
||||
long_help
|
||||
)]
|
||||
max_results: Option<usize>,
|
||||
|
||||
/// Limit the search to a single result and quit immediately.
|
||||
/// This is an alias for '--max-results=1'.
|
||||
#[arg(
|
||||
short = '1',
|
||||
hide_short_help = true,
|
||||
overrides_with("max_results"),
|
||||
help = "Limit search to a single result",
|
||||
long_help
|
||||
)]
|
||||
max_one_result: bool,
|
||||
|
||||
/// When the flag is present, the program does not print anything and will
|
||||
/// return with an exit code of 0 if there is at least one match. Otherwise, the
|
||||
/// exit code will be 1.
|
||||
/// '--has-results' can be used as an alias.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'q',
|
||||
alias = "has-results",
|
||||
hide_short_help = true,
|
||||
conflicts_with("max_results"),
|
||||
help = "Print nothing, exit code 0 if match found, 1 otherwise",
|
||||
long_help
|
||||
)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Enable the display of filesystem errors for situations such as
|
||||
/// insufficient permissions or dead symlinks.
|
||||
#[arg(
|
||||
long,
|
||||
hide_short_help = true,
|
||||
help = "Show filesystem errors",
|
||||
long_help
|
||||
)]
|
||||
pub show_errors: bool,
|
||||
|
||||
/// Change the current working directory of fd to the provided path. This
|
||||
/// means that search results will be shown with respect to the given base
|
||||
/// path. Note that relative paths which are passed to fd via the positional
|
||||
/// <path> argument or the '--search-path' option will also be resolved
|
||||
/// relative to this directory.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "path",
|
||||
hide_short_help = true,
|
||||
help = "Change current working directory",
|
||||
long_help
|
||||
)]
|
||||
pub base_directory: Option<PathBuf>,
|
||||
|
||||
/// the search pattern which is either a regular expression (default) or a glob
|
||||
/// pattern (if --glob is used). If no pattern has been specified, every entry
|
||||
/// is considered a match. If your pattern starts with a dash (-), make sure to
|
||||
/// pass '--' first, or it will be considered as a flag (fd -- '-foo').
|
||||
#[arg(
|
||||
default_value = "",
|
||||
hide_default_value = true,
|
||||
value_name = "pattern",
|
||||
help = "the search pattern (a regular expression, unless '--glob' is used; optional)",
|
||||
long_help
|
||||
)]
|
||||
pub pattern: String,
|
||||
|
||||
/// Set the path separator to use when printing file paths. The default is
|
||||
/// the OS-specific separator ('/' on Unix, '\' on Windows).
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "separator",
|
||||
hide_short_help = true,
|
||||
help = "Set path separator when printing file paths",
|
||||
long_help
|
||||
)]
|
||||
pub path_separator: Option<String>,
|
||||
|
||||
/// The directory where the filesystem search is rooted (optional). If
|
||||
/// omitted, search the current working directory.
|
||||
#[arg(action = ArgAction::Append,
|
||||
value_name = "path",
|
||||
help = "the root directories for the filesystem search (optional)",
|
||||
long_help,
|
||||
)]
|
||||
path: Vec<PathBuf>,
|
||||
|
||||
/// Provide paths to search as an alternative to the positional <path>
|
||||
/// argument. Changes the usage to `fd [OPTIONS] --search-path <path>
|
||||
/// --search-path <path2> [<pattern>]`
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with("path"),
|
||||
value_name = "search-path",
|
||||
hide_short_help = true,
|
||||
help = "Provides paths to search as an alternative to the positional <path> argument",
|
||||
long_help
|
||||
)]
|
||||
search_path: Vec<PathBuf>,
|
||||
|
||||
/// By default, relative paths are prefixed with './' when -x/--exec,
|
||||
/// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
||||
/// path starting with '-' being treated as a command line option. Use
|
||||
/// this flag to 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
|
||||
/// different file system than the one it started in. Comparable to the -mount
|
||||
/// or -xdev filters of find(1).
|
||||
#[cfg(any(unix, windows))]
|
||||
#[arg(long, aliases(&["mount", "xdev"]), hide_short_help = true, long_help)]
|
||||
pub one_file_system: bool,
|
||||
|
||||
#[cfg(feature = "completions")]
|
||||
#[arg(long, hide = true, exclusive = true)]
|
||||
gen_completions: Option<Option<Shell>>,
|
||||
}
|
||||
|
||||
impl Opts {
|
||||
pub fn search_paths(&self) -> anyhow::Result<Vec<PathBuf>> {
|
||||
// would it make sense to concatenate these?
|
||||
let paths = if !self.path.is_empty() {
|
||||
&self.path
|
||||
} else if !self.search_path.is_empty() {
|
||||
&self.search_path
|
||||
} else {
|
||||
let current_directory = Path::new("./");
|
||||
ensure_current_directory_exists(current_directory)?;
|
||||
return Ok(vec![self.normalize_path(current_directory)]);
|
||||
};
|
||||
Ok(paths
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
if filesystem::is_existing_directory(path) {
|
||||
Some(self.normalize_path(path))
|
||||
} else {
|
||||
print_error(format!(
|
||||
"Search path '{}' is not a directory.",
|
||||
path.to_string_lossy()
|
||||
));
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn normalize_path(&self, path: &Path) -> PathBuf {
|
||||
if self.absolute_path {
|
||||
filesystem::absolute_path(path.normalize().unwrap().as_path()).unwrap()
|
||||
} else if path == Path::new(".") {
|
||||
// Change "." to "./" as a workaround for https://github.com/BurntSushi/ripgrep/pull/2711
|
||||
PathBuf::from("./")
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_search_paths(&self) -> bool {
|
||||
self.path.is_empty() && self.search_path.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rg_alias_ignore(&self) -> bool {
|
||||
self.rg_alias_hidden_ignore > 0
|
||||
}
|
||||
|
||||
pub fn max_depth(&self) -> Option<usize> {
|
||||
self.max_depth.or(self.exact_depth)
|
||||
}
|
||||
|
||||
pub fn min_depth(&self) -> Option<usize> {
|
||||
self.min_depth.or(self.exact_depth)
|
||||
}
|
||||
|
||||
pub fn threads(&self) -> NonZeroUsize {
|
||||
self.threads.unwrap_or_else(default_num_threads)
|
||||
}
|
||||
|
||||
pub fn max_results(&self) -> Option<usize> {
|
||||
self.max_results
|
||||
.filter(|&m| m > 0)
|
||||
.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
|
||||
.map(|maybe_shell| match maybe_shell {
|
||||
Some(sh) => Ok(sh),
|
||||
None => {
|
||||
Shell::from_env().ok_or_else(|| anyhow!("Unable to get shell from environment"))
|
||||
}
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default number of threads to use, if not explicitly specified.
|
||||
fn default_num_threads() -> NonZeroUsize {
|
||||
// If we can't get the amount of parallelism for some reason, then
|
||||
// default to a single thread, because that is safe.
|
||||
let fallback = NonZeroUsize::MIN;
|
||||
// To limit startup overhead on massively parallel machines, don't use more
|
||||
// than 64 threads.
|
||||
let limit = NonZeroUsize::new(64).unwrap();
|
||||
|
||||
std::thread::available_parallelism()
|
||||
.unwrap_or(fallback)
|
||||
.min(limit)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
|
||||
pub enum FileType {
|
||||
#[value(alias = "f")]
|
||||
File,
|
||||
#[value(alias = "d", alias = "dir")]
|
||||
Directory,
|
||||
#[value(alias = "l")]
|
||||
Symlink,
|
||||
#[value(alias = "b")]
|
||||
BlockDevice,
|
||||
#[value(alias = "c")]
|
||||
CharDevice,
|
||||
/// A file which is executable by the current effective user
|
||||
#[value(alias = "x")]
|
||||
Executable,
|
||||
#[value(alias = "e")]
|
||||
Empty,
|
||||
#[value(alias = "s")]
|
||||
Socket,
|
||||
#[value(alias = "p")]
|
||||
Pipe,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
pub enum ColorWhen {
|
||||
/// show colors if the output goes to an interactive console (default)
|
||||
Auto,
|
||||
/// always use colorized output
|
||||
Always,
|
||||
/// do not use colorized output
|
||||
Never,
|
||||
}
|
||||
|
||||
#[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,
|
||||
// so we have to use hand-rolled parsing for exec and exec-batch
|
||||
pub struct Exec {
|
||||
pub command: Option<CommandSet>,
|
||||
}
|
||||
|
||||
impl clap::FromArgMatches for Exec {
|
||||
fn from_arg_matches(matches: &ArgMatches) -> clap::error::Result<Self> {
|
||||
let command = matches
|
||||
.get_occurrences::<String>("exec")
|
||||
.map(CommandSet::new)
|
||||
.or_else(|| {
|
||||
matches
|
||||
.get_occurrences::<String>("exec_batch")
|
||||
.map(CommandSet::new_batch)
|
||||
})
|
||||
.transpose()
|
||||
.map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?;
|
||||
Ok(Exec { command })
|
||||
}
|
||||
|
||||
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> clap::error::Result<()> {
|
||||
*self = Self::from_arg_matches(matches)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl clap::Args for Exec {
|
||||
fn augment_args(cmd: Command) -> Command {
|
||||
cmd.arg(Arg::new("exec")
|
||||
.action(ArgAction::Append)
|
||||
.long("exec")
|
||||
.short('x')
|
||||
.num_args(1..)
|
||||
.allow_hyphen_values(true)
|
||||
.value_terminator(";")
|
||||
.value_name("cmd")
|
||||
.conflicts_with("list_details")
|
||||
.help("Execute a command for each search result")
|
||||
.long_help(
|
||||
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
|
||||
There is no guarantee of the order commands are executed in, and the order should not be depended upon. \
|
||||
All positional arguments following --exec are considered to be arguments to the command - not to fd. \
|
||||
It is therefore recommended to place the '-x'/'--exec' option last.\n\
|
||||
The following placeholders are substituted before the command is executed:\n \
|
||||
'{}': path (of the current search result)\n \
|
||||
'{/}': basename\n \
|
||||
'{//}': parent directory\n \
|
||||
'{.}': path without file extension\n \
|
||||
'{/.}': basename without file extension\n \
|
||||
'{{': literal '{' (for escaping)\n \
|
||||
'}}': literal '}' (for escaping)\n\n\
|
||||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
||||
Examples:\n\n \
|
||||
- find all *.zip files and unzip them:\n\n \
|
||||
fd -e zip -x unzip\n\n \
|
||||
- find *.h and *.cpp files and run \"clang-format -i ..\" for each of them:\n\n \
|
||||
fd -e h -e cpp -x clang-format -i\n\n \
|
||||
- Convert all *.jpg files to *.png files:\n\n \
|
||||
fd -e jpg -x convert {} {.}.png\
|
||||
",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("exec_batch")
|
||||
.action(ArgAction::Append)
|
||||
.long("exec-batch")
|
||||
.short('X')
|
||||
.num_args(1..)
|
||||
.allow_hyphen_values(true)
|
||||
.value_terminator(";")
|
||||
.value_name("cmd")
|
||||
.conflicts_with_all(["exec", "list_details"])
|
||||
.help("Execute a command with all search results at once")
|
||||
.long_help(
|
||||
"Execute the given command once, with all search results as arguments.\n\
|
||||
The order of the arguments is non-deterministic, and should not be relied upon.\n\
|
||||
One of the following placeholders is substituted before the command is executed:\n \
|
||||
'{}': path (of all search results)\n \
|
||||
'{/}': basename\n \
|
||||
'{//}': parent directory\n \
|
||||
'{.}': path without file extension\n \
|
||||
'{/.}': basename without file extension\n \
|
||||
'{{': literal '{' (for escaping)\n \
|
||||
'}}': literal '}' (for escaping)\n\n\
|
||||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
||||
Examples:\n\n \
|
||||
- Find all test_*.py files and open them in your favorite editor:\n\n \
|
||||
fd -g 'test_*.py' -X vim\n\n \
|
||||
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
|
||||
fd -e rs -X wc -l\
|
||||
"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn augment_args_for_update(cmd: Command) -> Command {
|
||||
Self::augment_args(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_millis(arg: &str) -> Result<Duration, std::num::ParseIntError> {
|
||||
Ok(Duration::from_millis(arg.parse()?))
|
||||
}
|
||||
|
||||
fn ensure_current_directory_exists(current_directory: &Path) -> anyhow::Result<()> {
|
||||
if filesystem::is_existing_directory(current_directory) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Could not retrieve current directory (has it been deleted?)."
|
||||
))
|
||||
}
|
||||
}
|
@ -3,12 +3,11 @@ use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use lscolors::LsColors;
|
||||
use regex::bytes::RegexSet;
|
||||
|
||||
use crate::exec::CommandSet;
|
||||
use crate::exec::CommandTemplate;
|
||||
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 {
|
||||
@ -31,9 +30,6 @@ pub struct Config {
|
||||
/// Whether to respect VCS ignore files (`.gitignore`, ..) or not.
|
||||
pub read_vcsignore: bool,
|
||||
|
||||
/// Whether to require a `.git` directory to respect gitignore files.
|
||||
pub require_git_to_read_vcsignore: bool,
|
||||
|
||||
/// Whether to respect the global ignore file or not.
|
||||
pub read_global_ignore: bool,
|
||||
|
||||
@ -75,7 +71,6 @@ 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
|
||||
@ -87,11 +82,8 @@ 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>>,
|
||||
pub command: Option<Arc<CommandTemplate>>,
|
||||
|
||||
/// Maximum number of search results to pass to each `command`. If zero, the number is
|
||||
/// unlimited.
|
||||
@ -119,22 +111,9 @@ pub struct Config {
|
||||
/// The separator used to print file paths.
|
||||
pub path_separator: Option<String>,
|
||||
|
||||
/// The actual separator, either the system default separator or `path_separator`
|
||||
pub actual_path_separator: String,
|
||||
|
||||
/// The maximum number of search results
|
||||
pub max_results: Option<usize>,
|
||||
|
||||
/// 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 {
|
||||
/// Check whether results are being printed.
|
||||
pub fn is_printing(&self) -> bool {
|
||||
self.command.is_none()
|
||||
}
|
||||
}
|
||||
|
155
src/dir_entry.rs
155
src/dir_entry.rs
@ -1,155 +0,0 @@
|
||||
use std::cell::OnceCell;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::{FileType, Metadata};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use lscolors::{Colorable, LsColors, Style};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::filesystem::strip_current_dir;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DirEntryInner {
|
||||
Normal(ignore::DirEntry),
|
||||
BrokenSymlink(PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirEntry {
|
||||
inner: DirEntryInner,
|
||||
metadata: OnceCell<Option<Metadata>>,
|
||||
style: OnceCell<Option<Style>>,
|
||||
}
|
||||
|
||||
impl DirEntry {
|
||||
#[inline]
|
||||
pub fn normal(e: ignore::DirEntry) -> Self {
|
||||
Self {
|
||||
inner: DirEntryInner::Normal(e),
|
||||
metadata: OnceCell::new(),
|
||||
style: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn broken_symlink(path: PathBuf) -> Self {
|
||||
Self {
|
||||
inner: DirEntryInner::BrokenSymlink(path),
|
||||
metadata: OnceCell::new(),
|
||||
style: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
match &self.inner {
|
||||
DirEntryInner::Normal(e) => e.path(),
|
||||
DirEntryInner::BrokenSymlink(pathbuf) => pathbuf.as_path(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_path(self) -> PathBuf {
|
||||
match self.inner {
|
||||
DirEntryInner::Normal(e) => e.into_path(),
|
||||
DirEntryInner::BrokenSymlink(p) => p,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path as it should be presented to the user.
|
||||
pub fn stripped_path(&self, config: &Config) -> &Path {
|
||||
if config.strip_cwd_prefix {
|
||||
strip_current_dir(self.path())
|
||||
} else {
|
||||
self.path()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path as it should be presented to the user.
|
||||
pub fn into_stripped_path(self, config: &Config) -> PathBuf {
|
||||
if config.strip_cwd_prefix {
|
||||
self.stripped_path(config).to_path_buf()
|
||||
} else {
|
||||
self.into_path()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_type(&self) -> Option<FileType> {
|
||||
match &self.inner {
|
||||
DirEntryInner::Normal(e) => e.file_type(),
|
||||
DirEntryInner::BrokenSymlink(_) => self.metadata().map(|m| m.file_type()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata(&self) -> Option<&Metadata> {
|
||||
self.metadata
|
||||
.get_or_init(|| match &self.inner {
|
||||
DirEntryInner::Normal(e) => e.metadata().ok(),
|
||||
DirEntryInner::BrokenSymlink(path) => path.symlink_metadata().ok(),
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub fn depth(&self) -> Option<usize> {
|
||||
match &self.inner {
|
||||
DirEntryInner::Normal(e) => Some(e.depth()),
|
||||
DirEntryInner::BrokenSymlink(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self, ls_colors: &LsColors) -> Option<&Style> {
|
||||
self.style
|
||||
.get_or_init(|| ls_colors.style_for(self).cloned())
|
||||
.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DirEntry {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path() == other.path()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for DirEntry {}
|
||||
|
||||
impl PartialOrd for DirEntry {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for DirEntry {
|
||||
#[inline]
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.path().cmp(other.path())
|
||||
}
|
||||
}
|
||||
|
||||
impl Colorable for DirEntry {
|
||||
fn path(&self) -> PathBuf {
|
||||
self.path().to_owned()
|
||||
}
|
||||
|
||||
fn file_name(&self) -> OsString {
|
||||
let name = match &self.inner {
|
||||
DirEntryInner::Normal(e) => e.file_name(),
|
||||
DirEntryInner::BrokenSymlink(path) => {
|
||||
// Path::file_name() only works if the last component is Normal,
|
||||
// but we want it for all component types, so we open code it.
|
||||
// Copied from LsColors::style_for_path_with_metadata().
|
||||
path.components()
|
||||
.last()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| path.as_os_str())
|
||||
}
|
||||
};
|
||||
name.to_owned()
|
||||
}
|
||||
|
||||
fn file_type(&self) -> Option<FileType> {
|
||||
self.file_type()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<Metadata> {
|
||||
self.metadata().cloned()
|
||||
}
|
||||
}
|
@ -1,109 +1,51 @@
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use argmax::Command;
|
||||
|
||||
use crate::error::print_error;
|
||||
use crate::exit_codes::ExitCode;
|
||||
|
||||
struct Outputs {
|
||||
stdout: Vec<u8>,
|
||||
stderr: Vec<u8>,
|
||||
}
|
||||
struct OutputBuffer<'a> {
|
||||
output_permission: &'a Mutex<()>,
|
||||
outputs: Vec<Outputs>,
|
||||
}
|
||||
|
||||
impl<'a> OutputBuffer<'a> {
|
||||
fn new(output_permission: &'a Mutex<()>) -> Self {
|
||||
Self {
|
||||
output_permission,
|
||||
outputs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, stdout: Vec<u8>, stderr: Vec<u8>) {
|
||||
self.outputs.push(Outputs { stdout, stderr });
|
||||
}
|
||||
|
||||
fn write(self) {
|
||||
// avoid taking the lock if there is nothing to do
|
||||
if self.outputs.is_empty() {
|
||||
return;
|
||||
}
|
||||
// While this lock is active, this thread will be the only thread allowed
|
||||
// to write its outputs.
|
||||
let _lock = self.output_permission.lock().unwrap();
|
||||
|
||||
let stdout = io::stdout();
|
||||
let stderr = io::stderr();
|
||||
|
||||
let mut stdout = stdout.lock();
|
||||
let mut stderr = stderr.lock();
|
||||
|
||||
for output in self.outputs.iter() {
|
||||
let _ = stdout.write_all(&output.stdout);
|
||||
let _ = stderr.write_all(&output.stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a command.
|
||||
pub fn execute_commands<I: Iterator<Item = io::Result<Command>>>(
|
||||
cmds: I,
|
||||
pub fn execute_command(
|
||||
mut cmd: Command,
|
||||
out_perm: &Mutex<()>,
|
||||
enable_output_buffering: bool,
|
||||
) -> ExitCode {
|
||||
let mut output_buffer = OutputBuffer::new(out_perm);
|
||||
for result in cmds {
|
||||
let mut cmd = match result {
|
||||
Ok(cmd) => cmd,
|
||||
Err(e) => return handle_cmd_error(None, e),
|
||||
};
|
||||
// Spawn the supplied command.
|
||||
let output = if enable_output_buffering {
|
||||
cmd.output()
|
||||
} else {
|
||||
// If running on only one thread, don't buffer output
|
||||
// Allows for viewing and interacting with intermediate command output
|
||||
cmd.spawn().and_then(|c| c.wait_with_output())
|
||||
};
|
||||
|
||||
// Spawn the supplied command.
|
||||
let output = if enable_output_buffering {
|
||||
cmd.output()
|
||||
} else {
|
||||
// If running on only one thread, don't buffer output
|
||||
// Allows for viewing and interacting with intermediate command output
|
||||
cmd.spawn().and_then(|c| c.wait_with_output())
|
||||
};
|
||||
// Then wait for the command to exit, if it was spawned.
|
||||
match output {
|
||||
Ok(output) => {
|
||||
// While this lock is active, this thread will be the only thread allowed
|
||||
// to write its outputs.
|
||||
let _lock = out_perm.lock().unwrap();
|
||||
|
||||
// Then wait for the command to exit, if it was spawned.
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if enable_output_buffering {
|
||||
output_buffer.push(output.stdout, output.stderr);
|
||||
}
|
||||
if output.status.code() != Some(0) {
|
||||
output_buffer.write();
|
||||
return ExitCode::GeneralError;
|
||||
}
|
||||
}
|
||||
Err(why) => {
|
||||
output_buffer.write();
|
||||
return handle_cmd_error(Some(&cmd), why);
|
||||
let stdout = io::stdout();
|
||||
let stderr = io::stderr();
|
||||
|
||||
let _ = stdout.lock().write_all(&output.stdout);
|
||||
let _ = stderr.lock().write_all(&output.stderr);
|
||||
|
||||
if output.status.code() == Some(0) {
|
||||
ExitCode::Success
|
||||
} else {
|
||||
ExitCode::GeneralError
|
||||
}
|
||||
}
|
||||
}
|
||||
output_buffer.write();
|
||||
ExitCode::Success
|
||||
}
|
||||
|
||||
pub fn handle_cmd_error(cmd: Option<&Command>, err: io::Error) -> ExitCode {
|
||||
match (cmd, err) {
|
||||
(Some(cmd), err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
print_error(format!(
|
||||
"Command not found: {}",
|
||||
cmd.get_program().to_string_lossy()
|
||||
));
|
||||
Err(ref why) if why.kind() == io::ErrorKind::NotFound => {
|
||||
print_error(format!("Command not found: {:?}", cmd));
|
||||
ExitCode::GeneralError
|
||||
}
|
||||
(_, err) => {
|
||||
print_error(format!("Problem while executing command: {}", err));
|
||||
Err(why) => {
|
||||
print_error(format!("Problem while executing command: {}", why));
|
||||
ExitCode::GeneralError
|
||||
}
|
||||
}
|
||||
|
@ -5,13 +5,13 @@ use crate::filesystem::strip_current_dir;
|
||||
|
||||
/// Removes the parent component of the path
|
||||
pub fn basename(path: &Path) -> &OsStr {
|
||||
path.file_name().unwrap_or(path.as_os_str())
|
||||
path.file_name().unwrap_or_else(|| path.as_os_str())
|
||||
}
|
||||
|
||||
/// Removes the extension from the path
|
||||
pub fn remove_extension(path: &Path) -> OsString {
|
||||
let dirname = dirname(path);
|
||||
let stem = path.file_stem().unwrap_or(path.as_os_str());
|
||||
let stem = path.file_stem().unwrap_or_else(|| path.as_os_str());
|
||||
|
||||
let path = PathBuf::from(dirname).join(stem);
|
||||
|
||||
@ -34,10 +34,10 @@ pub fn dirname(path: &Path) -> OsString {
|
||||
#[cfg(test)]
|
||||
mod path_tests {
|
||||
use super::*;
|
||||
use std::path::MAIN_SEPARATOR_STR;
|
||||
use std::path::MAIN_SEPARATOR;
|
||||
|
||||
fn correct(input: &str) -> String {
|
||||
input.replace('/', MAIN_SEPARATOR_STR)
|
||||
input.replace('/', &MAIN_SEPARATOR.to_string())
|
||||
}
|
||||
|
||||
macro_rules! func_tests {
|
@ -1,67 +1,77 @@
|
||||
use std::sync::Mutex;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::print_error;
|
||||
use crate::exit_codes::{merge_exitcodes, ExitCode};
|
||||
use crate::walk::WorkerResult;
|
||||
|
||||
use super::CommandSet;
|
||||
use super::CommandTemplate;
|
||||
|
||||
/// An event loop that listens for inputs from the `rx` receiver. Each received input will
|
||||
/// generate a command with the supplied command template. The generated command will then
|
||||
/// be executed, and this process will continue until the receiver's sender has closed.
|
||||
pub fn job(
|
||||
results: impl IntoIterator<Item = WorkerResult>,
|
||||
cmd: &CommandSet,
|
||||
out_perm: &Mutex<()>,
|
||||
config: &Config,
|
||||
rx: Arc<Mutex<Receiver<WorkerResult>>>,
|
||||
cmd: Arc<CommandTemplate>,
|
||||
out_perm: Arc<Mutex<()>>,
|
||||
show_filesystem_errors: bool,
|
||||
buffer_output: bool,
|
||||
) -> ExitCode {
|
||||
// Output should be buffered when only running a single thread
|
||||
let buffer_output: bool = config.threads > 1;
|
||||
let mut results: Vec<ExitCode> = Vec::new();
|
||||
loop {
|
||||
// Create a lock on the shared receiver for this thread.
|
||||
let lock = rx.lock().unwrap();
|
||||
|
||||
let mut ret = ExitCode::Success;
|
||||
for result in results {
|
||||
// Obtain the next result from the receiver, else if the channel
|
||||
// has closed, exit from the loop
|
||||
let dir_entry = match result {
|
||||
WorkerResult::Entry(dir_entry) => dir_entry,
|
||||
WorkerResult::Error(err) => {
|
||||
if config.show_filesystem_errors {
|
||||
let value: PathBuf = match lock.recv() {
|
||||
Ok(WorkerResult::Entry(path)) => path,
|
||||
Ok(WorkerResult::Error(err)) => {
|
||||
if show_filesystem_errors {
|
||||
print_error(err.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
// Drop the lock so that other threads can read from the receiver.
|
||||
drop(lock);
|
||||
// Generate a command, execute it and store its exit code.
|
||||
let code = cmd.execute(
|
||||
dir_entry.stripped_path(config),
|
||||
config.path_separator.as_deref(),
|
||||
out_perm,
|
||||
buffer_output,
|
||||
);
|
||||
ret = merge_exitcodes([ret, code]);
|
||||
results.push(cmd.generate_and_execute(&value, Arc::clone(&out_perm), buffer_output))
|
||||
}
|
||||
// Returns error in case of any error.
|
||||
ret
|
||||
merge_exitcodes(results)
|
||||
}
|
||||
|
||||
pub fn batch(
|
||||
results: impl IntoIterator<Item = WorkerResult>,
|
||||
cmd: &CommandSet,
|
||||
config: &Config,
|
||||
rx: Receiver<WorkerResult>,
|
||||
cmd: &CommandTemplate,
|
||||
show_filesystem_errors: bool,
|
||||
buffer_output: bool,
|
||||
limit: usize,
|
||||
) -> ExitCode {
|
||||
let paths = results
|
||||
.into_iter()
|
||||
.filter_map(|worker_result| match worker_result {
|
||||
WorkerResult::Entry(dir_entry) => Some(dir_entry.into_stripped_path(config)),
|
||||
WorkerResult::Error(err) => {
|
||||
if config.show_filesystem_errors {
|
||||
print_error(err.to_string());
|
||||
}
|
||||
None
|
||||
let paths = rx.iter().filter_map(|value| match value {
|
||||
WorkerResult::Entry(path) => Some(path),
|
||||
WorkerResult::Error(err) => {
|
||||
if show_filesystem_errors {
|
||||
print_error(err.to_string());
|
||||
}
|
||||
});
|
||||
None
|
||||
}
|
||||
});
|
||||
if limit == 0 {
|
||||
// no limit
|
||||
return cmd.generate_and_execute_batch(paths, buffer_output);
|
||||
}
|
||||
|
||||
cmd.execute_batch(paths, config.batch_size, config.path_separator.as_deref())
|
||||
let mut exit_codes = Vec::new();
|
||||
let mut peekable = paths.peekable();
|
||||
while peekable.peek().is_some() {
|
||||
let limited = peekable.by_ref().take(limit);
|
||||
let exit_code = cmd.generate_and_execute_batch(limited, buffer_output);
|
||||
exit_codes.push(exit_code);
|
||||
}
|
||||
merge_exitcodes(exit_codes)
|
||||
}
|
||||
|
613
src/exec/mod.rs
613
src/exec/mod.rs
@ -1,24 +1,27 @@
|
||||
mod command;
|
||||
mod input;
|
||||
mod job;
|
||||
mod token;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::iter;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::sync::Mutex;
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::path::{Component, Path, PathBuf, Prefix};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use argmax::Command;
|
||||
use anyhow::{anyhow, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::exit_codes::{merge_exitcodes, ExitCode};
|
||||
use crate::fmt::{FormatTemplate, Token};
|
||||
use crate::exit_codes::ExitCode;
|
||||
|
||||
use self::command::{execute_commands, handle_cmd_error};
|
||||
use self::command::execute_command;
|
||||
use self::input::{basename, dirname, remove_extension};
|
||||
pub use self::job::{batch, job};
|
||||
use self::token::Token;
|
||||
|
||||
/// Execution mode of the command
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ExecutionMode {
|
||||
/// Command is executed for each search result
|
||||
OneByOne,
|
||||
@ -26,231 +29,104 @@ pub enum ExecutionMode {
|
||||
Batch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CommandSet {
|
||||
mode: ExecutionMode,
|
||||
commands: Vec<CommandTemplate>,
|
||||
}
|
||||
|
||||
impl CommandSet {
|
||||
pub fn new<I, T, S>(input: I) -> Result<CommandSet>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
Ok(CommandSet {
|
||||
mode: ExecutionMode::OneByOne,
|
||||
commands: input
|
||||
.into_iter()
|
||||
.map(CommandTemplate::new)
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_batch<I, T, S>(input: I) -> Result<CommandSet>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
Ok(CommandSet {
|
||||
mode: ExecutionMode::Batch,
|
||||
commands: input
|
||||
.into_iter()
|
||||
.map(|args| {
|
||||
let cmd = CommandTemplate::new(args)?;
|
||||
if cmd.number_of_tokens() > 1 {
|
||||
bail!("Only one placeholder allowed for batch commands");
|
||||
}
|
||||
if cmd.args[0].has_tokens() {
|
||||
bail!("First argument of exec-batch is expected to be a fixed executable");
|
||||
}
|
||||
Ok(cmd)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn in_batch_mode(&self) -> bool {
|
||||
self.mode == ExecutionMode::Batch
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
&self,
|
||||
input: &Path,
|
||||
path_separator: Option<&str>,
|
||||
out_perm: &Mutex<()>,
|
||||
buffer_output: bool,
|
||||
) -> ExitCode {
|
||||
let commands = self
|
||||
.commands
|
||||
.iter()
|
||||
.map(|c| c.generate(input, path_separator));
|
||||
execute_commands(commands, out_perm, buffer_output)
|
||||
}
|
||||
|
||||
pub fn execute_batch<I>(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode
|
||||
where
|
||||
I: Iterator<Item = PathBuf>,
|
||||
{
|
||||
let builders: io::Result<Vec<_>> = self
|
||||
.commands
|
||||
.iter()
|
||||
.map(|c| CommandBuilder::new(c, limit))
|
||||
.collect();
|
||||
|
||||
match builders {
|
||||
Ok(mut builders) => {
|
||||
for path in paths {
|
||||
for builder in &mut builders {
|
||||
if let Err(e) = builder.push(&path, path_separator) {
|
||||
return handle_cmd_error(Some(&builder.cmd), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for builder in &mut builders {
|
||||
if let Err(e) = builder.finish() {
|
||||
return handle_cmd_error(Some(&builder.cmd), e);
|
||||
}
|
||||
}
|
||||
|
||||
merge_exitcodes(builders.iter().map(|b| b.exit_code()))
|
||||
}
|
||||
Err(e) => handle_cmd_error(None, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a multi-exec command as it is built.
|
||||
#[derive(Debug)]
|
||||
struct CommandBuilder {
|
||||
pre_args: Vec<OsString>,
|
||||
path_arg: FormatTemplate,
|
||||
post_args: Vec<OsString>,
|
||||
cmd: Command,
|
||||
count: usize,
|
||||
limit: usize,
|
||||
exit_code: ExitCode,
|
||||
}
|
||||
|
||||
impl CommandBuilder {
|
||||
fn new(template: &CommandTemplate, limit: usize) -> io::Result<Self> {
|
||||
let mut pre_args = vec![];
|
||||
let mut path_arg = None;
|
||||
let mut post_args = vec![];
|
||||
|
||||
for arg in &template.args {
|
||||
if arg.has_tokens() {
|
||||
path_arg = Some(arg.clone());
|
||||
} else if path_arg.is_none() {
|
||||
pre_args.push(arg.generate("", None));
|
||||
} else {
|
||||
post_args.push(arg.generate("", None));
|
||||
}
|
||||
}
|
||||
|
||||
let cmd = Self::new_command(&pre_args)?;
|
||||
|
||||
Ok(Self {
|
||||
pre_args,
|
||||
path_arg: path_arg.unwrap(),
|
||||
post_args,
|
||||
cmd,
|
||||
count: 0,
|
||||
limit,
|
||||
exit_code: ExitCode::Success,
|
||||
})
|
||||
}
|
||||
|
||||
fn new_command(pre_args: &[OsString]) -> io::Result<Command> {
|
||||
let mut cmd = Command::new(&pre_args[0]);
|
||||
cmd.stdin(Stdio::inherit());
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.stderr(Stdio::inherit());
|
||||
cmd.try_args(&pre_args[1..])?;
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
fn push(&mut self, path: &Path, separator: Option<&str>) -> io::Result<()> {
|
||||
if self.limit > 0 && self.count >= self.limit {
|
||||
self.finish()?;
|
||||
}
|
||||
|
||||
let arg = self.path_arg.generate(path, separator);
|
||||
if !self
|
||||
.cmd
|
||||
.args_would_fit(iter::once(&arg).chain(&self.post_args))
|
||||
{
|
||||
self.finish()?;
|
||||
}
|
||||
|
||||
self.cmd.try_arg(arg)?;
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> io::Result<()> {
|
||||
if self.count > 0 {
|
||||
self.cmd.try_args(&self.post_args)?;
|
||||
if !self.cmd.status()?.success() {
|
||||
self.exit_code = ExitCode::GeneralError;
|
||||
}
|
||||
|
||||
self.cmd = Self::new_command(&self.pre_args)?;
|
||||
self.count = 0;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exit_code(&self) -> ExitCode {
|
||||
self.exit_code
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a template that is utilized to generate command strings.
|
||||
///
|
||||
/// The template is meant to be coupled with an input in order to generate a command. The
|
||||
/// `generate_and_execute()` method will be used to generate a command and execute it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct CommandTemplate {
|
||||
args: Vec<FormatTemplate>,
|
||||
pub struct CommandTemplate {
|
||||
args: Vec<ArgumentTemplate>,
|
||||
mode: ExecutionMode,
|
||||
path_separator: Option<String>,
|
||||
}
|
||||
|
||||
impl CommandTemplate {
|
||||
fn new<I, S>(input: I) -> Result<CommandTemplate>
|
||||
pub fn new<I, S>(input: I, path_separator: Option<String>) -> CommandTemplate
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
Self::build(input, ExecutionMode::OneByOne, path_separator)
|
||||
}
|
||||
|
||||
pub fn new_batch<I, S>(input: I, path_separator: Option<String>) -> Result<CommandTemplate>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let cmd = Self::build(input, ExecutionMode::Batch, path_separator);
|
||||
if cmd.number_of_tokens() > 1 {
|
||||
return Err(anyhow!("Only one placeholder allowed for batch commands"));
|
||||
}
|
||||
if cmd.args[0].has_tokens() {
|
||||
return Err(anyhow!(
|
||||
"First argument of exec-batch is expected to be a fixed executable"
|
||||
));
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
fn build<I, S>(input: I, mode: ExecutionMode, path_separator: Option<String>) -> CommandTemplate
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
static PLACEHOLDER_PATTERN: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"\{(/?\.?|//)\}").unwrap());
|
||||
|
||||
let mut args = Vec::new();
|
||||
let mut has_placeholder = false;
|
||||
|
||||
for arg in input {
|
||||
let arg = arg.as_ref();
|
||||
|
||||
let tmpl = FormatTemplate::parse(arg);
|
||||
has_placeholder |= tmpl.has_tokens();
|
||||
args.push(tmpl);
|
||||
}
|
||||
let mut tokens = Vec::new();
|
||||
let mut start = 0;
|
||||
|
||||
// We need to check that we have at least one argument, because if not
|
||||
// it will try to execute each file and directory it finds.
|
||||
//
|
||||
// Sadly, clap can't currently handle this for us, see
|
||||
// https://github.com/clap-rs/clap/issues/3542
|
||||
if args.is_empty() {
|
||||
bail!("No executable provided for --exec or --exec-batch");
|
||||
for placeholder in PLACEHOLDER_PATTERN.find_iter(arg) {
|
||||
// Leading text before the placeholder.
|
||||
if placeholder.start() > start {
|
||||
tokens.push(Token::Text(arg[start..placeholder.start()].to_owned()));
|
||||
}
|
||||
|
||||
start = placeholder.end();
|
||||
|
||||
match placeholder.as_str() {
|
||||
"{}" => tokens.push(Token::Placeholder),
|
||||
"{.}" => tokens.push(Token::NoExt),
|
||||
"{/}" => tokens.push(Token::Basename),
|
||||
"{//}" => tokens.push(Token::Parent),
|
||||
"{/.}" => tokens.push(Token::BasenameNoExt),
|
||||
_ => unreachable!("Unhandled placeholder"),
|
||||
}
|
||||
|
||||
has_placeholder = true;
|
||||
}
|
||||
|
||||
// Without a placeholder, the argument is just fixed text.
|
||||
if tokens.is_empty() {
|
||||
args.push(ArgumentTemplate::Text(arg.to_owned()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if start < arg.len() {
|
||||
// Trailing text after last placeholder.
|
||||
tokens.push(Token::Text(arg[start..].to_owned()));
|
||||
}
|
||||
|
||||
args.push(ArgumentTemplate::Tokens(tokens));
|
||||
}
|
||||
|
||||
// If a placeholder token was not supplied, append one at the end of the command.
|
||||
if !has_placeholder {
|
||||
args.push(FormatTemplate::Tokens(vec![Token::Placeholder]));
|
||||
args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder]));
|
||||
}
|
||||
|
||||
Ok(CommandTemplate { args })
|
||||
CommandTemplate {
|
||||
args,
|
||||
mode,
|
||||
path_separator,
|
||||
}
|
||||
}
|
||||
|
||||
fn number_of_tokens(&self) -> usize {
|
||||
@ -260,13 +136,162 @@ impl CommandTemplate {
|
||||
/// Generates and executes a command.
|
||||
///
|
||||
/// Using the internal `args` field, and a supplied `input` variable, a `Command` will be
|
||||
/// build.
|
||||
fn generate(&self, input: &Path, path_separator: Option<&str>) -> io::Result<Command> {
|
||||
let mut cmd = Command::new(self.args[0].generate(input, path_separator));
|
||||
/// build. Once all arguments have been processed, the command is executed.
|
||||
pub fn generate_and_execute(
|
||||
&self,
|
||||
input: &Path,
|
||||
out_perm: Arc<Mutex<()>>,
|
||||
buffer_output: bool,
|
||||
) -> ExitCode {
|
||||
let mut cmd = Command::new(self.args[0].generate(&input, self.path_separator.as_deref()));
|
||||
for arg in &self.args[1..] {
|
||||
cmd.try_arg(arg.generate(input, path_separator))?;
|
||||
cmd.arg(arg.generate(&input, self.path_separator.as_deref()));
|
||||
}
|
||||
Ok(cmd)
|
||||
|
||||
execute_command(cmd, &out_perm, buffer_output)
|
||||
}
|
||||
|
||||
pub fn in_batch_mode(&self) -> bool {
|
||||
self.mode == ExecutionMode::Batch
|
||||
}
|
||||
|
||||
pub fn generate_and_execute_batch<I>(&self, paths: I, buffer_output: bool) -> ExitCode
|
||||
where
|
||||
I: Iterator<Item = PathBuf>,
|
||||
{
|
||||
let mut cmd = Command::new(self.args[0].generate("", None));
|
||||
cmd.stdin(Stdio::inherit());
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.stderr(Stdio::inherit());
|
||||
|
||||
let mut paths: Vec<_> = paths.collect();
|
||||
let mut has_path = false;
|
||||
|
||||
for arg in &self.args[1..] {
|
||||
if arg.has_tokens() {
|
||||
paths.sort();
|
||||
|
||||
// A single `Tokens` is expected
|
||||
// So we can directly consume the iterator once and for all
|
||||
for path in &mut paths {
|
||||
cmd.arg(arg.generate(path, self.path_separator.as_deref()));
|
||||
has_path = true;
|
||||
}
|
||||
} else {
|
||||
cmd.arg(arg.generate("", None));
|
||||
}
|
||||
}
|
||||
|
||||
if has_path {
|
||||
execute_command(cmd, &Mutex::new(()), buffer_output)
|
||||
} else {
|
||||
ExitCode::Success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,27 +299,18 @@ impl CommandTemplate {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn generate_str(template: &CommandTemplate, input: &str) -> Vec<String> {
|
||||
template
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| arg.generate(input, None).into_string().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_with_placeholder() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Text("${SHELL}:".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Placeholder]),
|
||||
]
|
||||
}],
|
||||
CommandTemplate::new(&[&"echo", &"${SHELL}:"], None),
|
||||
CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Text("${SHELL}:".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
path_separator: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -302,15 +318,14 @@ mod tests {
|
||||
#[test]
|
||||
fn tokens_with_no_extension() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec!["echo", "{.}"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
}],
|
||||
CommandTemplate::new(&["echo", "{.}"], None),
|
||||
CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
path_separator: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -318,15 +333,14 @@ mod tests {
|
||||
#[test]
|
||||
fn tokens_with_basename() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec!["echo", "{/}"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Basename]),
|
||||
],
|
||||
}],
|
||||
CommandTemplate::new(&["echo", "{/}"], None),
|
||||
CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Basename]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
path_separator: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -334,15 +348,14 @@ mod tests {
|
||||
#[test]
|
||||
fn tokens_with_parent() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec!["echo", "{//}"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Parent]),
|
||||
],
|
||||
}],
|
||||
CommandTemplate::new(&["echo", "{//}"], None),
|
||||
CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Parent]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
path_separator: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -350,50 +363,33 @@ mod tests {
|
||||
#[test]
|
||||
fn tokens_with_basename_no_extension() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec!["echo", "{/.}"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::BasenameNoExt]),
|
||||
],
|
||||
}],
|
||||
CommandTemplate::new(&["echo", "{/.}"], None),
|
||||
CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
path_separator: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_with_literal_braces() {
|
||||
let template = CommandTemplate::new(vec!["{{}}", "{{", "{.}}"]).unwrap();
|
||||
assert_eq!(
|
||||
generate_str(&template, "foo"),
|
||||
vec!["{}", "{", "{.}", "foo"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_with_literal_braces_and_placeholder() {
|
||||
let template = CommandTemplate::new(vec!["{{{},end}"]).unwrap();
|
||||
assert_eq!(generate_str(&template, "foo"), vec!["{foo,end}"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_multiple() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("cp".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Placeholder]),
|
||||
FormatTemplate::Tokens(vec![
|
||||
Token::BasenameNoExt,
|
||||
Token::Text(".ext".into())
|
||||
]),
|
||||
],
|
||||
}],
|
||||
CommandTemplate::new(&["cp", "{}", "{/.}.ext"], None),
|
||||
CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("cp".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
|
||||
ArgumentTemplate::Tokens(vec![
|
||||
Token::BasenameNoExt,
|
||||
Token::Text(".ext".into())
|
||||
]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
path_separator: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -401,37 +397,26 @@ mod tests {
|
||||
#[test]
|
||||
fn tokens_single_batch() {
|
||||
assert_eq!(
|
||||
CommandSet::new_batch(vec![vec!["echo", "{.}"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
}],
|
||||
CommandTemplate::new_batch(&["echo", "{.}"], None).unwrap(),
|
||||
CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
mode: ExecutionMode::Batch,
|
||||
path_separator: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_multiple_batch() {
|
||||
assert!(CommandSet::new_batch(vec![vec!["echo", "{.}", "{}"]]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_no_args() {
|
||||
assert!(CommandTemplate::new::<Vec<_>, &'static str>(vec![]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_set_no_args() {
|
||||
assert!(CommandSet::new(vec![vec!["echo"], vec![]]).is_err());
|
||||
assert!(CommandTemplate::new_batch(&["echo", "{.}", "{}"], None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_custom_path_separator() {
|
||||
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||
let arg = ArgumentTemplate::Tokens(vec![Token::Placeholder]);
|
||||
macro_rules! check {
|
||||
($input:expr, $expected:expr) => {
|
||||
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
||||
@ -446,7 +431,7 @@ mod tests {
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn generate_custom_path_separator_windows() {
|
||||
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||
let arg = ArgumentTemplate::Tokens(vec![Token::Placeholder]);
|
||||
macro_rules! check {
|
||||
($input:expr, $expected:expr) => {
|
||||
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
||||
|
29
src/exec/token.rs
Normal file
29
src/exec/token.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// Designates what should be written to a buffer
|
||||
///
|
||||
/// Each `Token` contains either text, or a placeholder variant, which will be used to generate
|
||||
/// commands after all tokens for a given command template have been collected.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
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(())
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ use std::process;
|
||||
#[cfg(unix)]
|
||||
use nix::sys::signal::{raise, signal, SigHandler, Signal};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ExitCode {
|
||||
Success,
|
||||
HasResults(bool),
|
||||
|
@ -4,12 +4,12 @@ use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use normpath::PathExt;
|
||||
|
||||
use crate::dir_entry;
|
||||
use crate::walk;
|
||||
|
||||
pub fn path_absolute_form(path: &Path) -> io::Result<PathBuf> {
|
||||
if path.is_absolute() {
|
||||
@ -41,7 +41,17 @@ pub fn is_existing_directory(path: &Path) -> bool {
|
||||
path.is_dir() && (path.file_name().is_some() || path.normalize().is_ok())
|
||||
}
|
||||
|
||||
pub fn is_empty(entry: &dir_entry::DirEntry) -> bool {
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
pub fn is_executable(md: &fs::Metadata) -> bool {
|
||||
md.permissions().mode() & 0o111 != 0
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn is_executable(_: &fs::Metadata) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_empty(entry: &walk::DirEntry) -> bool {
|
||||
if let Some(file_type) = entry.file_type() {
|
||||
if file_type.is_dir() {
|
||||
if let Ok(mut entries) = fs::read_dir(entry.path()) {
|
||||
@ -59,26 +69,6 @@ pub fn is_empty(entry: &dir_entry::DirEntry) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
pub fn is_block_device(ft: fs::FileType) -> bool {
|
||||
ft.is_block_device()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn is_block_device(_: fs::FileType) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
pub fn is_char_device(ft: fs::FileType) -> bool {
|
||||
ft.is_char_device()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn is_char_device(_: fs::FileType) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
pub fn is_socket(ft: fs::FileType) -> bool {
|
||||
ft.is_socket()
|
||||
@ -128,11 +118,13 @@ pub fn strip_current_dir(path: &Path) -> &Path {
|
||||
pub fn default_path_separator() -> Option<String> {
|
||||
if cfg!(windows) {
|
||||
let msystem = env::var("MSYSTEM").ok()?;
|
||||
if !msystem.is_empty() {
|
||||
return Some("/".to_owned());
|
||||
match msystem.as_str() {
|
||||
"MINGW64" | "MINGW32" | "MSYS" => Some("/".to_owned()),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,7 +1,5 @@
|
||||
use crate::dir_entry;
|
||||
use crate::filesystem;
|
||||
|
||||
use faccess::PathExt;
|
||||
use crate::walk;
|
||||
|
||||
/// Whether or not to show
|
||||
#[derive(Default)]
|
||||
@ -9,8 +7,6 @@ pub struct FileTypes {
|
||||
pub files: bool,
|
||||
pub directories: bool,
|
||||
pub symlinks: bool,
|
||||
pub block_devices: bool,
|
||||
pub char_devices: bool,
|
||||
pub sockets: bool,
|
||||
pub pipes: bool,
|
||||
pub executables_only: bool,
|
||||
@ -18,22 +14,22 @@ pub struct FileTypes {
|
||||
}
|
||||
|
||||
impl FileTypes {
|
||||
pub fn should_ignore(&self, entry: &dir_entry::DirEntry) -> bool {
|
||||
pub fn should_ignore(&self, entry: &walk::DirEntry) -> bool {
|
||||
if let Some(ref entry_type) = entry.file_type() {
|
||||
(!self.files && entry_type.is_file())
|
||||
|| (!self.directories && entry_type.is_dir())
|
||||
|| (!self.symlinks && entry_type.is_symlink())
|
||||
|| (!self.block_devices && filesystem::is_block_device(*entry_type))
|
||||
|| (!self.char_devices && filesystem::is_char_device(*entry_type))
|
||||
|| (!self.sockets && filesystem::is_socket(*entry_type))
|
||||
|| (!self.pipes && filesystem::is_pipe(*entry_type))
|
||||
|| (self.executables_only && !entry.path().executable())
|
||||
|| (self.executables_only
|
||||
&& !entry
|
||||
.metadata()
|
||||
.map(filesystem::is_executable)
|
||||
.unwrap_or(false))
|
||||
|| (self.empty_only && !filesystem::is_empty(entry))
|
||||
|| !(entry_type.is_file()
|
||||
|| entry_type.is_dir()
|
||||
|| entry_type.is_symlink()
|
||||
|| filesystem::is_block_device(*entry_type)
|
||||
|| filesystem::is_char_device(*entry_type)
|
||||
|| filesystem::is_socket(*entry_type)
|
||||
|| filesystem::is_pipe(*entry_type))
|
||||
} else {
|
||||
|
@ -1,14 +1,13 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use nix::unistd::{Group, User};
|
||||
use std::fs;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct OwnerFilter {
|
||||
uid: Check<u32>,
|
||||
gid: Check<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Check<T> {
|
||||
Equal(T),
|
||||
NotEq(T),
|
||||
@ -16,15 +15,10 @@ enum Check<T> {
|
||||
}
|
||||
|
||||
impl OwnerFilter {
|
||||
const IGNORE: Self = OwnerFilter {
|
||||
uid: Check::Ignore,
|
||||
gid: Check::Ignore,
|
||||
};
|
||||
|
||||
/// Parses an owner constraint
|
||||
/// Returns an error if the string is invalid
|
||||
/// Returns Ok(None) when string is acceptable but a noop (such as "" or ":")
|
||||
pub fn from_string(input: &str) -> Result<Self> {
|
||||
pub fn from_string(input: &str) -> Result<Option<Self>> {
|
||||
let mut it = input.split(':');
|
||||
let (fst, snd) = (it.next(), it.next());
|
||||
|
||||
@ -36,33 +30,22 @@ impl OwnerFilter {
|
||||
}
|
||||
|
||||
let uid = Check::parse(fst, |s| {
|
||||
if let Ok(uid) = s.parse() {
|
||||
Ok(uid)
|
||||
} else {
|
||||
User::from_name(s)?
|
||||
.map(|user| user.uid.as_raw())
|
||||
.ok_or_else(|| anyhow!("'{}' is not a recognized user name", s))
|
||||
}
|
||||
s.parse()
|
||||
.ok()
|
||||
.or_else(|| users::get_user_by_name(s).map(|user| user.uid()))
|
||||
.ok_or_else(|| anyhow!("'{}' is not a recognized user name", s))
|
||||
})?;
|
||||
let gid = Check::parse(snd, |s| {
|
||||
if let Ok(gid) = s.parse() {
|
||||
Ok(gid)
|
||||
} else {
|
||||
Group::from_name(s)?
|
||||
.map(|group| group.gid.as_raw())
|
||||
.ok_or_else(|| anyhow!("'{}' is not a recognized group name", s))
|
||||
}
|
||||
s.parse()
|
||||
.ok()
|
||||
.or_else(|| users::get_group_by_name(s).map(|group| group.gid()))
|
||||
.ok_or_else(|| anyhow!("'{}' is not a recognized group name", s))
|
||||
})?;
|
||||
|
||||
Ok(OwnerFilter { uid, gid })
|
||||
}
|
||||
|
||||
/// If self is a no-op (ignore both uid and gid) then return `None`, otherwise wrap in a `Some`
|
||||
pub fn filter_ignore(self) -> Option<Self> {
|
||||
if self == Self::IGNORE {
|
||||
None
|
||||
if let (Check::Ignore, Check::Ignore) = (uid, gid) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Some(self)
|
||||
Ok(Some(OwnerFilter { uid, gid }))
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,16 +106,16 @@ mod owner_parsing {
|
||||
|
||||
use super::Check::*;
|
||||
owner_tests! {
|
||||
empty: "" => Ok(OwnerFilter::IGNORE),
|
||||
uid_only: "5" => Ok(OwnerFilter { uid: Equal(5), gid: Ignore }),
|
||||
uid_gid: "9:3" => Ok(OwnerFilter { uid: Equal(9), gid: Equal(3) }),
|
||||
gid_only: ":8" => Ok(OwnerFilter { uid: Ignore, gid: Equal(8) }),
|
||||
colon_only: ":" => Ok(OwnerFilter::IGNORE),
|
||||
trailing: "5:" => Ok(OwnerFilter { uid: Equal(5), gid: Ignore }),
|
||||
empty: "" => Ok(None),
|
||||
uid_only: "5" => Ok(Some(OwnerFilter { uid: Equal(5), gid: Ignore })),
|
||||
uid_gid: "9:3" => Ok(Some(OwnerFilter { uid: Equal(9), gid: Equal(3) })),
|
||||
gid_only: ":8" => Ok(Some(OwnerFilter { uid: Ignore, gid: Equal(8) })),
|
||||
colon_only: ":" => Ok(None),
|
||||
trailing: "5:" => Ok(Some(OwnerFilter { uid: Equal(5), gid: Ignore })),
|
||||
|
||||
uid_negate: "!5" => Ok(OwnerFilter { uid: NotEq(5), gid: Ignore }),
|
||||
both_negate:"!4:!3" => Ok(OwnerFilter { uid: NotEq(4), gid: NotEq(3) }),
|
||||
uid_not_gid:"6:!8" => Ok(OwnerFilter { uid: Equal(6), gid: NotEq(8) }),
|
||||
uid_negate: "!5" => Ok(Some(OwnerFilter { uid: NotEq(5), gid: Ignore })),
|
||||
both_negate:"!4:!3" => Ok(Some(OwnerFilter { uid: NotEq(4), gid: NotEq(3) })),
|
||||
uid_not_gid:"6:!8" => Ok(Some(OwnerFilter { uid: Equal(6), gid: NotEq(8) })),
|
||||
|
||||
more_colons:"3:5:" => Err(_),
|
||||
only_colons:"::" => Err(_),
|
||||
|
@ -1,11 +1,10 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
static SIZE_CAPTURES: OnceLock<Regex> = OnceLock::new();
|
||||
static SIZE_CAPTURES: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?i)^([+-]?)(\d+)(b|[kmgt]i?b?)$").unwrap());
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum SizeFilter {
|
||||
Max(u64),
|
||||
Min(u64),
|
||||
@ -25,19 +24,12 @@ const GIBI: u64 = MEBI * 1024;
|
||||
const TEBI: u64 = GIBI * 1024;
|
||||
|
||||
impl SizeFilter {
|
||||
pub fn from_string(s: &str) -> anyhow::Result<Self> {
|
||||
SizeFilter::parse_opt(s)
|
||||
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", s))
|
||||
}
|
||||
|
||||
fn parse_opt(s: &str) -> Option<Self> {
|
||||
let pattern =
|
||||
SIZE_CAPTURES.get_or_init(|| Regex::new(r"(?i)^([+-]?)(\d+)(b|[kmgt]i?b?)$").unwrap());
|
||||
if !pattern.is_match(s) {
|
||||
pub fn from_string(s: &str) -> Option<Self> {
|
||||
if !SIZE_CAPTURES.is_match(s) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let captures = pattern.captures(s)?;
|
||||
let captures = SIZE_CAPTURES.captures(s)?;
|
||||
let limit_kind = captures.get(1).map_or("+", |m| m.as_str());
|
||||
let quantity = captures
|
||||
.get(2)
|
||||
@ -173,7 +165,7 @@ mod tests {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let i = SizeFilter::from_string($value);
|
||||
assert!(i.is_err());
|
||||
assert!(i.is_none());
|
||||
}
|
||||
)*
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
use chrono::{DateTime, Local, NaiveDate, NaiveDateTime};
|
||||
use chrono::{offset::TimeZone, DateTime, Local, NaiveDate};
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// Filter based on time ranges.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum TimeFilter {
|
||||
Before(SystemTime),
|
||||
After(SystemTime),
|
||||
@ -20,21 +20,11 @@ impl TimeFilter {
|
||||
.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()
|
||||
})
|
||||
.or_else(|| {
|
||||
let timestamp_secs = s.strip_prefix('@')?.parse().ok()?;
|
||||
DateTime::from_timestamp(timestamp_secs, 0).map(Into::into)
|
||||
.map(|nd| nd.and_hms(0, 0, 0))
|
||||
.ok()
|
||||
.and_then(|ndt| Local.from_local_datetime(&ndt).single())
|
||||
})
|
||||
.or_else(|| Local.datetime_from_str(s, "%F %T").ok())
|
||||
.map(|dt| dt.into())
|
||||
})
|
||||
}
|
||||
@ -62,10 +52,8 @@ mod tests {
|
||||
|
||||
#[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()
|
||||
let ref_time = Local
|
||||
.datetime_from_str("2010-10-10 10:10:10", "%F %T")
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
@ -139,32 +127,5 @@ mod tests {
|
||||
assert!(!TimeFilter::after(&ref_time, t10s_before)
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago));
|
||||
|
||||
let ref_timestamp = 1707723412u64; // Mon Feb 12 07:36:52 UTC 2024
|
||||
let ref_time = DateTime::parse_from_rfc3339("2024-02-12T07:36:52+00:00")
|
||||
.unwrap()
|
||||
.into();
|
||||
let t1m_ago = ref_time - Duration::from_secs(60);
|
||||
let t1s_later = ref_time + Duration::from_secs(1);
|
||||
// Timestamp only supported via '@' prefix
|
||||
assert!(TimeFilter::before(&ref_time, &ref_timestamp.to_string()).is_none());
|
||||
assert!(
|
||||
TimeFilter::before(&ref_time, &format!("@{}", ref_timestamp))
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago)
|
||||
);
|
||||
assert!(
|
||||
!TimeFilter::before(&ref_time, &format!("@{}", ref_timestamp))
|
||||
.unwrap()
|
||||
.applies_to(&t1s_later)
|
||||
);
|
||||
assert!(
|
||||
!TimeFilter::after(&ref_time, &format!("@{}", ref_timestamp))
|
||||
.unwrap()
|
||||
.applies_to(&t1m_ago)
|
||||
);
|
||||
assert!(TimeFilter::after(&ref_time, &format!("@{}", ref_timestamp))
|
||||
.unwrap()
|
||||
.applies_to(&t1s_later));
|
||||
}
|
||||
}
|
||||
|
281
src/fmt/mod.rs
281
src/fmt/mod.rs
@ -1,281 +0,0 @@
|
||||
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 }"
|
||||
);
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
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, "%{:02X}", byte)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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",
|
||||
);
|
||||
}
|
||||
}
|
446
src/main.rs
446
src/main.rs
@ -1,38 +1,35 @@
|
||||
mod cli;
|
||||
mod app;
|
||||
mod config;
|
||||
mod dir_entry;
|
||||
mod error;
|
||||
mod exec;
|
||||
mod exit_codes;
|
||||
mod filesystem;
|
||||
mod filetypes;
|
||||
mod filter;
|
||||
mod fmt;
|
||||
mod hyperlink;
|
||||
mod output;
|
||||
mod regex_helper;
|
||||
mod walk;
|
||||
|
||||
use std::env;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use atty::Stream;
|
||||
use globset::GlobBuilder;
|
||||
use lscolors::LsColors;
|
||||
use regex::bytes::{Regex, RegexBuilder, RegexSetBuilder};
|
||||
use normpath::PathExt;
|
||||
use regex::bytes::{RegexBuilder, RegexSetBuilder};
|
||||
|
||||
use crate::cli::{ColorWhen, HyperlinkWhen, Opts};
|
||||
use crate::config::Config;
|
||||
use crate::exec::CommandSet;
|
||||
use crate::error::print_error;
|
||||
use crate::exec::CommandTemplate;
|
||||
use crate::exit_codes::ExitCode;
|
||||
use crate::filetypes::FileTypes;
|
||||
#[cfg(unix)]
|
||||
use crate::filter::OwnerFilter;
|
||||
use crate::filter::TimeFilter;
|
||||
use crate::filter::{SizeFilter, TimeFilter};
|
||||
use crate::regex_helper::{pattern_has_uppercase_char, pattern_matches_strings_with_leading_dot};
|
||||
|
||||
// We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/481
|
||||
@ -42,8 +39,7 @@ use crate::regex_helper::{pattern_has_uppercase_char, pattern_matches_strings_wi
|
||||
not(target_os = "android"),
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "freebsd"),
|
||||
not(target_os = "openbsd"),
|
||||
not(all(target_env = "musl", target_pointer_width = "32")),
|
||||
not(target_env = "musl"),
|
||||
not(target_arch = "riscv64"),
|
||||
feature = "use-jemalloc"
|
||||
))]
|
||||
@ -70,63 +66,26 @@ fn main() {
|
||||
}
|
||||
|
||||
fn run() -> Result<ExitCode> {
|
||||
let opts = Opts::parse();
|
||||
let matches = app::build_app().get_matches_from(env::args_os());
|
||||
|
||||
#[cfg(feature = "completions")]
|
||||
if let Some(shell) = opts.gen_completions()? {
|
||||
return print_completions(shell);
|
||||
}
|
||||
set_working_dir(&matches)?;
|
||||
let current_directory = Path::new(".");
|
||||
ensure_current_directory_exists(current_directory)?;
|
||||
let search_paths = extract_search_paths(&matches, current_directory)?;
|
||||
|
||||
set_working_dir(&opts)?;
|
||||
let search_paths = opts.search_paths()?;
|
||||
if search_paths.is_empty() {
|
||||
bail!("No valid search paths given.");
|
||||
}
|
||||
let pattern = extract_search_pattern(&matches)?;
|
||||
ensure_search_pattern_is_not_a_path(&matches, pattern)?;
|
||||
let pattern_regex = build_pattern_regex(&matches, pattern)?;
|
||||
|
||||
ensure_search_pattern_is_not_a_path(&opts)?;
|
||||
let pattern = &opts.pattern;
|
||||
let exprs = &opts.exprs;
|
||||
let empty = Vec::new();
|
||||
|
||||
let pattern_regexps = exprs
|
||||
.as_ref()
|
||||
.unwrap_or(&empty)
|
||||
.iter()
|
||||
.chain([pattern])
|
||||
.map(|pat| build_pattern_regex(pat, &opts))
|
||||
.collect::<Result<Vec<String>>>()?;
|
||||
|
||||
let config = construct_config(opts, &pattern_regexps)?;
|
||||
|
||||
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regexps)?;
|
||||
|
||||
let regexps = pattern_regexps
|
||||
.into_iter()
|
||||
.map(|pat| build_regex(pat, &config))
|
||||
.collect::<Result<Vec<Regex>>>()?;
|
||||
|
||||
walk::scan(&search_paths, regexps, config)
|
||||
let config = construct_config(matches, &pattern_regex)?;
|
||||
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regex)?;
|
||||
let re = build_regex(pattern_regex, &config)?;
|
||||
walk::scan(&search_paths, Arc::new(re), Arc::new(config))
|
||||
}
|
||||
|
||||
#[cfg(feature = "completions")]
|
||||
#[cold]
|
||||
fn print_completions(shell: clap_complete::Shell) -> Result<ExitCode> {
|
||||
// The program name is the first argument.
|
||||
let first_arg = env::args().next();
|
||||
let program_name = first_arg
|
||||
.as_ref()
|
||||
.map(Path::new)
|
||||
.and_then(|path| path.file_stem())
|
||||
.and_then(|file| file.to_str())
|
||||
.unwrap_or("fd");
|
||||
let mut cmd = Opts::command();
|
||||
cmd.build();
|
||||
clap_complete::generate(shell, &mut cmd, program_name, &mut std::io::stdout());
|
||||
Ok(ExitCode::Success)
|
||||
}
|
||||
|
||||
fn set_working_dir(opts: &Opts) -> Result<()> {
|
||||
if let Some(ref base_directory) = opts.base_directory {
|
||||
fn set_working_dir(matches: &clap::ArgMatches) -> Result<()> {
|
||||
if let Some(base_directory) = matches.value_of_os("base-directory") {
|
||||
let base_directory = Path::new(base_directory);
|
||||
if !filesystem::is_existing_directory(base_directory) {
|
||||
return Err(anyhow!(
|
||||
"The '--base-directory' path '{}' is not a directory.",
|
||||
@ -143,11 +102,74 @@ fn set_working_dir(opts: &Opts) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_current_directory_exists(current_directory: &Path) -> Result<()> {
|
||||
if filesystem::is_existing_directory(current_directory) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Could not retrieve current directory (has it been deleted?)."
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_search_pattern<'a>(matches: &'a clap::ArgMatches) -> Result<&'a str> {
|
||||
let pattern = matches
|
||||
.value_of_os("pattern")
|
||||
.map(|p| {
|
||||
p.to_str()
|
||||
.ok_or_else(|| anyhow!("The search pattern includes invalid UTF-8 sequences."))
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or("");
|
||||
Ok(pattern)
|
||||
}
|
||||
|
||||
fn extract_search_paths(
|
||||
matches: &clap::ArgMatches,
|
||||
current_directory: &Path,
|
||||
) -> Result<Vec<PathBuf>> {
|
||||
let mut search_paths = matches
|
||||
.values_of_os("path")
|
||||
.or_else(|| matches.values_of_os("search-path"))
|
||||
.map_or_else(
|
||||
|| vec![current_directory.to_path_buf()],
|
||||
|paths| {
|
||||
paths
|
||||
.filter_map(|path| {
|
||||
let path_buffer = PathBuf::from(path);
|
||||
if filesystem::is_existing_directory(&path_buffer) {
|
||||
Some(path_buffer)
|
||||
} else {
|
||||
print_error(format!(
|
||||
"Search path '{}' is not a directory.",
|
||||
path_buffer.to_string_lossy()
|
||||
));
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
if search_paths.is_empty() {
|
||||
return Err(anyhow!("No valid search paths given."));
|
||||
}
|
||||
if matches.is_present("absolute-path") {
|
||||
update_to_absolute_paths(&mut search_paths);
|
||||
}
|
||||
Ok(search_paths)
|
||||
}
|
||||
|
||||
fn update_to_absolute_paths(search_paths: &mut [PathBuf]) {
|
||||
for buffer in search_paths.iter_mut() {
|
||||
*buffer = filesystem::absolute_path(buffer.normalize().unwrap().as_path()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect if the user accidentally supplied a path instead of a search pattern
|
||||
fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
|
||||
if !opts.full_path
|
||||
&& opts.pattern.contains(std::path::MAIN_SEPARATOR)
|
||||
&& Path::new(&opts.pattern).is_dir()
|
||||
fn ensure_search_pattern_is_not_a_path(matches: &clap::ArgMatches, pattern: &str) -> Result<()> {
|
||||
if !matches.is_present("full-path")
|
||||
&& pattern.contains(std::path::MAIN_SEPARATOR)
|
||||
&& Path::new(pattern).is_dir()
|
||||
{
|
||||
Err(anyhow!(
|
||||
"The search pattern '{pattern}' contains a path-separation character ('{sep}') \
|
||||
@ -156,7 +178,7 @@ fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
|
||||
fd . '{pattern}'\n\n\
|
||||
Instead, if you want your pattern to match the full file path, use:\n\n \
|
||||
fd --full-path '{pattern}'",
|
||||
pattern = &opts.pattern,
|
||||
pattern = pattern,
|
||||
sep = std::path::MAIN_SEPARATOR,
|
||||
))
|
||||
} else {
|
||||
@ -164,11 +186,11 @@ fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_pattern_regex(pattern: &str, opts: &Opts) -> Result<String> {
|
||||
Ok(if opts.glob && !pattern.is_empty() {
|
||||
fn build_pattern_regex(matches: &clap::ArgMatches, pattern: &str) -> Result<String> {
|
||||
Ok(if matches.is_present("glob") && !pattern.is_empty() {
|
||||
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
|
||||
glob.regex().to_owned()
|
||||
} else if opts.fixed_strings {
|
||||
} else if matches.is_present("fixed-strings") {
|
||||
// Treat pattern as literal string if '--fixed-strings' is used
|
||||
regex::escape(pattern)
|
||||
} else {
|
||||
@ -190,44 +212,37 @@ fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config> {
|
||||
fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Config> {
|
||||
// The search will be case-sensitive if the command line flag is set or
|
||||
// if any of the patterns has an uppercase character (smart case).
|
||||
let case_sensitive = !opts.ignore_case
|
||||
&& (opts.case_sensitive
|
||||
|| pattern_regexps
|
||||
.iter()
|
||||
.any(|pat| pattern_has_uppercase_char(pat)));
|
||||
// if the pattern has an uppercase character (smart case).
|
||||
let case_sensitive = !matches.is_present("ignore-case")
|
||||
&& (matches.is_present("case-sensitive") || pattern_has_uppercase_char(pattern_regex));
|
||||
|
||||
let path_separator = opts
|
||||
.path_separator
|
||||
.take()
|
||||
.or_else(filesystem::default_path_separator);
|
||||
let actual_path_separator = path_separator
|
||||
.clone()
|
||||
.unwrap_or_else(|| std::path::MAIN_SEPARATOR.to_string());
|
||||
let path_separator = matches
|
||||
.value_of("path-separator")
|
||||
.map_or_else(filesystem::default_path_separator, |s| Some(s.to_owned()));
|
||||
check_path_separator_length(path_separator.as_deref())?;
|
||||
|
||||
let size_limits = std::mem::take(&mut opts.size);
|
||||
let time_constraints = extract_time_constraints(&opts)?;
|
||||
let size_limits = extract_size_limits(&matches)?;
|
||||
let time_constraints = extract_time_constraints(&matches)?;
|
||||
#[cfg(unix)]
|
||||
let owner_constraint: Option<OwnerFilter> = opts.owner.and_then(OwnerFilter::filter_ignore);
|
||||
let owner_constraint = matches
|
||||
.value_of("owner")
|
||||
.map(OwnerFilter::from_string)
|
||||
.transpose()?
|
||||
.flatten();
|
||||
|
||||
#[cfg(windows)]
|
||||
let ansi_colors_support =
|
||||
nu_ansi_term::enable_ansi_support().is_ok() || std::env::var_os("TERM").is_some();
|
||||
ansi_term::enable_ansi_support().is_ok() || std::env::var_os("TERM").is_some();
|
||||
#[cfg(not(windows))]
|
||||
let ansi_colors_support = true;
|
||||
|
||||
let interactive_terminal = std::io::stdout().is_terminal();
|
||||
|
||||
let colored_output = match opts.color {
|
||||
ColorWhen::Always => true,
|
||||
ColorWhen::Never => false,
|
||||
ColorWhen::Auto => {
|
||||
let no_color = env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty());
|
||||
ansi_colors_support && !no_color && interactive_terminal
|
||||
}
|
||||
let interactive_terminal = atty::is(Stream::Stdout);
|
||||
let colored_output = match matches.value_of("color") {
|
||||
Some("always") => true,
|
||||
Some("never") => false,
|
||||
_ => ansi_colors_support && env::var_os("NO_COLOR").is_none() && interactive_terminal,
|
||||
};
|
||||
|
||||
let ls_colors = if colored_output {
|
||||
@ -235,54 +250,80 @@ 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();
|
||||
let command = extract_command(&matches, path_separator.as_deref(), colored_output)?;
|
||||
|
||||
Ok(Config {
|
||||
case_sensitive,
|
||||
search_full_path: opts.full_path,
|
||||
ignore_hidden: !(opts.hidden || opts.rg_alias_ignore()),
|
||||
read_fdignore: !(opts.no_ignore || opts.rg_alias_ignore()),
|
||||
read_vcsignore: !(opts.no_ignore || opts.rg_alias_ignore() || opts.no_ignore_vcs),
|
||||
require_git_to_read_vcsignore: !opts.no_require_git,
|
||||
read_parent_ignore: !opts.no_ignore_parent,
|
||||
read_global_ignore: !(opts.no_ignore
|
||||
|| opts.rg_alias_ignore()
|
||||
|| opts.no_global_ignore_file),
|
||||
follow_links: opts.follow,
|
||||
one_file_system: opts.one_file_system,
|
||||
null_separator: opts.null_separator,
|
||||
quiet: opts.quiet,
|
||||
max_depth: opts.max_depth(),
|
||||
min_depth: opts.min_depth(),
|
||||
prune: opts.prune,
|
||||
threads: opts.threads().get(),
|
||||
max_buffer_time: opts.max_buffer_time,
|
||||
search_full_path: matches.is_present("full-path"),
|
||||
ignore_hidden: !(matches.is_present("hidden")
|
||||
|| matches.occurrences_of("rg-alias-hidden-ignore") >= 2),
|
||||
read_fdignore: !(matches.is_present("no-ignore")
|
||||
|| matches.is_present("rg-alias-hidden-ignore")),
|
||||
read_vcsignore: !(matches.is_present("no-ignore")
|
||||
|| matches.is_present("rg-alias-hidden-ignore")
|
||||
|| matches.is_present("no-ignore-vcs")),
|
||||
read_parent_ignore: !matches.is_present("no-ignore-parent"),
|
||||
read_global_ignore: !(matches.is_present("no-ignore")
|
||||
|| matches.is_present("rg-alias-hidden-ignore")
|
||||
|| matches.is_present("no-global-ignore-file")),
|
||||
follow_links: matches.is_present("follow"),
|
||||
one_file_system: matches.is_present("one-file-system"),
|
||||
null_separator: matches.is_present("null_separator"),
|
||||
quiet: matches.is_present("quiet"),
|
||||
max_depth: matches
|
||||
.value_of("max-depth")
|
||||
.or_else(|| matches.value_of("rg-depth"))
|
||||
.or_else(|| matches.value_of("exact-depth"))
|
||||
.map(|n| n.parse::<usize>())
|
||||
.transpose()
|
||||
.context("Failed to parse argument to --max-depth/--exact-depth")?,
|
||||
min_depth: matches
|
||||
.value_of("min-depth")
|
||||
.or_else(|| matches.value_of("exact-depth"))
|
||||
.map(|n| n.parse::<usize>())
|
||||
.transpose()
|
||||
.context("Failed to parse argument to --min-depth/--exact-depth")?,
|
||||
prune: matches.is_present("prune"),
|
||||
threads: std::cmp::max(
|
||||
matches
|
||||
.value_of("threads")
|
||||
.map(|n| n.parse::<usize>())
|
||||
.transpose()
|
||||
.context("Failed to parse number of threads")?
|
||||
.map(|n| {
|
||||
if n > 0 {
|
||||
Ok(n)
|
||||
} else {
|
||||
Err(anyhow!("Number of threads must be positive."))
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_else(num_cpus::get),
|
||||
1,
|
||||
),
|
||||
max_buffer_time: matches
|
||||
.value_of("max-buffer-time")
|
||||
.map(|n| n.parse::<u64>())
|
||||
.transpose()
|
||||
.context("Failed to parse max. buffer time argument")?
|
||||
.map(time::Duration::from_millis),
|
||||
ls_colors,
|
||||
hyperlink,
|
||||
interactive_terminal,
|
||||
file_types: opts.filetype.as_ref().map(|values| {
|
||||
use crate::cli::FileType::*;
|
||||
file_types: matches.values_of("file-type").map(|values| {
|
||||
let mut file_types = FileTypes::default();
|
||||
for value in values {
|
||||
match value {
|
||||
File => file_types.files = true,
|
||||
Directory => file_types.directories = true,
|
||||
Symlink => file_types.symlinks = true,
|
||||
Executable => {
|
||||
"f" | "file" => file_types.files = true,
|
||||
"d" | "directory" => file_types.directories = true,
|
||||
"l" | "symlink" => file_types.symlinks = true,
|
||||
"x" | "executable" => {
|
||||
file_types.executables_only = true;
|
||||
file_types.files = true;
|
||||
}
|
||||
Empty => file_types.empty_only = true,
|
||||
BlockDevice => file_types.block_devices = true,
|
||||
CharDevice => file_types.char_devices = true,
|
||||
Socket => file_types.sockets = true,
|
||||
Pipe => file_types.pipes = true,
|
||||
"e" | "empty" => file_types.empty_only = true,
|
||||
"s" | "socket" => file_types.sockets = true,
|
||||
"p" | "pipe" => file_types.pipes = true,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -294,12 +335,10 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
|
||||
|
||||
file_types
|
||||
}),
|
||||
extensions: opts
|
||||
.extensions
|
||||
.as_ref()
|
||||
extensions: matches
|
||||
.values_of("extension")
|
||||
.map(|exts| {
|
||||
let patterns = exts
|
||||
.iter()
|
||||
.map(|e| e.trim_start_matches('.'))
|
||||
.map(|e| format!(r".\.{}$", regex::escape(e)));
|
||||
RegexSetBuilder::new(patterns)
|
||||
@ -307,51 +346,84 @@ 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(),
|
||||
ignore_files: std::mem::take(&mut opts.ignore_file),
|
||||
batch_size: matches
|
||||
.value_of("batch-size")
|
||||
.map(|n| n.parse::<usize>())
|
||||
.transpose()
|
||||
.context("Failed to parse --batch-size argument")?
|
||||
.unwrap_or_default(),
|
||||
exclude_patterns: matches
|
||||
.values_of("exclude")
|
||||
.map(|v| v.map(|p| String::from("!") + p).collect())
|
||||
.unwrap_or_else(Vec::new),
|
||||
ignore_files: matches
|
||||
.values_of("ignore-file")
|
||||
.map(|vs| vs.map(PathBuf::from).collect())
|
||||
.unwrap_or_else(Vec::new),
|
||||
size_constraints: size_limits,
|
||||
time_constraints,
|
||||
#[cfg(unix)]
|
||||
owner_constraint,
|
||||
show_filesystem_errors: opts.show_errors,
|
||||
show_filesystem_errors: matches.is_present("show-errors"),
|
||||
path_separator,
|
||||
actual_path_separator,
|
||||
max_results: opts.max_results(),
|
||||
strip_cwd_prefix: opts.strip_cwd_prefix(|| !(opts.null_separator || has_command)),
|
||||
max_results: matches
|
||||
.value_of("max-results")
|
||||
.map(|n| n.parse::<usize>())
|
||||
.transpose()
|
||||
.context("Failed to parse --max-results argument")?
|
||||
.filter(|&n| n > 0)
|
||||
.or_else(|| {
|
||||
if matches.is_present("max-one-result") {
|
||||
Some(1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
strip_cwd_prefix: (!matches.is_present("path")
|
||||
&& !matches.is_present("search-path")
|
||||
&& (interactive_terminal || matches.is_present("strip-cwd-prefix"))),
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_command(opts: &mut Opts, colored_output: bool) -> Result<Option<CommandSet>> {
|
||||
opts.exec
|
||||
.command
|
||||
.take()
|
||||
.map(Ok)
|
||||
.or_else(|| {
|
||||
if !opts.list_details {
|
||||
return None;
|
||||
}
|
||||
|
||||
let res = determine_ls_command(colored_output)
|
||||
.map(|cmd| CommandSet::new_batch([cmd]).unwrap());
|
||||
Some(res)
|
||||
fn extract_command(
|
||||
matches: &clap::ArgMatches,
|
||||
path_separator: Option<&str>,
|
||||
colored_output: bool,
|
||||
) -> Result<Option<CommandTemplate>> {
|
||||
None.or_else(|| {
|
||||
matches.values_of("exec").map(|args| {
|
||||
Ok(CommandTemplate::new(
|
||||
args,
|
||||
path_separator.map(str::to_string),
|
||||
))
|
||||
})
|
||||
.transpose()
|
||||
})
|
||||
.or_else(|| {
|
||||
matches
|
||||
.values_of("exec-batch")
|
||||
.map(|args| CommandTemplate::new_batch(args, path_separator.map(str::to_string)))
|
||||
})
|
||||
.or_else(|| {
|
||||
if !matches.is_present("list-details") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let color = matches.value_of("color").unwrap_or("auto");
|
||||
let color_arg = format!("--color={}", color);
|
||||
|
||||
let res = determine_ls_command(&color_arg, colored_output).map(|cmd| {
|
||||
CommandTemplate::new_batch(cmd, path_separator.map(str::to_string)).unwrap()
|
||||
});
|
||||
|
||||
Some(res)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn determine_ls_command(colored_output: bool) -> Result<Vec<&'static str>> {
|
||||
fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&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![
|
||||
@ -428,10 +500,20 @@ fn determine_ls_command(colored_output: bool) -> Result<Vec<&'static str>> {
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
|
||||
fn extract_size_limits(matches: &clap::ArgMatches) -> Result<Vec<SizeFilter>> {
|
||||
matches.values_of("size").map_or(Ok(Vec::new()), |vs| {
|
||||
vs.map(|sf| {
|
||||
SizeFilter::from_string(sf)
|
||||
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", sf))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_time_constraints(matches: &clap::ArgMatches) -> Result<Vec<TimeFilter>> {
|
||||
let now = time::SystemTime::now();
|
||||
let mut time_constraints: Vec<TimeFilter> = Vec::new();
|
||||
if let Some(ref t) = opts.changed_within {
|
||||
if let Some(t) = matches.value_of("changed-within") {
|
||||
if let Some(f) = TimeFilter::after(&now, t) {
|
||||
time_constraints.push(f);
|
||||
} else {
|
||||
@ -441,7 +523,7 @@ fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(ref t) = opts.changed_before {
|
||||
if let Some(t) = matches.value_of("changed-before") {
|
||||
if let Some(f) = TimeFilter::before(&now, t) {
|
||||
time_constraints.push(f);
|
||||
} else {
|
||||
@ -456,18 +538,14 @@ fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
|
||||
|
||||
fn ensure_use_hidden_option_for_leading_dot_pattern(
|
||||
config: &Config,
|
||||
pattern_regexps: &[String],
|
||||
pattern_regex: &str,
|
||||
) -> Result<()> {
|
||||
if cfg!(unix)
|
||||
&& config.ignore_hidden
|
||||
&& pattern_regexps
|
||||
.iter()
|
||||
.any(|pat| pattern_matches_strings_with_leading_dot(pat))
|
||||
if cfg!(unix) && config.ignore_hidden && pattern_matches_strings_with_leading_dot(pattern_regex)
|
||||
{
|
||||
Err(anyhow!(
|
||||
"The pattern(s) seems to only match files with a leading dot, but hidden files are \
|
||||
"The pattern seems to only match files with a leading dot, but hidden files are \
|
||||
filtered by default. Consider adding -H/--hidden to search hidden files as well \
|
||||
or adjust your search pattern(s)."
|
||||
or adjust your search pattern."
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
|
123
src/output.rs
123
src/output.rs
@ -1,94 +1,52 @@
|
||||
use std::borrow::Cow;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use lscolors::{Indicator, LsColors, Style};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::dir_entry::DirEntry;
|
||||
use crate::fmt::FormatTemplate;
|
||||
use crate::hyperlink::PathUrl;
|
||||
use crate::error::print_error;
|
||||
use crate::exit_codes::ExitCode;
|
||||
use crate::filesystem::strip_current_dir;
|
||||
|
||||
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) -> io::Result<()> {
|
||||
let mut has_hyperlink = false;
|
||||
if config.hyperlink {
|
||||
if let Some(url) = PathUrl::new(entry.path()) {
|
||||
write!(stdout, "\x1B]8;;{}\x1B\\", url)?;
|
||||
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)?;
|
||||
pub fn print_entry<W: Write>(stdout: &mut W, entry: &Path, config: &Config) {
|
||||
let path = if config.strip_cwd_prefix {
|
||||
strip_current_dir(entry)
|
||||
} else {
|
||||
print_entry_uncolorized(stdout, entry, config)?;
|
||||
entry
|
||||
};
|
||||
|
||||
if has_hyperlink {
|
||||
write!(stdout, "\x1B]8;;\x1B\\")?;
|
||||
}
|
||||
|
||||
if config.null_separator {
|
||||
write!(stdout, "\0")
|
||||
let r = if let Some(ref ls_colors) = config.ls_colors {
|
||||
print_entry_colorized(stdout, path, config, ls_colors)
|
||||
} else {
|
||||
writeln!(stdout)
|
||||
}
|
||||
}
|
||||
print_entry_uncolorized(stdout, path, config)
|
||||
};
|
||||
|
||||
// Display a trailing slash if the path is a directory and the config option is enabled.
|
||||
// If the path_separator option is set, display that instead.
|
||||
// The trailing slash will not be colored.
|
||||
#[inline]
|
||||
fn print_trailing_slash<W: Write>(
|
||||
stdout: &mut W,
|
||||
entry: &DirEntry,
|
||||
config: &Config,
|
||||
style: Option<&Style>,
|
||||
) -> io::Result<()> {
|
||||
if entry.file_type().map_or(false, |ft| ft.is_dir()) {
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
style
|
||||
.map(Style::to_nu_ansi_term_style)
|
||||
.unwrap_or_default()
|
||||
.paint(&config.actual_path_separator)
|
||||
)?;
|
||||
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();
|
||||
}
|
||||
}
|
||||
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,
|
||||
entry: &DirEntry,
|
||||
path: &Path,
|
||||
config: &Config,
|
||||
ls_colors: &LsColors,
|
||||
) -> io::Result<()> {
|
||||
// Split the path between the parent and the last component
|
||||
let mut offset = 0;
|
||||
let path = entry.stripped_path(config);
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
@ -110,23 +68,22 @@ fn print_entry_colorized<W: Write>(
|
||||
|
||||
let style = ls_colors
|
||||
.style_for_indicator(Indicator::Directory)
|
||||
.map(Style::to_nu_ansi_term_style)
|
||||
.map(Style::to_ansi_term_style)
|
||||
.unwrap_or_default();
|
||||
write!(stdout, "{}", style.paint(parent_str))?;
|
||||
}
|
||||
|
||||
let style = entry
|
||||
.style(ls_colors)
|
||||
.map(Style::to_nu_ansi_term_style)
|
||||
let style = ls_colors
|
||||
.style_for_path(path)
|
||||
.map(Style::to_ansi_term_style)
|
||||
.unwrap_or_default();
|
||||
write!(stdout, "{}", style.paint(&path_str[offset..]))?;
|
||||
|
||||
print_trailing_slash(
|
||||
stdout,
|
||||
entry,
|
||||
config,
|
||||
ls_colors.style_for_indicator(Indicator::Directory),
|
||||
)?;
|
||||
if config.null_separator {
|
||||
write!(stdout, "\0")?;
|
||||
} else {
|
||||
writeln!(stdout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -134,42 +91,42 @@ fn print_entry_colorized<W: Write>(
|
||||
// TODO: this function is performance critical and can probably be optimized
|
||||
fn print_entry_uncolorized_base<W: Write>(
|
||||
stdout: &mut W,
|
||||
entry: &DirEntry,
|
||||
path: &Path,
|
||||
config: &Config,
|
||||
) -> io::Result<()> {
|
||||
let path = entry.stripped_path(config);
|
||||
let separator = if config.null_separator { "\0" } else { "\n" };
|
||||
|
||||
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, "{}{}", path_string, separator)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn print_entry_uncolorized<W: Write>(
|
||||
stdout: &mut W,
|
||||
entry: &DirEntry,
|
||||
path: &Path,
|
||||
config: &Config,
|
||||
) -> io::Result<()> {
|
||||
print_entry_uncolorized_base(stdout, entry, config)
|
||||
print_entry_uncolorized_base(stdout, path, config)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn print_entry_uncolorized<W: Write>(
|
||||
stdout: &mut W,
|
||||
entry: &DirEntry,
|
||||
path: &Path,
|
||||
config: &Config,
|
||||
) -> io::Result<()> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
if config.interactive_terminal || config.path_separator.is_some() {
|
||||
// Fall back to the base implementation
|
||||
print_entry_uncolorized_base(stdout, entry, config)
|
||||
print_entry_uncolorized_base(stdout, path, config)
|
||||
} else {
|
||||
// Print path as raw bytes, allowing invalid UTF-8 filenames to be passed to other processes
|
||||
stdout.write_all(entry.stripped_path(config).as_os_str().as_bytes())?;
|
||||
print_trailing_slash(stdout, entry, config, None)
|
||||
let separator = if config.null_separator { b"\0" } else { b"\n" };
|
||||
stdout.write_all(path.as_os_str().as_bytes())?;
|
||||
stdout.write_all(separator)
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use regex_syntax::ParserBuilder;
|
||||
|
||||
/// Determine if a regex pattern contains a literal uppercase character.
|
||||
pub fn pattern_has_uppercase_char(pattern: &str) -> bool {
|
||||
let mut parser = ParserBuilder::new().utf8(false).build();
|
||||
let mut parser = ParserBuilder::new().allow_invalid_utf8(true).build();
|
||||
|
||||
parser
|
||||
.parse(pattern)
|
||||
@ -15,21 +15,19 @@ pub fn pattern_has_uppercase_char(pattern: &str) -> bool {
|
||||
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) {
|
||||
Ok(s) => s.chars().any(|c| c.is_uppercase()),
|
||||
Err(_) => bytes.iter().any(|b| char::from(*b).is_uppercase()),
|
||||
},
|
||||
HirKind::Class(Class::Unicode(ranges)) => ranges
|
||||
match *hir.kind() {
|
||||
HirKind::Literal(Literal::Unicode(c)) => c.is_uppercase(),
|
||||
HirKind::Literal(Literal::Byte(b)) => char::from(b).is_uppercase(),
|
||||
HirKind::Class(Class::Unicode(ref ranges)) => ranges
|
||||
.iter()
|
||||
.any(|r| r.start().is_uppercase() || r.end().is_uppercase()),
|
||||
HirKind::Class(Class::Bytes(ranges)) => ranges
|
||||
HirKind::Class(Class::Bytes(ref ranges)) => ranges
|
||||
.iter()
|
||||
.any(|r| char::from(r.start()).is_uppercase() || char::from(r.end()).is_uppercase()),
|
||||
HirKind::Capture(Capture { sub, .. }) | HirKind::Repetition(Repetition { sub, .. }) => {
|
||||
hir_has_uppercase_char(sub)
|
||||
HirKind::Group(Group { ref hir, .. }) | HirKind::Repetition(Repetition { ref hir, .. }) => {
|
||||
hir_has_uppercase_char(hir)
|
||||
}
|
||||
HirKind::Concat(hirs) | HirKind::Alternation(hirs) => {
|
||||
HirKind::Concat(ref hirs) | HirKind::Alternation(ref hirs) => {
|
||||
hirs.iter().any(hir_has_uppercase_char)
|
||||
}
|
||||
_ => false,
|
||||
@ -38,7 +36,7 @@ fn hir_has_uppercase_char(hir: &Hir) -> bool {
|
||||
|
||||
/// Determine if a regex pattern only matches strings starting with a literal dot (hidden files)
|
||||
pub fn pattern_matches_strings_with_leading_dot(pattern: &str) -> bool {
|
||||
let mut parser = ParserBuilder::new().utf8(false).build();
|
||||
let mut parser = ParserBuilder::new().allow_invalid_utf8(true).build();
|
||||
|
||||
parser
|
||||
.parse(pattern)
|
||||
@ -54,11 +52,11 @@ fn hir_matches_strings_with_leading_dot(hir: &Hir) -> bool {
|
||||
// "^\\.", i.e. a start text anchor and a literal dot character. There are a lot
|
||||
// of other patterns that ONLY match hidden files, e.g. ^(\\.foo|\\.bar) which are
|
||||
// not (yet) detected by this algorithm.
|
||||
match hir.kind() {
|
||||
HirKind::Concat(hirs) => {
|
||||
match *hir.kind() {
|
||||
HirKind::Concat(ref hirs) => {
|
||||
let mut hirs = hirs.iter();
|
||||
if let Some(hir) = hirs.next() {
|
||||
if hir.kind() != &HirKind::Look(Look::Start) {
|
||||
if *hir.kind() != HirKind::Anchor(Anchor::StartText) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@ -66,10 +64,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"."),
|
||||
_ => false,
|
||||
}
|
||||
*hir.kind() == HirKind::Literal(Literal::Unicode('.'))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
910
src/walk.rs
910
src/walk.rs
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ use std::os::windows;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
use tempfile::TempDir;
|
||||
use tempdir::TempDir;
|
||||
|
||||
/// Environment for the integration tests.
|
||||
pub struct TestEnv {
|
||||
@ -20,9 +20,6 @@ pub struct TestEnv {
|
||||
|
||||
/// Normalize each line by sorting the whitespace-separated words
|
||||
normalize_line: bool,
|
||||
|
||||
/// Temporary directory for storing test config (global ignore file)
|
||||
config_dir: Option<TempDir>,
|
||||
}
|
||||
|
||||
/// Create the working directory and the test files.
|
||||
@ -30,7 +27,7 @@ fn create_working_directory(
|
||||
directories: &[&'static str],
|
||||
files: &[&'static str],
|
||||
) -> Result<TempDir, io::Error> {
|
||||
let temp_dir = tempfile::Builder::new().prefix("fd-tests").tempdir()?;
|
||||
let temp_dir = TempDir::new("fd-tests")?;
|
||||
|
||||
{
|
||||
let root = temp_dir.path();
|
||||
@ -62,16 +59,6 @@ fn create_working_directory(
|
||||
Ok(temp_dir)
|
||||
}
|
||||
|
||||
fn create_config_directory_with_global_ignore(ignore_file_content: &str) -> io::Result<TempDir> {
|
||||
let config_dir = tempfile::Builder::new().prefix("fd-config").tempdir()?;
|
||||
let fd_dir = config_dir.path().join("fd");
|
||||
fs::create_dir(&fd_dir)?;
|
||||
let mut ignore_file = fs::File::create(fd_dir.join("ignore"))?;
|
||||
ignore_file.write_all(ignore_file_content.as_bytes())?;
|
||||
|
||||
Ok(config_dir)
|
||||
}
|
||||
|
||||
/// Find the *fd* executable.
|
||||
fn find_fd_exe() -> PathBuf {
|
||||
// Tests exe is in target/debug/deps, the *fd* exe is in target/debug
|
||||
@ -129,7 +116,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_STR);
|
||||
let line = line.replace('/', &std::path::MAIN_SEPARATOR.to_string());
|
||||
if normalize_line {
|
||||
let mut words: Vec<_> = line.split_whitespace().collect();
|
||||
words.sort_unstable();
|
||||
@ -163,7 +150,6 @@ impl TestEnv {
|
||||
temp_dir,
|
||||
fd_exe,
|
||||
normalize_line: false,
|
||||
config_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,16 +158,6 @@ impl TestEnv {
|
||||
temp_dir: self.temp_dir,
|
||||
fd_exe: self.fd_exe,
|
||||
normalize_line: normalize,
|
||||
config_dir: self.config_dir,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn global_ignore_file(self, content: &str) -> TestEnv {
|
||||
let config_dir =
|
||||
create_config_directory_with_global_ignore(content).expect("config directory");
|
||||
TestEnv {
|
||||
config_dir: Some(config_dir),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,9 +169,7 @@ impl TestEnv {
|
||||
let root = self.test_root();
|
||||
let broken_symlink_link = root.join(link_path);
|
||||
{
|
||||
let temp_target_dir = tempfile::Builder::new()
|
||||
.prefix("fd-tests-broken-symlink")
|
||||
.tempdir()?;
|
||||
let temp_target_dir = TempDir::new("fd-tests-broken-symlink")?;
|
||||
let broken_symlink_target = temp_target_dir.path().join("broken_symlink_target");
|
||||
fs::File::create(&broken_symlink_target)?;
|
||||
#[cfg(unix)]
|
||||
@ -211,12 +185,6 @@ impl TestEnv {
|
||||
self.temp_dir.path().to_path_buf()
|
||||
}
|
||||
|
||||
/// Get the path of the fd executable.
|
||||
#[cfg_attr(windows, allow(unused))]
|
||||
pub fn test_exe(&self) -> &PathBuf {
|
||||
&self.fd_exe
|
||||
}
|
||||
|
||||
/// Get the root directory of the file system.
|
||||
pub fn system_root(&self) -> PathBuf {
|
||||
let mut components = self.temp_dir.path().components();
|
||||
@ -230,8 +198,13 @@ impl TestEnv {
|
||||
path: P,
|
||||
args: &[&str],
|
||||
) -> process::Output {
|
||||
// Setup *fd* command.
|
||||
let mut cmd = process::Command::new(&self.fd_exe);
|
||||
cmd.current_dir(self.temp_dir.path().join(path));
|
||||
cmd.arg("--no-global-ignore-file").args(args);
|
||||
|
||||
// Run *fd*.
|
||||
let output = self.run_command(path.as_ref(), args);
|
||||
let output = cmd.output().expect("fd output");
|
||||
|
||||
// Check for exit status.
|
||||
if !output.status.success() {
|
||||
@ -307,24 +280,6 @@ impl TestEnv {
|
||||
self.assert_error_subdirectory(".", args, Some(expected))
|
||||
}
|
||||
|
||||
fn run_command(&self, path: &Path, args: &[&str]) -> process::Output {
|
||||
// Setup *fd* command.
|
||||
let mut cmd = process::Command::new(&self.fd_exe);
|
||||
cmd.current_dir(self.temp_dir.path().join(path));
|
||||
if let Some(config_dir) = &self.config_dir {
|
||||
cmd.env("XDG_CONFIG_HOME", config_dir.path());
|
||||
} else {
|
||||
cmd.arg("--no-global-ignore-file");
|
||||
}
|
||||
// Make sure LS_COLORS is unset to ensure consistent
|
||||
// color output
|
||||
cmd.env("LS_COLORS", "");
|
||||
cmd.args(args);
|
||||
|
||||
// Run *fd*.
|
||||
cmd.output().expect("fd output")
|
||||
}
|
||||
|
||||
/// Assert that calling *fd* in the specified path under the root working directory,
|
||||
/// and with the specified arguments produces an error with the expected message.
|
||||
fn assert_error_subdirectory<P: AsRef<Path>>(
|
||||
@ -333,7 +288,13 @@ impl TestEnv {
|
||||
args: &[&str],
|
||||
expected: Option<&str>,
|
||||
) -> process::ExitStatus {
|
||||
let output = self.run_command(path.as_ref(), args);
|
||||
// Setup *fd* command.
|
||||
let mut cmd = process::Command::new(&self.fd_exe);
|
||||
cmd.current_dir(self.temp_dir.path().join(path));
|
||||
cmd.arg("--no-global-ignore-file").args(args);
|
||||
|
||||
// Run *fd*.
|
||||
let output = cmd.output().expect("fd output");
|
||||
|
||||
if let Some(expected) = expected {
|
||||
// Normalize both expected and actual output.
|
||||
|
1464
tests/tests.rs
1464
tests/tests.rs
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user