mirror of
https://github.com/sharkdp/fd.git
synced 2026-08-31 09:21:03 +08:00
Compare commits
87 Commits
abort-on-panic
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| cdea7f5633 | |||
| fe0e455fee | |||
| a89e394a0d | |||
| 4f81778774 | |||
| 711ba7fe67 | |||
| c276ac79f4 | |||
| ee20f426dd | |||
| 0b10bf2665 | |||
| aaecbc0a58 | |||
| cb4069ec55 | |||
| 69a25907ab | |||
| 868c3a34e2 | |||
| ee195f4dd6 | |||
| 0f1f9676dc | |||
| 2bb2e2b954 | |||
| a62a59dafe | |||
| 7639bced74 | |||
| 51e44c0626 | |||
| 370174528f | |||
| 30fd75b22b | |||
| 5becb9d52c | |||
| fb0426fed1 | |||
| 41532d114e | |||
| 81efb5028c | |||
| ca51233d27 | |||
| 3381ab89d7 | |||
| 5095e95a95 | |||
| 5a093fdfd6 | |||
| acc64aab12 | |||
| eead1886cd | |||
| fed92120af | |||
| 1bfeea237a | |||
| 369a71a4c5 | |||
| cd755e284c | |||
| 372584e760 | |||
| 5a5852e15b | |||
| dda13cccf6 | |||
| aa41f342b5 | |||
| cffed27a2e | |||
| 6b5d1ac9d6 | |||
| 7d93f3adc7 | |||
| fb1486f210 | |||
| 0c9c101e21 | |||
| 8ff051e801 | |||
| daefe07614 | |||
| cade0b5e76 | |||
| c73e5e58a4 | |||
| f898e95db5 | |||
| b12606438e | |||
| 95dc6454d5 | |||
| 61c5399fa4 | |||
| 126be98699 | |||
| 77629639ff | |||
| 3ae9c9567e | |||
| 8965502e8a | |||
| 86c1e7d30d | |||
| 6aac84e4ae | |||
| a8a77d2162 | |||
| e8730e6ca3 | |||
| 3b3e328b5c | |||
| fccbe9b1ee | |||
| b5ae476fb1 | |||
| 094446004d | |||
| 1161471b7e | |||
| 25461e5ce1 | |||
| f7d4d717c2 | |||
| 21facf8832 | |||
| 8e80bbe721 | |||
| b25ca81375 | |||
| 5ac75050d1 | |||
| f72512ff43 | |||
| 75527dd9c6 | |||
| 86564cd00d | |||
| b955593ac6 | |||
| 1d4fa16433 | |||
| 66be8485bc | |||
| 7d18db23a7 | |||
| 9f938bea1e | |||
| e60582f838 | |||
| c96398c1bb | |||
| a00259bf79 | |||
| 9d536e1a40 | |||
| aeecb5d8c0 | |||
| 825f308931 | |||
| 63a7d85d26 | |||
| 92c54d37c0 | |||
| 160d00f8fe |
+11
-43
@@ -21,7 +21,7 @@ jobs:
|
||||
name: Extract crate metadata
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Extract crate information
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: cargo fmt -- --check
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: cargo clippy --all-targets --all-features -- -Dwarnings
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
needs: crate_metadata
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
name: ${{ needs.crate_metadata.outputs.name }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -205,44 +205,12 @@ jobs:
|
||||
- name: Create tarball
|
||||
id: package
|
||||
shell: bash
|
||||
run: bash scripts/create-archive.sh
|
||||
env:
|
||||
BIN_PATH: ${{ steps.bin.outputs.BIN_PATH }}
|
||||
version: ${{ needs.crate_metadata.outputs.version }}
|
||||
run: |
|
||||
PKG_suffix=".tar.gz"
|
||||
case ${target} in
|
||||
*-pc-windows-*) PKG_suffix=".zip" ;;
|
||||
esac
|
||||
PKG_BASENAME=${name}-v${version}-${target}
|
||||
PKG_NAME=${PKG_BASENAME}${PKG_suffix}
|
||||
echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
PKG_STAGING="${CICD_INTERMEDIATES_DIR}/package"
|
||||
ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/"
|
||||
mkdir -p "${ARCHIVE_DIR}"
|
||||
|
||||
# Binary
|
||||
cp "${BIN_PATH}" "$ARCHIVE_DIR"
|
||||
|
||||
# README, LICENSE and CHANGELOG files
|
||||
cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR"
|
||||
|
||||
# Man page
|
||||
cp "doc/${name}.1" "$ARCHIVE_DIR"
|
||||
|
||||
# Autocompletion files
|
||||
cp -r autocomplete "${ARCHIVE_DIR}"
|
||||
|
||||
# base compressed package
|
||||
pushd "${PKG_STAGING}/" >/dev/null
|
||||
case ${target} in
|
||||
*-pc-windows-*) 7z -y a "${PKG_NAME}" "${PKG_BASENAME}"/* | tail -2 ;;
|
||||
*) tar czf "${PKG_NAME}" "${PKG_BASENAME}"/* ;;
|
||||
esac;
|
||||
popd >/dev/null
|
||||
|
||||
# Let subsequent steps know where to find the compressed package
|
||||
echo "PKG_PATH=${PKG_STAGING}/${PKG_NAME}" >> $GITHUB_OUTPUT
|
||||
TARGET: ${{ env.target }}
|
||||
ARCHIVE_NAME: ${{ env.name }}-v${{ needs.crate_metadata.outputs.version }}-${{ env.target }}
|
||||
PKG_STAGING: "${{ env.CICD_INTERMEDIATES_DIR }}/package"
|
||||
|
||||
- name: Create Debian package
|
||||
id: debian-package
|
||||
@@ -279,7 +247,7 @@ jobs:
|
||||
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Attest artifacts"
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4
|
||||
uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4
|
||||
if: steps.is-release.outputs.IS_RELEASE
|
||||
with:
|
||||
subject-path: |
|
||||
@@ -287,7 +255,7 @@ jobs:
|
||||
${{ steps.debian-package.outputs.DPKG_PATH }}
|
||||
|
||||
- name: Publish archives and packages
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
if: steps.is-release.outputs.IS_RELEASE
|
||||
with:
|
||||
files: |
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
target/
|
||||
/autocomplete/
|
||||
/package/
|
||||
**/*.rs.bk
|
||||
|
||||
+15
-1
@@ -1,13 +1,27 @@
|
||||
# Unreleased
|
||||
# Upcoming Release
|
||||
|
||||
## Features
|
||||
-
|
||||
|
||||
## Bugfixes
|
||||
-
|
||||
|
||||
# 10.5.0
|
||||
|
||||
## Features
|
||||
- Add `--ignore-parent` option to override `--no-ignore-parent`, see #1958 (@tmchow)
|
||||
- Add `--exact` option to match the entire filename exactly (literal, non-substring).
|
||||
|
||||
## Bugfixes
|
||||
- Sanitize control characters and bidirectional override characters in filenames
|
||||
when output goes to a terminal, to prevent terminal escape-sequence injection.
|
||||
Also reject a placeholder as the executable for `--exec-batch`, while still
|
||||
allowing it for `--exec`.
|
||||
- Handle invalid working directories gracefully when using `--full-path`, see #1900 (@Xavrir).
|
||||
- Fire the "search pattern contains a path separator" diagnostic for any pattern containing `/`, not just patterns that happen to name an existing directory. Preserves the legacy Windows behaviour that also flags native `\` separators when the pattern resolves to a real directory. See #1873.
|
||||
- Also fire the "search pattern contains a path separator" diagnostic for `--and` patterns, not only the primary positional pattern. `--and` patterns are matched against the file name just like the primary pattern, so a path separator in them silently returned zero results. See #1873.
|
||||
- Fix bug where passing "-" as a directory argument didn't actually search that directory, see #849 (@Sean-Kenneth-Doherty).
|
||||
- Fix panic when `--changed-before`/`--changed-within` is given an out-of-range `@` Unix timestamp; the value is now rejected gracefully, see #2081 (@nikolauspschuetz).
|
||||
|
||||
# 10.4.2
|
||||
|
||||
|
||||
Generated
+110
-63
@@ -63,9 +63,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "argmax"
|
||||
@@ -156,9 +156,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.6.2"
|
||||
version = "4.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb"
|
||||
checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
@@ -189,9 +189,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
version = "0.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
@@ -228,10 +228,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"nix 0.31.2",
|
||||
"nix 0.31.3",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"defmt-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-macros"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b"
|
||||
dependencies = [
|
||||
"defmt-parser",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-parser"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -295,7 +327,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fd-find"
|
||||
version = "10.4.2"
|
||||
version = "10.5.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -313,7 +345,7 @@ dependencies = [
|
||||
"jiff",
|
||||
"libc",
|
||||
"lscolors",
|
||||
"nix 0.31.2",
|
||||
"nix 0.31.3",
|
||||
"normpath",
|
||||
"nu-ansi-term",
|
||||
"regex",
|
||||
@@ -325,13 +357,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -361,9 +392,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.18"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||
checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
@@ -401,9 +432,9 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.25"
|
||||
version = "0.4.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
|
||||
checksum = "7f8a7b8211e695a1d0cd91cace480d4d0bd57667ab10277cc412c5f7f4884f83"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
@@ -441,24 +472,25 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.24"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d"
|
||||
checksum = "34f877a98676d2fb664698d74cc6a51ce6c484ce8c770f05d0108ec9090aeb46"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"jiff-static",
|
||||
"jiff-tzdb-platform",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.24"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7"
|
||||
checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -488,21 +520,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall",
|
||||
]
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
@@ -546,9 +566,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.2"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
@@ -558,9 +578,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "normpath"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b"
|
||||
checksum = "b9985ef7269fa99f3b12437bb698381da2428743ab90f20393f399fa14cab21a"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -601,12 +621,6 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
@@ -632,6 +646,28 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -656,20 +692,11 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
version = "1.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -690,9 +717,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
@@ -844,10 +871,30 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-sys"
|
||||
version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7"
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-sys"
|
||||
version = "0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a2825c78386b4ae0314074867860ba9577875de945f05992c38815cbec327f0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -855,9 +902,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemallocator"
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a"
|
||||
checksum = "249f09e49ab1609436f34c776e84231bead18d6a955f119f939bdc1d847561bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"tikv-jemalloc-sys",
|
||||
|
||||
+6
-6
@@ -16,7 +16,7 @@ license = "MIT OR Apache-2.0"
|
||||
name = "fd-find"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/sharkdp/fd"
|
||||
version = "10.4.2"
|
||||
version = "10.5.0"
|
||||
edition= "2024"
|
||||
rust-version = "1.90.0"
|
||||
|
||||
@@ -41,11 +41,11 @@ ctrlc = "3.5"
|
||||
globset = "0.4"
|
||||
anyhow = "1.0"
|
||||
etcetera = "0.11"
|
||||
normpath = "1.1.1"
|
||||
normpath = "1.5.1"
|
||||
crossbeam-channel = "0.5.15"
|
||||
clap_complete = {version = "4.6.2", optional = true}
|
||||
clap_complete = {version = "4.6.5", optional = true}
|
||||
faccess = "0.2.4"
|
||||
jiff = "0.2.24"
|
||||
jiff = "0.2.27"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.6.1"
|
||||
@@ -68,7 +68,7 @@ libc = "0.2"
|
||||
# This has to be kept in sync with src/main.rs where the allocator for
|
||||
# the program is set.
|
||||
[target.'cfg(all(not(windows), not(target_os = "android"), not(target_os = "macos"), not(target_os = "freebsd"), not(target_os = "openbsd"), not(target_os = "illumos"), not(all(target_env = "musl", target_pointer_width = "32")), not(target_arch = "riscv64")))'.dependencies]
|
||||
tikv-jemallocator = {version = "0.6.0", optional = true}
|
||||
tikv-jemallocator = {version = "0.7.0", optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
diff = "0.1"
|
||||
@@ -95,7 +95,7 @@ codegen-units = 1
|
||||
use-jemalloc = ["tikv-jemallocator"]
|
||||
completions = ["clap_complete"]
|
||||
base = ["use-jemalloc"]
|
||||
default = ["use-jemalloc", "completions"]
|
||||
default = ["completions"]
|
||||
|
||||
[package.metadata.binstall]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }.{ archive-format }"
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
PROFILE=release
|
||||
PROFILE?=release
|
||||
EXE=target/$(PROFILE)/fd
|
||||
prefix=/usr/local
|
||||
bindir=$(prefix)/bin
|
||||
datadir=$(prefix)/share
|
||||
exe_name=fd
|
||||
ifdef VERSION
|
||||
ARCHIVE_NAME = fd-v$(VERSION)
|
||||
else
|
||||
ARCHIVE_NAME = fd
|
||||
endif
|
||||
export ARCHIVE_NAME
|
||||
archive_path=package/$(ARCHIVE_NAME).tar.gz
|
||||
|
||||
$(EXE): Cargo.toml src/**/*.rs
|
||||
cargo build --profile $(PROFILE) --locked
|
||||
@@ -41,6 +48,11 @@ autocomplete/fdfind.fish: contrib/completion/fdfind.fish
|
||||
$(comp_dir)
|
||||
cp $< $@
|
||||
|
||||
archive: $(archive_path)
|
||||
|
||||
$(archive_path): completions $(EXE)
|
||||
bash scripts/create-archive.sh
|
||||
|
||||
install: $(EXE) completions
|
||||
install -Dm755 $(EXE) $(DESTDIR)$(bindir)/fd
|
||||
install -Dm644 autocomplete/fd.bash $(DESTDIR)/$(datadir)/bash-completion/completions/$(exe_name)
|
||||
|
||||
@@ -147,7 +147,7 @@ By default, *fd* only matches the filename of each file. However, using the `--f
|
||||
you can match against the full path.
|
||||
|
||||
```bash
|
||||
> fd -p -g '**/.git/config'
|
||||
> fd -u -p -g '**/.git/config'
|
||||
> fd -p '.*/lesson-\d+/[a-z]+.(jpg|png)'
|
||||
```
|
||||
|
||||
@@ -212,7 +212,7 @@ See below for more details on the placeholder syntax.
|
||||
The terminal output of commands run from parallel threads using `-x` will not be interlaced or garbled,
|
||||
so `fd -x` can be used to rudimentarily parallelize a task run over many files.
|
||||
An example of this is calculating the checksum of each individual file within a directory.
|
||||
```
|
||||
``` bash
|
||||
fd -tf -x md5sum > file_checksums.txt
|
||||
```
|
||||
|
||||
@@ -328,7 +328,7 @@ Options:
|
||||
-t, --type <filetype> Filter by type: file (f), directory (d/dir), symlink (l),
|
||||
executable (x), empty (e), socket (s), pipe (p), char-device
|
||||
(c), block-device (b)
|
||||
-e, --extension <ext> Filter by file extension
|
||||
-e, --extension <ext> Filter by extension
|
||||
-S, --size <size> Limit results based on the size of files
|
||||
--changed-within <date|dur> Filter by file modification time (newer than)
|
||||
--changed-before <date|dur> Filter by file modification time (older than)
|
||||
@@ -349,7 +349,7 @@ Note that options can be given after the pattern and/or path as well.
|
||||
|
||||
## Benchmark
|
||||
|
||||
Let's search my home folder for files that end in `[0-9].jpg`. It contains ~750.000
|
||||
Let's search my home folder for files that end in `[0-9].jpg`. It contains ~750,000
|
||||
subdirectories and about a 4 million files. For averaging and statistical analysis, I'm using
|
||||
[hyperfine](https://github.com/sharkdp/hyperfine). The following benchmarks are performed
|
||||
with a "warm"/pre-filled disk-cache (results for a "cold" disk-cache show the same trends).
|
||||
@@ -723,7 +723,7 @@ With Rust's package manager [cargo](https://github.com/rust-lang/cargo), you can
|
||||
```
|
||||
cargo install fd-find
|
||||
```
|
||||
Note that rust version *1.77.2* or later is required.
|
||||
Note that rust version *1.90.0* or later is required.
|
||||
|
||||
`make` is also needed for the build.
|
||||
|
||||
|
||||
@@ -257,9 +257,11 @@ Examples:
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-e, \-\-extension " ext
|
||||
Filter search results by file extension
|
||||
Filter search results by extension
|
||||
.IR ext .
|
||||
This option can be used repeatedly to allow for multiple possible file extensions.
|
||||
This option can be used repeatedly to allow for multiple possible extensions.
|
||||
By default, extension filters apply to both files and directories. Use
|
||||
\fB\-\-type\fR to restrict the search to specific types.
|
||||
|
||||
If you want to search for files without extension, you can use the regex '^[^.]+$'
|
||||
as a normal search pattern.
|
||||
|
||||
Vendored
+4
-1
@@ -5,6 +5,9 @@ necessary changes for the upcoming release.
|
||||
|
||||
## Version bump
|
||||
|
||||
This section can be done by running `scripts/version-bump.sh` with the new version
|
||||
as an argument.
|
||||
|
||||
- [ ] Create a new branch for the required changes for this release.
|
||||
- [ ] Update version in `Cargo.toml`. Run `cargo build` to update `Cargo.lock`.
|
||||
Make sure to `git add` the `Cargo.lock` changes as well.
|
||||
@@ -21,7 +24,7 @@ necessary changes for the upcoming release.
|
||||
new version).
|
||||
- [ ] Review `-h`, `--help`, and the `man` page.
|
||||
- [ ] Run `fd -h` and copy the output to the *"Command-line options"* section in
|
||||
the README
|
||||
the README. This can be done by running `gawk -i inplace -f scripts/update-help.awk README.md`.
|
||||
- [ ] Push all changes and wait for CI to succeed (before continuing with the
|
||||
next section).
|
||||
- [ ] Optional: manually test the new features and command-line options described
|
||||
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Create a .tar.gz archive of the fd project for distribution
|
||||
#
|
||||
# Expected environment variables:
|
||||
#
|
||||
# TARGET: rust target to publish. Defaults to default rustc target
|
||||
# BIN_PATH: path to fd binary
|
||||
# PKG_STAGING: directory to use for staging files to include in the archive
|
||||
# ARCHIVE_NAME: Name of output file, minus extension, defaults to "fd" with a version appended
|
||||
# if VERSION is specified
|
||||
# VERSION: version to publish
|
||||
# GITHUB_OUTPUT: file to store github output variables in
|
||||
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
TARGET="$(rustc -vV | sed -n 's/host: //p')"
|
||||
fi
|
||||
|
||||
PKG_suffix=".tar.gz"
|
||||
EXE_suffix=""
|
||||
case ${TARGET} in
|
||||
*-pc-windows-*)
|
||||
PKG_suffix=".zip"
|
||||
EXE_suffix=".exe"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z $BIN_PATH ]]; then
|
||||
BIN_PATH=target/${PROFILE:-release}/fd
|
||||
fi
|
||||
|
||||
if [[ -z "$ARCHIVE_NAME" ]]; then
|
||||
ARCHIVE_NAME=fd
|
||||
[[ -n "$VERSION" ]] && ARCHIVE_NAME+="-v$VERSION"
|
||||
fi
|
||||
|
||||
PKG_NAME=${ARCHIVE_NAME}${PKG_suffix}
|
||||
|
||||
staging_dir="${PKG_STAGING:-package}"
|
||||
ARCHIVE_DIR="${staging_dir}/${ARCHIVE_NAME}/"
|
||||
mkdir -p "${ARCHIVE_DIR}"
|
||||
|
||||
# Binary
|
||||
cp "${BIN_PATH}" "$ARCHIVE_DIR"
|
||||
|
||||
# README, LICENSE and CHANGELOG files
|
||||
cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR"
|
||||
|
||||
# Man page
|
||||
cp "doc/fd.1" "$ARCHIVE_DIR"
|
||||
|
||||
# Autocompletion files
|
||||
cp -r autocomplete "${ARCHIVE_DIR}"
|
||||
|
||||
# base compressed package
|
||||
pushd "${staging_dir}/" >/dev/null
|
||||
case ${TARGET} in
|
||||
*-pc-windows-*) 7z -y a "${PKG_NAME}" "${ARCHIVE_NAME}"/* | tail -2 ;;
|
||||
*) tar czf "${PKG_NAME}" "${ARCHIVE_NAME}"/* ;;
|
||||
esac;
|
||||
popd >/dev/null
|
||||
|
||||
if [[ -n "$GITHUB_OUTPUT" ]]; then
|
||||
echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT
|
||||
# Let subsequent steps know where to find the compressed package
|
||||
echo "PKG_PATH=${PKG_STAGING}/${PKG_NAME}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/gawk -f
|
||||
|
||||
BEGIN {
|
||||
inSection = 0
|
||||
inBlock = 0
|
||||
}
|
||||
|
||||
|
||||
/Command-line options/ { inSection=1 }
|
||||
|
||||
inSection && /^```/ {
|
||||
inBlock=1
|
||||
inSection=0
|
||||
# print the starting fence then move to next line
|
||||
print
|
||||
next
|
||||
}
|
||||
|
||||
inBlock && /^```/ {
|
||||
cmd="cargo run --release --quiet -- -h"
|
||||
# Output the results of fd -h
|
||||
u=0
|
||||
while (cmd | getline line) {
|
||||
# Skip everything before the usage line
|
||||
if (line ~ /^Usage/) {
|
||||
u=1;
|
||||
}
|
||||
if (u) {
|
||||
print line
|
||||
}
|
||||
}
|
||||
status = close(cmd)
|
||||
if (status) {
|
||||
print "failed to generate help output" > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
inBlock = 0
|
||||
}
|
||||
|
||||
!inBlock
|
||||
|
||||
+4
-3
@@ -377,8 +377,9 @@ pub struct Opts {
|
||||
)]
|
||||
pub filetype: Option<Vec<FileType>>,
|
||||
|
||||
/// (Additionally) filter search results by their file extension. Multiple
|
||||
/// allowable file extensions can be specified.
|
||||
/// Filter results by extension. By default, this matches all entry types
|
||||
/// (including directories whose names end with the extension). Use `--type`
|
||||
/// to restrict the search to specific types. Multiple extensions can be specified.
|
||||
///
|
||||
/// If you want to search for files without extension,
|
||||
/// you can use the regex '^[^.]+$' as a normal search pattern.
|
||||
@@ -386,7 +387,7 @@ pub struct Opts {
|
||||
long = "extension",
|
||||
short = 'e',
|
||||
value_name = "ext",
|
||||
help = "Filter by file extension",
|
||||
help = "Filter by extension",
|
||||
long_help
|
||||
)]
|
||||
pub extensions: Option<Vec<String>>,
|
||||
|
||||
+36
-2
@@ -54,11 +54,19 @@ impl DirEntry {
|
||||
}
|
||||
|
||||
/// Returns the path as it should be presented to the user.
|
||||
/// When stripping `./` would leave the path starting with `-`, keep the original
|
||||
/// (with `./`) so downstream tools don't interpret it as an option.
|
||||
pub fn stripped_path(&self, config: &Config) -> &Path {
|
||||
let path = self.path();
|
||||
if config.strip_cwd_prefix {
|
||||
strip_current_dir(self.path())
|
||||
let stripped = strip_current_dir(path);
|
||||
if starts_with_dash(stripped) {
|
||||
path
|
||||
} else {
|
||||
stripped
|
||||
}
|
||||
} else {
|
||||
self.path()
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +109,10 @@ impl DirEntry {
|
||||
}
|
||||
}
|
||||
|
||||
fn starts_with_dash(path: &Path) -> bool {
|
||||
path.as_os_str().as_encoded_bytes().first() == Some(&b'-')
|
||||
}
|
||||
|
||||
impl PartialEq for DirEntry {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
@@ -153,3 +165,25 @@ impl Colorable for DirEntry {
|
||||
self.metadata().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::starts_with_dash;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn dash_prefixed_paths_detected() {
|
||||
assert!(starts_with_dash(Path::new("-rf")));
|
||||
assert!(starts_with_dash(Path::new("--delete")));
|
||||
assert!(starts_with_dash(Path::new("-")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_paths_not_flagged() {
|
||||
assert!(!starts_with_dash(Path::new("foo")));
|
||||
assert!(!starts_with_dash(Path::new("./foo")));
|
||||
assert!(!starts_with_dash(Path::new("sub/-rf")));
|
||||
assert!(!starts_with_dash(Path::new("")));
|
||||
assert!(!starts_with_dash(Path::new(" -rf")));
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,3 +1,9 @@
|
||||
use std::io::IsTerminal;
|
||||
|
||||
use crate::sanitize::maybe_sanitize;
|
||||
|
||||
pub fn print_error(msg: impl Into<String>) {
|
||||
eprintln!("[fd error]: {}", msg.into());
|
||||
let msg = msg.into();
|
||||
let safe = maybe_sanitize(&msg, std::io::stderr().is_terminal());
|
||||
eprintln!("[fd error]: {safe}");
|
||||
}
|
||||
|
||||
+23
-10
@@ -43,7 +43,7 @@ impl CommandSet {
|
||||
mode: ExecutionMode::OneByOne,
|
||||
commands: input
|
||||
.into_iter()
|
||||
.map(CommandTemplate::new)
|
||||
.map(|args| CommandTemplate::new(args, ExecutionMode::OneByOne))
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
@@ -59,13 +59,10 @@ impl CommandSet {
|
||||
commands: input
|
||||
.into_iter()
|
||||
.map(|args| {
|
||||
let cmd = CommandTemplate::new(args)?;
|
||||
let cmd = CommandTemplate::new(args, ExecutionMode::Batch)?;
|
||||
if cmd.number_of_tokens() > 1 {
|
||||
bail!("Only one placeholder allowed for batch commands");
|
||||
}
|
||||
if cmd.args[0].has_tokens() {
|
||||
bail!("First argument of exec-batch is expected to be a fixed executable");
|
||||
}
|
||||
Ok(cmd)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
@@ -220,7 +217,7 @@ struct CommandTemplate {
|
||||
}
|
||||
|
||||
impl CommandTemplate {
|
||||
fn new<I, S>(input: I) -> Result<CommandTemplate>
|
||||
fn new<I, S>(input: I, mode: ExecutionMode) -> Result<CommandTemplate>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
@@ -245,6 +242,11 @@ impl CommandTemplate {
|
||||
bail!("No executable provided for --exec or --exec-batch");
|
||||
}
|
||||
|
||||
// A placeholder as the executable is meaningful for `--exec` but never for `--exec-batch`.
|
||||
if mode == ExecutionMode::Batch && args[0].has_tokens() {
|
||||
bail!("First argument of --exec-batch must be a fixed executable, not a placeholder");
|
||||
}
|
||||
|
||||
// If a placeholder token was not supplied, append one at the end of the command.
|
||||
if !has_placeholder {
|
||||
args.push(FormatTemplate::Tokens(vec![Token::Placeholder]));
|
||||
@@ -365,7 +367,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tokens_with_literal_braces() {
|
||||
let template = CommandTemplate::new(vec!["{{}}", "{{", "{.}}"]).unwrap();
|
||||
let template =
|
||||
CommandTemplate::new(vec!["{{}}", "{{", "{.}}"], ExecutionMode::OneByOne).unwrap();
|
||||
assert_eq!(
|
||||
generate_str(&template, "foo"),
|
||||
vec!["{}", "{", "{.}", "foo"]
|
||||
@@ -374,8 +377,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tokens_with_literal_braces_and_placeholder() {
|
||||
let template = CommandTemplate::new(vec!["{{{},end}"]).unwrap();
|
||||
assert_eq!(generate_str(&template, "foo"), vec!["{foo,end}"]);
|
||||
let template =
|
||||
CommandTemplate::new(vec!["echo", "{{{},end}"], ExecutionMode::OneByOne).unwrap();
|
||||
assert_eq!(generate_str(&template, "foo"), vec!["echo", "{foo,end}"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -421,7 +425,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn template_no_args() {
|
||||
assert!(CommandTemplate::new::<Vec<_>, &'static str>(vec![]).is_err());
|
||||
assert!(
|
||||
CommandTemplate::new::<Vec<_>, &'static str>(vec![], ExecutionMode::OneByOne).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -429,6 +435,13 @@ mod tests {
|
||||
assert!(CommandSet::new(vec![vec!["echo"], vec![]]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_as_executable_rejected() {
|
||||
assert!(CommandSet::new(vec![vec!["{}"]]).is_ok());
|
||||
assert!(CommandSet::new(vec![vec!["{/}", "arg"]]).is_ok());
|
||||
assert!(CommandSet::new_batch(vec![vec!["{}"]]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_custom_path_separator() {
|
||||
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||
|
||||
+9
-1
@@ -42,7 +42,7 @@ impl TimeFilter {
|
||||
)
|
||||
} else {
|
||||
let timestamp_secs: u64 = s.strip_prefix('@')?.parse().ok()?;
|
||||
Some(UNIX_EPOCH + Duration::from_secs(timestamp_secs))
|
||||
UNIX_EPOCH.checked_add(Duration::from_secs(timestamp_secs))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,4 +199,12 @@ mod tests {
|
||||
.applies_to(&t1s_later)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_unix_timestamp_is_rejected() {
|
||||
// A '@' timestamp large enough to overflow SystemTime must return
|
||||
// None rather than panicking.
|
||||
assert!(TimeFilter::before(&format!("@{}", u64::MAX)).is_none());
|
||||
assert!(TimeFilter::after(&format!("@{}", u64::MAX)).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
+21
-7
@@ -11,6 +11,7 @@ mod fmt;
|
||||
mod hyperlink;
|
||||
mod output;
|
||||
mod regex_helper;
|
||||
mod sanitize;
|
||||
mod walk;
|
||||
|
||||
use std::env;
|
||||
@@ -65,7 +66,7 @@ fn main() {
|
||||
exit_code.exit();
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("[fd error]: {err:#}");
|
||||
crate::error::print_error(format!("{err:#}"));
|
||||
ExitCode::GeneralError.exit();
|
||||
}
|
||||
}
|
||||
@@ -148,8 +149,10 @@ fn set_working_dir(opts: &Opts) -> Result<()> {
|
||||
/// Detect if the user accidentally supplied a path instead of a search pattern.
|
||||
///
|
||||
/// Without `--full-path`, fd matches patterns against file names, so any pattern
|
||||
/// containing a path separator can never match. Two cases are worth a friendly
|
||||
/// error rather than silent "no results":
|
||||
/// containing a path separator can never match. This applies to the primary
|
||||
/// positional pattern *and* to every `--and` pattern, since all of them are
|
||||
/// chained together and matched against the file name (see `run`). Two cases per
|
||||
/// pattern are worth a friendly error rather than silent "no results":
|
||||
///
|
||||
/// 1. The pattern contains '/'. '/' is always a path separator (including on
|
||||
/// Windows) and has no regex meaning, so flagging it is safe and catches the
|
||||
@@ -168,11 +171,23 @@ fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check the primary pattern and every `--and` pattern. They are all matched
|
||||
// against the file name (see `run`), so a path separator in any of them is
|
||||
// the same silent "no results" footgun.
|
||||
for pattern in std::iter::once(&opts.pattern).chain(opts.exprs.iter().flatten()) {
|
||||
ensure_single_search_pattern_is_not_a_path(pattern)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply the path-separator diagnostic to a single pattern. See
|
||||
/// [`ensure_search_pattern_is_not_a_path`] for the rationale of each case.
|
||||
fn ensure_single_search_pattern_is_not_a_path(pattern: &str) -> Result<()> {
|
||||
// Start with the cheap check: '/' is always a path separator, including on
|
||||
// Windows, and has no regex meaning, so flagging it is safe and catches the
|
||||
// Linux/macOS mistake of pasting a full path as the pattern.
|
||||
#[cfg_attr(not(windows), allow(unused_mut))]
|
||||
let mut should_warn = opts.pattern.contains('/');
|
||||
let mut should_warn = pattern.contains('/');
|
||||
|
||||
// On Windows we additionally accept the native `\` separator, but only when
|
||||
// the pattern actually resolves to an existing directory - `\` is also the
|
||||
@@ -182,8 +197,7 @@ fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
should_warn = should_warn
|
||||
|| (opts.pattern.contains(std::path::MAIN_SEPARATOR)
|
||||
&& Path::new(&opts.pattern).is_dir());
|
||||
|| (pattern.contains(std::path::MAIN_SEPARATOR) && Path::new(pattern).is_dir());
|
||||
}
|
||||
|
||||
if should_warn {
|
||||
@@ -194,7 +208,7 @@ fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
|
||||
fd . '{pattern}'\n\n\
|
||||
Instead, if you want your pattern to match the full file path, use:\n\n \
|
||||
fd --full-path '{pattern}'",
|
||||
pattern = &opts.pattern,
|
||||
pattern = pattern,
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
|
||||
+14
-7
@@ -7,6 +7,7 @@ use crate::config::Config;
|
||||
use crate::dir_entry::DirEntry;
|
||||
use crate::fmt::FormatTemplate;
|
||||
use crate::hyperlink::PathUrl;
|
||||
use crate::sanitize::maybe_sanitize;
|
||||
|
||||
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
|
||||
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
|
||||
@@ -76,7 +77,12 @@ fn print_entry_format<W: Write>(
|
||||
config.path_separator.as_deref(),
|
||||
);
|
||||
// TODO: support writing raw bytes on unix?
|
||||
write!(stdout, "{}", output.to_string_lossy())
|
||||
let s = output.to_string_lossy();
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
maybe_sanitize(&s, config.interactive_terminal)
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: this function is performance critical and can probably be optimized
|
||||
@@ -86,7 +92,6 @@ fn print_entry_colorized<W: Write>(
|
||||
config: &Config,
|
||||
ls_colors: &LsColors,
|
||||
) -> io::Result<()> {
|
||||
// Split the path between the parent and the last component
|
||||
let mut offset = 0;
|
||||
let path = entry.stripped_path(config);
|
||||
let path_str = path.to_string_lossy();
|
||||
@@ -112,14 +117,16 @@ fn print_entry_colorized<W: Write>(
|
||||
.style_for_indicator(Indicator::Directory)
|
||||
.map(Style::to_nu_ansi_term_style)
|
||||
.unwrap_or_default();
|
||||
write!(stdout, "{}", style.paint(parent_str))?;
|
||||
let safe_parent = maybe_sanitize(&parent_str, config.interactive_terminal);
|
||||
write!(stdout, "{}", style.paint(safe_parent.as_ref()))?;
|
||||
}
|
||||
|
||||
let style = entry
|
||||
.style(ls_colors)
|
||||
.map(Style::to_nu_ansi_term_style)
|
||||
.unwrap_or_default();
|
||||
write!(stdout, "{}", style.paint(&path_str[offset..]))?;
|
||||
let safe_basename = maybe_sanitize(&path_str[offset..], config.interactive_terminal);
|
||||
write!(stdout, "{}", style.paint(safe_basename.as_ref()))?;
|
||||
|
||||
print_trailing_slash(
|
||||
stdout,
|
||||
@@ -143,7 +150,8 @@ fn print_entry_uncolorized_base<W: Write>(
|
||||
if let Some(ref separator) = config.path_separator {
|
||||
*path_string.to_mut() = replace_path_separator(&path_string, separator);
|
||||
}
|
||||
write!(stdout, "{path_string}")?;
|
||||
let safe = maybe_sanitize(&path_string, config.interactive_terminal);
|
||||
write!(stdout, "{safe}")?;
|
||||
print_trailing_slash(stdout, entry, config, None)
|
||||
}
|
||||
|
||||
@@ -165,10 +173,9 @@ fn print_entry_uncolorized<W: Write>(
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
if config.interactive_terminal || config.path_separator.is_some() {
|
||||
// Fall back to the base implementation
|
||||
print_entry_uncolorized_base(stdout, entry, config)
|
||||
} else {
|
||||
// Print path as raw bytes, allowing invalid UTF-8 filenames to be passed to other processes
|
||||
// Piped output: raw bytes so invalid UTF-8 filenames reach downstream tools intact.
|
||||
stdout.write_all(entry.stripped_path(config).as_os_str().as_bytes())?;
|
||||
print_trailing_slash(stdout, entry, config, None)
|
||||
}
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
//! TTY-output sanitization to prevent terminal escape injection via filenames.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Write;
|
||||
|
||||
/// True for any char that is neither printable nor permitted whitespace (only HT).
|
||||
/// Covers C0/C1/DEL, bidi overrides, zero-width and format chars, and tag chars.
|
||||
#[inline]
|
||||
fn needs_escape(c: char) -> bool {
|
||||
if c == '\t' {
|
||||
return false;
|
||||
}
|
||||
c.is_control()
|
||||
|| matches!(c,
|
||||
'\u{00AD}' // soft hyphen (invisible)
|
||||
| '\u{180E}' // Mongolian vowel separator
|
||||
| '\u{200B}'..='\u{200F}' // zero-width + LRM/RLM
|
||||
| '\u{202A}'..='\u{202E}' // bidi embedding/override
|
||||
| '\u{2060}'..='\u{206F}' // word joiner, invisibles, deprecated formats
|
||||
| '\u{FEFF}' // BOM / zero-width no-break space
|
||||
| '\u{FFF9}'..='\u{FFFB}' // interlinear annotation
|
||||
| '\u{E0000}'..='\u{E007F}' // language tags
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a `Cow<str>` borrowing `s` when no escaping is needed, otherwise an owned
|
||||
/// escaped copy. Use this when an owned `&str`/`String` is required (e.g. ANSI paint).
|
||||
pub fn sanitize_for_terminal(s: &str) -> Cow<'_, str> {
|
||||
if !s.chars().any(needs_escape) {
|
||||
return Cow::Borrowed(s);
|
||||
}
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
if needs_escape(c) {
|
||||
let v = c as u32;
|
||||
if v <= 0xFF {
|
||||
let _ = write!(out, "\\x{v:02X}");
|
||||
} else {
|
||||
let _ = write!(out, "\\u{{{v:04X}}}");
|
||||
}
|
||||
} else {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
Cow::Owned(out)
|
||||
}
|
||||
|
||||
/// Sanitize for terminal output only; raw bytes pass through on pipes/files.
|
||||
pub fn maybe_sanitize<'a>(s: &'a str, is_terminal: bool) -> Cow<'a, str> {
|
||||
if is_terminal {
|
||||
sanitize_for_terminal(s)
|
||||
} else {
|
||||
Cow::Borrowed(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn preserves_safe_content() {
|
||||
for s in [
|
||||
"hello.txt",
|
||||
"résumé.pdf",
|
||||
"文档.txt",
|
||||
"🦀.rs",
|
||||
"a\tb",
|
||||
"a\u{FFFD}b",
|
||||
] {
|
||||
assert!(
|
||||
matches!(sanitize_for_terminal(s), Cow::Borrowed(_)),
|
||||
"{s:?}"
|
||||
);
|
||||
assert_eq!(sanitize_for_terminal(s), s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_osc52_clipboard_payload() {
|
||||
let attack = "innocent\x1b]52;c;cHduZWQ=\x1b\\.txt";
|
||||
let safe = sanitize_for_terminal(attack);
|
||||
assert!(!safe.contains('\x1b'));
|
||||
assert_eq!(safe, "innocent\\x1B]52;c;cHduZWQ=\\x1B\\.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_cr_output_forgery() {
|
||||
assert_eq!(sanitize_for_terminal("A\rFAKE OUTPUT"), "A\\x0DFAKE OUTPUT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_osc8_hyperlink_injection() {
|
||||
let attack = "phish\x1b]8;;https:evil.example\x1b\\phony.txt";
|
||||
assert!(!sanitize_for_terminal(attack).contains('\x1b'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_del() {
|
||||
assert_eq!(sanitize_for_terminal("a\x7fb"), "a\\x7Fb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_bel_and_null() {
|
||||
assert_eq!(sanitize_for_terminal("a\x07b"), "a\\x07b");
|
||||
assert_eq!(sanitize_for_terminal("a\0b"), "a\\x00b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_newline() {
|
||||
assert_eq!(sanitize_for_terminal("a\nb"), "a\\x0Ab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_preserves_information() {
|
||||
let s = "name\x1bX\x07Y.txt";
|
||||
assert_eq!(sanitize_for_terminal(s), "name\\x1BX\\x07Y.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_c1_csi_and_osc_initiators() {
|
||||
// U+009B is CSI, U+009D is OSC; dangerous on 8-bit-control terminals.
|
||||
assert_eq!(sanitize_for_terminal("\u{9b}31m"), "\\x9B31m");
|
||||
assert_eq!(
|
||||
sanitize_for_terminal("\u{9d}0;pwned\u{9c}"),
|
||||
"\\x9D0;pwned\\x9C"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_bidi_overrides_and_zero_width() {
|
||||
// Trojan-Source style RLO/LRO that flip rendered order of filename text.
|
||||
assert_eq!(
|
||||
sanitize_for_terminal("safe\u{202E}fil\u{202D}gnp.exe"),
|
||||
"safe\\u{202E}fil\\u{202D}gnp.exe"
|
||||
);
|
||||
// Zero-width space and BOM are also format chars used to disguise filenames.
|
||||
assert_eq!(sanitize_for_terminal("a\u{200B}b"), "a\\u{200B}b");
|
||||
assert_eq!(sanitize_for_terminal("\u{FEFF}name"), "\\u{FEFF}name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_legitimate_unicode_features() {
|
||||
// Variation selectors (U+FE0F, U+E0100..) modify preceding glyphs in CJK/emoji
|
||||
// and are legitimate in filenames. Private-use chars are used by icon fonts.
|
||||
for s in [
|
||||
"heart\u{2764}\u{FE0F}.txt",
|
||||
"icon\u{E000}.cfg",
|
||||
"cjk\u{6F22}\u{E0101}.txt",
|
||||
] {
|
||||
assert!(
|
||||
matches!(sanitize_for_terminal(s), Cow::Borrowed(_)),
|
||||
"{s:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_sanitize_passthrough_when_not_terminal() {
|
||||
let attack = "x\x1by";
|
||||
// Pipe context: bytes pass through unchanged (zero-copy).
|
||||
let out = maybe_sanitize(attack, false);
|
||||
assert!(matches!(out, Cow::Borrowed(_)));
|
||||
assert_eq!(out, attack);
|
||||
// TTY context: escapes apply.
|
||||
assert_eq!(maybe_sanitize(attack, true), "x\\x1By");
|
||||
}
|
||||
}
|
||||
+26
-20
@@ -487,26 +487,16 @@ impl WorkerState {
|
||||
Err(ignore::Error::WithPath {
|
||||
path,
|
||||
err: inner_err,
|
||||
}) => match inner_err.as_ref() {
|
||||
ignore::Error::Io(io_error)
|
||||
if io_error.kind() == io::ErrorKind::NotFound
|
||||
&& path
|
||||
.symlink_metadata()
|
||||
.ok()
|
||||
.is_some_and(|m| m.file_type().is_symlink()) =>
|
||||
{
|
||||
DirEntry::broken_symlink(path)
|
||||
}
|
||||
_ => {
|
||||
return match tx.send(WorkerResult::Error(ignore::Error::WithPath {
|
||||
path,
|
||||
err: inner_err,
|
||||
})) {
|
||||
Ok(_) => WalkState::Continue,
|
||||
Err(_) => WalkState::Quit,
|
||||
};
|
||||
}
|
||||
},
|
||||
}) if inner_err
|
||||
.io_error()
|
||||
.is_some_and(|io_error| io_error.kind() == io::ErrorKind::NotFound)
|
||||
&& path
|
||||
.symlink_metadata()
|
||||
.ok()
|
||||
.is_some_and(|m| m.file_type().is_symlink()) =>
|
||||
{
|
||||
DirEntry::broken_symlink(path)
|
||||
}
|
||||
Err(err) => {
|
||||
return match tx.send(WorkerResult::Error(err)) {
|
||||
Ok(_) => WalkState::Continue,
|
||||
@@ -735,4 +725,20 @@ mod tests {
|
||||
PathBuf::from("bar")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_str_no_base_dir_with_plain_relative_path() {
|
||||
assert_eq!(
|
||||
search_str_for_entry(Path::new("foo/bar"), None),
|
||||
PathBuf::from("bar")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_str_no_base_dir_with_file_in_current_dir() {
|
||||
assert_eq!(
|
||||
search_str_for_entry(Path::new("foo"), None),
|
||||
PathBuf::from("foo")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+40
-15
@@ -417,6 +417,31 @@ fn test_pattern_with_forward_slash_allowed_with_full_path() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `--and` patterns are matched against the file name exactly like the primary
|
||||
/// pattern, so a path separator in any of them is the same silent "no results"
|
||||
/// footgun and must trigger the same diagnostic. Regression for the sibling of
|
||||
/// #1873 left unchecked by #1975 (only the positional pattern was validated).
|
||||
#[test]
|
||||
fn test_and_pattern_with_forward_slash_is_rejected() {
|
||||
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
|
||||
|
||||
// Clean primary pattern, but a `--and` pattern carrying a path separator.
|
||||
te.assert_failure_with_error(
|
||||
&["foo", "--and", "nonexistent/path"],
|
||||
"[fd error]: The search pattern 'nonexistent/path' contains a path-separation character and will not lead to any search results.",
|
||||
);
|
||||
|
||||
// `--full-path` is the explicit opt-in and must still suppress the
|
||||
// diagnostic for `--and` patterns too (the invocation runs and matches
|
||||
// instead of erroring on the path separator).
|
||||
#[cfg(not(windows))]
|
||||
te.assert_output(
|
||||
&["--full-path", "one/two/c", "--and", "two/c"],
|
||||
"one/two/c.foo
|
||||
one/two/C.Foo2",
|
||||
);
|
||||
}
|
||||
|
||||
/// Explicit root path
|
||||
#[test]
|
||||
fn test_explicit_root_path() {
|
||||
@@ -1249,7 +1274,7 @@ fn test_absolute_path() {
|
||||
{abs_path}/one/two/three/d.foo
|
||||
{abs_path}/one/two/three/directory_foo/
|
||||
{abs_path}/symlink",
|
||||
abs_path = &abs_path
|
||||
abs_path = abs_path
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1262,7 +1287,7 @@ fn test_absolute_path() {
|
||||
{abs_path}/one/two/C.Foo2
|
||||
{abs_path}/one/two/three/d.foo
|
||||
{abs_path}/one/two/three/directory_foo/",
|
||||
abs_path = &abs_path
|
||||
abs_path = abs_path
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1281,7 +1306,7 @@ fn test_implicit_absolute_path() {
|
||||
{abs_path}/one/two/C.Foo2
|
||||
{abs_path}/one/two/three/d.foo
|
||||
{abs_path}/one/two/three/directory_foo/",
|
||||
abs_path = &abs_path
|
||||
abs_path = abs_path
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1301,7 +1326,7 @@ fn test_normalized_absolute_path() {
|
||||
{abs_path}/one/two/C.Foo2
|
||||
{abs_path}/one/two/three/d.foo
|
||||
{abs_path}/one/two/three/directory_foo/",
|
||||
abs_path = &abs_path
|
||||
abs_path = abs_path
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1535,7 +1560,7 @@ fn test_symlink_as_root() {
|
||||
{dir}/one/two/three/d.foo
|
||||
{dir}/one/two/three/directory_foo/
|
||||
{dir}/symlink",
|
||||
dir = &parent_parent
|
||||
dir = parent_parent
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1555,7 +1580,7 @@ fn test_symlink_and_absolute_path() {
|
||||
{abs_path}/{expected_path}/three/
|
||||
{abs_path}/{expected_path}/three/d.foo
|
||||
{abs_path}/{expected_path}/three/directory_foo/",
|
||||
abs_path = &abs_path,
|
||||
abs_path = abs_path,
|
||||
expected_path = expected_path
|
||||
),
|
||||
);
|
||||
@@ -1573,7 +1598,7 @@ fn test_symlink_as_absolute_root() {
|
||||
{abs_path}/symlink/three/
|
||||
{abs_path}/symlink/three/d.foo
|
||||
{abs_path}/symlink/three/directory_foo/",
|
||||
abs_path = &abs_path
|
||||
abs_path = abs_path
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1597,7 +1622,7 @@ fn test_symlink_and_full_path() {
|
||||
"{abs_path}/{expected_path}/three/
|
||||
{abs_path}/{expected_path}/three/d.foo
|
||||
{abs_path}/{expected_path}/three/directory_foo/",
|
||||
abs_path = &abs_path,
|
||||
abs_path = abs_path,
|
||||
expected_path = expected_path
|
||||
),
|
||||
);
|
||||
@@ -1618,7 +1643,7 @@ fn test_symlink_and_full_path_abs_path() {
|
||||
"{abs_path}/symlink/three/
|
||||
{abs_path}/symlink/three/d.foo
|
||||
{abs_path}/symlink/three/directory_foo/",
|
||||
abs_path = &abs_path
|
||||
abs_path = abs_path
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1747,7 +1772,7 @@ fn test_exec() {
|
||||
{abs_path}/one/two/c.foo
|
||||
{abs_path}/one/two/three/d.foo
|
||||
{abs_path}/one/two/three/directory_foo",
|
||||
abs_path = &abs_path
|
||||
abs_path = abs_path
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1846,7 +1871,7 @@ fn test_exec_multi() {
|
||||
test c.foo
|
||||
test d.foo
|
||||
test directory_foo",
|
||||
abs_path = &abs_path
|
||||
abs_path = abs_path
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1902,7 +1927,7 @@ fn test_exec_batch() {
|
||||
&["--absolute-path", "foo", "--exec-batch", "echo"],
|
||||
&format!(
|
||||
"{abs_path}/a.foo {abs_path}/one/b.foo {abs_path}/one/two/C.Foo2 {abs_path}/one/two/c.foo {abs_path}/one/two/three/d.foo {abs_path}/one/two/three/directory_foo",
|
||||
abs_path = &abs_path
|
||||
abs_path = abs_path
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1956,7 +1981,7 @@ fn test_exec_batch() {
|
||||
|
||||
te.assert_failure_with_error(
|
||||
&["foo", "--exec-batch", "echo {}"],
|
||||
"error: First argument of exec-batch is expected to be a fixed executable\n\
|
||||
"error: First argument of --exec-batch must be a fixed executable, not a placeholder\n\
|
||||
\n\
|
||||
Usage: fd [OPTIONS] [pattern] [path]...\n\
|
||||
\n\
|
||||
@@ -2514,7 +2539,7 @@ fn test_base_directory() {
|
||||
|
||||
// Ignore base directory when absolute path is used
|
||||
let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES);
|
||||
let abs_base_dir = &format!("{abs_path}/one/two/", abs_path = &abs_path);
|
||||
let abs_base_dir = &format!("{abs_path}/one/two/", abs_path = abs_path);
|
||||
te.assert_output(
|
||||
&["--base-directory", abs_base_dir, "foo", &abs_path],
|
||||
&format!(
|
||||
@@ -2524,7 +2549,7 @@ fn test_base_directory() {
|
||||
{abs_path}/one/two/C.Foo2
|
||||
{abs_path}/one/two/three/d.foo
|
||||
{abs_path}/one/two/three/directory_foo/",
|
||||
abs_path = &abs_path
|
||||
abs_path = abs_path
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user