Compare commits

..

5 Commits

Author SHA1 Message Date
Thayne McCombs bf37c27a38 Address feedback for removing buffering when running on a single thread 2021-08-09 01:02:30 -06:00
Thayne McCombs fcaebd2295 Merge branch 'master' into no-buffer-single-thread 2021-08-09 01:00:06 -06:00
Max Triano a8951f0641 Clarifying comments 2021-07-27 09:13:54 +02:00
Max Triano 5860dedb1e No buffering cmd output when running on 1 thread 2021-07-27 09:13:54 +02:00
Max Triano 7656358f44 Just trying some things out, initial functionality 2021-07-27 09:13:54 +02:00
43 changed files with 1985 additions and 4263 deletions
-1
View File
@@ -1 +0,0 @@
github: [sharkdp, tavianator]
+28
View File
@@ -0,0 +1,28 @@
---
name: Bug Report
about: Report a bug.
title: ""
labels: bug
assignees: ''
---
**Describe the bug you encountered:**
<!--
Please check out the troubleshooting section first:
https://github.com/sharkdp/fd#troubleshooting
-->
**Describe what you expected to happen:**
**What version of `fd` are you using?**
<!-- paste the output of `fd --version` here -->
**Which operating system / distribution are you on?**
<!--
Unix: paste the output of `uname -srm` and `lsb_release -a` here.
Windows: please tell us your Windows version
-->
-42
View File
@@ -1,42 +0,0 @@
name: Bug Report
description: Report a bug.
title: "[BUG] "
labels: bug
body:
- type: markdown
attributes:
value: |
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
- type: textarea
id: bug
attributes:
label: "Describe the bug you encountered:"
validations:
required: true
- type: textarea
id: expected
attributes:
label: "Describe what you expected to happen:"
- type: input
id: version
attributes:
label: "What version of `fd` are you using?"
placeholder: "paste the output of `fd --version` here"
validations:
required: true
- type: textarea
id: os
attributes:
label: Which operating system / distribution are you on?
placeholder: |
Unix: paste the output of `uname -srm` and `lsb_release -a` here.
Windows: please tell us your Windows version
render: shell
validations:
required: true
+1
View File
@@ -1 +1,2 @@
blank_issues_enabled: true
@@ -6,3 +6,5 @@ labels: feature-request
assignees: ''
---
+32 -64
View File
@@ -1,7 +1,7 @@
name: CICD
env:
MIN_SUPPORTED_RUST_VERSION: "1.60.0"
MIN_SUPPORTED_RUST_VERSION: "1.42.0"
CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
on:
@@ -14,35 +14,9 @@ on:
- '*'
jobs:
code_quality:
name: Code quality
runs-on: ubuntu-20.04
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
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:
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
runs-on: ubuntu-18.04
steps:
- name: Checkout source code
uses: actions/checkout@v2
@@ -52,18 +26,17 @@ jobs:
with:
toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }}
default: true
profile: minimal # minimal component installation (ie, no documentation)
components: clippy
profile: minimal
- name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
uses: actions-rs/cargo@v1
with:
command: clippy
args: --locked --all-targets --all-features
args: --all-targets --all-features
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
args: --locked
build:
name: ${{ matrix.job.os }} (${{ matrix.job.target }})
@@ -72,14 +45,14 @@ jobs:
fail-fast: false
matrix:
job:
- { 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 , use-cross: true }
- { os: ubuntu-20.04, target: x86_64-unknown-linux-musl , use-cross: true }
- { os: macos-latest , target: x86_64-apple-darwin }
- { os: ubuntu-18.04, target: arm-unknown-linux-gnueabihf , use-cross: true }
- { os: ubuntu-18.04, target: arm-unknown-linux-musleabihf, use-cross: true }
- { os: ubuntu-18.04, target: aarch64-unknown-linux-gnu , use-cross: true }
- { os: ubuntu-18.04, target: i686-unknown-linux-gnu , use-cross: true }
- { os: ubuntu-18.04, target: i686-unknown-linux-musl , use-cross: true }
- { os: ubuntu-18.04, target: x86_64-unknown-linux-gnu }
- { os: ubuntu-18.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 }
@@ -127,7 +100,7 @@ jobs:
with:
use-cross: ${{ matrix.job.use-cross }}
command: build
args: --locked --release --target=${{ matrix.job.target }}
args: --release --target=${{ matrix.job.target }}
- name: Strip debug information from executable
id: strip
@@ -162,29 +135,23 @@ jobs:
fi
# Let subsequent steps know where to find the (stripped) bin
echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT
echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT
echo ::set-output name=BIN_PATH::${BIN_PATH}
echo ::set-output name=BIN_NAME::${BIN_NAME}
- name: Set testing options
id: test-options
shell: bash
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 ${PROJECT_NAME}" ;; esac;
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS}
- name: Run tests
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: Generate completions
id: completions
shell: bash
run: make completions
args: --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
- name: Create tarball
id: package
@@ -193,11 +160,12 @@ jobs:
PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac;
PKG_BASENAME=${PROJECT_NAME}-v${PROJECT_VERSION}-${{ matrix.job.target }}
PKG_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.strip.outputs.BIN_PATH }}" "$ARCHIVE_DIR"
@@ -209,7 +177,10 @@ jobs:
cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$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
@@ -220,7 +191,7 @@ 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
@@ -247,7 +218,7 @@ jobs:
esac;
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT
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 }}"
@@ -257,9 +228,9 @@ jobs:
gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ env.PROJECT_NAME }}.1"
# Autocompletion files
install -Dm644 'autocomplete/fd.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/${{ env.PROJECT_NAME }}"
install -Dm644 'autocomplete/fd.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/${{ env.PROJECT_NAME }}.fish"
install -Dm644 'autocomplete/_fd' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ env.PROJECT_NAME }}"
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"
@@ -321,15 +292,12 @@ jobs:
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.
Description: cat(1) clone with wings.
A cat(1) clone with syntax highlighting and Git integration.
EOF
DPKG_PATH="${DPKG_STAGING}/${DPKG_NAME}"
echo "DPKG_PATH=${DPKG_PATH}" >> $GITHUB_OUTPUT
echo ::set-output name=DPKG_PATH::${DPKG_PATH}
# build dpkg
fakeroot dpkg-deb --build "${DPKG_DIR}" "${DPKG_PATH}"
@@ -352,7 +320,7 @@ 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@v1
-1
View File
@@ -1,3 +1,2 @@
target/
/autocomplete/
**/*.rs.bk
+14 -131
View File
@@ -1,129 +1,13 @@
# 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
# Upcoming release
## 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
## Bugfixes
- Invalid absolute path on windows when searching from the drive root, see #931 and #936 (@gbarta)
# v8.3.1
## Bugfixes
- Stop implying `--no-ignore-parent` when `--no-vcs-ignore` is supplied, see #907, #901, #908 (@tmccombs)
- fd no longer waits for the whole traversal if the only matches arrive within max_buffer_time, see #868 and #895 (@tavianator)
- `--max-results=1` now immediately quits after the first result, see #867
- `fd -h` does not panic anymore when stdout is closed, see #897
## Changes
- Disable jemalloc on FreeBSD, see #896 (@xanderio)
- Updated man page, see #912 (@rlue)
- Updated zsh completions, see #932 (@tmccombs)
# v8.3.0
## Performance improvements
- Colorized output is now significantly faster, see #720 and #853 (@tavianator)
- Writing to stdout is now buffered if the output does not go to a TTY. This increases performance
when the output of `fd` is piped to another program or to a file, see #885 (@tmccombs, original
implementation by @sourlemon207)
- File metadata is now cached between the different filters that require it (e.g. `--owner`,
`--size`), reducing the number of `stat` syscalls when multiple filters are used; see #863
(@tavianator, original implementation by @alexmaco)
## Features
- Don't buffer command output from `--exec` when using a single thread. See #522
- Add new `-q, --quiet` flag, see #303 (@Asha20)
- Add new `--no-ignore-parent` flag, see #787 (@will459)
- Add new `--batch-size` flag, see #410 (@devonhollowood)
- Add opposing command-line options, see #595 (@Asha20)
- Add support for more filesystem indicators in `LS_COLORS`, see
https://github.com/sharkdp/lscolors/pull/35 (@tavianator)
## Bugfixes
- Always show the `./` prefix for search results unless the output is a TTY or `--strip-cwd-prefix` is set, see #760 and #861 (@jcaplan)
- Set default path separator to `/` in MSYS, see #537 and #730 (@aswild)
- fd cannot search files under a RAM disk, see #752
- fd doesn't show substituted drive on Windows, see #365
- Properly handle write errors to devices that are full, see #737
- Use local time zone for time functions (`--change-newer-than`, `--change-older-than`), see #631 (@jacobmischka)
- Support `--list-details` on more platforms (like BusyBox), see #783
- The filters `--owner`, `--size`, and `--changed-{within,before}` now apply to symbolic links
themselves, rather than the link target, except when `--follow` is specified; see #863
- Change time comparisons to be exclusive, see #794 (@jacobmischka)
## Changes
@@ -133,7 +17,6 @@
- Many documentation updates
# v8.2.1
No functional changes with respect to v8.2.0. Bugfix in the release process.
@@ -572,7 +455,7 @@ I'd also like to take this chance to say a special Thank You to a few people tha
* Add option to force colored output: `--color always`, see #49 (@Detegr)
* Generate Shell completions for Bash, ZSH, Fish and Powershell, see #64 (@ImbaKnugel)
* Better & extended `--help` text (@abaez and @Detegr)
* Proper Windows support, see #70
* Proper Windows support, see #70
## Changes
@@ -600,9 +483,9 @@ I'd also like to take this chance to say a special Thank You to a few people tha
* Changed `--sensitive` to `--case-sensitive`
* Changed `--absolute` to `--absolute-path`
* Throw an error if root directory is not existent, see #39
* Use absolute paths if the root dir is an absolute path, see #40
* Handle invalid UTF-8, see #34 #38
* Throw an error if root directory is not existent, see #39
* Use absolute paths if the root dir is an absolute path, see #40
* Handle invalid UTF-8, see #34 #38
* Support `-V`, `--version` by switching from `getopts` to `clap`.
Misc:
@@ -610,8 +493,8 @@ Misc:
# v1.1.0
- Windows compatibility (@sebasv), see #29 #35
- Safely exit on broken output pipes (e.g.: usage with `head`, `tail`, ..), see #24
- Windows compatibility (@sebasv), see #29 #35
- Safely exit on broken output pipes (e.g.: usage with `head`, `tail`, ..), see #24
- Backport for rust 1.16, see #23
# v1.0.0
@@ -623,17 +506,17 @@ Misc:
# v0.3.0
- Parse dircolors files, closes #20
- Colorize each path component, closes #19
- Add short command line option for --hidden, see #18
- Parse dircolors files, closes #20
- Colorize each path component, closes #19
- Add short command line option for --hidden, see #18
# v0.2.0
- Option to follow symlinks, disable colors, closes #16, closes #17
- Option to follow symlinks, disable colors, closes #16, closes #17
- `--filename` instead of `--full-path`
- Option to search hidden directories, closes #12
- Configurable search depth, closes #13
- Detect interactive terminal, closes #11
- Option to search hidden directories, closes #12
- Configurable search depth, closes #13
- Detect interactive terminal, closes #11
# v0.1.0
Generated
+132 -678
View File
File diff suppressed because it is too large Load Diff
+15 -29
View File
@@ -16,8 +16,8 @@ license = "MIT/Apache-2.0"
name = "fd-find"
readme = "README.md"
repository = "https://github.com/sharkdp/fd"
version = "8.5.2"
edition= "2021"
version = "8.2.1"
edition= "2018"
[badges.appveyor]
repository = "sharkdp/fd"
@@ -30,60 +30,46 @@ name = "fd"
path = "src/main.rs"
[build-dependencies]
clap = "2.31.2"
version_check = "0.9"
[dependencies]
ansi_term = "0.12"
argmax = "0.3.1"
atty = "0.2"
ignore = "0.4.3"
num_cpus = "1.13"
regex = "1.6.0"
lazy_static = "1.1.0"
num_cpus = "1.8"
regex = "1.5.4"
regex-syntax = "0.6"
ctrlc = "3.2"
humantime = "2.1"
lscolors = "0.12"
ctrlc = "3.1"
humantime = "2.0"
lscolors = "0.7"
globset = "0.4"
anyhow = "1.0"
dirs-next = "2.0"
normpath = "0.3.2"
chrono = "0.4"
once_cell = "1.15.0"
crossbeam-channel = "0.5.6"
clap_complete = {version = "4.0", optional = true}
normpath = "0.3"
[dependencies.clap]
version = "4.0.12"
features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped", "derive"]
version = "2.31.2"
features = ["suggestions", "color", "wrap_help"]
[target.'cfg(unix)'.dependencies]
users = "0.11.0"
nix = { version = "0.24.2", default-features = false, features = ["signal"] }
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
libc = "0.2"
[target.'cfg(windows)'.dependencies]
faccess = "0.2.4"
# 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(all(target_env = "musl", target_pointer_width = "32")), not(target_arch = "riscv64")))'.dependencies]
jemallocator = {version = "0.5.0", optional = true}
[target.'cfg(all(not(windows), not(target_os = "android"), not(target_os = "macos"), not(target_env = "musl")))'.dependencies]
jemallocator = "0.3.0"
[dev-dependencies]
diff = "0.1"
tempdir = "0.3"
filetime = "0.2"
test-case = "2.2"
filetime = "0.2.14"
[profile.release]
lto = true
codegen-units = 1
[features]
use-jemalloc = ["jemallocator"]
completions = ["clap_complete"]
base = ["use-jemalloc"]
default = ["use-jemalloc", "completions"]
+1 -1
View File
@@ -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
View File
@@ -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)
.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
+41 -96
View File
@@ -20,7 +20,7 @@ Quick links:
* 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
@@ -115,15 +115,6 @@ src/lscolors/mod.rs
tests/testenv/mod.rs
```
### Searching for a particular file name
To find files with exactly the provided search pattern, use the `-g` (or `--glob`) option:
``` bash
> fd -g libc.so /usr
/usr/lib32/libc.so
/usr/lib/libc.so
```
### Hidden and ignored files
By default, *fd* does not search hidden directories and does not show hidden files in the
search results. To disable this behavior, we can use the `-H` (or `--hidden`) option:
@@ -145,15 +136,6 @@ 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`).
### Matching the full path
By default, *fd* only matches the filename of each file. However, using the `--full-path` or `-p` option,
you can match against the full path.
```bash
> fd -p -g '**/.git/config'
> fd -p '.*/lesson-\d+/[a-z]+.(jpg|png)'
```
### Command execution
Instead of just showing the search results, you often want to *do something* with them. `fd`
@@ -194,11 +176,6 @@ fd … -X ls -lhd --color=always
This pattern is so useful that `fd` provides a shortcut. You can use the `-l`/`--list-details`
option to execute `ls` in this way: `fd … -l`.
The `-X` option is also useful when combining `fd` with [ripgrep](https://github.com/BurntSushi/ripgrep/) (`rg`) in order to search within a certain class of files, like all C++ source files:
```bash
fd -e cpp -e cxx -e h -e hpp -X rg 'std::cout'
```
Convert all `*.jpg` files to `*.png` files:
``` bash
fd -e jpg -x convert {} {.}.png
@@ -206,13 +183,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).
@@ -295,37 +265,39 @@ 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 path (default: file-/dirname only)
-0, --print0 Separate results by the null character
-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), symlink (l),
executable (x), empty (e), socket (s), pipe (p)
-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
-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]
-h, --help Print help information (use `--help` for more detail)
-V, --version Print version information
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)
ARGS:
<pattern> the search pattern - a regular expression unless '--glob' is used (optional)
<path>... the root directory for the filesystem search (optional)
```
## Benchmark
@@ -428,14 +400,6 @@ use a character class with a single hyphen character:
> fd '[-]pattern'
```
### "Command not found" for `alias`es or shell functions
Shell `alias`es and shell functions can not be used for command execution via `fd -x` or
`fd -X`. In `zsh`, you can make the alias global via `alias -g myalias="…"`. In `bash`,
you can use `export -f my_function` to make available to child processes. You would still
need to call `fd -x bash -c 'my_function "$1"' bash`. For other use cases or shells, use
a (temporary) shell script.
## Integration with other programs
### Using fd with `fzf`
@@ -540,7 +504,7 @@ Make sure that `$HOME/.local/bin` is in your `$PATH`.
If you use an older version of Ubuntu, you can download the latest `.deb` package from the
[release page](https://github.com/sharkdp/fd/releases) and install it via:
``` bash
sudo dpkg -i fd_8.5.2_amd64.deb # adapt version number and architecture
sudo dpkg -i fd_8.2.1_amd64.deb # adapt version number and architecture
```
### On Debian
@@ -562,6 +526,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)
@@ -597,22 +567,6 @@ You can install `fd` via xbps-install:
xbps-install -S fd
```
### On RedHat Enterprise Linux 8 (RHEL8), Almalinux 8, EuroLinux 8 or Rocky Linux 8
Get the latest fd-v*-x86_64-unknown-linux-gnu.tar.gz file from [sharkdp on github](https://github.com/sharkdp/fd/releases)
```
tar xf fd-v*-x86_64-unknown-linux-gnu.tar.gz
chown -R root:root fd-v*-x86_64-unknown-linux-gnu
cd fd-v*-x86_64-unknown-linux-gnu
sudo cp fd /bin
gzip fd.1
chown root:root fd.1.gz
sudo cp fd.1.gz /usr/share/man/man1
sudo cp autocomplete/fd.bash /usr/share/bash-completion/completions/fd
source /usr/share/bash-completion/completions/fd
fd
```
### On macOS
You can install `fd` with [Homebrew](https://formulae.brew.sh/formula/fd):
@@ -639,13 +593,6 @@ Or via [Chocolatey](https://chocolatey.org):
choco install 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`:
@@ -660,7 +607,7 @@ You can install [the fd-find package](https://www.freshports.org/sysutils/fd) fr
pkg install fd-find
```
### From npm
### From NPM
On linux and macOS, you can install the [fd-find](https://npm.im/fd-find) package:
@@ -674,9 +621,7 @@ With Rust's package manager [cargo](https://github.com/rust-lang/cargo), you can
```
cargo install fd-find
```
Note that rust version *1.60.0* or later is required.
`make` is also needed for the build.
Note that rust version *1.42.0* or later is required.
### From binaries
+19 -1
View File
@@ -1,5 +1,11 @@
use std::fs;
use clap::Shell;
include!("src/app.rs");
fn main() {
let min_version = "1.60";
let min_version = "1.42";
match version_check::is_min_version(min_version) {
Some(true) => {}
@@ -9,4 +15,16 @@ 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();
app.gen_completions("fd", Shell::Bash, &outdir);
app.gen_completions("fd", Shell::Fish, &outdir);
app.gen_completions("fd", Shell::PowerShell, &outdir);
}
-1
View File
@@ -1 +0,0 @@
msrv = "1.60.0"
+2 -7
View File
@@ -57,7 +57,6 @@ _fd() {
+ no-ignore-partial # some ignore files
"(no-ignore-full --no-ignore-vcs)--no-ignore-vcs[don't respect .gitignore files]"
"!(no-ignore-full --no-global-ignore-file)--no-global-ignore-file[don't respect the global ignore file]"
$no'(no-ignore-full --no-ignore-parent)--no-ignore-parent[]'
+ '(case)' # case-sensitivity
{-s,--case-sensitive}'[perform a case-sensitive search]'
@@ -139,14 +138,13 @@ _fd() {
+ '(exec-cmds)' # execute command
'(long-listing max-results)'{-x+,--exec=}'[execute command for each search result]:command: _command_names -e:*\;::program arguments: _normal'
'(long-listing max-results)'{-X+,--exec-batch=}'[execute command for all search results at once]:command: _command_names -e:*\;::program arguments: _normal'
'(long-listing max-results)--batch-size=[max number of args for each -X call]:size'
+ other
'!(--max-buffer-time)--max-buffer-time=[set amount of time to buffer before showing output]:time (ms)'
+ '(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'
@@ -155,9 +153,6 @@ _fd() {
$no'(--base-directory)--base-directory=[change the current working directory to the given path]:directory:_files -/'
$no'(*)*--search-path=[set search path (instead of positional <path> arguments)]:directory:_files -/'
+ strip-cwd-prefix
$no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix[Strip ./ prefix when output is redirected]'
+ args # positional arguments
'1: :_guard "^-*" pattern'
'(--search-path)*:directory:_files -/'
@@ -225,7 +220,7 @@ _fd() {
_fd "$@"
# ------------------------------------------------------------------------------
# Copyright (c) 2011 GitHub zsh-users - http://github.com/zsh-users
# Copyright (c) 2011 Github zsh-users - http://github.com/zsh-users
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
Vendored
+20 -104
View File
@@ -24,56 +24,26 @@ 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.
.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'.
(default: hidden files and directories are skipped).
.TP
.B \-I, \-\-no\-ignore
Show search results from files and directories that would otherwise be ignored by
.RS
.IP \[bu] 2
.I .gitignore
.IP \[bu]
.I .git/info/exclude
.IP \[bu]
The global gitignore configuration (by default
.IR $HOME/.config/git/ignore )
.IP \[bu]
.I .ignore
.IP \[bu]
.I .fdignore
.IP \[bu]
The global fd ignore file (usually
.I $HOME/.config/fd/ignore
)
.RE
.IP
The flag can be overridden with '--ignore'.
.IR .gitignore ,
.IR .ignore ,
.IR .fdignore ,
or the global ignore file.
.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
including
.IR .gitignore ,
.IR .git/info/exclude ,
and the global gitignore configuration
.RI ( core.excludesFile
git setting, which defaults to
.IR $HOME/.config/git/ignore ).
The flag can be overridden with '--ignore-vcs'.
.TP
.B \-\-no\-ignore\-parent
Show search results from files and directories that would otherwise be ignored by gitignore files in
parent directories.
Show search results from files and directories that would otherwise be ignored by
.I .gitignore
files.
.TP
.B \-s, \-\-case\-sensitive
Perform a case-sensitive search. By default, fd uses case-insensitive searches, unless the
@@ -85,7 +55,6 @@ pattern contains an uppercase character (smart case).
.TP
.B \-g, \-\-glob
Perform a glob-based search instead of a regular expression search.
If combined with the '\-\-full-path' option, '**' can be used to match multiple path components.
.TP
.B \-\-regex
Perform a regular-expression based search (default). This can be used to override --glob.
@@ -96,7 +65,6 @@ performs substring comparison. If you want to match on an exact filename, consid
.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'.
.TP
.B \-l, \-\-list\-details
Use a detailed listing format like 'ls -l'. This is basically an alias
@@ -106,7 +74,7 @@ sort order.
.TP
.B \-L, \-\-follow
By default, fd does not descend into symlinked directories. Using this flag, symbolic links are
also traversed. The flag can be overridden with '--no-follow'.
also traversed.
.TP
.B \-p, \-\-full\-path
By default, the search pattern is only matched against the filename (or directory name). Using
@@ -124,21 +92,10 @@ Limit the number of search results to 'count' and quit immediately.
.B \-1
Limit the search to a single result and quit immediately. This is an alias for '--max-results=1'.
.TP
.B \-q, \-\-quiet
When the flag is present, the program does not print anything and will instead exit with a code of 0 if there is at least one search result.
Otherwise, the exit code will be 1.
This is mainly for usage in scripts and can be faster than checking for output because the search can be stopped early after the first match.
.B \-\-has\-results
can be used as an alias.
.TP
.B \-\-show-errors
Enable the display of filesystem errors for situations such as insufficient
permissions or dead symlinks.
.TP
.B \-\-strip-cwd-prefix
By default, relative paths are prefixed with './' when the output goes to a non interactive terminal
(TTY). Use this flag to disable this behaviour.
.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).
.TP
@@ -171,40 +128,18 @@ regular files
directories
.IP "l, symlink"
symbolic links
.IP "s, socket"
sockets
.IP "p, pipe"
named pipes (FIFOs)
.IP "x, executable"
executable (files)
.IP "e, empty"
empty files or directories
.IP "s, socket"
sockets
.IP "p, pipe"
named pipes (FIFOs)
.RE
.RS
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:
- Only search for files:
fd --type file …
fd -tf …
- Find both files and symlinks
fd --type file --type symlink …
fd -tf -tl …
- Find executable files:
fd --type executable
fd -tx
- Find empty files:
fd --type empty --type file
fd -te -tf
- Find empty directories:
fd --type empty --type directory
fd -te -td
This option can be used repeatedly to allow for multiple file types.
.RE
.TP
.BI "\-e, \-\-extension " ext
@@ -281,11 +216,8 @@ tebibytes
.RE
.TP
.BI "\-\-changed-within " date|duration
Filter results based on the file modification time.
Files with modification times greater than the argument will be returned.
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
in time in either full RFC3339 format with time zone, or as a date or datetime in the
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR).
Filter results based on the file modification time. The argument can be provided as a specific
point in time (\fIYYYY-MM-DD HH:MM:SS\fR) or as a duration (\fI10h, 1d, 35min\fR).
.B --change-newer-than
can be used as an alias.
@@ -294,11 +226,8 @@ Examples:
\-\-change-newer-than "2018-10-27 10:00:00"
.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 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).
Filter results based on the file modification time. The argument can be provided as a specific
point in time (\fIYYYY-MM-DD HH:MM:SS\fR) or as a duration (\fI10h, 1d, 35min\fR).
.B --change-older-than
can be used as an alias.
@@ -341,9 +270,6 @@ 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.
The following placeholders are substituted before the command is executed:
.RS
@@ -397,9 +323,6 @@ basename without file extension
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:
@@ -412,12 +335,7 @@ Examples:
fd -e rs -X wc -l
.RE
.TP
.BI "\-\-batch-size " size
Maximum number of arguments to pass to the command given with -X. If the number of results is
greater than the given size, the command given with -X is run again with remaining arguments. A
batch size of zero means there is no limit (default), but note that batching might still happen
due to OS restrictions on the maximum length of command lines.
.SH PATTERN SYNTAX
The regular expression syntax used by fd is documented here:
@@ -455,7 +373,5 @@ $ fd -e py
.TP
.RI "Open all search results with vim:"
$ fd pattern -X vim
.SH BUGS
Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues
.SH SEE ALSO
.BR find (1)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

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

Before

Width:  |  Height:  |  Size: 6.8 KiB

-66
View File
@@ -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 '^\s*MIN_SUPPORTED_RUST_VERSION' .github/workflows/CICD.yml`.
- [ ] 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
+3 -7
View File
@@ -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
@@ -22,7 +20,7 @@ enter() {
}
prompt() {
printf '%b ' "$PROMPT" | pv -q
printf '%b ' $PROMPT | pv -q
}
type() {
@@ -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"
+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 115 KiB

+615
View File
@@ -0,0 +1,615 @@
use clap::{crate_version, App, AppSettings, Arg};
pub fn build_app() -> App<'static, 'static> {
let clap_color_setting = if std::env::var_os("NO_COLOR").is_none() {
AppSettings::ColoredHelp
} else {
AppSettings::ColorNever
};
let mut app = App::new("fd")
.version(crate_version!())
.usage("fd [FLAGS/OPTIONS] [<pattern>] [<path>...]")
.setting(clap_color_setting)
.setting(AppSettings::DeriveDisplayOrder)
.after_help(
"Note: `fd -h` prints a short and concise overview while `fd --help` gives all \
details.",
)
.arg(
Arg::with_name("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).",
),
)
.arg(
Arg::with_name("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.",
),
)
.arg(
Arg::with_name("no-ignore-vcs")
.long("no-ignore-vcs")
.overrides_with("no-ignore-vcs")
.hidden_short_help(true)
.long_help(
"Show search results from files and directories that would otherwise be \
ignored by '.gitignore' files.",
),
)
.arg(
Arg::with_name("no-global-ignore-file")
.long("no-global-ignore-file")
.hidden(true)
.long_help("Do not respect the global ignore file."),
)
.arg(
Arg::with_name("rg-alias-hidden-ignore")
.short("u")
.long("unrestricted")
.multiple(true)
.hidden_short_help(true)
.long_help(
"Alias for '--no-ignore'. Can be repeated. '-uu' is an alias for \
'--no-ignore --hidden'.",
),
)
.arg(
Arg::with_name("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::with_name("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::with_name("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::with_name("regex")
.long("regex")
.overrides_with_all(&["glob", "regex"])
.hidden_short_help(true)
.long_help(
"Perform a regular-expression based search (default). This can be used to \
override --glob.",
),
)
.arg(
Arg::with_name("fixed-strings")
.long("fixed-strings")
.short("F")
.alias("literal")
.overrides_with("fixed-strings")
.hidden_short_help(true)
.long_help(
"Treat the pattern as a literal string instead of a regular expression. Note \
that this also performs substring comparison. If you want to match on an \
exact filename, consider using '--glob'.",
),
)
.arg(
Arg::with_name("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.",
),
)
.arg(
Arg::with_name("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::with_name("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.",
),
)
.arg(
Arg::with_name("full-path")
.long("full-path")
.short("p")
.overrides_with("full-path")
.help("Search full path (default: file-/dirname 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 path.",
),
)
.arg(
Arg::with_name("null_separator")
.long("print0")
.short("0")
.overrides_with("print0")
.conflicts_with("list-details")
.hidden_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::with_name("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::with_name("rg-depth")
.long("maxdepth")
.hidden(true)
.takes_value(true)
)
.arg(
Arg::with_name("min-depth")
.long("min-depth")
.takes_value(true)
.value_name("depth")
.hidden_short_help(true)
.long_help(
"Only show search results starting at the given depth. \
See also: '--max-depth' and '--exact-depth'",
),
)
.arg(
Arg::with_name("exact-depth")
.long("exact-depth")
.takes_value(true)
.value_name("depth")
.hidden_short_help(true)
.conflicts_with_all(&["max-depth", "min-depth"])
.long_help(
"Only show search results at the exact given depth. This is an alias for \
'--min-depth <depth> --max-depth <depth>'.",
),
)
.arg(
Arg::with_name("prune")
.long("prune")
.conflicts_with_all(&["size", "exact-depth"])
.hidden_short_help(true)
.long_help("Do not traverse into matching directories.")
)
.arg(
Arg::with_name("file-type")
.long("type")
.short("t")
.multiple(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 (multiple allowable filetypes can be specified):\n \
'f' or 'file': regular files\n \
'd' or 'directory': directories\n \
'l' or 'symlink': symbolic links\n \
'x' or 'executable': executables\n \
'e' or 'empty': empty files or directories\n \
's' or 'socket': socket\n \
'p' or 'pipe': named pipe (FIFO)",
),
)
.arg(
Arg::with_name("extension")
.long("extension")
.short("e")
.multiple(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::with_name("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::with_name("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::with_name("exclude")
.long("exclude")
.short("E")
.takes_value(true)
.value_name("pattern")
.number_of_values(1)
.multiple(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::with_name("ignore-file")
.long("ignore-file")
.takes_value(true)
.value_name("path")
.number_of_values(1)
.multiple(true)
.hidden_short_help(true)
.long_help(
"Add a custom ignore-file in '.gitignore' format. These files have a low \
precedence.",
),
)
.arg(
Arg::with_name("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::with_name("threads")
.long("threads")
.short("j")
.takes_value(true)
.value_name("num")
.hidden_short_help(true)
.long_help(
"Set number of threads to use for searching & executing (default: number \
of available CPU cores)",
),
)
.arg(
Arg::with_name("size")
.long("size")
.short("S")
.takes_value(true)
.number_of_values(1)
.allow_hyphen_values(true)
.multiple(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::with_name("max-buffer-time")
.long("max-buffer-time")
.takes_value(true)
.hidden(true)
.long_help(
"Amount of time in milliseconds to buffer, before streaming the search \
results to the console.",
),
)
.arg(
Arg::with_name("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::with_name("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::with_name("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"])
.hidden_short_help(true)
.long_help("Limit the number of search results to 'count' and quit immediately."),
)
.arg(
Arg::with_name("max-one-result")
.short("1")
.hidden_short_help(true)
.overrides_with("max-results")
.conflicts_with_all(&["exec", "exec-batch", "list-details"])
.long_help("Limit the search to a single result and quit immediately. \
This is an alias for '--max-results=1'.")
)
.arg(
Arg::with_name("show-errors")
.long("show-errors")
.hidden_short_help(true)
.overrides_with("show-errors")
.long_help(
"Enable the display of filesystem errors for situations such as \
insufficient permissions or dead symlinks.",
),
)
.arg(
Arg::with_name("base-directory")
.long("base-directory")
.takes_value(true)
.value_name("path")
.number_of_values(1)
.hidden_short_help(true)
.long_help(
"Change the current working directory of fd to the provided path. This \
means that search results will be shown with respect to the given base \
path. Note that relative paths which are passed to fd via the positional \
<path> argument or the '--search-path' option will also be resolved \
relative to this directory.",
),
)
.arg(
Arg::with_name("pattern").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::with_name("path-separator")
.takes_value(true)
.value_name("separator")
.long("path-separator")
.hidden_short_help(true)
.long_help(
"Set the path separator to use when printing file paths. The default is \
the OS-specific separator ('/' on Unix, '\\' on Windows).",
),
)
.arg(
Arg::with_name("path")
.multiple(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::with_name("search-path")
.long("search-path")
.takes_value(true)
.conflicts_with("path")
.multiple(true)
.hidden_short_help(true)
.number_of_values(1)
.long_help(
"Provide paths to search as an alternative to the positional <path> \
argument. Changes the usage to `fd [FLAGS/OPTIONS] --search-path <path> \
--search-path <path2> [<pattern>]`",
),
);
if cfg!(unix) {
app = app.arg(
Arg::with_name("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::with_name("one-file-system")
.long("one-file-system")
.aliases(&["mount", "xdev"])
.hidden_short_help(true)
.long_help(
"By default, fd will traverse the file system tree as far as other options \
dictate. With this flag, fd ensures that it does not descend into a \
different file system than the one it started in. Comparable to the -mount \
or -xdev filters of find(1).",
),
);
}
app
}
-849
View File
@@ -1,849 +0,0 @@
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(feature = "completions")]
use anyhow::anyhow;
use clap::{
error::ErrorKind, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, Parser,
ValueEnum,
};
#[cfg(feature = "completions")]
use clap_complete::Shell;
use normpath::PathExt;
use crate::error::print_error;
use crate::exec::CommandSet;
use crate::filesystem;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::SizeFilter;
// Type for options that don't have any values, but are used to negate
// earlier options
struct Negations;
impl clap::FromArgMatches for Negations {
fn from_arg_matches(_: &ArgMatches) -> clap::error::Result<Self> {
Ok(Negations)
}
fn update_from_arg_matches(&mut self, _: &ArgMatches) -> clap::error::Result<()> {
Ok(())
}
}
impl clap::Args for Negations {
fn augment_args(cmd: Command) -> Command {
Self::augment_args_for_update(cmd)
}
fn augment_args_for_update(cmd: Command) -> Command {
cmd.arg(
Arg::new("no_hidden")
.action(ArgAction::Count)
.long("no-hidden")
.overrides_with("hidden")
.hide(true)
.long_help("Overrides --hidden."),
)
.arg(
Arg::new("ignore")
.action(ArgAction::Count)
.long("ignore")
.overrides_with("no_ignore")
.hide(true)
.long_help("Overrides --no-ignore."),
)
.arg(
Arg::new("ignore_vcs")
.action(ArgAction::Count)
.long("ignore-vcs")
.overrides_with("no_ignore_vcs")
.hide(true)
.long_help("Overrides --no-ignore-vcs."),
)
.arg(
Arg::new("relative_path")
.action(ArgAction::Count)
.long("relative-path")
.overrides_with("absolute_path")
.hide(true)
.long_help("Overrides --absolute-path."),
)
.arg(
Arg::new("no_follow")
.action(ArgAction::Count)
.long("no-follow")
.overrides_with("follow")
.hide(true)
.long_help("Overrides --follow."),
)
}
}
#[derive(Parser)]
#[command(
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", "has_results", "count"])),
)]
pub struct Opts {
/// Search hidden files and directories
#[arg(
long,
short = 'H',
long_help = "Include hidden directories and files in the search results (default: \
hidden files and directories are skipped). Files and directories are \
considered to be hidden if their name starts with a `.` sign (dot). \
The flag can be overridden with --no-hidden."
)]
pub hidden: bool,
/// Do not respect .(git|fd)ignore files
#[arg(
long,
short = 'I',
long_help = "Show search results from files and directories that would otherwise be \
ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file. \
The flag can be overridden with --ignore."
)]
pub no_ignore: bool,
/// Do not respect .gitignore files
#[arg(
long,
hide_short_help = true,
long_help = "Show search results from files and directories that would otherwise be \
ignored by '.gitignore' files. The flag can be overridden with --ignore-vcs."
)]
pub no_ignore_vcs: bool,
/// Do not respect .(git|fd)ignore files in parent directories
#[arg(
long,
hide_short_help = true,
long_help = "Show search results from files and directories that would otherwise be \
ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories."
)]
pub no_ignore_parent: bool,
/// Do not respect the global ignore file
#[arg(long, hide = true)]
pub no_global_ignore_file: bool,
/// Unrestricted search, alias for '--no-ignore --hidden'
#[arg(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no_hidden"]), action(ArgAction::Count), hide_short_help = true,
long_help = "Perform an unrestricted search, including ignored and hidden files. This is \
an alias for '--no-ignore --hidden'."
)]
rg_alias_hidden_ignore: u8,
/// Case-sensitive search (default: smart case)
#[arg(
long,
short = 's',
overrides_with("ignore_case"),
long_help = "Perform a case-sensitive search. By default, fd uses case-insensitive \
searches, unless the pattern contains an uppercase character (smart \
case)."
)]
pub case_sensitive: bool,
/// Case-insensitive search (default: smart case)
#[arg(
long,
short = 'i',
overrides_with("case_sensitive"),
long_help = "Perform a case-insensitive search. By default, fd uses case-insensitive \
searches, unless the pattern contains an uppercase character (smart \
case)."
)]
pub ignore_case: bool,
/// Glob-based search (default: regular expression)
#[arg(
long,
short = 'g',
conflicts_with("fixed_strings"),
long_help = "Perform a glob-based search instead of a regular expression search."
)]
pub glob: bool,
/// Regular-expression based search (default)
#[arg(
long,
overrides_with("glob"),
hide_short_help = true,
long_help = "Perform a regular-expression based search (default). This can be used to \
override --glob."
)]
pub regex: bool,
/// Treat pattern as literal string stead of regex
#[arg(
long,
short = 'F',
alias = "literal",
hide_short_help = true,
long_help = "Treat the pattern as a literal string instead of a regular expression. Note \
that this also performs substring comparison. If you want to match on an \
exact filename, consider using '--glob'."
)]
pub fixed_strings: bool,
/// Show absolute instead of relative paths
#[arg(
long,
short = 'a',
long_help = "Shows the full path starting from the root as opposed to relative paths. \
The flag can be overridden with --relative-path."
)]
pub absolute_path: bool,
/// Use a long listing format with file metadata
#[arg(
long,
short = 'l',
conflicts_with("absolute_path"),
long_help = "Use a detailed listing format like 'ls -l'. This is basically an alias \
for '--exec-batch ls -l' with some additional 'ls' options. This can be \
used to see more metadata, to show symlink targets and to achieve a \
deterministic sort order."
)]
pub list_details: bool,
/// Follow symbolic links
#[arg(
long,
short = 'L',
alias = "dereference",
long_help = "By default, fd does not descend into symlinked directories. Using this \
flag, symbolic links are also traversed. \
Flag can be overriden with --no-follow."
)]
pub follow: bool,
/// Search full abs. path (default: filename only)
#[arg(
long,
short = 'p',
long_help = "By default, the search pattern is only matched against the filename (or \
directory name). Using this flag, the pattern is matched against the full \
(absolute) path. Example:\n \
fd --glob -p '**/.git/config'"
)]
pub full_path: bool,
/// Separate search results by the null character
#[arg(
long = "print0",
short = '0',
conflicts_with("list_details"),
hide_short_help = true,
long_help = "Separate search results by the null character (instead of newlines). \
Useful for piping results to 'xargs'."
)]
pub null_separator: bool,
/// Set maximum search depth (default: none)
#[arg(
long,
short = 'd',
value_name = "depth",
alias("maxdepth"),
long_help = "Limit the directory traversal to a given depth. By default, there is no \
limit on the search depth."
)]
max_depth: Option<usize>,
/// Only show search results starting at the given depth.
#[arg(
long,
value_name = "depth",
hide_short_help = true,
long_help = "Only show search results starting at the given depth. \
See also: '--max-depth' and '--exact-depth'"
)]
min_depth: Option<usize>,
/// Only show search results at the exact given depth
#[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]),
long_help = "Only show search results at the exact given depth. This is an alias for \
'--min-depth <depth> --max-depth <depth>'.",
)]
exact_depth: Option<usize>,
/// Exclude entries that match the given glob pattern
#[arg(
long,
short = 'E',
value_name = "pattern",
long_help = "Exclude files/directories that match the given glob pattern. This \
overrides any other ignore logic. Multiple exclude patterns can be \
specified.\n\n\
Examples:\n \
--exclude '*.pyc'\n \
--exclude node_modules"
)]
pub exclude: Vec<String>,
/// Do not traverse into directories that match the search criteria. If
/// you want to exclude specific directories, use the '--exclude=…' option.
#[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]),
long_help = "Do not traverse into directories that match the search criteria. If \
you want to exclude specific directories, use the '--exclude=…' option.",
)]
pub prune: bool,
/// Filter by type: file (f), directory (d), symlink (l),
/// executable (x), empty (e), socket (s), pipe (p)
#[arg(
long = "type",
short = 't',
value_name = "filetype",
hide_possible_values = true,
value_enum,
long_help = "Filter the search by type:\n \
'f' or 'file': regular files\n \
'd' or 'directory': directories\n \
'l' or 'symlink': symbolic links\n \
's' or 'socket': socket\n \
'p' or 'pipe': named pipe (FIFO)\n\n \
'x' or 'executable': executables\n \
'e' or 'empty': empty files or directories\n\n\
This option can be specified more than once to include multiple file types. \
Searching for '--type file --type symlink' will show both regular files as \
well as symlinks. Note that the 'executable' and 'empty' filters work differently: \
'--type executable' implies '--type file' by default. And '--type empty' searches \
for empty files and directories, unless either '--type file' or '--type directory' \
is specified in addition.\n\n\
Examples:\n \
- Only search for files:\n \
fd --type file …\n \
fd -tf …\n \
- Find both files and symlinks\n \
fd --type file --type symlink …\n \
fd -tf -tl …\n \
- Find executable files:\n \
fd --type executable\n \
fd -tx\n \
- Find empty files:\n \
fd --type empty --type file\n \
fd -te -tf\n \
- Find empty directories:\n \
fd --type empty --type directory\n \
fd -te -td"
)]
pub filetype: Option<Vec<FileType>>,
/// Filter by file extension
#[arg(
long = "extension",
short = 'e',
value_name = "ext",
long_help = "(Additionally) filter search results by their file extension. Multiple \
allowable file extensions can be specified.\n\
If you want to search for files without extension, \
you can use the regex '^[^.]+$' as a normal search pattern."
)]
pub extensions: Option<Vec<String>>,
/// Limit results based on the size of files
#[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment, value_name = "size",
long_help = "Limit results based on the size of files using the format <+-><NUM><UNIT>.\n \
'+': file size must be greater than or equal to this\n \
'-': file size must be less than or equal to this\n\
If neither '+' nor '-' is specified, file size must be exactly equal to this.\n \
'NUM': The numeric size (e.g. 500)\n \
'UNIT': The units for NUM. They are not case-sensitive.\n\
Allowed unit values:\n \
'b': bytes\n \
'k': kilobytes (base ten, 10^3 = 1000 bytes)\n \
'm': megabytes\n \
'g': gigabytes\n \
't': terabytes\n \
'ki': kibibytes (base two, 2^10 = 1024 bytes)\n \
'mi': mebibytes\n \
'gi': gibibytes\n \
'ti': tebibytes",
)]
pub size: Vec<SizeFilter>,
/// Filter by file modification time (newer than)
#[arg(
long,
alias("change-newer-than"),
alias("newer"),
value_name = "date|dur",
long_help = "Filter results based on the file modification time. The argument can be provided \
as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min). \
If the time is not specified, it defaults to 00:00:00. \
'--change-newer-than' or '--newer' can be used as aliases.\n\
Examples:\n \
--changed-within 2weeks\n \
--change-newer-than '2018-10-27 10:00:00'\n \
--newer 2018-10-27"
)]
pub changed_within: Option<String>,
/// Filter by file modification time (older than)
#[arg(
long,
alias("change-older-than"),
alias("older"),
value_name = "date|dur",
long_help = "Filter results based on the file modification time. The argument can be provided \
as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min). \
'--change-older-than' or '--older' can be used as aliases.\n\
Examples:\n \
--changed-before '2018-10-27 10:00:00'\n \
--change-older-than 2weeks\n \
--older 2018-10-27"
)]
pub changed_before: Option<String>,
/// Filter by owning user and/or group
#[cfg(unix)]
#[arg(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group",
long_help = "Filter files by their user and/or group. \
Format: [(user|uid)][:(group|gid)]. Either side is optional. \
Precede either side with a '!' to exclude files instead.\n\
Examples:\n \
--owner john\n \
--owner :students\n \
--owner '!john:students'"
)]
pub owner: Option<OwnerFilter>,
#[command(flatten)]
pub exec: Exec,
/// Max number of arguments to run as a batch size with -X
#[arg(
long,
value_name = "size",
hide_short_help = true,
requires("exec_batch"),
value_parser = value_parser!(usize),
default_value_t,
long_help = "Maximum number of arguments to pass to the command given with -X. \
If the number of results is greater than the given size, \
the command given with -X is run again with remaining arguments. \
A batch size of zero means there is no limit (default), but note \
that batching might still happen due to OS restrictions on the \
maximum length of command lines.",
)]
pub batch_size: usize,
/// Add a custom ignore-file in '.gitignore' format
#[arg(
long,
value_name = "path",
hide_short_help = true,
long_help = "Add a custom ignore-file in '.gitignore' format. These files have a low precedence."
)]
pub ignore_file: Vec<PathBuf>,
/// When to use colors
#[arg(
long,
short = 'c',
value_enum,
default_value_t = ColorWhen::Auto,
value_name = "when",
long_help = "Declare when to use color for the pattern match output",
)]
pub color: ColorWhen,
/// Set number of threads to use for searching & executing (default: number
/// of available CPU cores)
#[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = clap::value_parser!(u32).range(1..))]
pub threads: Option<u32>,
/// 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 number of search results
#[arg(
long,
value_name = "count",
hide_short_help = true,
long_help = "Limit the number of search results to 'count' and quit immediately."
)]
max_results: Option<usize>,
/// Limit search to a single result
#[arg(
short = '1',
hide_short_help = true,
overrides_with("max_results"),
long_help = "Limit the search to a single result and quit immediately. \
This is an alias for '--max-results=1'."
)]
max_one_result: bool,
/// Print nothing, exit code 0 if match found, 1 otherwise
#[arg(
long,
short = 'q',
alias = "has-results",
hide_short_help = true,
conflicts_with("max_results"),
long_help = "When the flag is present, the program does not print anything and will \
return with an exit code of 0 if there is at least one match. Otherwise, the \
exit code will be 1. \
'--has-results' can be used as an alias."
)]
pub quiet: bool,
/// Show filesystem errors
#[arg(
long,
hide_short_help = true,
long_help = "Enable the display of filesystem errors for situations such as \
insufficient permissions or dead symlinks."
)]
pub show_errors: bool,
/// Change current working directory
#[arg(
long,
value_name = "path",
hide_short_help = true,
long_help = "Change the current working directory of fd to the provided path. This \
means that search results will be shown with respect to the given base \
path. Note that relative paths which are passed to fd via the positional \
<path> argument or the '--search-path' option will also be resolved \
relative to this directory."
)]
pub base_directory: Option<PathBuf>,
/// the search pattern (a regular expression, unless '--glob' is used; optional)
#[arg(
default_value = "",
hide_default_value = true,
value_name = "pattern",
long_help = "the search pattern which is either a regular expression (default) or a glob \
pattern (if --glob is used). If no pattern has been specified, every entry \
is considered a match. If your pattern starts with a dash (-), make sure to \
pass '--' first, or it will be considered as a flag (fd -- '-foo')."
)]
pub pattern: String,
/// Set path separator when printing file paths
#[arg(
long,
value_name = "separator",
hide_short_help = true,
long_help = "Set the path separator to use when printing file paths. The default is \
the OS-specific separator ('/' on Unix, '\\' on Windows)."
)]
pub path_separator: Option<String>,
/// the root directories for the filesystem search (optional)
#[arg(action = ArgAction::Append,
value_name = "path",
long_help = "The directory where the filesystem search is rooted (optional). If \
omitted, search the current working directory.",
)]
path: Vec<PathBuf>,
/// Provides paths to search as an alternative to the positional <path> argument
#[arg(
long,
conflicts_with("path"),
value_name = "search-path",
hide_short_help = true,
long_help = "Provide paths to search as an alternative to the positional <path> \
argument. Changes the usage to `fd [OPTIONS] --search-path <path> \
--search-path <path2> [<pattern>]`"
)]
search_path: Vec<PathBuf>,
/// By default, relative paths are prefixed with './' when -x/--exec,
/// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
/// path starting with '-' being treated as a command line option. Use
/// this flag to disable this behaviour.
#[arg(long, conflicts_with_all(&["path", "search_path"]), hide_short_help = true,
long_help = "By default, relative paths are prefixed with './' when -x/--exec, \
-X/--exec-batch, or -0/--print0 are given, to reduce the risk of a \
path starting with '-' being treated as a command line option. Use \
this flag to disable this behaviour.",
)]
pub strip_cwd_prefix: bool,
#[cfg(any(unix, windows))]
#[arg(long, aliases(&["mount", "xdev"]), hide_short_help = true,
long_help = "By default, fd will traverse the file system tree as far as other options \
dictate. With this flag, fd ensures that it does not descend into a \
different file system than the one it started in. Comparable to the -mount \
or -xdev filters of find(1).")]
pub one_file_system: bool,
#[cfg(feature = "completions")]
#[arg(long, hide = true, exclusive = true)]
gen_completions: Option<Option<Shell>>,
#[clap(flatten)]
_negations: Negations,
}
impl Opts {
pub fn search_paths(&self) -> anyhow::Result<Vec<PathBuf>> {
// would it make sense to concatenate these?
let paths = if !self.path.is_empty() {
&self.path
} else if !self.search_path.is_empty() {
&self.search_path
} else {
let current_directory = Path::new(".");
ensure_current_directory_exists(current_directory)?;
return Ok(vec![self.normalize_path(current_directory)]);
};
Ok(paths
.iter()
.filter_map(|path| {
if filesystem::is_existing_directory(path) {
Some(self.normalize_path(path))
} else {
print_error(format!(
"Search path '{}' is not a directory.",
path.to_string_lossy()
));
None
}
})
.collect())
}
fn normalize_path(&self, path: &Path) -> PathBuf {
if self.absolute_path {
filesystem::absolute_path(path.normalize().unwrap().as_path()).unwrap()
} else {
path.to_path_buf()
}
}
pub fn no_search_paths(&self) -> bool {
self.path.is_empty() && self.search_path.is_empty()
}
#[inline]
pub fn rg_alias_ignore(&self) -> bool {
self.rg_alias_hidden_ignore > 0
}
pub fn max_depth(&self) -> Option<usize> {
self.max_depth.or(self.exact_depth)
}
pub fn min_depth(&self) -> Option<usize> {
self.min_depth.or(self.exact_depth)
}
pub fn threads(&self) -> usize {
// This will panic if the number of threads passed in is more than usize::MAX in an environment
// where usize is less than 32 bits (for example 16-bit architectures). It's pretty
// unlikely fd will be running in such an environment, and even more unlikely someone would
// be trying to use that many threads on such an environment, so I think panicing is an
// appropriate way to handle that.
std::cmp::max(
self.threads
.map_or_else(num_cpus::get, |n| n.try_into().expect("too many threads")),
1,
)
}
pub fn max_results(&self) -> Option<usize> {
self.max_results
.filter(|&m| m > 0)
.or_else(|| self.max_one_result.then(|| 1))
}
#[cfg(feature = "completions")]
pub fn gen_completions(&self) -> anyhow::Result<Option<Shell>> {
self.gen_completions
.map(|maybe_shell| match maybe_shell {
Some(sh) => Ok(sh),
None => guess_shell(),
})
.transpose()
}
}
#[cfg(feature = "completions")]
fn guess_shell() -> anyhow::Result<Shell> {
let env_shell = std::env::var_os("SHELL").map(PathBuf::from);
if let Some(shell) = env_shell
.as_ref()
.and_then(|s| s.file_name())
.and_then(|s| s.to_str())
{
shell
.parse::<Shell>()
.map_err(|_| anyhow!("Unknown shell {}", shell))
} else {
// Assume powershell on windows
#[cfg(windows)]
return Ok(Shell::PowerShell);
#[cfg(not(windows))]
return Err(anyhow!("Unable to get shell from environment"));
}
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
pub enum FileType {
#[value(alias = "f")]
File,
#[value(alias = "d")]
Directory,
#[value(alias = "l")]
Symlink,
#[value(alias = "x")]
Executable,
#[value(alias = "e")]
Empty,
#[value(alias = "s")]
Socket,
#[value(alias = "p")]
Pipe,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
pub enum ColorWhen {
/// show colors if the output goes to an interactive console (default)
Auto,
/// always use colorized output
Always,
/// do not use colorized output
Never,
}
impl ColorWhen {
pub fn as_str(&self) -> &'static str {
use ColorWhen::*;
match *self {
Auto => "auto",
Never => "never",
Always => "always",
}
}
}
// there isn't a derive api for getting grouped values yet,
// so we have to use hand-rolled parsing for exec and exec-batch
pub struct Exec {
pub command: Option<CommandSet>,
}
impl clap::FromArgMatches for Exec {
fn from_arg_matches(matches: &ArgMatches) -> clap::error::Result<Self> {
let command = matches
.grouped_values_of("exec")
.map(CommandSet::new)
.or_else(|| {
matches
.grouped_values_of("exec_batch")
.map(CommandSet::new_batch)
})
.transpose()
.map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?;
Ok(Exec { command })
}
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> clap::error::Result<()> {
*self = Self::from_arg_matches(matches)?;
Ok(())
}
}
impl clap::Args for Exec {
fn augment_args(cmd: Command) -> Command {
cmd.arg(Arg::new("exec")
.action(ArgAction::Append)
.long("exec")
.short('x')
.num_args(1..)
.allow_hyphen_values(true)
.value_terminator(";")
.value_name("cmd")
.conflicts_with("list_details")
.help("Execute a command for each search result")
.long_help(
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
All positional arguments following --exec are considered to be arguments to the command - not to fd. \
It is therefore recommended to place the '-x'/'--exec' option last.\n\
The following placeholders are substituted before the command is executed:\n \
'{}': path (of the current search result)\n \
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension\n\n\
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
Examples:\n\n \
- find all *.zip files and unzip them:\n\n \
fd -e zip -x unzip\n\n \
- find *.h and *.cpp files and run \"clang-format -i ..\" for each of them:\n\n \
fd -e h -e cpp -x clang-format -i\n\n \
- Convert all *.jpg files to *.png files:\n\n \
fd -e jpg -x convert {} {.}.png\
",
),
)
.arg(
Arg::new("exec_batch")
.action(ArgAction::Append)
.long("exec-batch")
.short('X')
.num_args(1..)
.allow_hyphen_values(true)
.value_terminator(";")
.value_name("cmd")
.conflicts_with_all(&["exec", "list_details"])
.help("Execute a command with all search results at once")
.long_help(
"Execute the given command once, with all search results as arguments.\n\
One of the following placeholders is substituted before the command is executed:\n \
'{}': path (of all search results)\n \
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension\n\n\
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
Examples:\n\n \
- Find all test_*.py files and open them in your favorite editor:\n\n \
fd -g 'test_*.py' -X vim\n\n \
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
fd -e rs -X wc -l\
"
),
)
}
fn augment_args_for_update(cmd: Command) -> Command {
Self::augment_args(cmd)
}
}
fn parse_millis(arg: &str) -> Result<Duration, std::num::ParseIntError> {
Ok(Duration::from_millis(arg.parse()?))
}
fn ensure_current_directory_exists(current_directory: &Path) -> anyhow::Result<()> {
if filesystem::is_existing_directory(current_directory) {
Ok(())
} else {
Err(anyhow!(
"Could not retrieve current directory (has it been deleted?)."
))
}
}
-154
View File
@@ -1,154 +0,0 @@
use std::ffi::OsString;
use std::fs::{FileType, Metadata};
use std::path::{Path, PathBuf};
use lscolors::{Colorable, LsColors, Style};
use once_cell::unsync::OnceCell;
use crate::config::Config;
use crate::filesystem::strip_current_dir;
enum DirEntryInner {
Normal(ignore::DirEntry),
BrokenSymlink(PathBuf),
}
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> {
self.path().partial_cmp(other.path())
}
}
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()
}
}
+31 -89
View File
@@ -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
}
}
+2 -2
View File
@@ -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);
+32 -34
View File
@@ -1,35 +1,34 @@
use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use crossbeam_channel::Receiver;
use crate::config::Config;
use crate::dir_entry::DirEntry;
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(
rx: Receiver<WorkerResult>,
cmd: Arc<CommandSet>,
rx: Arc<Mutex<Receiver<WorkerResult>>>,
cmd: Arc<CommandTemplate>,
out_perm: Arc<Mutex<()>>,
config: &Config,
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();
// Obtain the next result from the receiver, else if the channel
// has closed, exit from the loop
let dir_entry: DirEntry = match rx.recv() {
Ok(WorkerResult::Entry(dir_entry)) => dir_entry,
let value: PathBuf = match lock.recv() {
Ok(WorkerResult::Entry(val)) => val,
Ok(WorkerResult::Error(err)) => {
if config.show_filesystem_errors {
if show_filesystem_errors {
print_error(err.to_string());
}
continue;
@@ -37,30 +36,29 @@ pub fn job(
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.
results.push(cmd.execute(
dir_entry.stripped_path(config),
config.path_separator.as_deref(),
Arc::clone(&out_perm),
buffer_output,
))
results.push(cmd.generate_and_execute(&value, Arc::clone(&out_perm), buffer_output))
}
// Returns error in case of any error.
merge_exitcodes(results)
merge_exitcodes(&results)
}
pub fn batch(rx: Receiver<WorkerResult>, cmd: &CommandSet, config: &Config) -> ExitCode {
let paths = rx
.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
pub fn batch(
rx: Receiver<WorkerResult>,
cmd: &CommandTemplate,
show_filesystem_errors: bool,
buffer_output: bool,
) -> ExitCode {
let paths = rx.iter().filter_map(|value| match value {
WorkerResult::Entry(val) => Some(val),
WorkerResult::Error(err) => {
if show_filesystem_errors {
print_error(err.to_string());
}
});
cmd.execute_batch(paths, config.batch_size, config.path_separator.as_deref())
None
}
});
cmd.generate_and_execute_batch(paths, buffer_output)
}
+152 -284
View File
@@ -5,26 +5,24 @@ mod token;
use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::io;
use std::iter;
use std::path::{Component, Path, PathBuf, Prefix};
use std::process::Stdio;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use anyhow::{bail, Result};
use argmax::Command;
use once_cell::sync::Lazy;
use anyhow::{anyhow, Result};
use lazy_static::lazy_static;
use regex::Regex;
use crate::exit_codes::{merge_exitcodes, ExitCode};
use crate::exit_codes::ExitCode;
use crate::filesystem::strip_current_dir;
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,
@@ -32,205 +30,51 @@ pub enum ExecutionMode {
Batch,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CommandSet {
mode: ExecutionMode,
commands: Vec<CommandTemplate>,
}
impl CommandSet {
pub fn new<I, S>(input: I) -> Result<CommandSet>
where
I: IntoIterator<Item = Vec<S>>,
S: AsRef<str>,
{
Ok(CommandSet {
mode: ExecutionMode::OneByOne,
commands: input
.into_iter()
.map(CommandTemplate::new)
.collect::<Result<_>>()?,
})
}
pub fn new_batch<I, S>(input: I) -> Result<CommandSet>
where
I: IntoIterator<Item = Vec<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: Arc<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: ArgumentTemplate,
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 == 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 {
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>,
{
static PLACEHOLDER_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\{(/?\.?|//)\}").unwrap());
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>,
{
lazy_static! {
static ref PLACEHOLDER_PATTERN: Regex = Regex::new(r"\{(/?\.?|//)\}").unwrap();
}
let mut args = Vec::new();
let mut has_placeholder = false;
@@ -275,21 +119,16 @@ impl CommandTemplate {
args.push(ArgumentTemplate::Tokens(tokens));
}
// We need to check that we have at least one argument, because if not
// it will try to execute each file and directory it finds.
//
// Sadly, clap can't currently handle this for us, see
// https://github.com/clap-rs/clap/issues/3542
if args.is_empty() {
bail!("No executable provided for --exec or --exec-batch");
}
// If a placeholder token was not supplied, append one at the end of the command.
if !has_placeholder {
args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder]));
}
Ok(CommandTemplate { args })
CommandTemplate {
args,
mode,
path_separator,
}
}
fn number_of_tokens(&self) -> usize {
@@ -299,13 +138,59 @@ 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 input = strip_current_dir(input);
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()));
}
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(strip_current_dir(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
}
Ok(cmd)
}
}
@@ -421,16 +306,15 @@ mod tests {
#[test]
fn tokens_with_placeholder() {
assert_eq!(
CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::Text("${SHELL}:".into()),
ArgumentTemplate::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,
}
);
}
@@ -438,15 +322,14 @@ mod tests {
#[test]
fn tokens_with_no_extension() {
assert_eq!(
CommandSet::new(vec![vec!["echo", "{.}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::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,
}
);
}
@@ -454,15 +337,14 @@ mod tests {
#[test]
fn tokens_with_basename() {
assert_eq!(
CommandSet::new(vec![vec!["echo", "{/}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::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,
}
);
}
@@ -470,15 +352,14 @@ mod tests {
#[test]
fn tokens_with_parent() {
assert_eq!(
CommandSet::new(vec![vec!["echo", "{//}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::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,
}
);
}
@@ -486,15 +367,14 @@ mod tests {
#[test]
fn tokens_with_basename_no_extension() {
assert_eq!(
CommandSet::new(vec![vec!["echo", "{/.}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::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,
}
);
}
@@ -502,19 +382,18 @@ mod tests {
#[test]
fn tokens_multiple() {
assert_eq!(
CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("cp".into()),
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
ArgumentTemplate::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,
}
);
}
@@ -522,32 +401,21 @@ mod tests {
#[test]
fn tokens_single_batch() {
assert_eq!(
CommandSet::new_batch(vec![vec!["echo", "{.}"]]).unwrap(),
CommandSet {
commands: vec![CommandTemplate {
args: vec![
ArgumentTemplate::Text("echo".into()),
ArgumentTemplate::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]
@@ -582,11 +450,11 @@ mod tests {
// This is uncommon, but valid
check!(r"C:foo\bar", "C:foo#bar");
// forward slashes should get normalized and interpreted as separators
// forward slashses should get normalized and interpreted as separators
check!("C:/foo/bar", "C:#foo#bar");
check!("C:foo/bar", "C:foo#bar");
// Rust does not interpret "//server/share" as a UNC path, but rather as a normal
// Rust does not intrepret "//server/share" as a UNC path, but rather as a normal
// absolute path that begins with RootDir, and the two slashes get combined together as
// a single path separator during normalization.
//check!("//server/share/path", "##server#share#path");
+1 -1
View File
@@ -4,7 +4,7 @@ use std::fmt::{self, Display, Formatter};
///
/// 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)]
#[derive(Clone, Debug, PartialEq)]
pub enum Token {
Placeholder,
Basename,
+12 -34
View File
@@ -1,12 +1,6 @@
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),
GeneralError,
KilledBySigint,
}
@@ -15,7 +9,6 @@ impl From<ExitCode> for i32 {
fn from(code: ExitCode) -> Self {
match code {
ExitCode::Success => 0,
ExitCode::HasResults(has_results) => !has_results as i32,
ExitCode::GeneralError => 1,
ExitCode::KilledBySigint => 130,
}
@@ -24,27 +17,12 @@ impl From<ExitCode> for i32 {
impl ExitCode {
fn is_error(self) -> bool {
i32::from(self) != 0
}
/// Exit the process with the appropriate code.
pub fn exit(self) -> ! {
#[cfg(unix)]
if self == ExitCode::KilledBySigint {
// Get rid of the SIGINT handler, if present, and raise SIGINT
unsafe {
if signal(Signal::SIGINT, SigHandler::SigDfl).is_ok() {
let _ = raise(Signal::SIGINT);
}
}
}
process::exit(self.into())
self != ExitCode::Success
}
}
pub fn merge_exitcodes(results: impl IntoIterator<Item = ExitCode>) -> ExitCode {
if results.into_iter().any(ExitCode::is_error) {
pub fn merge_exitcodes(results: &[ExitCode]) -> ExitCode {
if results.iter().any(|&c| ExitCode::is_error(c)) {
return ExitCode::GeneralError;
}
ExitCode::Success
@@ -56,38 +34,38 @@ mod tests {
#[test]
fn success_when_no_results() {
assert_eq!(merge_exitcodes([]), ExitCode::Success);
assert_eq!(merge_exitcodes(&[]), ExitCode::Success);
}
#[test]
fn general_error_if_at_least_one_error() {
assert_eq!(
merge_exitcodes([ExitCode::GeneralError]),
merge_exitcodes(&[ExitCode::GeneralError]),
ExitCode::GeneralError
);
assert_eq!(
merge_exitcodes([ExitCode::KilledBySigint]),
merge_exitcodes(&[ExitCode::KilledBySigint]),
ExitCode::GeneralError
);
assert_eq!(
merge_exitcodes([ExitCode::KilledBySigint, ExitCode::Success]),
merge_exitcodes(&[ExitCode::KilledBySigint, ExitCode::Success]),
ExitCode::GeneralError
);
assert_eq!(
merge_exitcodes([ExitCode::Success, ExitCode::GeneralError]),
merge_exitcodes(&[ExitCode::Success, ExitCode::GeneralError]),
ExitCode::GeneralError
);
assert_eq!(
merge_exitcodes([ExitCode::GeneralError, ExitCode::KilledBySigint]),
merge_exitcodes(&[ExitCode::GeneralError, ExitCode::KilledBySigint]),
ExitCode::GeneralError
);
}
#[test]
fn success_if_no_error() {
assert_eq!(merge_exitcodes([ExitCode::Success]), ExitCode::Success);
assert_eq!(merge_exitcodes(&[ExitCode::Success]), ExitCode::Success);
assert_eq!(
merge_exitcodes([ExitCode::Success, ExitCode::Success]),
merge_exitcodes(&[ExitCode::Success, ExitCode::Success]),
ExitCode::Success
);
}
+5 -8
View File
@@ -7,12 +7,9 @@ use std::io;
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
use std::path::{Path, PathBuf};
#[cfg(windows)]
use faccess::PathExt as _;
use normpath::PathExt;
use crate::dir_entry;
use crate::walk;
pub fn path_absolute_form(path: &Path) -> io::Result<PathBuf> {
if path.is_absolute() {
@@ -45,16 +42,16 @@ pub fn is_existing_directory(path: &Path) -> bool {
}
#[cfg(any(unix, target_os = "redox"))]
pub fn is_executable(_: &Path, md: &fs::Metadata) -> bool {
pub fn is_executable(md: &fs::Metadata) -> bool {
md.permissions().mode() & 0o111 != 0
}
#[cfg(windows)]
pub fn is_executable(path: &Path, _: &fs::Metadata) -> bool {
path.executable()
pub fn is_executable(_: &fs::Metadata) -> bool {
false
}
pub fn is_empty(entry: &dir_entry::DirEntry) -> bool {
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()) {
+10 -25
View File
@@ -1,8 +1,4 @@
use crate::dir_entry;
use crate::filesystem;
/// Whether or not to show
#[derive(Default)]
pub struct FileTypes {
pub files: bool,
pub directories: bool,
@@ -13,27 +9,16 @@ pub struct FileTypes {
pub empty_only: bool,
}
impl FileTypes {
pub fn should_ignore(&self, entry: &dir_entry::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.sockets && filesystem::is_socket(*entry_type))
|| (!self.pipes && filesystem::is_pipe(*entry_type))
|| (self.executables_only
&& !entry
.metadata()
.map(|md| filesystem::is_executable(entry.path(), md))
.unwrap_or(false))
|| (self.empty_only && !filesystem::is_empty(entry))
|| !(entry_type.is_file()
|| entry_type.is_dir()
|| entry_type.is_symlink()
|| filesystem::is_socket(*entry_type)
|| filesystem::is_pipe(*entry_type))
} else {
true
impl Default for FileTypes {
fn default() -> FileTypes {
FileTypes {
files: false,
directories: false,
symlinks: false,
sockets: false,
pipes: false,
executables_only: false,
empty_only: false,
}
}
}
+15 -25
View File
@@ -1,13 +1,13 @@
use anyhow::{anyhow, Result};
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),
@@ -15,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());
@@ -47,15 +42,10 @@ impl OwnerFilter {
.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 }))
}
}
@@ -116,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(_),
+7 -12
View File
@@ -1,11 +1,11 @@
use anyhow::anyhow;
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
use regex::Regex;
static SIZE_CAPTURES: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)^([+-]?)(\d+)(b|[kmgt]i?b?)$").unwrap());
lazy_static! {
static ref SIZE_CAPTURES: Regex = 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,12 +25,7 @@ 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> {
pub fn from_string(s: &str) -> Option<Self> {
if !SIZE_CAPTURES.is_match(s) {
return None;
}
@@ -171,7 +166,7 @@ mod tests {
#[test]
fn $name() {
let i = SizeFilter::from_string($value);
assert!(i.is_err());
assert!(i.is_none());
}
)*
};
+6 -57
View File
@@ -1,9 +1,7 @@
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),
@@ -13,20 +11,9 @@ impl TimeFilter {
fn from_str(ref_time: &SystemTime, s: &str) -> Option<SystemTime> {
humantime::parse_duration(s)
.map(|duration| *ref_time - duration)
.or_else(|_| humantime::parse_rfc3339_weak(s))
.or_else(|_| humantime::parse_rfc3339_weak(&(s.to_owned() + " 00:00:00")))
.ok()
.or_else(|| {
DateTime::parse_from_rfc3339(s)
.map(|dt| dt.into())
.ok()
.or_else(|| {
NaiveDate::parse_from_str(s, "%F")
.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())
})
}
pub fn before(ref_time: &SystemTime, s: &str) -> Option<TimeFilter> {
@@ -39,8 +26,8 @@ impl TimeFilter {
pub fn applies_to(&self, t: &SystemTime) -> bool {
match self {
TimeFilter::Before(limit) => t < limit,
TimeFilter::After(limit) => t > limit,
TimeFilter::Before(limit) => t <= limit,
TimeFilter::After(limit) => t >= limit,
}
}
}
@@ -52,11 +39,7 @@ mod tests {
#[test]
fn is_time_filter_applicable() {
let ref_time = Local
.datetime_from_str("2010-10-10 10:10:10", "%F %T")
.unwrap()
.into();
let ref_time = humantime::parse_rfc3339("2010-10-10T10:10:10Z").unwrap();
assert!(TimeFilter::after(&ref_time, "1min")
.unwrap()
.applies_to(&ref_time));
@@ -93,39 +76,5 @@ mod tests {
assert!(!TimeFilter::after(&ref_time, t10s_before)
.unwrap()
.applies_to(&t1m_ago));
let same_day = "2010-10-10";
assert!(!TimeFilter::before(&ref_time, same_day)
.unwrap()
.applies_to(&ref_time));
assert!(!TimeFilter::before(&ref_time, same_day)
.unwrap()
.applies_to(&t1m_ago));
assert!(TimeFilter::after(&ref_time, same_day)
.unwrap()
.applies_to(&ref_time));
assert!(TimeFilter::after(&ref_time, same_day)
.unwrap()
.applies_to(&t1m_ago));
let ref_time = DateTime::parse_from_rfc3339("2010-10-10T10:10:10+00:00")
.unwrap()
.into();
let t1m_ago = ref_time - Duration::from_secs(60);
let t10s_before = "2010-10-10T10:10:00+00:00";
assert!(!TimeFilter::before(&ref_time, t10s_before)
.unwrap()
.applies_to(&ref_time));
assert!(TimeFilter::before(&ref_time, t10s_before)
.unwrap()
.applies_to(&t1m_ago));
assert!(TimeFilter::after(&ref_time, t10s_before)
.unwrap()
.applies_to(&ref_time));
assert!(!TimeFilter::after(&ref_time, t10s_before)
.unwrap()
.applies_to(&t1m_ago));
}
}
+344 -292
View File
@@ -1,36 +1,36 @@
mod cli;
mod config;
mod dir_entry;
mod app;
mod error;
mod exec;
mod exit_codes;
mod filesystem;
mod filetypes;
mod filter;
mod options;
mod output;
mod regex_helper;
mod walk;
use std::env;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::Arc;
use std::time;
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{anyhow, Context, Result};
use atty::Stream;
use clap::{CommandFactory, Parser};
use globset::GlobBuilder;
use lscolors::LsColors;
use regex::bytes::{RegexBuilder, RegexSetBuilder};
use normpath::PathExt;
use crate::cli::{ColorWhen, 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::options::Options;
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
@@ -39,10 +39,7 @@ use crate::regex_helper::{pattern_has_uppercase_char, pattern_matches_strings_wi
not(windows),
not(target_os = "android"),
not(target_os = "macos"),
not(target_os = "freebsd"),
not(all(target_env = "musl", target_pointer_width = "32")),
not(target_arch = "riscv64"),
feature = "use-jemalloc"
not(target_env = "musl")
))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
@@ -53,55 +50,12 @@ ow=0:or=0;38;5;16;48;5;203:no=0:ex=1;38;5;203:cd=0;38;5;203;48;5;236:mi=0;38;5;1
38;5;185:*.jpg=0;38;5;208:*.mir=0;38;5;48:*.sxi=0;38;5;186:*.bz2=4;38;5;203:*.odt=0;38;5;186:*.mov=0;38;5;208:*.toc=0;38;5;243:*.bat=1;38;5;203:*.asa=0;38;5;48:*.awk=0;38;5;48:*.sbt=0;38;5;48:*.vcd=4;38;5;203:*.kts=0;38;5;48:*.arj=4;38;5;203:*.blg=0;38;5;243:*.c++=0;38;5;48:*.odp=0;38;5;186:*.bbl=0;38;5;243:*.idx=0;38;5;243:*.com=1;38;5;203:*.mp3=0;38;5;208:*.avi=0;38;5;208:*.def=0;38;5;48:*.cgi=0;38;5;48:*.zip=4;38;5;203:*.ttf=0;38;5;208:*.ppt=0;38;5;186:*.tml=0;38;5;149:*.fsx=0;38;5;48:*.h++=0;38;5;48:*.rtf=0;38;5;186:*.inl=0;38;5;48:*.yaml=0;38;5;149:*.html=0;38;5;185:*.mpeg=0;38;5;208:*.java=0;38;5;48:*.hgrc=0;38;5;149:*.orig=0;38;5;243:*.conf=0;38;5;149:*.dart=0;38;5;48:*.psm1=0;38;5;48:*.rlib=0;38;5;243:*.fish=0;38;5;48:*.bash=0;38;5;48:*.make=0;38;5;149:*.docx=0;38;5;186:*.json=0;38;5;149:*.psd1=0;38;5;48:*.lisp=0;38;5;48:*.tbz2=4;38;5;203:*.diff=0;38;5;48:*.epub=0;38;5;186:*.xlsx=0;38;5;186:*.pptx=0;38;5;186:*.toml=0;38;5;149:*.h264=0;38;5;208:*.purs=0;38;5;48:*.flac=0;38;5;208:*.tiff=0;38;5;208:*.jpeg=0;38;5;208:*.lock=0;38;5;243:*.less=0;38;5;48:*.dyn_o=0;38;5;243:*.scala=0;38;5;48:*.mdown=0;38;5;185:*.shtml=0;38;5;185:*.class=0;38;5;243:*.cache=0;38;5;243:*.cmake=0;38;5;149:*passwd=0;38;5;149:*.swift=0;38;5;48:*shadow=0;38;5;149:*.xhtml=0;38;5;185:*.patch=0;38;5;48:*.cabal=0;38;5;48:*README=0;38;5;16;48;5;186:*.toast=4;38;5;203:*.ipynb=0;38;5;48:*COPYING=0;38;5;249:*.gradle=0;38;5;48:*.matlab=0;38;5;48:*.config=0;38;5;149:*LICENSE=0;38;5;249:*.dyn_hi=0;38;5;243:*.flake8=0;38;5;149:*.groovy=0;38;5;48:*INSTALL=0;38;5;16;48;5;186:*TODO.md=1:*.ignore=0;38;5;149:*Doxyfile=0;38;5;149:*TODO.txt=1:*setup.py=0;38;5;149:*Makefile=0;38;5;149:*.gemspec=0;38;5;149:*.desktop=0;38;5;149:*.rgignore=0;38;5;149:*.markdown=0;38;5;185:*COPYRIGHT=0;38;5;249:*configure=0;38;5;149:*.DS_Store=0;38;5;243:*.kdevelop=0;38;5;149:*.fdignore=0;38;5;149:*README.md=0;38;5;16;48;5;186:*.cmake.in=0;38;5;149:*SConscript=0;38;5;149:*CODEOWNERS=0;38;5;149:*.localized=0;38;5;243:*.gitignore=0;38;5;149:*Dockerfile=0;38;5;149:*.gitconfig=0;38;5;149:*INSTALL.md=0;38;5;16;48;5;186:*README.txt=0;38;5;16;48;5;186:*SConstruct=0;38;5;149:*.scons_opt=0;38;5;243:*.travis.yml=0;38;5;186:*.gitmodules=0;38;5;149:*.synctex.gz=0;38;5;243:*LICENSE-MIT=0;38;5;249:*MANIFEST.in=0;38;5;149:*Makefile.in=0;38;5;243:*Makefile.am=0;38;5;149:*INSTALL.txt=0;38;5;16;48;5;186:*configure.ac=0;38;5;149:*.applescript=0;38;5;48:*appveyor.yml=0;38;5;186:*.fdb_latexmk=0;38;5;243:*CONTRIBUTORS=0;38;5;16;48;5;186:*.clang-format=0;38;5;149:*LICENSE-APACHE=0;38;5;249:*CMakeLists.txt=0;38;5;149:*CMakeCache.txt=0;38;5;243:*.gitattributes=0;38;5;149:*CONTRIBUTORS.md=0;38;5;16;48;5;186:*.sconsign.dblite=0;38;5;243:*requirements.txt=0;38;5;149:*CONTRIBUTORS.txt=0;38;5;16;48;5;186:*package-lock.json=0;38;5;243:*.CFUserTextEncoding=0;38;5;243
";
fn main() {
let result = run();
match result {
Ok(exit_code) => {
exit_code.exit();
}
Err(err) => {
eprintln!("[fd error]: {:#}", err);
ExitCode::GeneralError.exit();
}
}
}
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(&opts)?;
let search_paths = opts.search_paths()?;
if search_paths.is_empty() {
bail!("No valid search paths given.");
}
ensure_search_pattern_is_not_a_path(&opts)?;
let pattern_regex = build_pattern_regex(&opts)?;
let config = construct_config(opts, &pattern_regex)?;
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regex)?;
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 program_name = env::args().next().unwrap_or_else(|| "fd".to_string());
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 {
// Set the current working directory of the process
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.",
@@ -115,134 +69,335 @@ fn set_working_dir(opts: &Opts) -> Result<()> {
)
})?;
}
Ok(())
}
/// 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()
let current_directory = Path::new(".");
if !filesystem::is_existing_directory(current_directory) {
return Err(anyhow!(
"Could not retrieve current directory (has it been deleted?)."
));
}
// Get the search pattern
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("");
// Get one or more root directories to search.
let passed_arguments = matches
.values_of_os("path")
.or_else(|| matches.values_of_os("search-path"));
let mut search_paths = if let Some(paths) = passed_arguments {
let mut directories = vec![];
for path in paths {
let path_buffer = PathBuf::from(path);
if filesystem::is_existing_directory(&path_buffer) {
directories.push(path_buffer);
} else {
print_error(format!(
"Search path '{}' is not a directory.",
path_buffer.to_string_lossy()
));
}
}
directories
} else {
vec![current_directory.to_path_buf()]
};
// Check if we have no valid search paths.
if search_paths.is_empty() {
return Err(anyhow!("No valid search paths given."));
}
if matches.is_present("absolute-path") {
search_paths = search_paths
.iter()
.map(|path_buffer| {
path_buffer
.normalize()
.and_then(|pb| filesystem::absolute_path(pb.as_path()))
.unwrap()
})
.collect();
}
// Detect if the user accidentally supplied a path instead of a search pattern
if !matches.is_present("full-path")
&& pattern.contains(std::path::MAIN_SEPARATOR)
&& Path::new(pattern).is_dir()
{
Err(anyhow!(
return Err(anyhow!(
"The search pattern '{pattern}' contains a path-separation character ('{sep}') \
and will not lead to any search results.\n\n\
If you want to search for all files inside the '{pattern}' directory, use a match-all pattern:\n\n \
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 {
Ok(())
));
}
}
fn build_pattern_regex(opts: &Opts) -> Result<String> {
let pattern = &opts.pattern;
Ok(if opts.glob && !pattern.is_empty() {
let pattern_regex = 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 {
String::from(pattern)
})
}
};
fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> {
match (cfg!(windows), path_separator) {
(true, Some(sep)) if sep.len() > 1 => Err(anyhow!(
"A path separator must be exactly one byte, but \
the given separator is {} bytes: '{}'.\n\
In some shells on Windows, '/' is automatically \
expanded. Try to use '//' instead.",
sep.len(),
sep
)),
_ => Ok(()),
}
}
fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
// The search will be case-sensitive if the command line flag is set or
// if the pattern has an uppercase character (smart case).
let case_sensitive =
!opts.ignore_case && (opts.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());
check_path_separator_length(path_separator.as_deref())?;
let size_limits = std::mem::take(&mut opts.size);
let time_constraints = extract_time_constraints(&opts)?;
#[cfg(unix)]
let owner_constraint: Option<OwnerFilter> = opts.owner.and_then(OwnerFilter::filter_ignore);
let case_sensitive = !matches.is_present("ignore-case")
&& (matches.is_present("case-sensitive") || pattern_has_uppercase_char(&pattern_regex));
#[cfg(windows)]
let ansi_colors_support =
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 = atty::is(Stream::Stdout);
let colored_output = match opts.color {
ColorWhen::Always => true,
ColorWhen::Never => false,
ColorWhen::Auto => {
ansi_colors_support && env::var_os("NO_COLOR").is_none() && interactive_terminal
}
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 path_separator = matches
.value_of("path-separator")
.map_or_else(filesystem::default_path_separator, |s| Some(s.to_owned()));
#[cfg(windows)]
{
if let Some(ref sep) = path_separator {
if sep.len() > 1 {
return Err(anyhow!(
"A path separator must be exactly one byte, but \
the given separator is {} bytes: '{}'.\n\
In some shells on Windows, '/' is automatically \
expanded. Try to use '//' instead.",
sep.len(),
sep
));
};
};
}
let ls_colors = if colored_output {
Some(LsColors::from_env().unwrap_or_else(|| LsColors::from_string(DEFAULT_LS_COLORS)))
} else {
None
};
let command = extract_command(&mut opts, colored_output)?;
let has_command = command.is_some();
Ok(Config {
let command = if let Some(args) = matches.values_of("exec") {
Some(CommandTemplate::new(args, path_separator.clone()))
} else if let Some(args) = matches.values_of("exec-batch") {
Some(CommandTemplate::new_batch(args, path_separator.clone())?)
} else if matches.is_present("list-details") {
let color = matches.value_of("color").unwrap_or("auto");
let color_arg = ["--color=", color].concat();
#[allow(unused)]
let gnu_ls = |command_name| {
vec![
command_name,
"-l", // long listing format
"--human-readable", // human readable file sizes
"--directory", // list directories themselves, not their contents
&color_arg,
]
};
let cmd: Vec<&str> = if cfg!(unix) {
if !cfg!(any(
target_os = "macos",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)) {
// Assume ls is GNU ls
gnu_ls("ls")
} else {
// MacOS, DragonFlyBSD, FreeBSD
use std::process::{Command, Stdio};
// Use GNU ls, if available (support for --color=auto, better LS_COLORS support)
let gnu_ls_exists = Command::new("gls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("gls")
} else {
let mut cmd = vec![
"ls", // BSD version of ls
"-l", // long listing format
"-h", // '--human-readable' is not available, '-h' is
"-d", // '--directory' is not available, but '-d' is
];
if !cfg!(any(target_os = "netbsd", target_os = "openbsd")) && colored_output {
// -G is not available in NetBSD's and OpenBSD's ls
cmd.push("-G");
}
cmd
}
}
} else if cfg!(windows) {
use std::process::{Command, Stdio};
// Use GNU ls, if available
let gnu_ls_exists = Command::new("ls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("ls")
} else {
return Err(anyhow!(
"'fd --list-details' is not supported on Windows unless GNU 'ls' is installed."
));
}
} else {
return Err(anyhow!(
"'fd --list-details' is not supported on this platform."
));
};
Some(CommandTemplate::new_batch(&cmd, path_separator.clone()).unwrap())
} else {
None
};
let size_limits = if let Some(vs) = matches.values_of("size") {
vs.map(|sf| {
SizeFilter::from_string(sf)
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", sf))
})
.collect::<Result<Vec<_>>>()?
} else {
vec![]
};
let now = time::SystemTime::now();
let mut time_constraints: Vec<TimeFilter> = Vec::new();
if let Some(t) = matches.value_of("changed-within") {
if let Some(f) = TimeFilter::after(&now, t) {
time_constraints.push(f);
} else {
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
if let Some(t) = matches.value_of("changed-before") {
if let Some(f) = TimeFilter::before(&now, t) {
time_constraints.push(f);
} else {
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
#[cfg(unix)]
let owner_constraint = if let Some(s) = matches.value_of("owner") {
OwnerFilter::from_string(s)?
} else {
None
};
let config = Options {
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),
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(),
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_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"),
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,
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,
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!(),
}
}
@@ -254,12 +409,10 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> 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)
@@ -268,163 +421,47 @@ fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
})
.transpose()?,
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),
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.no_search_paths()
&& (opts.strip_cwd_prefix || !(opts.null_separator || has_command))),
})
}
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 color_arg = format!("--color={}", opts.color.as_str());
let res = determine_ls_command(&color_arg, colored_output)
.map(|cmd| CommandSet::new_batch([cmd]).unwrap());
Some(res)
})
.transpose()
}
fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&str>> {
#[allow(unused)]
let gnu_ls = |command_name| {
// Note: we use short options here (instead of --long-options) to support more
// platforms (like BusyBox).
vec![
command_name,
"-l", // long listing format
"-h", // human readable file sizes
"-d", // list directories themselves, not their contents
color_arg,
]
};
let cmd: Vec<&str> = if cfg!(unix) {
if !cfg!(any(
target_os = "macos",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)) {
// Assume ls is GNU ls
gnu_ls("ls")
} else {
// MacOS, DragonFlyBSD, FreeBSD
use std::process::{Command, Stdio};
// Use GNU ls, if available (support for --color=auto, better LS_COLORS support)
let gnu_ls_exists = Command::new("gls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("gls")
} else {
let mut cmd = vec![
"ls", // BSD version of ls
"-l", // long listing format
"-h", // '--human-readable' is not available, '-h' is
"-d", // '--directory' is not available, but '-d' is
];
if !cfg!(any(target_os = "netbsd", target_os = "openbsd")) && colored_output {
// -G is not available in NetBSD's and OpenBSD's ls
cmd.push("-G");
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
}
cmd
}
}
} else if cfg!(windows) {
use std::process::{Command, Stdio};
// Use GNU ls, if available
let gnu_ls_exists = Command::new("ls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("ls")
} else {
return Err(anyhow!(
"'fd --list-details' is not supported on Windows unless GNU 'ls' is installed."
));
}
} else {
return Err(anyhow!(
"'fd --list-details' is not supported on this platform."
));
}),
};
Ok(cmd)
}
fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
let now = time::SystemTime::now();
let mut time_constraints: Vec<TimeFilter> = Vec::new();
if let Some(ref t) = opts.changed_within {
if let Some(f) = TimeFilter::after(&now, t) {
time_constraints.push(f);
} else {
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
if let Some(ref t) = opts.changed_before {
if let Some(f) = TimeFilter::before(&now, t) {
time_constraints.push(f);
} else {
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
Ok(time_constraints)
}
fn ensure_use_hidden_option_for_leading_dot_pattern(
config: &Config,
pattern_regex: &str,
) -> Result<()> {
if cfg!(unix) && config.ignore_hidden && pattern_matches_strings_with_leading_dot(pattern_regex)
if cfg!(unix)
&& config.ignore_hidden
&& pattern_matches_strings_with_leading_dot(&pattern_regex)
{
Err(anyhow!(
return Err(anyhow!(
"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."
))
} else {
Ok(())
));
}
}
fn build_regex(pattern_regex: String, config: &Config) -> Result<regex::bytes::Regex> {
RegexBuilder::new(&pattern_regex)
let re = RegexBuilder::new(&pattern_regex)
.case_insensitive(!config.case_sensitive)
.dot_matches_new_line(true)
.build()
@@ -435,5 +472,20 @@ fn build_regex(pattern_regex: String, config: &Config) -> Result<regex::bytes::R
also use the '--glob' option to match on a glob pattern.",
e.to_string()
)
})
})?;
walk::scan(&search_paths, Arc::new(re), Arc::new(config))
}
fn main() {
let result = run();
match result {
Ok(exit_code) => {
process::exit(exit_code.into());
}
Err(err) => {
eprintln!("[fd error]: {:#}", err);
process::exit(ExitCode::GeneralError.into());
}
}
}
+3 -27
View File
@@ -3,14 +3,14 @@ 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};
/// Configuration options for *fd*.
pub struct Config {
pub struct Options {
/// Whether the search is case-sensitive or case-insensitive.
pub case_sensitive: bool,
@@ -24,9 +24,6 @@ pub struct Config {
/// Whether to respect `.fdignore` files or not.
pub read_fdignore: bool,
/// Whether to respect ignore files in parent directories or not.
pub read_parent_ignore: bool,
/// Whether to respect VCS ignore files (`.gitignore`, ..) or not.
pub read_vcsignore: bool,
@@ -57,10 +54,6 @@ pub struct Config {
/// The number of threads to use.
pub threads: usize,
/// If true, the program doesn't print anything and will instead return an exit code of 0
/// if there's at least one match. Otherwise, the exit code will be 1.
pub quiet: bool,
/// Time to buffer results internally before streaming to the console. This is useful to
/// provide a sorted output, in case the total execution time is shorter than
/// `max_buffer_time`.
@@ -83,11 +76,7 @@ pub struct Config {
pub extensions: Option<RegexSet>,
/// If a value is supplied, each item found will be used to generate and execute commands.
pub command: Option<Arc<CommandSet>>,
/// Maximum number of search results to pass to each `command`. If zero, the number is
/// unlimited.
pub batch_size: usize,
pub command: Option<Arc<CommandTemplate>>,
/// A list of glob patterns that should be excluded from the search.
pub exclude_patterns: Vec<String>,
@@ -111,19 +100,6 @@ 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,
}
impl Config {
/// Check whether results are being printed.
pub fn is_printing(&self) -> bool {
self.command.is_none()
}
}
+65 -100
View File
@@ -1,160 +1,125 @@
use std::borrow::Cow;
use std::io::{self, Write};
use std::io::{self, StdoutLock, Write};
use std::path::Path;
use std::process;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use lscolors::{Indicator, LsColors, Style};
use lscolors::{LsColors, Style};
use crate::config::Config;
use crate::dir_entry::DirEntry;
use crate::error::print_error;
use crate::exit_codes::ExitCode;
use crate::filesystem::strip_current_dir;
use crate::options::Options;
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
}
// TODO: this function is performance critical and can probably be optimized
pub fn print_entry<W: Write>(stdout: &mut W, entry: &DirEntry, config: &Config) {
let r = if let Some(ref ls_colors) = config.ls_colors {
print_entry_colorized(stdout, entry, config, ls_colors)
pub fn print_entry(
stdout: &mut StdoutLock,
entry: &Path,
config: &Options,
wants_to_quit: &Arc<AtomicBool>,
) {
let path = if entry.is_absolute() {
entry
} else {
print_entry_uncolorized(stdout, entry, config)
strip_current_dir(entry)
};
let r = if let Some(ref ls_colors) = config.ls_colors {
print_entry_colorized(stdout, path, config, ls_colors, wants_to_quit)
} else {
print_entry_uncolorized(stdout, path, config)
};
if let Err(e) = r {
if e.kind() == ::std::io::ErrorKind::BrokenPipe {
// Exit gracefully in case of a broken pipe (e.g. 'fd ... | head -n 3').
ExitCode::Success.exit();
process::exit(0);
} else {
print_error(format!("Could not write to output: {}", e));
ExitCode::GeneralError.exit();
process::exit(ExitCode::GeneralError.into());
}
}
}
// 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_ansi_term_style)
.unwrap_or_default()
.paint(&config.actual_path_separator)
)?;
}
Ok(())
}
// TODO: this function is performance critical and can probably be optimized
fn print_entry_colorized<W: Write>(
stdout: &mut W,
entry: &DirEntry,
config: &Config,
fn print_entry_colorized(
stdout: &mut StdoutLock,
path: &Path,
config: &Options,
ls_colors: &LsColors,
wants_to_quit: &Arc<AtomicBool>,
) -> 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();
let default_style = ansi_term::Style::default();
if let Some(parent) = path.parent() {
offset = parent.to_string_lossy().len();
for c in path_str[offset..].chars() {
if std::path::is_separator(c) {
offset += c.len_utf8();
} else {
break;
}
}
}
if offset > 0 {
let mut parent_str = Cow::from(&path_str[..offset]);
if let Some(ref separator) = config.path_separator {
*parent_str.to_mut() = replace_path_separator(&parent_str, separator);
}
let style = ls_colors
.style_for_indicator(Indicator::Directory)
// Traverse the path and colorize each component
for (component, style) in ls_colors.style_for_path_components(path) {
let style = style
.map(Style::to_ansi_term_style)
.unwrap_or_default();
write!(stdout, "{}", style.paint(parent_str))?;
.unwrap_or(default_style);
let mut path_string = component.to_string_lossy();
if let Some(ref separator) = config.path_separator {
*path_string.to_mut() = replace_path_separator(&path_string, separator);
}
write!(stdout, "{}", style.paint(path_string))?;
// TODO: can we move this out of the if-statement? Why do we call it that often?
if wants_to_quit.load(Ordering::Relaxed) {
writeln!(stdout)?;
process::exit(ExitCode::KilledBySigint.into());
}
}
let style = entry
.style(ls_colors)
.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")?;
write!(stdout, "\0")
} else {
writeln!(stdout)?;
writeln!(stdout)
}
Ok(())
}
// TODO: this function is performance critical and can probably be optimized
fn print_entry_uncolorized_base<W: Write>(
stdout: &mut W,
entry: &DirEntry,
config: &Config,
fn print_entry_uncolorized_base(
stdout: &mut StdoutLock,
path: &Path,
config: &Options,
) -> io::Result<()> {
let separator = if config.null_separator { "\0" } else { "\n" };
let path = entry.stripped_path(config);
let mut path_string = path.to_string_lossy();
if let Some(ref separator) = config.path_separator {
*path_string.to_mut() = replace_path_separator(&path_string, separator);
}
write!(stdout, "{}", path_string)?;
print_trailing_slash(stdout, entry, config, None)?;
write!(stdout, "{}", separator)
write!(stdout, "{}{}", path_string, separator)
}
#[cfg(not(unix))]
fn print_entry_uncolorized<W: Write>(
stdout: &mut W,
entry: &DirEntry,
config: &Config,
fn print_entry_uncolorized(
stdout: &mut StdoutLock,
path: &Path,
config: &Options,
) -> 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,
config: &Config,
fn print_entry_uncolorized(
stdout: &mut StdoutLock,
path: &Path,
config: &Options,
) -> 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
let separator = if config.null_separator { b"\0" } else { b"\n" };
stdout.write_all(entry.stripped_path(config).as_os_str().as_bytes())?;
print_trailing_slash(stdout, entry, config, None)?;
stdout.write_all(path.as_os_str().as_bytes())?;
stdout.write_all(separator)
}
}
+10 -10
View File
@@ -15,19 +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() {
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(ranges)) => ranges
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::Group(Group { hir, .. }) | HirKind::Repetition(Repetition { hir, .. }) => {
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,
@@ -52,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::Anchor(Anchor::StartText) {
if *hir.kind() != HirKind::Anchor(Anchor::StartText) {
return false;
}
} else {
@@ -64,7 +64,7 @@ fn hir_matches_strings_with_leading_dot(hir: &Hir) -> bool {
}
if let Some(hir) = hirs.next() {
hir.kind() == &HirKind::Literal(Literal::Unicode('.'))
*hir.kind() == HirKind::Literal(Literal::Unicode('.'))
} else {
false
}
+218 -251
View File
@@ -1,29 +1,28 @@
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs::{FileType, Metadata};
use std::io;
use std::mem;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use std::{borrow::Cow, io::Write};
use std::time;
use anyhow::{anyhow, Result};
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender};
use ignore::overrides::OverrideBuilder;
use ignore::{self, WalkBuilder};
use regex::bytes::Regex;
use crate::config::Config;
use crate::dir_entry::DirEntry;
use crate::error::print_error;
use crate::exec;
use crate::exit_codes::{merge_exitcodes, ExitCode};
use crate::filesystem;
use crate::options::Options;
use crate::output;
/// The receiver thread can either be buffering results or directly streaming to the console.
#[derive(PartialEq)]
enum ReceiverMode {
/// Receiver is still buffering in order to sort the results, if the search finishes fast
/// enough.
@@ -34,31 +33,27 @@ enum ReceiverMode {
}
/// The Worker threads can result in a valid entry having PathBuf or an error.
#[allow(clippy::large_enum_variant)]
pub enum WorkerResult {
// Errors should be rare, so it's probably better to allow large_enum_variant than
// to box the Entry variant
Entry(DirEntry),
Entry(PathBuf),
Error(ignore::Error),
}
/// Maximum size of the output buffer before flushing results to the console
pub const MAX_BUFFER_LENGTH: usize = 1000;
/// Default duration until output buffering switches to streaming.
pub const DEFAULT_MAX_BUFFER_TIME: Duration = Duration::from_millis(100);
/// Recursively scan the given search path for files / pathnames matching the pattern.
///
/// If the `--exec` argument was supplied, this will create a thread pool for executing
/// jobs in parallel from a given command line and the discovered paths. Otherwise, each
/// path will simply be written to standard output.
pub fn scan(paths: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> Result<ExitCode> {
let first_path = &paths[0];
pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<Options>) -> Result<ExitCode> {
let mut path_iter = path_vec.iter();
let first_path_buf = path_iter
.next()
.expect("Error: Path vector can not be empty");
let (tx, rx) = channel();
// Channel capacity was chosen empircally to perform similarly to an unbounded channel
let (tx, rx) = bounded(0x4000 * config.threads);
let mut override_builder = OverrideBuilder::new(first_path);
let mut override_builder = OverrideBuilder::new(first_path_buf.as_path());
for pattern in &config.exclude_patterns {
override_builder
@@ -69,11 +64,11 @@ pub fn scan(paths: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> Resu
.build()
.map_err(|_| anyhow!("Mismatch in exclude patterns"))?;
let mut walker = WalkBuilder::new(first_path);
let mut walker = WalkBuilder::new(first_path_buf.as_path());
walker
.hidden(config.ignore_hidden)
.ignore(config.read_fdignore)
.parents(config.read_parent_ignore && (config.read_fdignore || config.read_vcsignore))
.parents(config.read_fdignore || config.read_vcsignore)
.git_ignore(config.read_vcsignore)
.git_global(config.read_vcsignore)
.git_exclude(config.read_vcsignore)
@@ -105,7 +100,10 @@ pub fn scan(paths: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> Resu
match result {
Some(ignore::Error::Partial(_)) => (),
Some(err) => {
print_error(format!("Malformed pattern in global ignore file. {}.", err));
print_error(format!(
"Malformed pattern in global ignore file. {}.",
err.to_string()
));
}
None => (),
}
@@ -117,272 +115,222 @@ pub fn scan(paths: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> Resu
match result {
Some(ignore::Error::Partial(_)) => (),
Some(err) => {
print_error(format!("Malformed pattern in custom ignore file. {}.", err));
print_error(format!(
"Malformed pattern in custom ignore file. {}.",
err.to_string()
));
}
None => (),
}
}
for path in &paths[1..] {
walker.add(path);
for path_entry in path_iter {
walker.add(path_entry.as_path());
}
let parallel_walker = walker.threads(config.threads).build_parallel();
// Flag for cleanly shutting down the parallel walk
let quit_flag = Arc::new(AtomicBool::new(false));
// Flag specifically for quitting due to ^C
let interrupt_flag = Arc::new(AtomicBool::new(false));
if config.ls_colors.is_some() && config.is_printing() {
let quit_flag = Arc::clone(&quit_flag);
let interrupt_flag = Arc::clone(&interrupt_flag);
let wants_to_quit = Arc::new(AtomicBool::new(false));
if config.ls_colors.is_some() && config.command.is_none() {
let wq = Arc::clone(&wants_to_quit);
ctrlc::set_handler(move || {
quit_flag.store(true, Ordering::Relaxed);
if interrupt_flag.fetch_or(true, Ordering::Relaxed) {
if wq.load(Ordering::Relaxed) {
// Ctrl-C has been pressed twice, exit NOW
ExitCode::KilledBySigint.exit();
process::exit(ExitCode::KilledBySigint.into());
} else {
wq.store(true, Ordering::Relaxed);
}
})
.unwrap();
}
// Spawn the thread that receives all results through the channel.
let receiver_thread = spawn_receiver(&config, &quit_flag, &interrupt_flag, rx);
let receiver_thread = spawn_receiver(&config, &wants_to_quit, rx);
// Spawn the sender threads.
spawn_senders(&config, &quit_flag, pattern, parallel_walker, tx);
spawn_senders(&config, &wants_to_quit, pattern, parallel_walker, tx);
// Wait for the receiver thread to print out all results.
let exit_code = receiver_thread.join().unwrap();
if interrupt_flag.load(Ordering::Relaxed) {
if wants_to_quit.load(Ordering::Relaxed) {
Ok(ExitCode::KilledBySigint)
} else {
Ok(exit_code)
}
}
/// Wrapper for the receiver thread's buffering behavior.
struct ReceiverBuffer<W> {
/// The configuration.
config: Arc<Config>,
/// For shutting down the senders.
quit_flag: Arc<AtomicBool>,
/// The ^C notifier.
interrupt_flag: Arc<AtomicBool>,
/// Receiver for worker results.
rx: Receiver<WorkerResult>,
/// Standard output.
stdout: W,
/// The current buffer mode.
mode: ReceiverMode,
/// The deadline to switch to streaming mode.
deadline: Instant,
/// The buffer of quickly received paths.
buffer: Vec<DirEntry>,
/// Result count.
num_results: usize,
}
impl<W: Write> ReceiverBuffer<W> {
/// Create a new receiver buffer.
fn new(
config: Arc<Config>,
quit_flag: Arc<AtomicBool>,
interrupt_flag: Arc<AtomicBool>,
rx: Receiver<WorkerResult>,
stdout: W,
) -> Self {
let max_buffer_time = config.max_buffer_time.unwrap_or(DEFAULT_MAX_BUFFER_TIME);
let deadline = Instant::now() + max_buffer_time;
Self {
config,
quit_flag,
interrupt_flag,
rx,
stdout,
mode: ReceiverMode::Buffering,
deadline,
buffer: Vec::with_capacity(MAX_BUFFER_LENGTH),
num_results: 0,
}
}
/// Process results until finished.
fn process(&mut self) -> ExitCode {
loop {
if let Err(ec) = self.poll() {
self.quit_flag.store(true, Ordering::Relaxed);
return ec;
}
}
}
/// Receive the next worker result.
fn recv(&self) -> Result<WorkerResult, RecvTimeoutError> {
match self.mode {
ReceiverMode::Buffering => {
// Wait at most until we should switch to streaming
self.rx.recv_deadline(self.deadline)
}
ReceiverMode::Streaming => {
// Wait however long it takes for a result
Ok(self.rx.recv()?)
}
}
}
/// Wait for a result or state change.
fn poll(&mut self) -> Result<(), ExitCode> {
match self.recv() {
Ok(WorkerResult::Entry(dir_entry)) => {
if self.config.quiet {
return Err(ExitCode::HasResults(true));
}
match self.mode {
ReceiverMode::Buffering => {
self.buffer.push(dir_entry);
if self.buffer.len() > MAX_BUFFER_LENGTH {
self.stream()?;
}
}
ReceiverMode::Streaming => {
self.print(&dir_entry)?;
self.flush()?;
}
}
self.num_results += 1;
if let Some(max_results) = self.config.max_results {
if self.num_results >= max_results {
return self.stop();
}
}
}
Ok(WorkerResult::Error(err)) => {
if self.config.show_filesystem_errors {
print_error(err.to_string());
}
}
Err(RecvTimeoutError::Timeout) => {
self.stream()?;
}
Err(RecvTimeoutError::Disconnected) => {
return self.stop();
}
}
Ok(())
}
/// Output a path.
fn print(&mut self, entry: &DirEntry) -> Result<(), ExitCode> {
output::print_entry(&mut self.stdout, entry, &self.config);
if self.interrupt_flag.load(Ordering::Relaxed) {
// Ignore any errors on flush, because we're about to exit anyway
let _ = self.flush();
return Err(ExitCode::KilledBySigint);
}
Ok(())
}
/// Switch ourselves into streaming mode.
fn stream(&mut self) -> Result<(), ExitCode> {
self.mode = ReceiverMode::Streaming;
let buffer = mem::take(&mut self.buffer);
for path in buffer {
self.print(&path)?;
}
self.flush()
}
/// Stop looping.
fn stop(&mut self) -> Result<(), ExitCode> {
if self.mode == ReceiverMode::Buffering {
self.buffer.sort();
self.stream()?;
}
if self.config.quiet {
Err(ExitCode::HasResults(self.num_results > 0))
} else {
Err(ExitCode::Success)
}
}
/// Flush stdout if necessary.
fn flush(&mut self) -> Result<(), ExitCode> {
if self.config.interactive_terminal && self.stdout.flush().is_err() {
// Probably a broken pipe. Exit gracefully.
return Err(ExitCode::GeneralError);
}
Ok(())
}
}
fn spawn_receiver(
config: &Arc<Config>,
quit_flag: &Arc<AtomicBool>,
interrupt_flag: &Arc<AtomicBool>,
config: &Arc<Options>,
wants_to_quit: &Arc<AtomicBool>,
rx: Receiver<WorkerResult>,
) -> thread::JoinHandle<ExitCode> {
let config = Arc::clone(config);
let quit_flag = Arc::clone(quit_flag);
let interrupt_flag = Arc::clone(interrupt_flag);
let wants_to_quit = Arc::clone(wants_to_quit);
let show_filesystem_errors = config.show_filesystem_errors;
let threads = config.threads;
// This will be used to check if output should be buffered when only running a single thread
let enable_output_buffering: bool = threads > 1;
thread::spawn(move || {
// This will be set to `Some` if the `--exec` argument was supplied.
if let Some(ref cmd) = config.command {
if cmd.in_batch_mode() {
exec::batch(rx, cmd, &config)
exec::batch(rx, cmd, show_filesystem_errors, enable_output_buffering)
} else {
let shared_rx = Arc::new(Mutex::new(rx));
let out_perm = Arc::new(Mutex::new(()));
// Each spawned job will store it's thread handle in here.
let mut handles = Vec::with_capacity(threads);
for _ in 0..threads {
let config = Arc::clone(&config);
let rx = rx.clone();
let rx = Arc::clone(&shared_rx);
let cmd = Arc::clone(cmd);
let out_perm = Arc::clone(&out_perm);
// Spawn a job thread that will listen for and execute inputs.
let handle = thread::spawn(move || exec::job(rx, cmd, out_perm, &config));
let handle = thread::spawn(move || {
exec::job(
rx,
cmd,
out_perm,
show_filesystem_errors,
enable_output_buffering,
)
});
// Push the handle of the spawned thread into the vector for later joining.
handles.push(handle);
}
let exit_codes = handles
.into_iter()
.map(|handle| handle.join().unwrap())
.collect::<Vec<_>>();
merge_exitcodes(exit_codes)
// Wait for all threads to exit before exiting the program.
let mut results: Vec<ExitCode> = Vec::new();
for h in handles {
results.push(h.join().unwrap());
}
merge_exitcodes(&results)
}
} else {
let stdout = io::stdout();
let stdout = stdout.lock();
let stdout = io::BufWriter::new(stdout);
let start = time::Instant::now();
let mut rxbuffer = ReceiverBuffer::new(config, quit_flag, interrupt_flag, rx, stdout);
rxbuffer.process()
let mut buffer = vec![];
// Start in buffering mode
let mut mode = ReceiverMode::Buffering;
// Maximum time to wait before we start streaming to the console.
let max_buffer_time = config
.max_buffer_time
.unwrap_or_else(|| time::Duration::from_millis(100));
let stdout = io::stdout();
let mut stdout = stdout.lock();
let mut num_results = 0;
for worker_result in rx {
match worker_result {
WorkerResult::Entry(value) => {
match mode {
ReceiverMode::Buffering => {
buffer.push(value);
// Have we reached the maximum buffer size or maximum buffering time?
if buffer.len() > MAX_BUFFER_LENGTH
|| time::Instant::now() - start > max_buffer_time
{
// Flush the buffer
for v in &buffer {
output::print_entry(
&mut stdout,
v,
&config,
&wants_to_quit,
);
}
buffer.clear();
// Start streaming
mode = ReceiverMode::Streaming;
}
}
ReceiverMode::Streaming => {
output::print_entry(&mut stdout, &value, &config, &wants_to_quit);
}
}
num_results += 1;
}
WorkerResult::Error(err) => {
if show_filesystem_errors {
print_error(err.to_string());
}
}
}
if let Some(max_results) = config.max_results {
if num_results >= max_results {
break;
}
}
}
// If we have finished fast enough (faster than max_buffer_time), we haven't streamed
// anything to the console, yet. In this case, sort the results and print them:
if !buffer.is_empty() {
buffer.sort();
for value in buffer {
output::print_entry(&mut stdout, &value, &config, &wants_to_quit);
}
}
ExitCode::Success
}
})
}
pub enum DirEntry {
Normal(ignore::DirEntry),
BrokenSymlink(PathBuf),
}
impl DirEntry {
pub fn path(&self) -> &Path {
match self {
DirEntry::Normal(e) => e.path(),
DirEntry::BrokenSymlink(pathbuf) => pathbuf.as_path(),
}
}
pub fn file_type(&self) -> Option<FileType> {
match self {
DirEntry::Normal(e) => e.file_type(),
DirEntry::BrokenSymlink(pathbuf) => {
pathbuf.symlink_metadata().map(|m| m.file_type()).ok()
}
}
}
pub fn metadata(&self) -> Option<Metadata> {
match self {
DirEntry::Normal(e) => e.metadata().ok(),
DirEntry::BrokenSymlink(_) => None,
}
}
pub fn depth(&self) -> Option<usize> {
match self {
DirEntry::Normal(e) => Some(e.depth()),
DirEntry::BrokenSymlink(_) => None,
}
}
}
fn spawn_senders(
config: &Arc<Config>,
quit_flag: &Arc<AtomicBool>,
config: &Arc<Options>,
wants_to_quit: &Arc<AtomicBool>,
pattern: Arc<Regex>,
parallel_walker: ignore::WalkParallel,
tx: Sender<WorkerResult>,
@@ -391,10 +339,10 @@ fn spawn_senders(
let config = Arc::clone(config);
let pattern = Arc::clone(&pattern);
let tx_thread = tx.clone();
let quit_flag = Arc::clone(quit_flag);
let wants_to_quit = Arc::clone(wants_to_quit);
Box::new(move |entry_o| {
if quit_flag.load(Ordering::Relaxed) {
if wants_to_quit.load(Ordering::Relaxed) {
return ignore::WalkState::Quit;
}
@@ -403,7 +351,7 @@ fn spawn_senders(
// Skip the root directory entry.
return ignore::WalkState::Continue;
}
Ok(e) => DirEntry::normal(e),
Ok(e) => DirEntry::Normal(e),
Err(ignore::Error::WithPath {
path,
err: inner_err,
@@ -415,24 +363,30 @@ fn spawn_senders(
.ok()
.map_or(false, |m| m.file_type().is_symlink()) =>
{
DirEntry::broken_symlink(path)
DirEntry::BrokenSymlink(path)
}
_ => {
return match tx_thread.send(WorkerResult::Error(ignore::Error::WithPath {
match tx_thread.send(WorkerResult::Error(ignore::Error::WithPath {
path,
err: inner_err,
})) {
Ok(_) => ignore::WalkState::Continue,
Err(_) => ignore::WalkState::Quit,
Ok(_) => {
return ignore::WalkState::Continue;
}
Err(_) => {
return ignore::WalkState::Quit;
}
}
}
},
Err(err) => {
return match tx_thread.send(WorkerResult::Error(err)) {
Ok(_) => ignore::WalkState::Continue,
Err(_) => ignore::WalkState::Quit,
Err(err) => match tx_thread.send(WorkerResult::Error(err)) {
Ok(_) => {
return ignore::WalkState::Continue;
}
}
Err(_) => {
return ignore::WalkState::Quit;
}
},
};
if let Some(min_depth) = config.min_depth {
@@ -476,7 +430,27 @@ fn spawn_senders(
// Filter out unwanted file types.
if let Some(ref file_types) = config.file_types {
if file_types.should_ignore(&entry) {
if let Some(ref entry_type) = entry.file_type() {
if (!file_types.files && entry_type.is_file())
|| (!file_types.directories && entry_type.is_dir())
|| (!file_types.symlinks && entry_type.is_symlink())
|| (!file_types.sockets && filesystem::is_socket(*entry_type))
|| (!file_types.pipes && filesystem::is_pipe(*entry_type))
|| (file_types.executables_only
&& !entry
.metadata()
.map(|m| filesystem::is_executable(&m))
.unwrap_or(false))
|| (file_types.empty_only && !filesystem::is_empty(&entry))
|| !(entry_type.is_file()
|| entry_type.is_dir()
|| entry_type.is_symlink()
|| filesystem::is_socket(*entry_type)
|| filesystem::is_pipe(*entry_type))
{
return ignore::WalkState::Continue;
}
} else {
return ignore::WalkState::Continue;
}
}
@@ -484,7 +458,7 @@ fn spawn_senders(
#[cfg(unix)]
{
if let Some(ref owner_constraint) = config.owner_constraint {
if let Some(metadata) = entry.metadata() {
if let Ok(ref metadata) = entry_path.metadata() {
if !owner_constraint.matches(metadata) {
return ignore::WalkState::Continue;
}
@@ -497,7 +471,7 @@ fn spawn_senders(
// Filter out unwanted sizes if it is a file and we have been given size constraints.
if !config.size_constraints.is_empty() {
if entry_path.is_file() {
if let Some(metadata) = entry.metadata() {
if let Ok(metadata) = entry_path.metadata() {
let file_size = metadata.len();
if config
.size_constraints
@@ -517,7 +491,7 @@ fn spawn_senders(
// Filter out unwanted modification times
if !config.time_constraints.is_empty() {
let mut matched = false;
if let Some(metadata) = entry.metadata() {
if let Ok(metadata) = entry_path.metadata() {
if let Ok(modified) = metadata.modified() {
matched = config
.time_constraints
@@ -530,14 +504,7 @@ fn spawn_senders(
}
}
if config.is_printing() {
if let Some(ls_colors) = &config.ls_colors {
// Compute colors in parallel
entry.style(ls_colors);
}
}
let send_result = tx_thread.send(WorkerResult::Entry(entry));
let send_result = tx_thread.send(WorkerResult::Entry(entry_path.to_owned()));
if send_result.is_err() {
return ignore::WalkState::Quit;
+13 -32
View File
@@ -130,17 +130,6 @@ fn normalize_output(s: &str, trim_start: bool, normalize_line: bool) -> String {
lines.join("\n")
}
/// Trim whitespace from the beginning of each line.
fn trim_lines(s: &str) -> String {
s.lines()
.map(|line| line.trim_start())
.fold(String::new(), |mut str, line| {
str.push_str(line);
str.push('\n');
str
})
}
impl TestEnv {
pub fn new(directories: &[&'static str], files: &[&'static str]) -> TestEnv {
let temp_dir = create_working_directory(directories, files).expect("working directory");
@@ -185,11 +174,6 @@ impl TestEnv {
self.temp_dir.path().to_path_buf()
}
/// Get the path of the fd executable.
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();
@@ -219,19 +203,6 @@ impl TestEnv {
output
}
pub fn assert_success_and_get_normalized_output<P: AsRef<Path>>(
&self,
path: P,
args: &[&str],
) -> String {
let output = self.assert_success_and_get_output(path, args);
normalize_output(
&String::from_utf8_lossy(&output.stdout),
false,
self.normalize_line,
)
}
/// Assert that calling *fd* with the specified arguments produces the expected output.
pub fn assert_output(&self, args: &[&str], expected: &str) {
self.assert_output_subdirectory(".", args, expected)
@@ -253,9 +224,15 @@ impl TestEnv {
args: &[&str],
expected: &str,
) {
let output = self.assert_success_and_get_output(path, args);
// Normalize both expected and actual output.
let expected = normalize_output(expected, true, self.normalize_line);
let actual = self.assert_success_and_get_normalized_output(path, args);
let actual = normalize_output(
&String::from_utf8_lossy(&output.stdout),
false,
self.normalize_line,
);
// Compare actual output to expected output.
if expected != actual {
@@ -303,8 +280,12 @@ impl TestEnv {
if let Some(expected) = expected {
// Normalize both expected and actual output.
let expected_error = trim_lines(expected);
let actual_err = trim_lines(&String::from_utf8_lossy(&output.stderr));
let expected_error = normalize_output(expected, true, self.normalize_line);
let actual_err = normalize_output(
&String::from_utf8_lossy(&output.stderr),
false,
self.normalize_line,
);
// Compare actual output to expected output.
if !actual_err.trim_start().starts_with(&expected_error) {
+133 -549
View File
File diff suppressed because it is too large Load Diff