mirror of https://github.com/sharkdp/fd.git
Compare commits
120 Commits
Author | SHA1 | Date |
---|---|---|
Thayne McCombs | be815c261a | |
Thayne McCombs | d8a808c0e3 | |
Thayne McCombs | c92290c3d7 | |
Thayne McCombs | 0649c2b379 | |
dependabot[bot] | 755970deba | |
dependabot[bot] | f897b82d76 | |
dependabot[bot] | dbbabed606 | |
dependabot[bot] | ac178e20af | |
David Peter | 29936f0fba | |
David Peter | bfc16a1dee | |
Thayne McCombs | 289a68bac3 | |
Thayne McCombs | fcaff0f385 | |
Thayne McCombs | 36163f9c3a | |
Thayne McCombs | d90ec1758e | |
Thayne McCombs | ea22cbd712 | |
Thayne McCombs | d44badc190 | |
Thayne McCombs | 6becb66185 | |
Thayne McCombs | 1a1f057e5d | |
Thayne McCombs | 10a269bd3f | |
Thayne McCombs | 90d3381814 | |
Thayne McCombs | b1f83a0bb0 | |
Thayne McCombs | 3bc70925a9 | |
Thayne McCombs | f287f08b9f | |
Tavian Barnes | 0e4488e9dc | |
Tavian Barnes | d7d63eddbe | |
Thayne McCombs | 8acd7722f0 | |
Thayne McCombs | 92fab6e058 | |
Thayne McCombs | a0ee0856db | |
Thayne McCombs | b8df500a70 | |
Thayne McCombs | cd96ca071d | |
Tavian Barnes | 216472ff9f | |
Thayne McCombs | 3680d10e5c | |
dependabot[bot] | abe3b9cd78 | |
Thayne McCombs | 7aad6c9edf | |
Thayne McCombs | ddd3aae249 | |
dependabot[bot] | 6d3bb68faf | |
dependabot[bot] | 21d50dae8c | |
David Peter | 9279b1f0af | |
Thayne McCombs | 6647085015 | |
Thayne McCombs | 6af8f092ee | |
Thayne McCombs | c4094c7a05 | |
AlbydS | 6d58df5f0c | |
AlbydS | ffecccf209 | |
Thayne McCombs | 31f2839751 | |
Thayne McCombs | e10a4eab2b | |
Thayne McCombs | 8eb047945e | |
Tavian Barnes | 1031325cca | |
Tavian Barnes | 9fc2167cf9 | |
Tavian Barnes | ae1de4de24 | |
Thayne McCombs | 7e5d14b733 | |
Thayne McCombs | 85cbea8dcb | |
Thayne McCombs | bc6782624e | |
Thayne McCombs | cf6ff87c7d | |
Thayne McCombs | 3cd73d7927 | |
binlingyu | 7794c4aae5 | |
Thayne McCombs | 8c7a84ea30 | |
Thayne McCombs | e262ade74e | |
Thayne McCombs | 11069e284a | |
Thayne McCombs | 6e2e86decb | |
Thayne McCombs | 15d3b63ccc | |
dependabot[bot] | 453577651e | |
dependabot[bot] | 39c07b7b4c | |
dependabot[bot] | 5910285db0 | |
Thayne McCombs | 68fe31da3f | |
Jian Wang | f875ea9a52 | |
one230six | 138919907b | |
Thayne McCombs | b8744626e7 | |
dependabot[bot] | b08d78f6fc | |
Tavian Barnes | 4efc05ef27 | |
garlic-hub | 0788c43c3f | |
Thayne McCombs | 3b2fd158b5 | |
Thayne McCombs | c38dbacbd0 | |
Thayne McCombs | 728b3200c0 | |
dependabot[bot] | 7f74cd9e56 | |
dependabot[bot] | 6ae8da6a39 | |
dependabot[bot] | f699c8bb6a | |
Nathan Bellows | ffde94c10e | |
Nathan Bellows | b0a8848f68 | |
AlbydS | d651a595d4 | |
David Peter | 969316cc0e | |
David Peter | 5b46867507 | |
Thayne McCombs | e117a373a7 | |
dependabot[bot] | a4aed14337 | |
Thayne McCombs | 9cde3c12a2 | |
Thayne McCombs | 906e7a933e | |
dependabot[bot] | 077d28d13a | |
dependabot[bot] | b55bb1e9be | |
Thayne McCombs | 7a6cc92d6d | |
Thayne McCombs | b694c6e673 | |
dependabot[bot] | 17895538a0 | |
dependabot[bot] | 72ff1f9a87 | |
Thayne McCombs | ef3194a510 | |
Maksim Bondarenkov | 8773402246 | |
Rob | ff3fc81db4 | |
Tavian Barnes | 0dc3342c33 | |
Thayne McCombs | c66fc812ac | |
Thayne McCombs | 14ed023875 | |
Tavian Barnes | 58284b8dbe | |
Tavian Barnes | 60889d0b99 | |
dependabot[bot] | 7e19bad0a4 | |
Thayne McCombs | 4b1d73d39d | |
dependabot[bot] | 03e19a1ad2 | |
Thayne McCombs | 8fb9499c20 | |
Thayne McCombs | 38fb6a5958 | |
dependabot[bot] | 49cd62d65e | |
dependabot[bot] | 24bb5216bb | |
dependabot[bot] | 7f8760fd1f | |
Alexandru-Constantin Atomei | 3cb6b9d93a | |
Atomei Alexandru | c591106b86 | |
Alexandru-Constantin Atomei | 9f096737db | |
Alexandru-Constantin Atomei | 1bda165b25 | |
Thayne McCombs | f48372624d | |
Roshan Jossy | 5cd15536b6 | |
Sayan Goswami | aeb4a5fdad | |
Thayne McCombs | 9529f30129 | |
Thayne McCombs | 266311ca33 | |
Tavian Barnes | 954a3900b9 | |
David Peter | 07343b5baf | |
David Peter | a03ed8b300 | |
David Peter | 13a93e5cbe |
|
@ -44,6 +44,16 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- run: cargo fmt -- --check
|
- run: cargo fmt -- --check
|
||||||
|
|
||||||
|
lint_check:
|
||||||
|
name: Ensure 'cargo clippy' has no warnings
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: clippy
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: cargo clippy --all-targets --all-features -- -Dwarnings
|
||||||
|
|
||||||
min_version:
|
min_version:
|
||||||
name: Minimum supported rust version
|
name: Minimum supported rust version
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
@ -71,12 +81,14 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
job:
|
job:
|
||||||
- { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
- { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||||
|
- { target: aarch64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||||
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
|
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
|
||||||
- { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
- { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
||||||
- { target: i686-pc-windows-msvc , os: windows-2019 }
|
- { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||||
- { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
- { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||||
- { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
- { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||||
- { target: x86_64-apple-darwin , os: macos-12 }
|
- { target: x86_64-apple-darwin , os: macos-12 }
|
||||||
|
- { target: aarch64-apple-darwin , os: macos-14 }
|
||||||
- { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
- { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||||
- { target: x86_64-pc-windows-msvc , os: windows-2019 }
|
- { target: x86_64-pc-windows-msvc , os: windows-2019 }
|
||||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||||
|
@ -330,7 +342,7 @@ jobs:
|
||||||
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
|
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Publish archives and packages
|
- name: Publish archives and packages
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
if: steps.is-release.outputs.IS_RELEASE
|
if: steps.is-release.outputs.IS_RELEASE
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
|
42
CHANGELOG.md
42
CHANGELOG.md
|
@ -1,9 +1,49 @@
|
||||||
|
# 10.1.0
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Allow passing an optional argument to `--strip-cwd-prefix` of "always", "never", or "auto". to force whether the cwd prefix is stripped or not.
|
||||||
|
- Add a `--format` option which allows using a format template for direct ouput similar to the template used for `--exec`. (#1043)
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
- Fix aarch64 page size again. This time it should actually work. (#1085, #1549) (@tavianator)
|
||||||
|
|
||||||
|
|
||||||
|
## Other
|
||||||
|
|
||||||
|
- aarch64-apple-darwin target added to builds on the release page. Note that this is a tier 2 rust target.
|
||||||
|
|
||||||
|
# v10.0.0
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Add `dir` as an alias to `directory` when using `-t` \ `--type`, see #1460 and #1464 (@Ato2207).
|
||||||
|
- Add support for @%s date format in time filters similar to GNU date (seconds since Unix epoch for --older/--newer), see #1493 (@nabellows)
|
||||||
|
- Breaking: No longer automatically ignore `.git` when using `--hidden` with vcs ignore enabled. This reverts the change in v9.0.0. While this feature
|
||||||
|
was often useful, it also broke some existing workflows, and there wasn't a good way to opt out of it. And there isn't really a good way for us to add
|
||||||
|
a way to opt out of it. And you can easily get similar behavior by adding `.git/` to your global fdignore file.
|
||||||
|
See #1457.
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
- Respect NO_COLOR environment variable with `--list-details` option. (#1455)
|
||||||
|
- Fix bug that would cause hidden files to be included despite gitignore rules
|
||||||
|
if search path is "." (#1461, BurntSushi/ripgrep#2711).
|
||||||
|
- aarch64 builds now use 64k page sizes with jemalloc. This fixes issues on some systems, such as ARM Macs that
|
||||||
|
have a larger system page size than the system that the binary was built on. (#1547)
|
||||||
|
- Address [CVE-2024-24576](https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html), by increasing minimum rust version.
|
||||||
|
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
- Minimum supported rust version is now 1.77.2
|
||||||
|
|
||||||
|
|
||||||
# v9.0.0
|
# v9.0.0
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
- Performance has been *significantly improved*, both due to optimizations in the underlying `ignore`
|
- Performance has been *significantly improved*, both due to optimizations in the underlying `ignore`
|
||||||
crate (#1429), and in `fd` itself (#1422, #1408, #13620) - @tavianator.
|
crate (#1429), and in `fd` itself (#1422, #1408, #1362) - @tavianator.
|
||||||
[Benchmarks results](https://gist.github.com/tavianator/32edbe052f33ef60570cf5456b59de81) show gains
|
[Benchmarks results](https://gist.github.com/tavianator/32edbe052f33ef60570cf5456b59de81) show gains
|
||||||
of 6-8x for full traversals of smaller directories (100k files) and up to 13x for larger directories (1M files).
|
of 6-8x for full traversals of smaller directories (100k files) and up to 13x for larger directories (1M files).
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
|
@ -16,9 +16,9 @@ license = "MIT OR Apache-2.0"
|
||||||
name = "fd-find"
|
name = "fd-find"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/sharkdp/fd"
|
repository = "https://github.com/sharkdp/fd"
|
||||||
version = "9.0.0"
|
version = "10.1.0"
|
||||||
edition= "2021"
|
edition= "2021"
|
||||||
rust-version = "1.70.0"
|
rust-version = "1.77.2"
|
||||||
|
|
||||||
[badges.appveyor]
|
[badges.appveyor]
|
||||||
repository = "sharkdp/fd"
|
repository = "sharkdp/fd"
|
||||||
|
@ -34,11 +34,11 @@ path = "src/main.rs"
|
||||||
version_check = "0.9"
|
version_check = "0.9"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aho-corasick = "1.0"
|
aho-corasick = "1.1"
|
||||||
nu-ansi-term = "0.49"
|
nu-ansi-term = "0.50"
|
||||||
argmax = "0.3.1"
|
argmax = "0.3.1"
|
||||||
ignore = "0.4.21"
|
ignore = "0.4.22"
|
||||||
regex = "1.9.6"
|
regex = "1.10.3"
|
||||||
regex-syntax = "0.8"
|
regex-syntax = "0.8"
|
||||||
ctrlc = "3.2"
|
ctrlc = "3.2"
|
||||||
humantime = "2.1"
|
humantime = "2.1"
|
||||||
|
@ -46,26 +46,26 @@ globset = "0.4"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
etcetera = "0.8"
|
etcetera = "0.8"
|
||||||
normpath = "1.1.1"
|
normpath = "1.1.1"
|
||||||
crossbeam-channel = "0.5.8"
|
crossbeam-channel = "0.5.13"
|
||||||
clap_complete = {version = "4.4.4", optional = true}
|
clap_complete = {version = "4.4.9", optional = true}
|
||||||
faccess = "0.2.4"
|
faccess = "0.2.4"
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
version = "4.4.10"
|
version = "4.4.13"
|
||||||
features = ["suggestions", "color", "wrap_help", "cargo", "derive"]
|
features = ["suggestions", "color", "wrap_help", "cargo", "derive"]
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
version = "0.4.31"
|
version = "0.4.38"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["std", "clock"]
|
features = ["std", "clock"]
|
||||||
|
|
||||||
[dependencies.lscolors]
|
[dependencies.lscolors]
|
||||||
version = "0.16"
|
version = "0.17"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["nu-ansi-term"]
|
features = ["nu-ansi-term"]
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
nix = { version = "0.27.1", default-features = false, features = ["signal", "user"] }
|
nix = { version = "0.29.0", default-features = false, features = ["signal", "user"] }
|
||||||
|
|
||||||
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
@ -78,7 +78,7 @@ jemallocator = {version = "0.5.4", optional = true}
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
diff = "0.1"
|
diff = "0.1"
|
||||||
tempfile = "3.8"
|
tempfile = "3.10"
|
||||||
filetime = "0.2"
|
filetime = "0.2"
|
||||||
test-case = "3.3"
|
test-case = "3.3"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
# https://github.com/sharkdp/fd/issues/1085
|
||||||
|
[target.aarch64-unknown-linux-gnu.env]
|
||||||
|
passthrough = ["JEMALLOC_SYS_WITH_LG_PAGE=16"]
|
||||||
|
|
||||||
|
[target.aarch64-unknown-linux-musl.env]
|
||||||
|
passthrough = ["JEMALLOC_SYS_WITH_LG_PAGE=16"]
|
2
Makefile
2
Makefile
|
@ -6,7 +6,7 @@ datadir=$(prefix)/share
|
||||||
exe_name=fd
|
exe_name=fd
|
||||||
|
|
||||||
$(EXE): Cargo.toml src/**/*.rs
|
$(EXE): Cargo.toml src/**/*.rs
|
||||||
cargo build --profile $(PROFILE)
|
cargo build --profile $(PROFILE) --locked
|
||||||
|
|
||||||
.PHONY: completions
|
.PHONY: completions
|
||||||
completions: autocomplete/fd.bash autocomplete/fd.fish autocomplete/fd.ps1 autocomplete/_fd
|
completions: autocomplete/fd.bash autocomplete/fd.fish autocomplete/fd.ps1 autocomplete/_fd
|
||||||
|
|
68
README.md
68
README.md
|
@ -10,10 +10,7 @@ It is a simple, fast and user-friendly alternative to [`find`](https://www.gnu.o
|
||||||
While it does not aim to support all of `find`'s powerful functionality, it provides sensible
|
While it does not aim to support all of `find`'s powerful functionality, it provides sensible
|
||||||
(opinionated) defaults for a majority of use cases.
|
(opinionated) defaults for a majority of use cases.
|
||||||
|
|
||||||
Quick links:
|
[Installation](#installation) • [How to use](#how-to-use) • [Troubleshooting](#troubleshooting)
|
||||||
* [How to use](#how-to-use)
|
|
||||||
* [Installation](#installation)
|
|
||||||
* [Troubleshooting](#troubleshooting)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -261,12 +258,17 @@ To make exclude-patterns like these permanent, you can create a `.fdignore` file
|
||||||
/mnt/external-drive
|
/mnt/external-drive
|
||||||
*.bak
|
*.bak
|
||||||
```
|
```
|
||||||
Note: `fd` also supports `.ignore` files that are used by other programs such as `rg` or `ag`.
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `fd` also supports `.ignore` files that are used by other programs such as `rg` or `ag`.
|
||||||
|
|
||||||
If you want `fd` to ignore these patterns globally, you can put them in `fd`'s global ignore file.
|
If you want `fd` to ignore these patterns globally, you can put them in `fd`'s global ignore file.
|
||||||
This is usually located in `~/.config/fd/ignore` in macOS or Linux, and `%APPDATA%\fd\ignore` in
|
This is usually located in `~/.config/fd/ignore` in macOS or Linux, and `%APPDATA%\fd\ignore` in
|
||||||
Windows.
|
Windows.
|
||||||
|
|
||||||
|
You may wish to include `.git/` in your `fd/ignore` file so that `.git` directories, and their contents
|
||||||
|
are not included in output if you use the `--hidden` option.
|
||||||
|
|
||||||
### Deleting files
|
### Deleting files
|
||||||
|
|
||||||
You can use `fd` to remove all files and directories that are matched by your search pattern.
|
You can use `fd` to remove all files and directories that are matched by your search pattern.
|
||||||
|
@ -284,7 +286,8 @@ option:
|
||||||
If you also want to remove a certain class of directories, you can use the same technique. You will
|
If you also want to remove a certain class of directories, you can use the same technique. You will
|
||||||
have to use `rm`s `--recursive`/`-r` flag to remove directories.
|
have to use `rm`s `--recursive`/`-r` flag to remove directories.
|
||||||
|
|
||||||
Note: there are scenarios where using `fd … -X rm -r` can cause race conditions: if you have a
|
> [!NOTE]
|
||||||
|
> There are scenarios where using `fd … -X rm -r` can cause race conditions: if you have a
|
||||||
path like `…/foo/bar/foo/…` and want to remove all directories named `foo`, you can end up in a
|
path like `…/foo/bar/foo/…` and want to remove all directories named `foo`, you can end up in a
|
||||||
situation where the outer `foo` directory is removed first, leading to (harmless) *"'foo/bar/foo':
|
situation where the outer `foo` directory is removed first, leading to (harmless) *"'foo/bar/foo':
|
||||||
No such file or directory"* errors in the `rm` call.
|
No such file or directory"* errors in the `rm` call.
|
||||||
|
@ -313,7 +316,7 @@ Options:
|
||||||
-p, --full-path Search full abs. path (default: filename only)
|
-p, --full-path Search full abs. path (default: filename only)
|
||||||
-d, --max-depth <depth> Set maximum search depth (default: none)
|
-d, --max-depth <depth> Set maximum search depth (default: none)
|
||||||
-E, --exclude <pattern> Exclude entries that match the given glob pattern
|
-E, --exclude <pattern> Exclude entries that match the given glob pattern
|
||||||
-t, --type <filetype> Filter by type: file (f), directory (d), symlink (l),
|
-t, --type <filetype> Filter by type: file (f), directory (d/dir), symlink (l),
|
||||||
executable (x), empty (e), socket (s), pipe (p), char-device
|
executable (x), empty (e), socket (s), pipe (p), char-device
|
||||||
(c), block-device (b)
|
(c), block-device (b)
|
||||||
-e, --extension <ext> Filter by file extension
|
-e, --extension <ext> Filter by file extension
|
||||||
|
@ -321,6 +324,7 @@ Options:
|
||||||
--changed-within <date|dur> Filter by file modification time (newer than)
|
--changed-within <date|dur> Filter by file modification time (newer than)
|
||||||
--changed-before <date|dur> Filter by file modification time (older than)
|
--changed-before <date|dur> Filter by file modification time (older than)
|
||||||
-o, --owner <user:group> Filter by owning user and/or group
|
-o, --owner <user:group> Filter by owning user and/or group
|
||||||
|
--format <fmt> Print results according to template
|
||||||
-x, --exec <cmd>... Execute a command for each search result
|
-x, --exec <cmd>... Execute a command for each search result
|
||||||
-X, --exec-batch <cmd>... Execute a command with all search results at once
|
-X, --exec-batch <cmd>... Execute a command with all search results at once
|
||||||
-c, --color <when> When to use colors [default: auto] [possible values: auto,
|
-c, --color <when> When to use colors [default: auto] [possible values: auto,
|
||||||
|
@ -351,7 +355,7 @@ Benchmark 2: find ~ -iname '*[0-9].jpg'
|
||||||
```
|
```
|
||||||
|
|
||||||
Now let's try the same for `fd`. Note that `fd` performs a regular expression
|
Now let's try the same for `fd`. Note that `fd` performs a regular expression
|
||||||
search by defautl. The options `-u`/`--unrestricted` option is needed here for
|
search by default. The options `-u`/`--unrestricted` option is needed here for
|
||||||
a fair comparison. Otherwise `fd` does not have to traverse hidden folders and
|
a fair comparison. Otherwise `fd` does not have to traverse hidden folders and
|
||||||
ignored paths (see below):
|
ignored paths (see below):
|
||||||
```
|
```
|
||||||
|
@ -373,6 +377,15 @@ also used in [ripgrep](https://github.com/BurntSushi/ripgrep) (check it out!).
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### `fd` does not find my file!
|
||||||
|
|
||||||
|
Remember that `fd` ignores hidden directories and files by default. It also ignores patterns
|
||||||
|
from `.gitignore` files. If you want to make sure to find absolutely every possible file, always
|
||||||
|
use the options `-u`/`--unrestricted` option (or `-HI` to enable hidden and ignored files):
|
||||||
|
``` bash
|
||||||
|
> fd -u …
|
||||||
|
```
|
||||||
|
|
||||||
### Colorized output
|
### Colorized output
|
||||||
|
|
||||||
`fd` can colorize files by extension, just like `ls`. In order for this to work, the environment
|
`fd` can colorize files by extension, just like `ls`. In order for this to work, the environment
|
||||||
|
@ -386,15 +399,6 @@ for alternative, more complete (or more colorful) variants, see [here](https://g
|
||||||
|
|
||||||
`fd` also honors the [`NO_COLOR`](https://no-color.org/) environment variable.
|
`fd` also honors the [`NO_COLOR`](https://no-color.org/) environment variable.
|
||||||
|
|
||||||
### `fd` does not find my file!
|
|
||||||
|
|
||||||
Remember that `fd` ignores hidden directories and files by default. It also ignores patterns
|
|
||||||
from `.gitignore` files. If you want to make sure to find absolutely every possible file, always
|
|
||||||
use the options `-u`/`--unrestricted` option (or `-HI` to enable hidden and ignored files):
|
|
||||||
``` bash
|
|
||||||
> fd -u …
|
|
||||||
```
|
|
||||||
|
|
||||||
### `fd` doesn't seem to interpret my regex pattern correctly
|
### `fd` doesn't seem to interpret my regex pattern correctly
|
||||||
|
|
||||||
A lot of special regex characters (like `[]`, `^`, `$`, ..) are also special characters in your
|
A lot of special regex characters (like `[]`, `^`, `$`, ..) are also special characters in your
|
||||||
|
@ -517,7 +521,7 @@ newlines). In the same way, the `-0` option of `xargs` tells it to read the inpu
|
||||||
If you run Ubuntu 19.04 (Disco Dingo) or newer, you can install the
|
If you run Ubuntu 19.04 (Disco Dingo) or newer, you can install the
|
||||||
[officially maintained package](https://packages.ubuntu.com/fd-find):
|
[officially maintained package](https://packages.ubuntu.com/fd-find):
|
||||||
```
|
```
|
||||||
sudo apt install fd-find
|
apt install fd-find
|
||||||
```
|
```
|
||||||
Note that the binary is called `fdfind` as the binary name `fd` is already used by another package.
|
Note that the binary is called `fdfind` as the binary name `fd` is already used by another package.
|
||||||
It is recommended that after installation, you add a link to `fd` by executing command
|
It is recommended that after installation, you add a link to `fd` by executing command
|
||||||
|
@ -527,7 +531,7 @@ Make sure that `$HOME/.local/bin` is in your `$PATH`.
|
||||||
If you use an older version of Ubuntu, you can download the latest `.deb` package from the
|
If you use an older version of Ubuntu, you can download the latest `.deb` package from the
|
||||||
[release page](https://github.com/sharkdp/fd/releases) and install it via:
|
[release page](https://github.com/sharkdp/fd/releases) and install it via:
|
||||||
``` bash
|
``` bash
|
||||||
sudo dpkg -i fd_9.0.0_amd64.deb # adapt version number and architecture
|
dpkg -i fd_9.0.0_amd64.deb # adapt version number and architecture
|
||||||
```
|
```
|
||||||
|
|
||||||
### On Debian
|
### On Debian
|
||||||
|
@ -535,7 +539,7 @@ sudo dpkg -i fd_9.0.0_amd64.deb # adapt version number and architecture
|
||||||
If you run Debian Buster or newer, you can install the
|
If you run Debian Buster or newer, you can install the
|
||||||
[officially maintained Debian package](https://tracker.debian.org/pkg/rust-fd-find):
|
[officially maintained Debian package](https://tracker.debian.org/pkg/rust-fd-find):
|
||||||
```
|
```
|
||||||
sudo apt-get install fd-find
|
apt-get install fd-find
|
||||||
```
|
```
|
||||||
Note that the binary is called `fdfind` as the binary name `fd` is already used by another package.
|
Note that the binary is called `fdfind` as the binary name `fd` is already used by another package.
|
||||||
It is recommended that after installation, you add a link to `fd` by executing command
|
It is recommended that after installation, you add a link to `fd` by executing command
|
||||||
|
@ -563,6 +567,8 @@ You can install [the fd package](https://www.archlinux.org/packages/community/x8
|
||||||
```
|
```
|
||||||
pacman -S fd
|
pacman -S fd
|
||||||
```
|
```
|
||||||
|
You can also install fd [from the AUR](https://aur.archlinux.org/packages/fd-git).
|
||||||
|
|
||||||
### On Gentoo Linux
|
### On Gentoo Linux
|
||||||
|
|
||||||
You can use [the fd ebuild](https://packages.gentoo.org/packages/sys-apps/fd) from the official repo:
|
You can use [the fd ebuild](https://packages.gentoo.org/packages/sys-apps/fd) from the official repo:
|
||||||
|
@ -584,6 +590,20 @@ You can install `fd` via xbps-install:
|
||||||
xbps-install -S fd
|
xbps-install -S fd
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### On ALT Linux
|
||||||
|
|
||||||
|
You can install [the fd package](https://packages.altlinux.org/en/sisyphus/srpms/fd/) from the official repo:
|
||||||
|
```
|
||||||
|
apt-get install fd
|
||||||
|
```
|
||||||
|
|
||||||
|
### On Solus
|
||||||
|
|
||||||
|
You can install [the fd package](https://github.com/getsolus/packages/tree/main/packages/f/fd) from the official repo:
|
||||||
|
```
|
||||||
|
eopkg install fd
|
||||||
|
```
|
||||||
|
|
||||||
### On RedHat Enterprise Linux 8/9 (RHEL8/9), Almalinux 8/9, EuroLinux 8/9 or Rocky Linux 8/9
|
### On RedHat Enterprise Linux 8/9 (RHEL8/9), Almalinux 8/9, EuroLinux 8/9 or Rocky Linux 8/9
|
||||||
|
|
||||||
You can install [the `fd` package](https://copr.fedorainfracloud.org/coprs/tkbcopr/fd/) from Fedora Copr.
|
You can install [the `fd` package](https://copr.fedorainfracloud.org/coprs/tkbcopr/fd/) from Fedora Copr.
|
||||||
|
@ -604,7 +624,7 @@ brew install fd
|
||||||
|
|
||||||
… or with MacPorts:
|
… or with MacPorts:
|
||||||
```
|
```
|
||||||
sudo port install fd
|
port install fd
|
||||||
```
|
```
|
||||||
|
|
||||||
### On Windows
|
### On Windows
|
||||||
|
@ -649,7 +669,7 @@ pkg install fd-find
|
||||||
|
|
||||||
### From npm
|
### From npm
|
||||||
|
|
||||||
On linux and macOS, you can install the [fd-find](https://npm.im/fd-find) package:
|
On Linux and macOS, you can install the [fd-find](https://npm.im/fd-find) package:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install -g fd-find
|
npm install -g fd-find
|
||||||
|
@ -661,7 +681,7 @@ With Rust's package manager [cargo](https://github.com/rust-lang/cargo), you can
|
||||||
```
|
```
|
||||||
cargo install fd-find
|
cargo install fd-find
|
||||||
```
|
```
|
||||||
Note that rust version *1.70.0* or later is required.
|
Note that rust version *1.77.2* or later is required.
|
||||||
|
|
||||||
`make` is also needed for the build.
|
`make` is also needed for the build.
|
||||||
|
|
||||||
|
@ -692,8 +712,6 @@ cargo install --path .
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2017-2021 The fd developers
|
|
||||||
|
|
||||||
`fd` is distributed under the terms of both the MIT License and the Apache License 2.0.
|
`fd` is distributed under the terms of both the MIT License and the Apache License 2.0.
|
||||||
|
|
||||||
See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details.
|
See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details.
|
||||||
|
|
|
@ -162,7 +162,7 @@ _fd() {
|
||||||
$no'(*)*--search-path=[set search path (instead of positional <path> arguments)]:directory:_files -/'
|
$no'(*)*--search-path=[set search path (instead of positional <path> arguments)]:directory:_files -/'
|
||||||
|
|
||||||
+ strip-cwd-prefix
|
+ strip-cwd-prefix
|
||||||
$no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix[Strip ./ prefix when output is redirected]'
|
$no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix=[When to strip ./]:when:(always never auto)'
|
||||||
|
|
||||||
+ and
|
+ and
|
||||||
'--and=[additional required search path]:pattern'
|
'--and=[additional required search path]:pattern'
|
||||||
|
|
|
@ -33,16 +33,14 @@ with the '\-\-glob' option.
|
||||||
By default
|
By default
|
||||||
.B fd
|
.B fd
|
||||||
will exclude hidden files and directories, as well as any files that match gitignore rules
|
will exclude hidden files and directories, as well as any files that match gitignore rules
|
||||||
or ignore rules in .ignore or .fdignore files. For convenenience, '.git' is treated as if it
|
or ignore rules in .ignore or .fdignore files.
|
||||||
was always included in gitignore rules. These files can be included with options such as
|
|
||||||
'\-\-hidden' and '\-\-no\-ignore'.
|
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
.TP
|
.TP
|
||||||
.B \-H, \-\-hidden
|
.B \-H, \-\-hidden
|
||||||
Include hidden files and directories in the search results
|
Include hidden files and directories in the search results
|
||||||
(default: hidden files and directories are skipped). The flag can be overridden with '--no-hidden'.
|
(default: hidden files and directories are skipped). The flag can be overridden with '--no-hidden'.
|
||||||
.IP
|
.IP
|
||||||
Ignored files and .git/ are still excluded unless \-\-no\-ignore or \-\-no\-ignore\-vcs
|
Ignored files are still excluded unless \-\-no\-ignore or \-\-no\-ignore\-vcs
|
||||||
is also used.
|
is also used.
|
||||||
.TP
|
.TP
|
||||||
.B \-I, \-\-no\-ignore
|
.B \-I, \-\-no\-ignore
|
||||||
|
@ -79,7 +77,6 @@ and the global gitignore configuration
|
||||||
.RI ( core.excludesFile
|
.RI ( core.excludesFile
|
||||||
git setting, which defaults to
|
git setting, which defaults to
|
||||||
.IR $HOME/.config/git/ignore ).
|
.IR $HOME/.config/git/ignore ).
|
||||||
The pattern ".git/" is automatically added to the list of VCS ignore rules.
|
|
||||||
The flag can be overridden with '--ignore-vcs'.
|
The flag can be overridden with '--ignore-vcs'.
|
||||||
.TP
|
.TP
|
||||||
.B \-\-no\-require\-git
|
.B \-\-no\-require\-git
|
||||||
|
@ -159,9 +156,20 @@ can be used as an alias.
|
||||||
Enable the display of filesystem errors for situations such as insufficient
|
Enable the display of filesystem errors for situations such as insufficient
|
||||||
permissions or dead symlinks.
|
permissions or dead symlinks.
|
||||||
.TP
|
.TP
|
||||||
.B \-\-strip-cwd-prefix
|
.B \-\-strip-cwd-prefix [when]
|
||||||
By default, relative paths are prefixed with './' when the output goes to a non interactive terminal
|
By default, relative paths are prefixed with './' when -x/--exec,
|
||||||
(TTY). Use this flag to disable this behaviour.
|
-X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
||||||
|
path starting with '-' being treated as a command line option. Use
|
||||||
|
this flag to change this behavior. If this flag is used without a value,
|
||||||
|
it is equivalent to passing "always". Possible values are:
|
||||||
|
.RS
|
||||||
|
.IP never
|
||||||
|
Never strip the ./ at the beginning of paths
|
||||||
|
.IP always
|
||||||
|
Always strip the ./ at the beginning of paths
|
||||||
|
.IP auto
|
||||||
|
Only strip if used with --exec, --exec-batch, or --print0. That is, it resets to the default behavior.
|
||||||
|
.RE
|
||||||
.TP
|
.TP
|
||||||
.B \-\-one\-file\-system, \-\-mount, \-\-xdev
|
.B \-\-one\-file\-system, \-\-mount, \-\-xdev
|
||||||
By default, fd will traverse the file system tree as far as other options dictate. With this flag, fd ensures that it does not descend into a different file system than the one it started in. Comparable to the -mount or -xdev filters of find(1).
|
By default, fd will traverse the file system tree as far as other options dictate. With this flag, fd ensures that it does not descend into a different file system than the one it started in. Comparable to the -mount or -xdev filters of find(1).
|
||||||
|
@ -191,7 +199,7 @@ Filter search by type:
|
||||||
.RS
|
.RS
|
||||||
.IP "f, file"
|
.IP "f, file"
|
||||||
regular files
|
regular files
|
||||||
.IP "d, directory"
|
.IP "d, dir, directory"
|
||||||
directories
|
directories
|
||||||
.IP "l, symlink"
|
.IP "l, symlink"
|
||||||
symbolic links
|
symbolic links
|
||||||
|
@ -312,8 +320,9 @@ tebibytes
|
||||||
Filter results based on the file modification time.
|
Filter results based on the file modification time.
|
||||||
Files with modification times greater than the argument will be returned.
|
Files with modification times greater than the argument will be returned.
|
||||||
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
||||||
in time in either full RFC3339 format with time zone, or as a date or datetime in the
|
in time as full RFC3339 format with time zone, as a date or datetime in the
|
||||||
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR).
|
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR), or as the prefix '@'
|
||||||
|
followed by the number of seconds since the Unix epoch (@[0-9]+).
|
||||||
\fB\-\-change-newer-than\fR,
|
\fB\-\-change-newer-than\fR,
|
||||||
.B --newer
|
.B --newer
|
||||||
or
|
or
|
||||||
|
@ -324,13 +333,15 @@ Examples:
|
||||||
\-\-changed-within 2weeks
|
\-\-changed-within 2weeks
|
||||||
\-\-change-newer-than "2018-10-27 10:00:00"
|
\-\-change-newer-than "2018-10-27 10:00:00"
|
||||||
\-\-newer 2018-10-27
|
\-\-newer 2018-10-27
|
||||||
|
\-\-changed-after @1704067200
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-changed-before " date|duration
|
.BI "\-\-changed-before " date|duration
|
||||||
Filter results based on the file modification time.
|
Filter results based on the file modification time.
|
||||||
Files with modification times less than the argument will be returned.
|
Files with modification times less than the argument will be returned.
|
||||||
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
||||||
in time in either full RFC3339 format with time zone, or as a date or datetime in the
|
in time as full RFC3339 format with time zone, as a date or datetime in the
|
||||||
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR).
|
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR), or as the prefix '@'
|
||||||
|
followed by the number of seconds since the Unix epoch (@[0-9]+).
|
||||||
.B --change-older-than
|
.B --change-older-than
|
||||||
or
|
or
|
||||||
.B --older
|
.B --older
|
||||||
|
@ -339,6 +350,7 @@ can be used as aliases.
|
||||||
Examples:
|
Examples:
|
||||||
\-\-changed-before "2018-10-27 10:00:00"
|
\-\-changed-before "2018-10-27 10:00:00"
|
||||||
\-\-change-older-than 2weeks
|
\-\-change-older-than 2weeks
|
||||||
|
\-\-older @1704067200
|
||||||
.TP
|
.TP
|
||||||
.BI "-o, \-\-owner " [user][:group]
|
.BI "-o, \-\-owner " [user][:group]
|
||||||
Filter files by their user and/or group. Format: [(user|uid)][:(group|gid)]. Either side
|
Filter files by their user and/or group. Format: [(user|uid)][:(group|gid)]. Either side
|
||||||
|
@ -363,6 +375,30 @@ Set the path separator to use when printing file paths. The default is the OS-sp
|
||||||
Provide paths to search as an alternative to the positional \fIpath\fR argument. Changes the usage to
|
Provide paths to search as an alternative to the positional \fIpath\fR argument. Changes the usage to
|
||||||
\'fd [FLAGS/OPTIONS] \-\-search\-path PATH \-\-search\-path PATH2 [PATTERN]\'
|
\'fd [FLAGS/OPTIONS] \-\-search\-path PATH \-\-search\-path PATH2 [PATTERN]\'
|
||||||
.TP
|
.TP
|
||||||
|
.BI "\-\-format " fmt
|
||||||
|
Specify a template string that is used for printing a line for each file found.
|
||||||
|
|
||||||
|
The following placeholders are substituted into the string for each file before printing:
|
||||||
|
.RS
|
||||||
|
.IP {}
|
||||||
|
path (of the current search result)
|
||||||
|
.IP {/}
|
||||||
|
basename
|
||||||
|
.IP {//}
|
||||||
|
parent directory
|
||||||
|
.IP {.}
|
||||||
|
path without file extension
|
||||||
|
.IP {/.}
|
||||||
|
basename without file extension
|
||||||
|
.IP {{
|
||||||
|
literal '{' (an escape sequence)
|
||||||
|
.IP }}
|
||||||
|
literal '}' (an escape sequence)
|
||||||
|
.P
|
||||||
|
Notice that you can use "{{" and "}}" to escape "{" and "}" respectively, which is especially
|
||||||
|
useful if you need to include the literal text of one of the above placeholders.
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.BI "\-x, \-\-exec " command
|
.BI "\-x, \-\-exec " command
|
||||||
.RS
|
.RS
|
||||||
Execute
|
Execute
|
||||||
|
@ -383,29 +419,12 @@ If parallelism is enabled, the order commands will be executed in is non-determi
|
||||||
--threads=1, the order is determined by the operating system and may not be what you expect. Thus, it is
|
--threads=1, the order is determined by the operating system and may not be what you expect. Thus, it is
|
||||||
recommended that you don't rely on any ordering of the results.
|
recommended that you don't rely on any ordering of the results.
|
||||||
|
|
||||||
The following placeholders are substituted before the command is executed:
|
Before executing the command, any placeholder patterns in the command are replaced with the
|
||||||
.RS
|
corresponding values for the current file. The same placeholders are used as in the "\-\-format"
|
||||||
.IP {}
|
option.
|
||||||
path (of the current search result)
|
|
||||||
.IP {/}
|
|
||||||
basename
|
|
||||||
.IP {//}
|
|
||||||
parent directory
|
|
||||||
.IP {.}
|
|
||||||
path without file extension
|
|
||||||
.IP {/.}
|
|
||||||
basename without file extension
|
|
||||||
.IP {{
|
|
||||||
literal '{' (an escape sequence)
|
|
||||||
.IP }}
|
|
||||||
literal '}' (an escape sequence)
|
|
||||||
.RE
|
|
||||||
|
|
||||||
If no placeholder is present, an implicit "{}" at the end is assumed.
|
If no placeholder is present, an implicit "{}" at the end is assumed.
|
||||||
|
|
||||||
Notice that you can use "{{" and "}}" to escape "{" and "}" respectively, which is especially
|
|
||||||
useful if you need to include the literal text of one of the above placeholders.
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
- find all *.zip files and unzip them:
|
- find all *.zip files and unzip them:
|
||||||
|
@ -429,19 +448,9 @@ once, with all search results as arguments.
|
||||||
|
|
||||||
The order of the arguments is non-deterministic and should not be relied upon.
|
The order of the arguments is non-deterministic and should not be relied upon.
|
||||||
|
|
||||||
One of the following placeholders is substituted before the command is executed:
|
This uses the same placeholders as "\-\-format" and "\-\-exec", but instead of expanding
|
||||||
.RS
|
once per command invocation each argument containing a placeholder is expanding for every
|
||||||
.IP {}
|
file in a batch and passed as separate arguments.
|
||||||
path (of all search results)
|
|
||||||
.IP {/}
|
|
||||||
basename
|
|
||||||
.IP {//}
|
|
||||||
parent directory
|
|
||||||
.IP {.}
|
|
||||||
path without file extension
|
|
||||||
.IP {/.}
|
|
||||||
basename without file extension
|
|
||||||
.RE
|
|
||||||
|
|
||||||
If no placeholder is present, an implicit "{}" at the end is assumed.
|
If no placeholder is present, an implicit "{}" at the end is assumed.
|
||||||
|
|
||||||
|
@ -490,6 +499,17 @@ is set, use
|
||||||
.IR $XDG_CONFIG_HOME/fd/ignore .
|
.IR $XDG_CONFIG_HOME/fd/ignore .
|
||||||
Otherwise, use
|
Otherwise, use
|
||||||
.IR $HOME/.config/fd/ignore .
|
.IR $HOME/.config/fd/ignore .
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.B .fdignore
|
||||||
|
This file works similarly to a .gitignore file anywhere in the searched tree and specifies patterns
|
||||||
|
that should be excluded from the search. However, this file is specific to fd, and will be used even
|
||||||
|
if the --no-ignore-vcs option is used.
|
||||||
|
.TP
|
||||||
|
.B $XDG_CONFIG_HOME/fd/ignore
|
||||||
|
Global ignore file. Unless ignore mode is turned off (such as with --no-ignore)
|
||||||
|
ignore entries in this file will be ignored, as if it was an .fdignore file in the
|
||||||
|
current directory.
|
||||||
.SH EXAMPLES
|
.SH EXAMPLES
|
||||||
.TP
|
.TP
|
||||||
.RI "Find files and directories that match the pattern '" needle "':"
|
.RI "Find files and directories that match the pattern '" needle "':"
|
||||||
|
@ -503,6 +523,16 @@ $ fd -e py
|
||||||
.TP
|
.TP
|
||||||
.RI "Open all search results with vim:"
|
.RI "Open all search results with vim:"
|
||||||
$ fd pattern -X vim
|
$ fd pattern -X vim
|
||||||
|
.SH Tips and Tricks
|
||||||
|
.IP \[bu]
|
||||||
|
If you add ".git/" to your global ignore file ($XDG_CONFIG_HOME/fd/ignore), then
|
||||||
|
".git" folders will be ignored by default, even when the --hidden option is used.
|
||||||
|
.IP \[bu]
|
||||||
|
You can use a shell alias or a wrapper script in order to pass desired flags to fd
|
||||||
|
by default. For example if you do not like the default behavior of respecting gitignore,
|
||||||
|
you can use
|
||||||
|
`alias fd="/usr/bin/fd --no-ignore-vcs"`
|
||||||
|
in your .bashrc to create an alias for fd that doesn't ignore git files by default.
|
||||||
.SH BUGS
|
.SH BUGS
|
||||||
Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues
|
Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues
|
||||||
.SH SEE ALSO
|
.SH SEE ALSO
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
`fd` development is sponsored by many individuals and companies. Thank you very much!
|
||||||
|
|
||||||
|
Please note, that being sponsored does not affect the individuality of the `fd`
|
||||||
|
project or affect the maintainers' actions in any way.
|
||||||
|
We remain impartial and continue to assess pull requests solely on merit - the
|
||||||
|
features added, bugs solved, and effect on the overall complexity of the code.
|
||||||
|
No issue will have a different priority based on sponsorship status of the
|
||||||
|
reporter.
|
||||||
|
|
||||||
|
Contributions from anybody are most welcomed, please see our [`CONTRIBUTING.md`](../CONTRIBUTING.md) guide.
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.2 KiB |
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# This script automates the "Version bump" section
|
||||||
|
|
||||||
|
version="$1"
|
||||||
|
|
||||||
|
if [[ -z $version ]]; then
|
||||||
|
echo "Usage: must supply version as first argument" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git switch -C "release-$version"
|
||||||
|
sed -i -e "0,/^\[badges/{s/^version =.*/version = \"$version\"/}" Cargo.toml
|
||||||
|
|
||||||
|
msrv="$(grep -F rust-version Cargo.toml | sed -e 's/^rust-version= "\(.*\)"/\1/')"
|
||||||
|
|
||||||
|
sed -i -e "s/Note that rust version \*[0-9.]+\* or later/Note that rust version *$msrv* or later/" README.md
|
||||||
|
|
||||||
|
sed -i -e "s/^# Upcoming release/# $version/" CHANGELOG.md
|
||||||
|
|
68
src/cli.rs
68
src/cli.rs
|
@ -49,8 +49,7 @@ pub struct Opts {
|
||||||
no_hidden: (),
|
no_hidden: (),
|
||||||
|
|
||||||
/// Show search results from files and directories that would otherwise be
|
/// Show search results from files and directories that would otherwise be
|
||||||
/// ignored by '.gitignore', '.ignore', '.fdignore', the global ignore file,
|
/// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file,
|
||||||
/// or the default rule that excludes .git/.
|
|
||||||
/// The flag can be overridden with --ignore.
|
/// The flag can be overridden with --ignore.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
|
@ -64,7 +63,7 @@ pub struct Opts {
|
||||||
#[arg(long, overrides_with = "no_ignore", hide = true, action = ArgAction::SetTrue)]
|
#[arg(long, overrides_with = "no_ignore", hide = true, action = ArgAction::SetTrue)]
|
||||||
ignore: (),
|
ignore: (),
|
||||||
|
|
||||||
///Show search results from '.git/' folders and files and directories that
|
///Show search results from files and directories that
|
||||||
///would otherwise be ignored by '.gitignore' files.
|
///would otherwise be ignored by '.gitignore' files.
|
||||||
///The flag can be overridden with --ignore-vcs.
|
///The flag can be overridden with --ignore-vcs.
|
||||||
#[arg(
|
#[arg(
|
||||||
|
@ -314,7 +313,7 @@ pub struct Opts {
|
||||||
|
|
||||||
/// Filter the search by type:
|
/// Filter the search by type:
|
||||||
/// {n} 'f' or 'file': regular files
|
/// {n} 'f' or 'file': regular files
|
||||||
/// {n} 'd' or 'directory': directories
|
/// {n} 'd' or 'dir' or 'directory': directories
|
||||||
/// {n} 'l' or 'symlink': symbolic links
|
/// {n} 'l' or 'symlink': symbolic links
|
||||||
/// {n} 's' or 'socket': socket
|
/// {n} 's' or 'socket': socket
|
||||||
/// {n} 'p' or 'pipe': named pipe (FIFO)
|
/// {n} 'p' or 'pipe': named pipe (FIFO)
|
||||||
|
@ -352,7 +351,7 @@ pub struct Opts {
|
||||||
value_name = "filetype",
|
value_name = "filetype",
|
||||||
hide_possible_values = true,
|
hide_possible_values = true,
|
||||||
value_enum,
|
value_enum,
|
||||||
help = "Filter by type: file (f), directory (d), symlink (l), \
|
help = "Filter by type: file (f), directory (d/dir), symlink (l), \
|
||||||
executable (x), empty (e), socket (s), pipe (p), \
|
executable (x), empty (e), socket (s), pipe (p), \
|
||||||
char-device (c), block-device (b)",
|
char-device (c), block-device (b)",
|
||||||
long_help
|
long_help
|
||||||
|
@ -399,7 +398,7 @@ pub struct Opts {
|
||||||
|
|
||||||
/// Filter results based on the file modification time. Files with modification times
|
/// Filter results based on the file modification time. Files with modification times
|
||||||
/// greater than the argument are returned. The argument can be provided
|
/// greater than the argument are returned. The argument can be provided
|
||||||
/// as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min).
|
/// as a specific point in time (YYYY-MM-DD HH:MM:SS or @timestamp) or as a duration (10h, 1d, 35min).
|
||||||
/// If the time is not specified, it defaults to 00:00:00.
|
/// If the time is not specified, it defaults to 00:00:00.
|
||||||
/// '--change-newer-than', '--newer', or '--changed-after' can be used as aliases.
|
/// '--change-newer-than', '--newer', or '--changed-after' can be used as aliases.
|
||||||
///
|
///
|
||||||
|
@ -421,7 +420,7 @@ pub struct Opts {
|
||||||
|
|
||||||
/// Filter results based on the file modification time. Files with modification times
|
/// Filter results based on the file modification time. Files with modification times
|
||||||
/// less than the argument are returned. The argument can be provided
|
/// less than the argument are returned. The argument can be provided
|
||||||
/// as a specific point in time (YYYY-MM-DD HH:MM:SS) or as a duration (10h, 1d, 35min).
|
/// as a specific point in time (YYYY-MM-DD HH:MM:SS or @timestamp) or as a duration (10h, 1d, 35min).
|
||||||
/// '--change-older-than' or '--older' can be used as aliases.
|
/// '--change-older-than' or '--older' can be used as aliases.
|
||||||
///
|
///
|
||||||
/// Examples:
|
/// Examples:
|
||||||
|
@ -453,6 +452,20 @@ pub struct Opts {
|
||||||
)]
|
)]
|
||||||
pub owner: Option<OwnerFilter>,
|
pub owner: Option<OwnerFilter>,
|
||||||
|
|
||||||
|
/// Instead of printing the file normally, print the format string with the following placeholders replaced:
|
||||||
|
/// '{}': path (of the current search result)
|
||||||
|
/// '{/}': basename
|
||||||
|
/// '{//}': parent directory
|
||||||
|
/// '{.}': path without file extension
|
||||||
|
/// '{/.}': basename without file extension
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
value_name = "fmt",
|
||||||
|
help = "Print results according to template",
|
||||||
|
conflicts_with = "list_details"
|
||||||
|
)]
|
||||||
|
pub format: Option<String>,
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub exec: Exec,
|
pub exec: Exec,
|
||||||
|
|
||||||
|
@ -618,9 +631,10 @@ pub struct Opts {
|
||||||
/// By default, relative paths are prefixed with './' when -x/--exec,
|
/// By default, relative paths are prefixed with './' when -x/--exec,
|
||||||
/// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
/// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
||||||
/// path starting with '-' being treated as a command line option. Use
|
/// path starting with '-' being treated as a command line option. Use
|
||||||
/// this flag to disable this behaviour.
|
/// this flag to change this behavior. If this flag is used without a value,
|
||||||
#[arg(long, conflicts_with_all(&["path", "search_path"]), hide_short_help = true, long_help)]
|
/// it is equivalent to passing "always".
|
||||||
pub strip_cwd_prefix: bool,
|
#[arg(long, conflicts_with_all(&["path", "search_path"]), value_name = "when", hide_short_help = true, require_equals = true, long_help)]
|
||||||
|
strip_cwd_prefix: Option<Option<StripCwdWhen>>,
|
||||||
|
|
||||||
/// By default, fd will traverse the file system tree as far as other options
|
/// 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
|
/// dictate. With this flag, fd ensures that it does not descend into a
|
||||||
|
@ -643,7 +657,7 @@ impl Opts {
|
||||||
} else if !self.search_path.is_empty() {
|
} else if !self.search_path.is_empty() {
|
||||||
&self.search_path
|
&self.search_path
|
||||||
} else {
|
} else {
|
||||||
let current_directory = Path::new(".");
|
let current_directory = Path::new("./");
|
||||||
ensure_current_directory_exists(current_directory)?;
|
ensure_current_directory_exists(current_directory)?;
|
||||||
return Ok(vec![self.normalize_path(current_directory)]);
|
return Ok(vec![self.normalize_path(current_directory)]);
|
||||||
};
|
};
|
||||||
|
@ -666,6 +680,9 @@ impl Opts {
|
||||||
fn normalize_path(&self, path: &Path) -> PathBuf {
|
fn normalize_path(&self, path: &Path) -> PathBuf {
|
||||||
if self.absolute_path {
|
if self.absolute_path {
|
||||||
filesystem::absolute_path(path.normalize().unwrap().as_path()).unwrap()
|
filesystem::absolute_path(path.normalize().unwrap().as_path()).unwrap()
|
||||||
|
} else if path == Path::new(".") {
|
||||||
|
// Change "." to "./" as a workaround for https://github.com/BurntSushi/ripgrep/pull/2711
|
||||||
|
PathBuf::from("./")
|
||||||
} else {
|
} else {
|
||||||
path.to_path_buf()
|
path.to_path_buf()
|
||||||
}
|
}
|
||||||
|
@ -698,6 +715,16 @@ impl Opts {
|
||||||
.or_else(|| self.max_one_result.then_some(1))
|
.or_else(|| self.max_one_result.then_some(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn strip_cwd_prefix<P: FnOnce() -> bool>(&self, auto_pred: P) -> bool {
|
||||||
|
use self::StripCwdWhen::*;
|
||||||
|
self.no_search_paths()
|
||||||
|
&& match self.strip_cwd_prefix.map_or(Auto, |o| o.unwrap_or(Always)) {
|
||||||
|
Auto => auto_pred(),
|
||||||
|
Always => true,
|
||||||
|
Never => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "completions")]
|
#[cfg(feature = "completions")]
|
||||||
pub fn gen_completions(&self) -> anyhow::Result<Option<Shell>> {
|
pub fn gen_completions(&self) -> anyhow::Result<Option<Shell>> {
|
||||||
self.gen_completions
|
self.gen_completions
|
||||||
|
@ -729,7 +756,7 @@ fn default_num_threads() -> NonZeroUsize {
|
||||||
pub enum FileType {
|
pub enum FileType {
|
||||||
#[value(alias = "f")]
|
#[value(alias = "f")]
|
||||||
File,
|
File,
|
||||||
#[value(alias = "d")]
|
#[value(alias = "d", alias = "dir")]
|
||||||
Directory,
|
Directory,
|
||||||
#[value(alias = "l")]
|
#[value(alias = "l")]
|
||||||
Symlink,
|
Symlink,
|
||||||
|
@ -758,15 +785,14 @@ pub enum ColorWhen {
|
||||||
Never,
|
Never,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColorWhen {
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub enum StripCwdWhen {
|
||||||
use ColorWhen::*;
|
/// Use the default behavior
|
||||||
match *self {
|
Auto,
|
||||||
Auto => "auto",
|
/// Always strip the ./ at the beginning of paths
|
||||||
Never => "never",
|
Always,
|
||||||
Always => "always",
|
/// Never strip the ./
|
||||||
}
|
Never,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// there isn't a derive api for getting grouped values yet,
|
// there isn't a derive api for getting grouped values yet,
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::filetypes::FileTypes;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use crate::filter::OwnerFilter;
|
use crate::filter::OwnerFilter;
|
||||||
use crate::filter::{SizeFilter, TimeFilter};
|
use crate::filter::{SizeFilter, TimeFilter};
|
||||||
|
use crate::fmt::FormatTemplate;
|
||||||
|
|
||||||
/// Configuration options for *fd*.
|
/// Configuration options for *fd*.
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -85,6 +86,9 @@ pub struct Config {
|
||||||
/// The value (if present) will be a lowercase string without leading dots.
|
/// The value (if present) will be a lowercase string without leading dots.
|
||||||
pub extensions: Option<RegexSet>,
|
pub extensions: Option<RegexSet>,
|
||||||
|
|
||||||
|
/// A format string to use to format results, similarly to exec
|
||||||
|
pub format: Option<FormatTemplate>,
|
||||||
|
|
||||||
/// If a value is supplied, each item found will be used to generate and execute commands.
|
/// If a value is supplied, each item found will be used to generate and execute commands.
|
||||||
pub command: Option<Arc<CommandSet>>,
|
pub command: Option<Arc<CommandSet>>,
|
||||||
|
|
||||||
|
|
|
@ -113,7 +113,7 @@ impl Eq for DirEntry {}
|
||||||
impl PartialOrd for DirEntry {
|
impl PartialOrd for DirEntry {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
self.path().partial_cmp(other.path())
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
159
src/exec/mod.rs
159
src/exec/mod.rs
|
@ -1,13 +1,10 @@
|
||||||
mod command;
|
mod command;
|
||||||
mod input;
|
|
||||||
mod job;
|
mod job;
|
||||||
mod token;
|
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::ffi::OsString;
|
||||||
use std::ffi::{OsStr, OsString};
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::path::{Component, Path, PathBuf, Prefix};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
@ -15,11 +12,10 @@ use anyhow::{bail, Result};
|
||||||
use argmax::Command;
|
use argmax::Command;
|
||||||
|
|
||||||
use crate::exit_codes::{merge_exitcodes, ExitCode};
|
use crate::exit_codes::{merge_exitcodes, ExitCode};
|
||||||
|
use crate::fmt::{FormatTemplate, Token};
|
||||||
|
|
||||||
use self::command::{execute_commands, handle_cmd_error};
|
use self::command::{execute_commands, handle_cmd_error};
|
||||||
use self::input::{basename, dirname, remove_extension};
|
|
||||||
pub use self::job::{batch, job};
|
pub use self::job::{batch, job};
|
||||||
use self::token::{tokenize, Token};
|
|
||||||
|
|
||||||
/// Execution mode of the command
|
/// Execution mode of the command
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
@ -131,7 +127,7 @@ impl CommandSet {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct CommandBuilder {
|
struct CommandBuilder {
|
||||||
pre_args: Vec<OsString>,
|
pre_args: Vec<OsString>,
|
||||||
path_arg: ArgumentTemplate,
|
path_arg: FormatTemplate,
|
||||||
post_args: Vec<OsString>,
|
post_args: Vec<OsString>,
|
||||||
cmd: Command,
|
cmd: Command,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
@ -220,7 +216,7 @@ impl CommandBuilder {
|
||||||
/// `generate_and_execute()` method will be used to generate a command and execute it.
|
/// `generate_and_execute()` method will be used to generate a command and execute it.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
struct CommandTemplate {
|
struct CommandTemplate {
|
||||||
args: Vec<ArgumentTemplate>,
|
args: Vec<FormatTemplate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandTemplate {
|
impl CommandTemplate {
|
||||||
|
@ -235,7 +231,7 @@ impl CommandTemplate {
|
||||||
for arg in input {
|
for arg in input {
|
||||||
let arg = arg.as_ref();
|
let arg = arg.as_ref();
|
||||||
|
|
||||||
let tmpl = tokenize(arg);
|
let tmpl = FormatTemplate::parse(arg);
|
||||||
has_placeholder |= tmpl.has_tokens();
|
has_placeholder |= tmpl.has_tokens();
|
||||||
args.push(tmpl);
|
args.push(tmpl);
|
||||||
}
|
}
|
||||||
|
@ -251,7 +247,7 @@ impl CommandTemplate {
|
||||||
|
|
||||||
// If a placeholder token was not supplied, append one at the end of the command.
|
// If a placeholder token was not supplied, append one at the end of the command.
|
||||||
if !has_placeholder {
|
if !has_placeholder {
|
||||||
args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder]));
|
args.push(FormatTemplate::Tokens(vec![Token::Placeholder]));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(CommandTemplate { args })
|
Ok(CommandTemplate { args })
|
||||||
|
@ -274,111 +270,6 @@ impl CommandTemplate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a template for a single command argument.
|
|
||||||
///
|
|
||||||
/// The argument is either a collection of `Token`s including at least one placeholder variant, or
|
|
||||||
/// a fixed text.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
enum ArgumentTemplate {
|
|
||||||
Tokens(Vec<Token>),
|
|
||||||
Text(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ArgumentTemplate {
|
|
||||||
pub fn has_tokens(&self) -> bool {
|
|
||||||
matches!(self, ArgumentTemplate::Tokens(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate an argument from this template. If path_separator is Some, then it will replace
|
|
||||||
/// the path separator in all placeholder tokens. Text arguments and tokens are not affected by
|
|
||||||
/// path separator substitution.
|
|
||||||
pub fn generate(&self, path: impl AsRef<Path>, path_separator: Option<&str>) -> OsString {
|
|
||||||
use self::Token::*;
|
|
||||||
let path = path.as_ref();
|
|
||||||
|
|
||||||
match *self {
|
|
||||||
ArgumentTemplate::Tokens(ref tokens) => {
|
|
||||||
let mut s = OsString::new();
|
|
||||||
for token in tokens {
|
|
||||||
match *token {
|
|
||||||
Basename => s.push(Self::replace_separator(basename(path), path_separator)),
|
|
||||||
BasenameNoExt => s.push(Self::replace_separator(
|
|
||||||
&remove_extension(basename(path).as_ref()),
|
|
||||||
path_separator,
|
|
||||||
)),
|
|
||||||
NoExt => s.push(Self::replace_separator(
|
|
||||||
&remove_extension(path),
|
|
||||||
path_separator,
|
|
||||||
)),
|
|
||||||
Parent => s.push(Self::replace_separator(&dirname(path), path_separator)),
|
|
||||||
Placeholder => {
|
|
||||||
s.push(Self::replace_separator(path.as_ref(), path_separator))
|
|
||||||
}
|
|
||||||
Text(ref string) => s.push(string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s
|
|
||||||
}
|
|
||||||
ArgumentTemplate::Text(ref text) => OsString::from(text),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the path separator in the input with the custom separator string. If path_separator
|
|
||||||
/// is None, simply return a borrowed Cow<OsStr> of the input. Otherwise, the input is
|
|
||||||
/// interpreted as a Path and its components are iterated through and re-joined into a new
|
|
||||||
/// OsString.
|
|
||||||
fn replace_separator<'a>(path: &'a OsStr, path_separator: Option<&str>) -> Cow<'a, OsStr> {
|
|
||||||
// fast-path - no replacement necessary
|
|
||||||
if path_separator.is_none() {
|
|
||||||
return Cow::Borrowed(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
let path_separator = path_separator.unwrap();
|
|
||||||
let mut out = OsString::with_capacity(path.len());
|
|
||||||
let mut components = Path::new(path).components().peekable();
|
|
||||||
|
|
||||||
while let Some(comp) = components.next() {
|
|
||||||
match comp {
|
|
||||||
// Absolute paths on Windows are tricky. A Prefix component is usually a drive
|
|
||||||
// letter or UNC path, and is usually followed by RootDir. There are also
|
|
||||||
// "verbatim" prefixes beginning with "\\?\" that skip normalization. We choose to
|
|
||||||
// ignore verbatim path prefixes here because they're very rare, might be
|
|
||||||
// impossible to reach here, and there's no good way to deal with them. If users
|
|
||||||
// are doing something advanced involving verbatim windows paths, they can do their
|
|
||||||
// own output filtering with a tool like sed.
|
|
||||||
Component::Prefix(prefix) => {
|
|
||||||
if let Prefix::UNC(server, share) = prefix.kind() {
|
|
||||||
// Prefix::UNC is a parsed version of '\\server\share'
|
|
||||||
out.push(path_separator);
|
|
||||||
out.push(path_separator);
|
|
||||||
out.push(server);
|
|
||||||
out.push(path_separator);
|
|
||||||
out.push(share);
|
|
||||||
} else {
|
|
||||||
// All other Windows prefix types are rendered as-is. This results in e.g. "C:" for
|
|
||||||
// drive letters. DeviceNS and Verbatim* prefixes won't have backslashes converted,
|
|
||||||
// but they're not returned by directories fd can search anyway so we don't worry
|
|
||||||
// about them.
|
|
||||||
out.push(comp.as_os_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root directory is always replaced with the custom separator.
|
|
||||||
Component::RootDir => out.push(path_separator),
|
|
||||||
|
|
||||||
// Everything else is joined normally, with a trailing separator if we're not last
|
|
||||||
_ => {
|
|
||||||
out.push(comp.as_os_str());
|
|
||||||
if components.peek().is_some() {
|
|
||||||
out.push(path_separator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Cow::Owned(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -398,9 +289,9 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Text("${SHELL}:".into()),
|
FormatTemplate::Text("${SHELL}:".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
|
FormatTemplate::Tokens(vec![Token::Placeholder]),
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
|
@ -415,8 +306,8 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
FormatTemplate::Tokens(vec![Token::NoExt]),
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
|
@ -431,8 +322,8 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::Basename]),
|
FormatTemplate::Tokens(vec![Token::Basename]),
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
|
@ -447,8 +338,8 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::Parent]),
|
FormatTemplate::Tokens(vec![Token::Parent]),
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
|
@ -463,8 +354,8 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]),
|
FormatTemplate::Tokens(vec![Token::BasenameNoExt]),
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::OneByOne,
|
mode: ExecutionMode::OneByOne,
|
||||||
|
@ -494,9 +385,9 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("cp".into()),
|
FormatTemplate::Text("cp".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
|
FormatTemplate::Tokens(vec![Token::Placeholder]),
|
||||||
ArgumentTemplate::Tokens(vec![
|
FormatTemplate::Tokens(vec![
|
||||||
Token::BasenameNoExt,
|
Token::BasenameNoExt,
|
||||||
Token::Text(".ext".into())
|
Token::Text(".ext".into())
|
||||||
]),
|
]),
|
||||||
|
@ -514,8 +405,8 @@ mod tests {
|
||||||
CommandSet {
|
CommandSet {
|
||||||
commands: vec![CommandTemplate {
|
commands: vec![CommandTemplate {
|
||||||
args: vec![
|
args: vec![
|
||||||
ArgumentTemplate::Text("echo".into()),
|
FormatTemplate::Text("echo".into()),
|
||||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
FormatTemplate::Tokens(vec![Token::NoExt]),
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
mode: ExecutionMode::Batch,
|
mode: ExecutionMode::Batch,
|
||||||
|
@ -540,7 +431,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generate_custom_path_separator() {
|
fn generate_custom_path_separator() {
|
||||||
let arg = ArgumentTemplate::Tokens(vec![Token::Placeholder]);
|
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||||
macro_rules! check {
|
macro_rules! check {
|
||||||
($input:expr, $expected:expr) => {
|
($input:expr, $expected:expr) => {
|
||||||
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
||||||
|
@ -555,7 +446,7 @@ mod tests {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[test]
|
#[test]
|
||||||
fn generate_custom_path_separator_windows() {
|
fn generate_custom_path_separator_windows() {
|
||||||
let arg = ArgumentTemplate::Tokens(vec![Token::Placeholder]);
|
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||||
macro_rules! check {
|
macro_rules! check {
|
||||||
($input:expr, $expected:expr) => {
|
($input:expr, $expected:expr) => {
|
||||||
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
use aho_corasick::AhoCorasick;
|
|
||||||
use std::fmt::{self, Display, Formatter};
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use super::ArgumentTemplate;
|
|
||||||
|
|
||||||
/// Designates what should be written to a buffer
|
|
||||||
///
|
|
||||||
/// Each `Token` contains either text, or a placeholder variant, which will be used to generate
|
|
||||||
/// commands after all tokens for a given command template have been collected.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Token {
|
|
||||||
Placeholder,
|
|
||||||
Basename,
|
|
||||||
Parent,
|
|
||||||
NoExt,
|
|
||||||
BasenameNoExt,
|
|
||||||
Text(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Token {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
Token::Placeholder => f.write_str("{}")?,
|
|
||||||
Token::Basename => f.write_str("{/}")?,
|
|
||||||
Token::Parent => f.write_str("{//}")?,
|
|
||||||
Token::NoExt => f.write_str("{.}")?,
|
|
||||||
Token::BasenameNoExt => f.write_str("{/.}")?,
|
|
||||||
Token::Text(ref string) => f.write_str(string)?,
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static PLACEHOLDERS: OnceLock<AhoCorasick> = OnceLock::new();
|
|
||||||
|
|
||||||
pub(super) fn tokenize(input: &str) -> ArgumentTemplate {
|
|
||||||
// NOTE: we assume that { and } have the same length
|
|
||||||
const BRACE_LEN: usize = '{'.len_utf8();
|
|
||||||
let mut tokens = Vec::new();
|
|
||||||
let mut remaining = input;
|
|
||||||
let mut buf = String::new();
|
|
||||||
let placeholders = PLACEHOLDERS.get_or_init(|| {
|
|
||||||
AhoCorasick::new(&["{{", "}}", "{}", "{/}", "{//}", "{.}", "{/.}"]).unwrap()
|
|
||||||
});
|
|
||||||
while let Some(m) = placeholders.find(remaining) {
|
|
||||||
match m.pattern().as_u32() {
|
|
||||||
0 | 1 => {
|
|
||||||
// we found an escaped {{ or }}, so add
|
|
||||||
// everything up to the first char to the buffer
|
|
||||||
// then skip the second one.
|
|
||||||
buf += &remaining[..m.start() + BRACE_LEN];
|
|
||||||
remaining = &remaining[m.end()..];
|
|
||||||
}
|
|
||||||
id if !remaining[m.end()..].starts_with('}') => {
|
|
||||||
buf += &remaining[..m.start()];
|
|
||||||
if !buf.is_empty() {
|
|
||||||
tokens.push(Token::Text(std::mem::take(&mut buf)));
|
|
||||||
}
|
|
||||||
tokens.push(token_from_pattern_id(id));
|
|
||||||
remaining = &remaining[m.end()..];
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// We got a normal pattern, but the final "}"
|
|
||||||
// is escaped, so add up to that to the buffer, then
|
|
||||||
// skip the final }
|
|
||||||
buf += &remaining[..m.end()];
|
|
||||||
remaining = &remaining[m.end() + BRACE_LEN..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add the rest of the string to the buffer, and add the final buffer to the tokens
|
|
||||||
if !remaining.is_empty() {
|
|
||||||
buf += remaining;
|
|
||||||
}
|
|
||||||
if tokens.is_empty() {
|
|
||||||
// No placeholders were found, so just return the text
|
|
||||||
return ArgumentTemplate::Text(buf);
|
|
||||||
}
|
|
||||||
// Add final text segment
|
|
||||||
if !buf.is_empty() {
|
|
||||||
tokens.push(Token::Text(buf));
|
|
||||||
}
|
|
||||||
debug_assert!(!tokens.is_empty());
|
|
||||||
ArgumentTemplate::Tokens(tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn token_from_pattern_id(id: u32) -> Token {
|
|
||||||
use Token::*;
|
|
||||||
match id {
|
|
||||||
2 => Placeholder,
|
|
||||||
3 => Basename,
|
|
||||||
4 => Parent,
|
|
||||||
5 => NoExt,
|
|
||||||
6 => BasenameNoExt,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -128,13 +128,11 @@ pub fn strip_current_dir(path: &Path) -> &Path {
|
||||||
pub fn default_path_separator() -> Option<String> {
|
pub fn default_path_separator() -> Option<String> {
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
let msystem = env::var("MSYSTEM").ok()?;
|
let msystem = env::var("MSYSTEM").ok()?;
|
||||||
match msystem.as_str() {
|
if !msystem.is_empty() {
|
||||||
"MINGW64" | "MINGW32" | "MSYS" => Some("/".to_owned()),
|
return Some("/".to_owned());
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -31,6 +31,10 @@ impl TimeFilter {
|
||||||
.and_local_timezone(Local)
|
.and_local_timezone(Local)
|
||||||
.latest()
|
.latest()
|
||||||
})
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
let timestamp_secs = s.strip_prefix('@')?.parse().ok()?;
|
||||||
|
DateTime::from_timestamp(timestamp_secs, 0).map(Into::into)
|
||||||
|
})
|
||||||
.map(|dt| dt.into())
|
.map(|dt| dt.into())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -135,5 +139,32 @@ mod tests {
|
||||||
assert!(!TimeFilter::after(&ref_time, t10s_before)
|
assert!(!TimeFilter::after(&ref_time, t10s_before)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.applies_to(&t1m_ago));
|
.applies_to(&t1m_ago));
|
||||||
|
|
||||||
|
let ref_timestamp = 1707723412u64; // Mon Feb 12 07:36:52 UTC 2024
|
||||||
|
let ref_time = DateTime::parse_from_rfc3339("2024-02-12T07:36:52+00:00")
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
let t1m_ago = ref_time - Duration::from_secs(60);
|
||||||
|
let t1s_later = ref_time + Duration::from_secs(1);
|
||||||
|
// Timestamp only supported via '@' prefix
|
||||||
|
assert!(TimeFilter::before(&ref_time, &ref_timestamp.to_string()).is_none());
|
||||||
|
assert!(
|
||||||
|
TimeFilter::before(&ref_time, &format!("@{}", ref_timestamp))
|
||||||
|
.unwrap()
|
||||||
|
.applies_to(&t1m_ago)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!TimeFilter::before(&ref_time, &format!("@{}", ref_timestamp))
|
||||||
|
.unwrap()
|
||||||
|
.applies_to(&t1s_later)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!TimeFilter::after(&ref_time, &format!("@{}", ref_timestamp))
|
||||||
|
.unwrap()
|
||||||
|
.applies_to(&t1m_ago)
|
||||||
|
);
|
||||||
|
assert!(TimeFilter::after(&ref_time, &format!("@{}", ref_timestamp))
|
||||||
|
.unwrap()
|
||||||
|
.applies_to(&t1s_later));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,10 +34,10 @@ pub fn dirname(path: &Path) -> OsString {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod path_tests {
|
mod path_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::path::MAIN_SEPARATOR;
|
use std::path::MAIN_SEPARATOR_STR;
|
||||||
|
|
||||||
fn correct(input: &str) -> String {
|
fn correct(input: &str) -> String {
|
||||||
input.replace('/', &MAIN_SEPARATOR.to_string())
|
input.replace('/', MAIN_SEPARATOR_STR)
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! func_tests {
|
macro_rules! func_tests {
|
|
@ -0,0 +1,281 @@
|
||||||
|
mod input;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::path::{Component, Path, Prefix};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use aho_corasick::AhoCorasick;
|
||||||
|
|
||||||
|
use self::input::{basename, dirname, remove_extension};
|
||||||
|
|
||||||
|
/// Designates what should be written to a buffer
|
||||||
|
///
|
||||||
|
/// Each `Token` contains either text, or a placeholder variant, which will be used to generate
|
||||||
|
/// commands after all tokens for a given command template have been collected.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Token {
|
||||||
|
Placeholder,
|
||||||
|
Basename,
|
||||||
|
Parent,
|
||||||
|
NoExt,
|
||||||
|
BasenameNoExt,
|
||||||
|
Text(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Token {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Token::Placeholder => f.write_str("{}")?,
|
||||||
|
Token::Basename => f.write_str("{/}")?,
|
||||||
|
Token::Parent => f.write_str("{//}")?,
|
||||||
|
Token::NoExt => f.write_str("{.}")?,
|
||||||
|
Token::BasenameNoExt => f.write_str("{/.}")?,
|
||||||
|
Token::Text(ref string) => f.write_str(string)?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A parsed format string
|
||||||
|
///
|
||||||
|
/// This is either a collection of `Token`s including at least one placeholder variant,
|
||||||
|
/// or a fixed text.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum FormatTemplate {
|
||||||
|
Tokens(Vec<Token>),
|
||||||
|
Text(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
static PLACEHOLDERS: OnceLock<AhoCorasick> = OnceLock::new();
|
||||||
|
|
||||||
|
impl FormatTemplate {
|
||||||
|
pub fn has_tokens(&self) -> bool {
|
||||||
|
matches!(self, FormatTemplate::Tokens(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(fmt: &str) -> Self {
|
||||||
|
// NOTE: we assume that { and } have the same length
|
||||||
|
const BRACE_LEN: usize = '{'.len_utf8();
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
let mut remaining = fmt;
|
||||||
|
let mut buf = String::new();
|
||||||
|
let placeholders = PLACEHOLDERS.get_or_init(|| {
|
||||||
|
AhoCorasick::new(["{{", "}}", "{}", "{/}", "{//}", "{.}", "{/.}"]).unwrap()
|
||||||
|
});
|
||||||
|
while let Some(m) = placeholders.find(remaining) {
|
||||||
|
match m.pattern().as_u32() {
|
||||||
|
0 | 1 => {
|
||||||
|
// we found an escaped {{ or }}, so add
|
||||||
|
// everything up to the first char to the buffer
|
||||||
|
// then skip the second one.
|
||||||
|
buf += &remaining[..m.start() + BRACE_LEN];
|
||||||
|
remaining = &remaining[m.end()..];
|
||||||
|
}
|
||||||
|
id if !remaining[m.end()..].starts_with('}') => {
|
||||||
|
buf += &remaining[..m.start()];
|
||||||
|
if !buf.is_empty() {
|
||||||
|
tokens.push(Token::Text(std::mem::take(&mut buf)));
|
||||||
|
}
|
||||||
|
tokens.push(token_from_pattern_id(id));
|
||||||
|
remaining = &remaining[m.end()..];
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// We got a normal pattern, but the final "}"
|
||||||
|
// is escaped, so add up to that to the buffer, then
|
||||||
|
// skip the final }
|
||||||
|
buf += &remaining[..m.end()];
|
||||||
|
remaining = &remaining[m.end() + BRACE_LEN..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the rest of the string to the buffer, and add the final buffer to the tokens
|
||||||
|
if !remaining.is_empty() {
|
||||||
|
buf += remaining;
|
||||||
|
}
|
||||||
|
if tokens.is_empty() {
|
||||||
|
// No placeholders were found, so just return the text
|
||||||
|
return FormatTemplate::Text(buf);
|
||||||
|
}
|
||||||
|
// Add final text segment
|
||||||
|
if !buf.is_empty() {
|
||||||
|
tokens.push(Token::Text(buf));
|
||||||
|
}
|
||||||
|
debug_assert!(!tokens.is_empty());
|
||||||
|
FormatTemplate::Tokens(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a result string from this template. If path_separator is Some, then it will replace
|
||||||
|
/// the path separator in all placeholder tokens. Fixed text and tokens are not affected by
|
||||||
|
/// path separator substitution.
|
||||||
|
pub fn generate(&self, path: impl AsRef<Path>, path_separator: Option<&str>) -> OsString {
|
||||||
|
use Token::*;
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
match *self {
|
||||||
|
Self::Tokens(ref tokens) => {
|
||||||
|
let mut s = OsString::new();
|
||||||
|
for token in tokens {
|
||||||
|
match token {
|
||||||
|
Basename => s.push(Self::replace_separator(basename(path), path_separator)),
|
||||||
|
BasenameNoExt => s.push(Self::replace_separator(
|
||||||
|
&remove_extension(basename(path).as_ref()),
|
||||||
|
path_separator,
|
||||||
|
)),
|
||||||
|
NoExt => s.push(Self::replace_separator(
|
||||||
|
&remove_extension(path),
|
||||||
|
path_separator,
|
||||||
|
)),
|
||||||
|
Parent => s.push(Self::replace_separator(&dirname(path), path_separator)),
|
||||||
|
Placeholder => {
|
||||||
|
s.push(Self::replace_separator(path.as_ref(), path_separator))
|
||||||
|
}
|
||||||
|
Text(ref string) => s.push(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
Self::Text(ref text) => OsString::from(text),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the path separator in the input with the custom separator string. If path_separator
|
||||||
|
/// is None, simply return a borrowed Cow<OsStr> of the input. Otherwise, the input is
|
||||||
|
/// interpreted as a Path and its components are iterated through and re-joined into a new
|
||||||
|
/// OsString.
|
||||||
|
fn replace_separator<'a>(path: &'a OsStr, path_separator: Option<&str>) -> Cow<'a, OsStr> {
|
||||||
|
// fast-path - no replacement necessary
|
||||||
|
if path_separator.is_none() {
|
||||||
|
return Cow::Borrowed(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_separator = path_separator.unwrap();
|
||||||
|
let mut out = OsString::with_capacity(path.len());
|
||||||
|
let mut components = Path::new(path).components().peekable();
|
||||||
|
|
||||||
|
while let Some(comp) = components.next() {
|
||||||
|
match comp {
|
||||||
|
// Absolute paths on Windows are tricky. A Prefix component is usually a drive
|
||||||
|
// letter or UNC path, and is usually followed by RootDir. There are also
|
||||||
|
// "verbatim" prefixes beginning with "\\?\" that skip normalization. We choose to
|
||||||
|
// ignore verbatim path prefixes here because they're very rare, might be
|
||||||
|
// impossible to reach here, and there's no good way to deal with them. If users
|
||||||
|
// are doing something advanced involving verbatim windows paths, they can do their
|
||||||
|
// own output filtering with a tool like sed.
|
||||||
|
Component::Prefix(prefix) => {
|
||||||
|
if let Prefix::UNC(server, share) = prefix.kind() {
|
||||||
|
// Prefix::UNC is a parsed version of '\\server\share'
|
||||||
|
out.push(path_separator);
|
||||||
|
out.push(path_separator);
|
||||||
|
out.push(server);
|
||||||
|
out.push(path_separator);
|
||||||
|
out.push(share);
|
||||||
|
} else {
|
||||||
|
// All other Windows prefix types are rendered as-is. This results in e.g. "C:" for
|
||||||
|
// drive letters. DeviceNS and Verbatim* prefixes won't have backslashes converted,
|
||||||
|
// but they're not returned by directories fd can search anyway so we don't worry
|
||||||
|
// about them.
|
||||||
|
out.push(comp.as_os_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root directory is always replaced with the custom separator.
|
||||||
|
Component::RootDir => out.push(path_separator),
|
||||||
|
|
||||||
|
// Everything else is joined normally, with a trailing separator if we're not last
|
||||||
|
_ => {
|
||||||
|
out.push(comp.as_os_str());
|
||||||
|
if components.peek().is_some() {
|
||||||
|
out.push(path_separator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cow::Owned(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the id from an aho-corasick match to the
|
||||||
|
// appropriate token
|
||||||
|
fn token_from_pattern_id(id: u32) -> Token {
|
||||||
|
use Token::*;
|
||||||
|
match id {
|
||||||
|
2 => Placeholder,
|
||||||
|
3 => Basename,
|
||||||
|
4 => Parent,
|
||||||
|
5 => NoExt,
|
||||||
|
6 => BasenameNoExt,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod fmt_tests {
|
||||||
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_no_placeholders() {
|
||||||
|
let templ = FormatTemplate::parse("This string has no placeholders");
|
||||||
|
assert_eq!(
|
||||||
|
templ,
|
||||||
|
FormatTemplate::Text("This string has no placeholders".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_only_brace_escapes() {
|
||||||
|
let templ = FormatTemplate::parse("This string only has escapes like {{ and }}");
|
||||||
|
assert_eq!(
|
||||||
|
templ,
|
||||||
|
FormatTemplate::Text("This string only has escapes like { and }".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_placeholders() {
|
||||||
|
use Token::*;
|
||||||
|
|
||||||
|
let templ = FormatTemplate::parse(
|
||||||
|
"{{path={} \
|
||||||
|
basename={/} \
|
||||||
|
parent={//} \
|
||||||
|
noExt={.} \
|
||||||
|
basenameNoExt={/.} \
|
||||||
|
}}",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
templ,
|
||||||
|
FormatTemplate::Tokens(vec![
|
||||||
|
Text("{path=".into()),
|
||||||
|
Placeholder,
|
||||||
|
Text(" basename=".into()),
|
||||||
|
Basename,
|
||||||
|
Text(" parent=".into()),
|
||||||
|
Parent,
|
||||||
|
Text(" noExt=".into()),
|
||||||
|
NoExt,
|
||||||
|
Text(" basenameNoExt=".into()),
|
||||||
|
BasenameNoExt,
|
||||||
|
Text(" }".into()),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut path = PathBuf::new();
|
||||||
|
path.push("a");
|
||||||
|
path.push("folder");
|
||||||
|
path.push("file.txt");
|
||||||
|
|
||||||
|
let expanded = templ.generate(&path, Some("/")).into_string().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
expanded,
|
||||||
|
"{path=a/folder/file.txt \
|
||||||
|
basename=file.txt \
|
||||||
|
parent=a/folder \
|
||||||
|
noExt=a/folder/file \
|
||||||
|
basenameNoExt=file }"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
src/main.rs
18
src/main.rs
|
@ -7,6 +7,7 @@ mod exit_codes;
|
||||||
mod filesystem;
|
mod filesystem;
|
||||||
mod filetypes;
|
mod filetypes;
|
||||||
mod filter;
|
mod filter;
|
||||||
|
mod fmt;
|
||||||
mod output;
|
mod output;
|
||||||
mod regex_helper;
|
mod regex_helper;
|
||||||
mod walk;
|
mod walk;
|
||||||
|
@ -299,6 +300,10 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
|
||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
|
format: opts
|
||||||
|
.format
|
||||||
|
.as_deref()
|
||||||
|
.map(crate::fmt::FormatTemplate::parse),
|
||||||
command: command.map(Arc::new),
|
command: command.map(Arc::new),
|
||||||
batch_size: opts.batch_size,
|
batch_size: opts.batch_size,
|
||||||
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
|
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
|
||||||
|
@ -311,8 +316,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
|
||||||
path_separator,
|
path_separator,
|
||||||
actual_path_separator,
|
actual_path_separator,
|
||||||
max_results: opts.max_results(),
|
max_results: opts.max_results(),
|
||||||
strip_cwd_prefix: (opts.no_search_paths()
|
strip_cwd_prefix: opts.strip_cwd_prefix(|| !(opts.null_separator || has_command)),
|
||||||
&& (opts.strip_cwd_prefix || !(opts.null_separator || has_command))),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,18 +329,22 @@ fn extract_command(opts: &mut Opts, colored_output: bool) -> Result<Option<Comma
|
||||||
if !opts.list_details {
|
if !opts.list_details {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let color_arg = format!("--color={}", opts.color.as_str());
|
|
||||||
|
|
||||||
let res = determine_ls_command(&color_arg, colored_output)
|
let res = determine_ls_command(colored_output)
|
||||||
.map(|cmd| CommandSet::new_batch([cmd]).unwrap());
|
.map(|cmd| CommandSet::new_batch([cmd]).unwrap());
|
||||||
Some(res)
|
Some(res)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&str>> {
|
fn determine_ls_command(colored_output: bool) -> Result<Vec<&'static str>> {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
let gnu_ls = |command_name| {
|
let gnu_ls = |command_name| {
|
||||||
|
let color_arg = if colored_output {
|
||||||
|
"--color=always"
|
||||||
|
} else {
|
||||||
|
"--color=never"
|
||||||
|
};
|
||||||
// Note: we use short options here (instead of --long-options) to support more
|
// Note: we use short options here (instead of --long-options) to support more
|
||||||
// platforms (like BusyBox).
|
// platforms (like BusyBox).
|
||||||
vec![
|
vec![
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::config::Config;
|
||||||
use crate::dir_entry::DirEntry;
|
use crate::dir_entry::DirEntry;
|
||||||
use crate::error::print_error;
|
use crate::error::print_error;
|
||||||
use crate::exit_codes::ExitCode;
|
use crate::exit_codes::ExitCode;
|
||||||
|
use crate::fmt::FormatTemplate;
|
||||||
|
|
||||||
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
|
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
|
||||||
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
|
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
|
||||||
|
@ -14,7 +15,10 @@ fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
|
||||||
|
|
||||||
// TODO: this function is performance critical and can probably be optimized
|
// TODO: this function is performance critical and can probably be optimized
|
||||||
pub fn print_entry<W: Write>(stdout: &mut W, entry: &DirEntry, config: &Config) {
|
pub fn print_entry<W: Write>(stdout: &mut W, entry: &DirEntry, config: &Config) {
|
||||||
let r = if let Some(ref ls_colors) = config.ls_colors {
|
// TODO: use format if supplied
|
||||||
|
let r = if let Some(ref format) = config.format {
|
||||||
|
print_entry_format(stdout, entry, config, format)
|
||||||
|
} else if let Some(ref ls_colors) = config.ls_colors {
|
||||||
print_entry_colorized(stdout, entry, config, ls_colors)
|
print_entry_colorized(stdout, entry, config, ls_colors)
|
||||||
} else {
|
} else {
|
||||||
print_entry_uncolorized(stdout, entry, config)
|
print_entry_uncolorized(stdout, entry, config)
|
||||||
|
@ -54,6 +58,22 @@ fn print_trailing_slash<W: Write>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this function is performance critical and can probably be optimized
|
||||||
|
fn print_entry_format<W: Write>(
|
||||||
|
stdout: &mut W,
|
||||||
|
entry: &DirEntry,
|
||||||
|
config: &Config,
|
||||||
|
format: &FormatTemplate,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let separator = if config.null_separator { "\0" } else { "\n" };
|
||||||
|
let output = format.generate(
|
||||||
|
entry.stripped_path(config),
|
||||||
|
config.path_separator.as_deref(),
|
||||||
|
);
|
||||||
|
// TODO: support writing raw bytes on unix?
|
||||||
|
write!(stdout, "{}{}", output.to_string_lossy(), separator)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: this function is performance critical and can probably be optimized
|
// TODO: this function is performance critical and can probably be optimized
|
||||||
fn print_entry_colorized<W: Write>(
|
fn print_entry_colorized<W: Write>(
|
||||||
stdout: &mut W,
|
stdout: &mut W,
|
||||||
|
|
|
@ -16,7 +16,7 @@ fn hir_has_uppercase_char(hir: &Hir) -> bool {
|
||||||
use regex_syntax::hir::*;
|
use regex_syntax::hir::*;
|
||||||
|
|
||||||
match hir.kind() {
|
match hir.kind() {
|
||||||
HirKind::Literal(Literal(bytes)) => match std::str::from_utf8(&bytes) {
|
HirKind::Literal(Literal(bytes)) => match std::str::from_utf8(bytes) {
|
||||||
Ok(s) => s.chars().any(|c| c.is_uppercase()),
|
Ok(s) => s.chars().any(|c| c.is_uppercase()),
|
||||||
Err(_) => bytes.iter().any(|b| char::from(*b).is_uppercase()),
|
Err(_) => bytes.iter().any(|b| char::from(*b).is_uppercase()),
|
||||||
},
|
},
|
||||||
|
|
12
src/walk.rs
12
src/walk.rs
|
@ -12,7 +12,7 @@ use anyhow::{anyhow, Result};
|
||||||
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, SendError, Sender};
|
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, SendError, Sender};
|
||||||
use etcetera::BaseStrategy;
|
use etcetera::BaseStrategy;
|
||||||
use ignore::overrides::{Override, OverrideBuilder};
|
use ignore::overrides::{Override, OverrideBuilder};
|
||||||
use ignore::{self, WalkBuilder, WalkParallel, WalkState};
|
use ignore::{WalkBuilder, WalkParallel, WalkState};
|
||||||
use regex::bytes::Regex;
|
use regex::bytes::Regex;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
@ -250,7 +250,7 @@ impl<'a, W: Write> ReceiverBuffer<'a, W> {
|
||||||
|
|
||||||
/// Output a path.
|
/// Output a path.
|
||||||
fn print(&mut self, entry: &DirEntry) -> Result<(), ExitCode> {
|
fn print(&mut self, entry: &DirEntry) -> Result<(), ExitCode> {
|
||||||
output::print_entry(&mut self.stdout, entry, &self.config);
|
output::print_entry(&mut self.stdout, entry, self.config);
|
||||||
|
|
||||||
if self.interrupt_flag.load(Ordering::Relaxed) {
|
if self.interrupt_flag.load(Ordering::Relaxed) {
|
||||||
// Ignore any errors on flush, because we're about to exit anyway
|
// Ignore any errors on flush, because we're about to exit anyway
|
||||||
|
@ -334,10 +334,6 @@ impl WorkerState {
|
||||||
.map_err(|e| anyhow!("Malformed exclude pattern: {}", e))?;
|
.map_err(|e| anyhow!("Malformed exclude pattern: {}", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.read_vcsignore {
|
|
||||||
builder.add("!.git/").expect("Invalid exclude pattern");
|
|
||||||
}
|
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.build()
|
.build()
|
||||||
.map_err(|_| anyhow!("Mismatch in exclude patterns"))
|
.map_err(|_| anyhow!("Mismatch in exclude patterns"))
|
||||||
|
@ -413,7 +409,7 @@ impl WorkerState {
|
||||||
// This will be set to `Some` if the `--exec` argument was supplied.
|
// This will be set to `Some` if the `--exec` argument was supplied.
|
||||||
if let Some(ref cmd) = config.command {
|
if let Some(ref cmd) = config.command {
|
||||||
if cmd.in_batch_mode() {
|
if cmd.in_batch_mode() {
|
||||||
exec::batch(rx.into_iter().flatten(), cmd, &config)
|
exec::batch(rx.into_iter().flatten(), cmd, config)
|
||||||
} else {
|
} else {
|
||||||
let out_perm = Mutex::new(());
|
let out_perm = Mutex::new(());
|
||||||
|
|
||||||
|
@ -426,7 +422,7 @@ impl WorkerState {
|
||||||
|
|
||||||
// Spawn a job thread that will listen for and execute inputs.
|
// Spawn a job thread that will listen for and execute inputs.
|
||||||
let handle = scope
|
let handle = scope
|
||||||
.spawn(|| exec::job(rx.into_iter().flatten(), cmd, &out_perm, &config));
|
.spawn(|| exec::job(rx.into_iter().flatten(), cmd, &out_perm, config));
|
||||||
|
|
||||||
// Push the handle of the spawned thread into the vector for later joining.
|
// Push the handle of the spawned thread into the vector for later joining.
|
||||||
handles.push(handle);
|
handles.push(handle);
|
||||||
|
|
|
@ -129,7 +129,7 @@ fn normalize_output(s: &str, trim_start: bool, normalize_line: bool) -> String {
|
||||||
.lines()
|
.lines()
|
||||||
.map(|line| {
|
.map(|line| {
|
||||||
let line = if trim_start { line.trim_start() } else { line };
|
let line = if trim_start { line.trim_start() } else { line };
|
||||||
let line = line.replace('/', &std::path::MAIN_SEPARATOR.to_string());
|
let line = line.replace('/', std::path::MAIN_SEPARATOR_STR);
|
||||||
if normalize_line {
|
if normalize_line {
|
||||||
let mut words: Vec<_> = line.split_whitespace().collect();
|
let mut words: Vec<_> = line.split_whitespace().collect();
|
||||||
words.sort_unstable();
|
words.sort_unstable();
|
||||||
|
|
|
@ -1311,7 +1311,8 @@ fn test_type_executable() {
|
||||||
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||||
|
|
||||||
fs::OpenOptions::new()
|
fs::OpenOptions::new()
|
||||||
.create(true)
|
.create_new(true)
|
||||||
|
.truncate(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.mode(0o777)
|
.mode(0o777)
|
||||||
.open(te.test_root().join("executable-file.sh"))
|
.open(te.test_root().join("executable-file.sh"))
|
||||||
|
@ -1319,6 +1320,7 @@ fn test_type_executable() {
|
||||||
|
|
||||||
fs::OpenOptions::new()
|
fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.mode(0o645)
|
.mode(0o645)
|
||||||
.open(te.test_root().join("not-user-executable-file.sh"))
|
.open(te.test_root().join("not-user-executable-file.sh"))
|
||||||
|
@ -1622,6 +1624,66 @@ fn test_excludes() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format() {
|
||||||
|
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["--format", "path={}", "--path-separator=/"],
|
||||||
|
"path=a.foo
|
||||||
|
path=e1 e2
|
||||||
|
path=one
|
||||||
|
path=one/b.foo
|
||||||
|
path=one/two
|
||||||
|
path=one/two/C.Foo2
|
||||||
|
path=one/two/c.foo
|
||||||
|
path=one/two/three
|
||||||
|
path=one/two/three/d.foo
|
||||||
|
path=one/two/three/directory_foo
|
||||||
|
path=symlink",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--format", "noExt={.}", "--path-separator=/"],
|
||||||
|
"noExt=a
|
||||||
|
noExt=one/b
|
||||||
|
noExt=one/two/C
|
||||||
|
noExt=one/two/c
|
||||||
|
noExt=one/two/three/d
|
||||||
|
noExt=one/two/three/directory_foo",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--format", "basename={/}", "--path-separator=/"],
|
||||||
|
"basename=a.foo
|
||||||
|
basename=b.foo
|
||||||
|
basename=C.Foo2
|
||||||
|
basename=c.foo
|
||||||
|
basename=d.foo
|
||||||
|
basename=directory_foo",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--format", "name={/.}", "--path-separator=/"],
|
||||||
|
"name=a
|
||||||
|
name=b
|
||||||
|
name=C
|
||||||
|
name=c
|
||||||
|
name=d
|
||||||
|
name=directory_foo",
|
||||||
|
);
|
||||||
|
|
||||||
|
te.assert_output(
|
||||||
|
&["foo", "--format", "parent={//}", "--path-separator=/"],
|
||||||
|
"parent=.
|
||||||
|
parent=one
|
||||||
|
parent=one/two
|
||||||
|
parent=one/two
|
||||||
|
parent=one/two/three
|
||||||
|
parent=one/two/three",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Shell script execution (--exec)
|
/// Shell script execution (--exec)
|
||||||
#[test]
|
#[test]
|
||||||
fn test_exec() {
|
fn test_exec() {
|
||||||
|
@ -2571,7 +2633,14 @@ fn test_git_dir() {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
te.assert_output(&["--hidden", "foo"], "");
|
te.assert_output(
|
||||||
|
&["--hidden", "foo"],
|
||||||
|
".git/one/foo.a
|
||||||
|
.git/.foo
|
||||||
|
.git/a.foo
|
||||||
|
other_dir/.git/foo1
|
||||||
|
nested/dir/.git/foo2",
|
||||||
|
);
|
||||||
te.assert_output(&["--no-ignore", "foo"], "");
|
te.assert_output(&["--no-ignore", "foo"], "");
|
||||||
te.assert_output(
|
te.assert_output(
|
||||||
&["--hidden", "--no-ignore", "foo"],
|
&["--hidden", "--no-ignore", "foo"],
|
||||||
|
@ -2590,3 +2659,16 @@ fn test_git_dir() {
|
||||||
nested/dir/.git/foo2",
|
nested/dir/.git/foo2",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gitignore_parent() {
|
||||||
|
let te = TestEnv::new(&["sub"], &[".abc", "sub/.abc"]);
|
||||||
|
|
||||||
|
fs::File::create(te.test_root().join(".gitignore"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(b".abc\n")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
te.assert_output_subdirectory("sub", &["--hidden"], "");
|
||||||
|
te.assert_output_subdirectory("sub", &["--hidden", "--search-path", "."], "");
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue