Merge remote-tracking branch 'origin/master' into pr/opposing-options

This commit is contained in:
David Peter 2021-11-14 16:47:53 +01:00
commit cdc6a37ed6
20 changed files with 716 additions and 372 deletions

View File

@ -1,28 +0,0 @@
---
name: Bug Report
about: Report a bug.
title: ""
labels: bug
assignees: ''
---
**Describe the bug you encountered:**
<!--
Please check out the troubleshooting section first:
https://github.com/sharkdp/fd#troubleshooting
-->
**Describe what you expected to happen:**
**What version of `fd` are you using?**
<!-- paste the output of `fd --version` here -->
**Which operating system / distribution are you on?**
<!--
Unix: paste the output of `uname -srm` and `lsb_release -a` here.
Windows: please tell us your Windows version
-->

41
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Bug Report
description: Report a bug.
title: "[BUG] "
labels: bug
body:
- type: markdown
attributes:
value: |
Please check out the [troubleshooting section](https://github.com/sharkdp/fd#troubleshooting) first.
- type: checkboxes
attributes:
options:
- label: I have read the troubleshooting section and still think this is a bug.
required: true
- type: textarea
id: bug
attributes:
label: "Describe the bug you encountered:"
validations:
required: true
- type: textarea
id: expected
attributes:
label: "Describe what you expected to happen:"
- type: input
id: version
attributes:
label: "What version of `fd` are you using?"
placeholder: "paste the output of `fd --version` here"
validations:
required: true
- type: textarea
id: os
attributes:
label: Which operating system / distribution are you on?
placeholder: |
Unix: paste the output of `uname -srm` and lsb_release -a` here.
Windows: please tell us your Windows version
render: shell
validations:
required: true

View File

@ -1,7 +1,7 @@
name: CICD
env:
MIN_SUPPORTED_RUST_VERSION: "1.42.0"
MIN_SUPPORTED_RUST_VERSION: "1.53.0"
CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
on:
@ -14,17 +14,16 @@ on:
- '*'
jobs:
min_version:
name: Minimum supported rust version
code_quality:
name: Code quality
runs-on: ubuntu-20.04
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }}
toolchain: stable
default: true
profile: minimal # minimal component installation (ie, no documentation)
components: clippy, rustfmt
@ -33,11 +32,26 @@ jobs:
with:
command: fmt
args: -- --check
- name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
- name: Ensure MSRV is set in `clippy.toml`
run: grep "^msrv = \"${{ env.MIN_SUPPORTED_RUST_VERSION }}\"\$" clippy.toml
- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --locked --all-targets --all-features
min_version:
name: Minimum supported rust version
runs-on: ubuntu-20.04
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }}
default: true
profile: minimal # minimal component installation (ie, no documentation)
- name: Run tests
uses: actions-rs/cargo@v1
with:

View File

@ -1,10 +1,19 @@
# Upcoming release
## Performance improvements
- File metadata is now cached between the different filters that require it (e.g. `--owner`,
`--size`), reducing the number of `stat` syscalls when multiple filters are used; see #863
## Features
- Don't buffer command output from `--exec` when using a single thread. See #522
- Add new `-q, --quiet` flag, see #303 (@Asha20)
- Add new `--no-ignore-parent` flag, see #787 (@will459)
- Add new `--batch-size` flag, see #410 (@devonhollowood)
- Add opposing command-line options, see #595 (@Asha20)
## Bugfixes
@ -15,6 +24,8 @@
- Properly handle write errors to devices that are full, see #737
- Use local time zone for time functions (`--change-newer-than`, `--change-older-than`), see #631 (@jacobmischka)
- Support `--list-details` on more platforms (like BusyBox), see #783
- The filters `--owner`, `--size`, and `--changed-{within,before}` now apply to symbolic links
themselves, rather than the link target, except when `--follow` is specified; see #863
## Changes

61
Cargo.lock generated
View File

@ -31,9 +31,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.42"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "atty"
@ -54,24 +54,24 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitflags"
version = "1.2.1"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bstr"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"memchr",
]
[[package]]
name = "cc"
version = "1.0.69"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
[[package]]
name = "cfg-if"
@ -120,9 +120,9 @@ dependencies = [
[[package]]
name = "ctrlc"
version = "3.2.0"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "377c9b002a72a0b2c1a18c62e2f3864bdfea4a015e3683a96e24aa45dd6c02d1"
checksum = "a19c6cedffdc8c03a3346d723eb20bd85a13362bb96dc2ac000842c6381ec7bf"
dependencies = [
"nix",
"winapi",
@ -177,6 +177,7 @@ dependencies = [
"lscolors",
"normpath",
"num_cpus",
"once_cell",
"regex",
"regex-syntax",
"tempdir",
@ -301,9 +302,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.99"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673"
[[package]]
name = "log"
@ -316,18 +317,18 @@ dependencies = [
[[package]]
name = "lscolors"
version = "0.7.1"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24b894c45c9da468621cdd615a5a79ee5e5523dd4f75c76ebc03d458940c16e"
checksum = "bd0aa49b10c47f9a4391a99198b5e65c74f9ca771c0dcc856bb75a3f46c8627d"
dependencies = [
"ansi_term 0.12.1",
]
[[package]]
name = "memchr"
version = "2.4.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memoffset"
@ -340,9 +341,9 @@ dependencies = [
[[package]]
name = "nix"
version = "0.22.0"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1e25ee6b412c2a1e3fcb6a4499a5c1bfe7f43e014bdce9a6b6666e5aa2d187"
checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188"
dependencies = [
"bitflags",
"cc",
@ -353,9 +354,9 @@ dependencies = [
[[package]]
name = "normpath"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27e6e8f70e9fbbe3752d330d769e3424f24b9458ce266df93a3b456902fd696a"
checksum = "640c20e9df4a2d4a5adad5b47e17d76dac3e824346b181931c3ec9f7a85687b1"
dependencies = [
"winapi",
]
@ -397,18 +398,18 @@ checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "proc-macro2"
version = "1.0.28"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.9"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [
"proc-macro2",
]
@ -512,9 +513,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "syn"
version = "1.0.74"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
dependencies = [
"proc-macro2",
"quote",
@ -543,9 +544,9 @@ dependencies = [
[[package]]
name = "test-case"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b114ece25254e97bf48dd4bfc2a12bad0647adacfe4cae1247a9ca6ad302cec"
checksum = "c7cad0a06f9a61e94355aa3b3dc92d85ab9c83406722b1ca5e918d4297c12c23"
dependencies = [
"cfg-if",
"proc-macro2",
@ -585,9 +586,9 @@ dependencies = [
[[package]]
name = "unicode-width"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"

View File

@ -41,17 +41,18 @@ lazy_static = "1.1.0"
num_cpus = "1.8"
regex = "1.5.4"
regex-syntax = "0.6"
ctrlc = "3.1"
humantime = "2.0"
lscolors = "0.7"
ctrlc = "3.2"
humantime = "2.1"
lscolors = "0.8"
globset = "0.4"
anyhow = "1.0"
dirs-next = "2.0"
normpath = "0.3"
chrono = "0.4"
once_cell = "1.8.0"
[dependencies.clap]
version = "2.31.2"
version = "2.31.3"
features = ["suggestions", "color", "wrap_help"]
[target.'cfg(unix)'.dependencies]
@ -69,8 +70,8 @@ jemallocator = "0.3.0"
[dev-dependencies]
diff = "0.1"
tempdir = "0.3"
filetime = "0.2.14"
test-case = "1.2.0"
filetime = "0.2"
test-case = "1.2"
[profile.release]
lto = true

View File

@ -418,6 +418,14 @@ use a character class with a single hyphen character:
> fd '[-]pattern'
```
### "Command not found" for `alias`es or shell functions
Shell `alias`es and shell functions can not be used for command execution via `fd -x` or
`fd -X`. In `zsh`, you can make the alias global via `alias -g myalias="…"`. In `bash`,
you can use `export -f my_function` to make available to child processes. You would still
need to call `fd -x bash -c 'my_function "$1"' bash`. For other use cases or shells, use
a (temporary) shell script.
## Integration with other programs
### Using fd with `fzf`
@ -625,7 +633,7 @@ You can install [the fd-find package](https://www.freshports.org/sysutils/fd) fr
pkg install fd-find
```
### From NPM
### From npm
On linux and macOS, you can install the [fd-find](https://npm.im/fd-find) package:
@ -639,7 +647,7 @@ With Rust's package manager [cargo](https://github.com/rust-lang/cargo), you can
```
cargo install fd-find
```
Note that rust version *1.42.0* or later is required.
Note that rust version *1.53.0* or later is required.
`make` is also needed for the build.
@ -667,7 +675,6 @@ cargo install --path .
- [sharkdp](https://github.com/sharkdp)
- [tmccombs](https://github.com/tmccombs)
- [tavianator](https://github.com/tavianator)
- [pemistahl](https://github.com/pemistahl/)
## License

View File

@ -5,7 +5,7 @@ use clap::Shell;
include!("src/app.rs");
fn main() {
let min_version = "1.42";
let min_version = "1.53";
match version_check::is_min_version(min_version) {
Some(true) => {}

1
clippy.toml Normal file
View File

@ -0,0 +1 @@
msrv = "1.53.0"

View File

@ -138,6 +138,7 @@ _fd() {
+ '(exec-cmds)' # execute command
'(long-listing max-results)'{-x+,--exec=}'[execute command for each search result]:command: _command_names -e:*\;::program arguments: _normal'
'(long-listing max-results)'{-X+,--exec-batch=}'[execute command for all search results at once]:command: _command_names -e:*\;::program arguments: _normal'
'(long-listing max-results)--batch-size=[max number of args for each -X call]:size'
+ other
'!(--max-buffer-time)--max-buffer-time=[set amount of time to buffer before showing output]:time (ms)'
@ -220,7 +221,7 @@ _fd() {
_fd "$@"
# ------------------------------------------------------------------------------
# Copyright (c) 2011 Github zsh-users - http://github.com/zsh-users
# Copyright (c) 2011 GitHub zsh-users - http://github.com/zsh-users
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without

6
doc/fd.1 vendored
View File

@ -409,5 +409,11 @@ $ fd -e py
.TP
.RI "Open all search results with vim:"
$ fd pattern -X vim
.TP
.BI "\-\-batch\-size " size
Pass at most
.I size
arguments to each call to the command given with -X.
.TP
.SH SEE ALSO
.BR find (1)

View File

@ -64,6 +64,7 @@ pub fn build_app() -> App<'static, 'static> {
.long("no-ignore-vcs")
.overrides_with("no-ignore-vcs")
.hidden_short_help(true)
.help("Do not respect .gitignore files")
.long_help(
"Show search results from files and directories that would otherwise be \
ignored by '.gitignore' files. The flag can be overridden with --ignore-vcs.",
@ -78,10 +79,22 @@ pub fn build_app() -> App<'static, 'static> {
"Overrides --no-ignore-vcs.",
),
)
.arg(
Arg::with_name("no-ignore-parent")
.long("no-ignore-parent")
.overrides_with("no-ignore-parent")
.hidden_short_help(true)
.help("Do not respect .(git|fd)ignore files in parent directories")
.long_help(
"Show search results from files and directories that would otherwise be \
ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories.",
),
)
.arg(
Arg::with_name("no-global-ignore-file")
.long("no-global-ignore-file")
.hidden(true)
.help("Do not respect the global ignore file")
.long_help("Do not respect the global ignore file."),
)
.arg(
@ -91,6 +104,7 @@ pub fn build_app() -> App<'static, 'static> {
.overrides_with_all(&["ignore", "no-hidden"])
.multiple(true)
.hidden_short_help(true)
.help("Alias for '--no-ignore', and '--hidden' when given twice")
.long_help(
"Alias for '--no-ignore'. Can be repeated. '-uu' is an alias for \
'--no-ignore --hidden'.",
@ -134,6 +148,7 @@ pub fn build_app() -> App<'static, 'static> {
.long("regex")
.overrides_with_all(&["glob", "regex"])
.hidden_short_help(true)
.help("Regular-expression based search (default)")
.long_help(
"Perform a regular-expression based search (default). This can be used to \
override --glob.",
@ -146,6 +161,7 @@ pub fn build_app() -> App<'static, 'static> {
.alias("literal")
.overrides_with("fixed-strings")
.hidden_short_help(true)
.help("Treat pattern as literal string instead of regex")
.long_help(
"Treat the pattern as a literal string instead of a regular expression. Note \
that this also performs substring comparison. If you want to match on an \
@ -250,6 +266,7 @@ pub fn build_app() -> App<'static, 'static> {
.long("maxdepth")
.hidden(true)
.takes_value(true)
.help("Set maximum search depth (default: none)")
)
.arg(
Arg::with_name("min-depth")
@ -257,6 +274,7 @@ pub fn build_app() -> App<'static, 'static> {
.takes_value(true)
.value_name("depth")
.hidden_short_help(true)
.help("Only show results starting at given depth")
.long_help(
"Only show search results starting at the given depth. \
See also: '--max-depth' and '--exact-depth'",
@ -269,6 +287,7 @@ pub fn build_app() -> App<'static, 'static> {
.value_name("depth")
.hidden_short_help(true)
.conflicts_with_all(&["max-depth", "min-depth"])
.help("Only show results at exact given depth")
.long_help(
"Only show search results at the exact given depth. This is an alias for \
'--min-depth <depth> --max-depth <depth>'.",
@ -279,7 +298,9 @@ pub fn build_app() -> App<'static, 'static> {
.long("prune")
.conflicts_with_all(&["size", "exact-depth"])
.hidden_short_help(true)
.long_help("Do not traverse into matching directories.")
.help("Do not traverse into matching directories")
.long_help("Do not traverse into directories that match the search criteria. If \
you want to exclude specific directories, use the '--exclude=' option.")
)
.arg(
Arg::with_name("file-type")
@ -395,6 +416,21 @@ pub fn build_app() -> App<'static, 'static> {
"
),
)
.arg(
Arg::with_name("batch-size")
.long("batch-size")
.takes_value(true)
.value_name("size")
.hidden_short_help(true)
.requires("exec-batch")
.help("Max number of arguments to run as a batch with -X")
.long_help(
"Maximum number of arguments to pass to the command given with -X. \
If the number of results is greater than the given size, \
the command given with -X is run again with remaining arguments. \
A batch size of zero means there is no limit.",
),
)
.arg(
Arg::with_name("exclude")
.long("exclude")
@ -421,6 +457,7 @@ pub fn build_app() -> App<'static, 'static> {
.number_of_values(1)
.multiple(true)
.hidden_short_help(true)
.help("Add custom ignore-file in '.gitignore' format")
.long_help(
"Add a custom ignore-file in '.gitignore' format. These files have a low \
precedence.",
@ -449,6 +486,7 @@ pub fn build_app() -> App<'static, 'static> {
.takes_value(true)
.value_name("num")
.hidden_short_help(true)
.help("Set number of threads")
.long_help(
"Set number of threads to use for searching & executing (default: number \
of available CPU cores)",
@ -462,7 +500,7 @@ pub fn build_app() -> App<'static, 'static> {
.number_of_values(1)
.allow_hyphen_values(true)
.multiple(true)
.help("Limit results based on the size of files.")
.help("Limit results based on the size of files")
.long_help(
"Limit results based on the size of files using the format <+-><NUM><UNIT>.\n \
'+': file size must be greater than or equal to this\n \
@ -487,6 +525,7 @@ pub fn build_app() -> App<'static, 'static> {
.long("max-buffer-time")
.takes_value(true)
.hidden(true)
.help("Milliseconds to buffer before streaming search results to console")
.long_help(
"Amount of time in milliseconds to buffer, before streaming the search \
results to the console.",
@ -543,6 +582,7 @@ pub fn build_app() -> App<'static, 'static> {
// the files they saw in the previous search.
.conflicts_with_all(&["exec", "exec-batch", "list-details"])
.hidden_short_help(true)
.help("Limit number of search results")
.long_help("Limit the number of search results to 'count' and quit immediately."),
)
.arg(
@ -551,6 +591,7 @@ pub fn build_app() -> App<'static, 'static> {
.hidden_short_help(true)
.overrides_with("max-results")
.conflicts_with_all(&["exec", "exec-batch", "list-details"])
.help("Limit search to a single result")
.long_help("Limit the search to a single result and quit immediately. \
This is an alias for '--max-results=1'.")
)
@ -561,6 +602,7 @@ pub fn build_app() -> App<'static, 'static> {
.alias("has-results")
.hidden_short_help(true)
.conflicts_with_all(&["exec", "exec-batch", "list-details", "max-results"])
.help("Print nothing, exit code 0 if match found, 1 otherwise")
.long_help(
"When the flag is present, the program does not print anything and will \
return with an exit code of 0 if there is at least one match. Otherwise, the \
@ -573,6 +615,7 @@ pub fn build_app() -> App<'static, 'static> {
.long("show-errors")
.hidden_short_help(true)
.overrides_with("show-errors")
.help("Show filesystem errors")
.long_help(
"Enable the display of filesystem errors for situations such as \
insufficient permissions or dead symlinks.",
@ -585,6 +628,7 @@ pub fn build_app() -> App<'static, 'static> {
.value_name("path")
.number_of_values(1)
.hidden_short_help(true)
.help("Change current working directory")
.long_help(
"Change the current working directory of fd to the provided path. This \
means that search results will be shown with respect to the given base \
@ -608,6 +652,7 @@ pub fn build_app() -> App<'static, 'static> {
.value_name("separator")
.long("path-separator")
.hidden_short_help(true)
.help("Set path separator when printing file paths")
.long_help(
"Set the path separator to use when printing file paths. The default is \
the OS-specific separator ('/' on Unix, '\\' on Windows).",
@ -630,6 +675,7 @@ pub fn build_app() -> App<'static, 'static> {
.multiple(true)
.hidden_short_help(true)
.number_of_values(1)
.help("Provide paths to search as an alternative to the positional <path>")
.long_help(
"Provide paths to search as an alternative to the positional <path> \
argument. Changes the usage to `fd [FLAGS/OPTIONS] --search-path <path> \
@ -666,6 +712,7 @@ pub fn build_app() -> App<'static, 'static> {
.long("one-file-system")
.aliases(&["mount", "xdev"])
.hidden_short_help(true)
.help("Do not descend into a different file system")
.long_help(
"By default, fd will traverse the file system tree as far as other options \
dictate. With this flag, fd ensures that it does not descend into a \

View File

@ -10,7 +10,7 @@ use crate::filter::OwnerFilter;
use crate::filter::{SizeFilter, TimeFilter};
/// Configuration options for *fd*.
pub struct Options {
pub struct Config {
/// Whether the search is case-sensitive or case-insensitive.
pub case_sensitive: bool,
@ -24,6 +24,9 @@ pub struct Options {
/// Whether to respect `.fdignore` files or not.
pub read_fdignore: bool,
/// Whether to respect ignore files in parent directories or not.
pub read_parent_ignore: bool,
/// Whether to respect VCS ignore files (`.gitignore`, ..) or not.
pub read_vcsignore: bool,
@ -82,6 +85,10 @@ pub struct Options {
/// If a value is supplied, each item found will be used to generate and execute commands.
pub command: Option<Arc<CommandTemplate>>,
/// Maximum number of search results to pass to each `command`. If zero, the number is
/// unlimited.
pub batch_size: usize,
/// A list of glob patterns that should be excluded from the search.
pub exclude_patterns: Vec<String>,

View File

@ -42,7 +42,7 @@ pub fn job(
results.push(cmd.generate_and_execute(&value, Arc::clone(&out_perm), buffer_output))
}
// Returns error in case of any error.
merge_exitcodes(&results)
merge_exitcodes(results)
}
pub fn batch(
@ -50,6 +50,7 @@ pub fn batch(
cmd: &CommandTemplate,
show_filesystem_errors: bool,
buffer_output: bool,
limit: usize,
) -> ExitCode {
let paths = rx.iter().filter_map(|value| match value {
WorkerResult::Entry(val) => Some(val),
@ -60,5 +61,17 @@ pub fn batch(
None
}
});
cmd.generate_and_execute_batch(paths, buffer_output)
if limit == 0 {
// no limit
return cmd.generate_and_execute_batch(paths, buffer_output);
}
let mut exit_codes = Vec::new();
let mut peekable = paths.peekable();
while peekable.peek().is_some() {
let limited = peekable.by_ref().take(limit);
let exit_code = cmd.generate_and_execute_batch(limited, buffer_output);
exit_codes.push(exit_code);
}
merge_exitcodes(exit_codes)
}

View File

@ -23,8 +23,8 @@ impl ExitCode {
}
}
pub fn merge_exitcodes(results: &[ExitCode]) -> ExitCode {
if results.iter().any(|&c| ExitCode::is_error(c)) {
pub fn merge_exitcodes(results: impl IntoIterator<Item = ExitCode>) -> ExitCode {
if results.into_iter().any(ExitCode::is_error) {
return ExitCode::GeneralError;
}
ExitCode::Success
@ -36,38 +36,38 @@ mod tests {
#[test]
fn success_when_no_results() {
assert_eq!(merge_exitcodes(&[]), ExitCode::Success);
assert_eq!(merge_exitcodes([]), ExitCode::Success);
}
#[test]
fn general_error_if_at_least_one_error() {
assert_eq!(
merge_exitcodes(&[ExitCode::GeneralError]),
merge_exitcodes([ExitCode::GeneralError]),
ExitCode::GeneralError
);
assert_eq!(
merge_exitcodes(&[ExitCode::KilledBySigint]),
merge_exitcodes([ExitCode::KilledBySigint]),
ExitCode::GeneralError
);
assert_eq!(
merge_exitcodes(&[ExitCode::KilledBySigint, ExitCode::Success]),
merge_exitcodes([ExitCode::KilledBySigint, ExitCode::Success]),
ExitCode::GeneralError
);
assert_eq!(
merge_exitcodes(&[ExitCode::Success, ExitCode::GeneralError]),
merge_exitcodes([ExitCode::Success, ExitCode::GeneralError]),
ExitCode::GeneralError
);
assert_eq!(
merge_exitcodes(&[ExitCode::GeneralError, ExitCode::KilledBySigint]),
merge_exitcodes([ExitCode::GeneralError, ExitCode::KilledBySigint]),
ExitCode::GeneralError
);
}
#[test]
fn success_if_no_error() {
assert_eq!(merge_exitcodes(&[ExitCode::Success]), ExitCode::Success);
assert_eq!(merge_exitcodes([ExitCode::Success]), ExitCode::Success);
assert_eq!(
merge_exitcodes(&[ExitCode::Success, ExitCode::Success]),
merge_exitcodes([ExitCode::Success, ExitCode::Success]),
ExitCode::Success
);
}

View File

@ -37,9 +37,9 @@ impl FileTypes {
|| (self.executables_only
&& !entry
.metadata()
.map(|m| filesystem::is_executable(&m))
.map(|m| filesystem::is_executable(m))
.unwrap_or(false))
|| (self.empty_only && !filesystem::is_empty(&entry))
|| (self.empty_only && !filesystem::is_empty(entry))
|| !(entry_type.is_file()
|| entry_type.is_dir()
|| entry_type.is_symlink()

View File

@ -1,11 +1,11 @@
mod app;
mod config;
mod error;
mod exec;
mod exit_codes;
mod filesystem;
mod filetypes;
mod filter;
mod options;
mod output;
mod regex_helper;
mod walk;
@ -23,6 +23,7 @@ use lscolors::LsColors;
use normpath::PathExt;
use regex::bytes::{RegexBuilder, RegexSetBuilder};
use crate::config::Config;
use crate::error::print_error;
use crate::exec::CommandTemplate;
use crate::exit_codes::ExitCode;
@ -30,7 +31,6 @@ use crate::filetypes::FileTypes;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::{SizeFilter, TimeFilter};
use crate::options::Options;
use crate::regex_helper::{pattern_has_uppercase_char, pattern_matches_strings_with_leading_dot};
// We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/481
@ -50,10 +50,38 @@ ow=0:or=0;38;5;16;48;5;203:no=0:ex=1;38;5;203:cd=0;38;5;203;48;5;236:mi=0;38;5;1
38;5;185:*.jpg=0;38;5;208:*.mir=0;38;5;48:*.sxi=0;38;5;186:*.bz2=4;38;5;203:*.odt=0;38;5;186:*.mov=0;38;5;208:*.toc=0;38;5;243:*.bat=1;38;5;203:*.asa=0;38;5;48:*.awk=0;38;5;48:*.sbt=0;38;5;48:*.vcd=4;38;5;203:*.kts=0;38;5;48:*.arj=4;38;5;203:*.blg=0;38;5;243:*.c++=0;38;5;48:*.odp=0;38;5;186:*.bbl=0;38;5;243:*.idx=0;38;5;243:*.com=1;38;5;203:*.mp3=0;38;5;208:*.avi=0;38;5;208:*.def=0;38;5;48:*.cgi=0;38;5;48:*.zip=4;38;5;203:*.ttf=0;38;5;208:*.ppt=0;38;5;186:*.tml=0;38;5;149:*.fsx=0;38;5;48:*.h++=0;38;5;48:*.rtf=0;38;5;186:*.inl=0;38;5;48:*.yaml=0;38;5;149:*.html=0;38;5;185:*.mpeg=0;38;5;208:*.java=0;38;5;48:*.hgrc=0;38;5;149:*.orig=0;38;5;243:*.conf=0;38;5;149:*.dart=0;38;5;48:*.psm1=0;38;5;48:*.rlib=0;38;5;243:*.fish=0;38;5;48:*.bash=0;38;5;48:*.make=0;38;5;149:*.docx=0;38;5;186:*.json=0;38;5;149:*.psd1=0;38;5;48:*.lisp=0;38;5;48:*.tbz2=4;38;5;203:*.diff=0;38;5;48:*.epub=0;38;5;186:*.xlsx=0;38;5;186:*.pptx=0;38;5;186:*.toml=0;38;5;149:*.h264=0;38;5;208:*.purs=0;38;5;48:*.flac=0;38;5;208:*.tiff=0;38;5;208:*.jpeg=0;38;5;208:*.lock=0;38;5;243:*.less=0;38;5;48:*.dyn_o=0;38;5;243:*.scala=0;38;5;48:*.mdown=0;38;5;185:*.shtml=0;38;5;185:*.class=0;38;5;243:*.cache=0;38;5;243:*.cmake=0;38;5;149:*passwd=0;38;5;149:*.swift=0;38;5;48:*shadow=0;38;5;149:*.xhtml=0;38;5;185:*.patch=0;38;5;48:*.cabal=0;38;5;48:*README=0;38;5;16;48;5;186:*.toast=4;38;5;203:*.ipynb=0;38;5;48:*COPYING=0;38;5;249:*.gradle=0;38;5;48:*.matlab=0;38;5;48:*.config=0;38;5;149:*LICENSE=0;38;5;249:*.dyn_hi=0;38;5;243:*.flake8=0;38;5;149:*.groovy=0;38;5;48:*INSTALL=0;38;5;16;48;5;186:*TODO.md=1:*.ignore=0;38;5;149:*Doxyfile=0;38;5;149:*TODO.txt=1:*setup.py=0;38;5;149:*Makefile=0;38;5;149:*.gemspec=0;38;5;149:*.desktop=0;38;5;149:*.rgignore=0;38;5;149:*.markdown=0;38;5;185:*COPYRIGHT=0;38;5;249:*configure=0;38;5;149:*.DS_Store=0;38;5;243:*.kdevelop=0;38;5;149:*.fdignore=0;38;5;149:*README.md=0;38;5;16;48;5;186:*.cmake.in=0;38;5;149:*SConscript=0;38;5;149:*CODEOWNERS=0;38;5;149:*.localized=0;38;5;243:*.gitignore=0;38;5;149:*Dockerfile=0;38;5;149:*.gitconfig=0;38;5;149:*INSTALL.md=0;38;5;16;48;5;186:*README.txt=0;38;5;16;48;5;186:*SConstruct=0;38;5;149:*.scons_opt=0;38;5;243:*.travis.yml=0;38;5;186:*.gitmodules=0;38;5;149:*.synctex.gz=0;38;5;243:*LICENSE-MIT=0;38;5;249:*MANIFEST.in=0;38;5;149:*Makefile.in=0;38;5;243:*Makefile.am=0;38;5;149:*INSTALL.txt=0;38;5;16;48;5;186:*configure.ac=0;38;5;149:*.applescript=0;38;5;48:*appveyor.yml=0;38;5;186:*.fdb_latexmk=0;38;5;243:*CONTRIBUTORS=0;38;5;16;48;5;186:*.clang-format=0;38;5;149:*LICENSE-APACHE=0;38;5;249:*CMakeLists.txt=0;38;5;149:*CMakeCache.txt=0;38;5;243:*.gitattributes=0;38;5;149:*CONTRIBUTORS.md=0;38;5;16;48;5;186:*.sconsign.dblite=0;38;5;243:*requirements.txt=0;38;5;149:*CONTRIBUTORS.txt=0;38;5;16;48;5;186:*package-lock.json=0;38;5;243:*.CFUserTextEncoding=0;38;5;243
";
fn main() {
let result = run();
match result {
Ok(exit_code) => {
process::exit(exit_code.into());
}
Err(err) => {
eprintln!("[fd error]: {:#}", err);
process::exit(ExitCode::GeneralError.into());
}
}
}
fn run() -> Result<ExitCode> {
let matches = app::build_app().get_matches_from(env::args_os());
// Set the current working directory of the process
set_working_dir(&matches)?;
let current_directory = Path::new(".");
ensure_current_directory_exists(current_directory)?;
let search_paths = extract_search_paths(&matches, current_directory)?;
let pattern = extract_search_pattern(&matches)?;
ensure_search_pattern_is_not_a_path(&matches, pattern)?;
let pattern_regex = build_pattern_regex(&matches, pattern)?;
let config = construct_config(matches, &pattern_regex)?;
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regex)?;
let re = build_regex(pattern_regex, &config)?;
walk::scan(&search_paths, Arc::new(re), Arc::new(config))
}
fn set_working_dir(matches: &clap::ArgMatches) -> Result<()> {
if let Some(base_directory) = matches.value_of_os("base-directory") {
let base_directory = Path::new(base_directory);
if !filesystem::is_existing_directory(base_directory) {
@ -69,15 +97,20 @@ fn run() -> Result<ExitCode> {
)
})?;
}
Ok(())
}
let current_directory = Path::new(".");
if !filesystem::is_existing_directory(current_directory) {
return Err(anyhow!(
fn ensure_current_directory_exists(current_directory: &Path) -> Result<()> {
if filesystem::is_existing_directory(current_directory) {
Ok(())
} else {
Err(anyhow!(
"Could not retrieve current directory (has it been deleted?)."
));
))
}
}
// Get the search pattern
fn extract_search_pattern<'a>(matches: &'a clap::ArgMatches) -> Result<&'a str> {
let pattern = matches
.value_of_os("pattern")
.map(|p| {
@ -86,54 +119,57 @@ fn run() -> Result<ExitCode> {
})
.transpose()?
.unwrap_or("");
Ok(pattern)
}
// Get one or more root directories to search.
let passed_arguments = matches
fn extract_search_paths(
matches: &clap::ArgMatches,
current_directory: &Path,
) -> Result<Vec<PathBuf>> {
let mut search_paths = matches
.values_of_os("path")
.or_else(|| matches.values_of_os("search-path"));
let mut search_paths = if let Some(paths) = passed_arguments {
let mut directories = vec![];
for path in paths {
let path_buffer = PathBuf::from(path);
if filesystem::is_existing_directory(&path_buffer) {
directories.push(path_buffer);
} else {
print_error(format!(
"Search path '{}' is not a directory.",
path_buffer.to_string_lossy()
));
}
}
directories
} else {
vec![current_directory.to_path_buf()]
};
// Check if we have no valid search paths.
.or_else(|| matches.values_of_os("search-path"))
.map_or_else(
|| vec![current_directory.to_path_buf()],
|paths| {
paths
.filter_map(|path| {
let path_buffer = PathBuf::from(path);
if filesystem::is_existing_directory(&path_buffer) {
Some(path_buffer)
} else {
print_error(format!(
"Search path '{}' is not a directory.",
path_buffer.to_string_lossy()
));
None
}
})
.collect()
},
);
if search_paths.is_empty() {
return Err(anyhow!("No valid search paths given."));
}
if matches.is_present("absolute-path") {
search_paths = search_paths
.iter()
.map(|path_buffer| {
path_buffer
.normalize()
.and_then(|pb| filesystem::absolute_path(pb.as_path()))
.unwrap()
})
.collect();
update_to_absolute_paths(&mut search_paths);
}
Ok(search_paths)
}
// Detect if the user accidentally supplied a path instead of a search pattern
fn update_to_absolute_paths(search_paths: &mut [PathBuf]) {
for buffer in search_paths.iter_mut() {
*buffer = filesystem::absolute_path(buffer.normalize().unwrap().as_path()).unwrap();
}
}
/// Detect if the user accidentally supplied a path instead of a search pattern
fn ensure_search_pattern_is_not_a_path(matches: &clap::ArgMatches, pattern: &str) -> Result<()> {
if !matches.is_present("full-path")
&& pattern.contains(std::path::MAIN_SEPARATOR)
&& Path::new(pattern).is_dir()
{
return Err(anyhow!(
Err(anyhow!(
"The search pattern '{pattern}' contains a path-separation character ('{sep}') \
and will not lead to any search results.\n\n\
If you want to search for all files inside the '{pattern}' directory, use a match-all pattern:\n\n \
@ -142,10 +178,14 @@ fn run() -> Result<ExitCode> {
fd --full-path '{pattern}'",
pattern = pattern,
sep = std::path::MAIN_SEPARATOR,
));
))
} else {
Ok(())
}
}
let pattern_regex = if matches.is_present("glob") && !pattern.is_empty() {
fn build_pattern_regex(matches: &clap::ArgMatches, pattern: &str) -> Result<String> {
Ok(if matches.is_present("glob") && !pattern.is_empty() {
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
glob.regex().to_owned()
} else if matches.is_present("fixed-strings") {
@ -153,17 +193,46 @@ fn run() -> Result<ExitCode> {
regex::escape(pattern)
} else {
String::from(pattern)
};
})
}
fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> {
match (cfg!(windows), path_separator) {
(true, Some(sep)) if sep.len() > 1 => Err(anyhow!(
"A path separator must be exactly one byte, but \
the given separator is {} bytes: '{}'.\n\
In some shells on Windows, '/' is automatically \
expanded. Try to use '//' instead.",
sep.len(),
sep
)),
_ => Ok(()),
}
}
fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Config> {
// The search will be case-sensitive if the command line flag is set or
// if the pattern has an uppercase character (smart case).
let case_sensitive = !matches.is_present("ignore-case")
&& (matches.is_present("case-sensitive") || pattern_has_uppercase_char(&pattern_regex));
&& (matches.is_present("case-sensitive") || pattern_has_uppercase_char(pattern_regex));
let path_separator = matches
.value_of("path-separator")
.map_or_else(filesystem::default_path_separator, |s| Some(s.to_owned()));
check_path_separator_length(path_separator.as_deref())?;
let size_limits = extract_size_limits(&matches)?;
let time_constraints = extract_time_constraints(&matches)?;
#[cfg(unix)]
let owner_constraint = matches
.value_of("owner")
.map(OwnerFilter::from_string)
.transpose()?
.flatten();
#[cfg(windows)]
let ansi_colors_support =
ansi_term::enable_ansi_support().is_ok() || std::env::var_os("TERM").is_some();
#[cfg(not(windows))]
let ansi_colors_support = true;
@ -174,163 +243,14 @@ fn run() -> Result<ExitCode> {
_ => ansi_colors_support && env::var_os("NO_COLOR").is_none() && interactive_terminal,
};
let path_separator = matches
.value_of("path-separator")
.map_or_else(filesystem::default_path_separator, |s| Some(s.to_owned()));
#[cfg(windows)]
{
if let Some(ref sep) = path_separator {
if sep.len() > 1 {
return Err(anyhow!(
"A path separator must be exactly one byte, but \
the given separator is {} bytes: '{}'.\n\
In some shells on Windows, '/' is automatically \
expanded. Try to use '//' instead.",
sep.len(),
sep
));
};
};
}
let ls_colors = if colored_output {
Some(LsColors::from_env().unwrap_or_else(|| LsColors::from_string(DEFAULT_LS_COLORS)))
} else {
None
};
let command = extract_command(&matches, path_separator.as_deref(), colored_output)?;
let command = if let Some(args) = matches.values_of("exec") {
Some(CommandTemplate::new(args, path_separator.clone()))
} else if let Some(args) = matches.values_of("exec-batch") {
Some(CommandTemplate::new_batch(args, path_separator.clone())?)
} else if matches.is_present("list-details") {
let color = matches.value_of("color").unwrap_or("auto");
let color_arg = ["--color=", color].concat();
#[allow(unused)]
let gnu_ls = |command_name| {
// Note: we use short options here (instead of --long-options) to support more
// platforms (like BusyBox).
vec![
command_name,
"-l", // long listing format
"-h", // human readable file sizes
"-d", // list directories themselves, not their contents
&color_arg,
]
};
let cmd: Vec<&str> = if cfg!(unix) {
if !cfg!(any(
target_os = "macos",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)) {
// Assume ls is GNU ls
gnu_ls("ls")
} else {
// MacOS, DragonFlyBSD, FreeBSD
use std::process::{Command, Stdio};
// Use GNU ls, if available (support for --color=auto, better LS_COLORS support)
let gnu_ls_exists = Command::new("gls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("gls")
} else {
let mut cmd = vec![
"ls", // BSD version of ls
"-l", // long listing format
"-h", // '--human-readable' is not available, '-h' is
"-d", // '--directory' is not available, but '-d' is
];
if !cfg!(any(target_os = "netbsd", target_os = "openbsd")) && colored_output {
// -G is not available in NetBSD's and OpenBSD's ls
cmd.push("-G");
}
cmd
}
}
} else if cfg!(windows) {
use std::process::{Command, Stdio};
// Use GNU ls, if available
let gnu_ls_exists = Command::new("ls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("ls")
} else {
return Err(anyhow!(
"'fd --list-details' is not supported on Windows unless GNU 'ls' is installed."
));
}
} else {
return Err(anyhow!(
"'fd --list-details' is not supported on this platform."
));
};
Some(CommandTemplate::new_batch(&cmd, path_separator.clone()).unwrap())
} else {
None
};
let size_limits = if let Some(vs) = matches.values_of("size") {
vs.map(|sf| {
SizeFilter::from_string(sf)
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", sf))
})
.collect::<Result<Vec<_>>>()?
} else {
vec![]
};
let now = time::SystemTime::now();
let mut time_constraints: Vec<TimeFilter> = Vec::new();
if let Some(t) = matches.value_of("changed-within") {
if let Some(f) = TimeFilter::after(&now, t) {
time_constraints.push(f);
} else {
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
if let Some(t) = matches.value_of("changed-before") {
if let Some(f) = TimeFilter::before(&now, t) {
time_constraints.push(f);
} else {
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
#[cfg(unix)]
let owner_constraint = if let Some(s) = matches.value_of("owner") {
OwnerFilter::from_string(s)?
} else {
None
};
let config = Options {
Ok(Config {
case_sensitive,
search_full_path: matches.is_present("full-path"),
ignore_hidden: !(matches.is_present("hidden")
@ -340,6 +260,10 @@ fn run() -> Result<ExitCode> {
read_vcsignore: !(matches.is_present("no-ignore")
|| matches.is_present("rg-alias-hidden-ignore")
|| matches.is_present("no-ignore-vcs")),
read_parent_ignore: !(matches.is_present("no-ignore")
|| matches.is_present("rg-alias-hidden-ignore")
|| matches.is_present("no-ignore-vcs")
|| matches.is_present("no-ignore-parent")),
read_global_ignore: !(matches.is_present("no-ignore")
|| matches.is_present("rg-alias-hidden-ignore")
|| matches.is_present("no-global-ignore-file")),
@ -424,6 +348,12 @@ fn run() -> Result<ExitCode> {
})
.transpose()?,
command: command.map(Arc::new),
batch_size: matches
.value_of("batch-size")
.map(|n| n.parse::<usize>())
.transpose()
.context("Failed to parse --batch-size argument")?
.unwrap_or_default(),
exclude_patterns: matches
.values_of("exclude")
.map(|v| v.map(|p| String::from("!") + p).collect())
@ -451,20 +381,177 @@ fn run() -> Result<ExitCode> {
None
}
}),
};
})
}
if cfg!(unix)
&& config.ignore_hidden
&& pattern_matches_strings_with_leading_dot(&pattern_regex)
{
fn extract_command(
matches: &clap::ArgMatches,
path_separator: Option<&str>,
colored_output: bool,
) -> Result<Option<CommandTemplate>> {
None.or_else(|| {
matches.values_of("exec").map(|args| {
Ok(CommandTemplate::new(
args,
path_separator.map(str::to_string),
))
})
})
.or_else(|| {
matches
.values_of("exec-batch")
.map(|args| CommandTemplate::new_batch(args, path_separator.map(str::to_string)))
})
.or_else(|| {
if !matches.is_present("list-details") {
return None;
}
let color = matches.value_of("color").unwrap_or("auto");
let color_arg = format!("--color={}", color);
let res = determine_ls_command(&color_arg, colored_output).map(|cmd| {
CommandTemplate::new_batch(cmd, path_separator.map(str::to_string)).unwrap()
});
Some(res)
})
.transpose()
}
fn determine_ls_command(color_arg: &str, colored_output: bool) -> Result<Vec<&str>> {
#[allow(unused)]
let gnu_ls = |command_name| {
// Note: we use short options here (instead of --long-options) to support more
// platforms (like BusyBox).
vec![
command_name,
"-l", // long listing format
"-h", // human readable file sizes
"-d", // list directories themselves, not their contents
color_arg,
]
};
let cmd: Vec<&str> = if cfg!(unix) {
if !cfg!(any(
target_os = "macos",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)) {
// Assume ls is GNU ls
gnu_ls("ls")
} else {
// MacOS, DragonFlyBSD, FreeBSD
use std::process::{Command, Stdio};
// Use GNU ls, if available (support for --color=auto, better LS_COLORS support)
let gnu_ls_exists = Command::new("gls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("gls")
} else {
let mut cmd = vec![
"ls", // BSD version of ls
"-l", // long listing format
"-h", // '--human-readable' is not available, '-h' is
"-d", // '--directory' is not available, but '-d' is
];
if !cfg!(any(target_os = "netbsd", target_os = "openbsd")) && colored_output {
// -G is not available in NetBSD's and OpenBSD's ls
cmd.push("-G");
}
cmd
}
}
} else if cfg!(windows) {
use std::process::{Command, Stdio};
// Use GNU ls, if available
let gnu_ls_exists = Command::new("ls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("ls")
} else {
return Err(anyhow!(
"'fd --list-details' is not supported on Windows unless GNU 'ls' is installed."
));
}
} else {
return Err(anyhow!(
"'fd --list-details' is not supported on this platform."
));
};
Ok(cmd)
}
fn extract_size_limits(matches: &clap::ArgMatches) -> Result<Vec<SizeFilter>> {
matches.values_of("size").map_or(Ok(Vec::new()), |vs| {
vs.map(|sf| {
SizeFilter::from_string(sf)
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", sf))
})
.collect::<Result<Vec<_>>>()
})
}
fn extract_time_constraints(matches: &clap::ArgMatches) -> Result<Vec<TimeFilter>> {
let now = time::SystemTime::now();
let mut time_constraints: Vec<TimeFilter> = Vec::new();
if let Some(t) = matches.value_of("changed-within") {
if let Some(f) = TimeFilter::after(&now, t) {
time_constraints.push(f);
} else {
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
if let Some(t) = matches.value_of("changed-before") {
if let Some(f) = TimeFilter::before(&now, t) {
time_constraints.push(f);
} else {
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
Ok(time_constraints)
}
fn ensure_use_hidden_option_for_leading_dot_pattern(
config: &Config,
pattern_regex: &str,
) -> Result<()> {
if cfg!(unix) && config.ignore_hidden && pattern_matches_strings_with_leading_dot(pattern_regex)
{
Err(anyhow!(
"The pattern seems to only match files with a leading dot, but hidden files are \
filtered by default. Consider adding -H/--hidden to search hidden files as well \
or adjust your search pattern."
));
))
} else {
Ok(())
}
}
let re = RegexBuilder::new(&pattern_regex)
fn build_regex(pattern_regex: String, config: &Config) -> Result<regex::bytes::Regex> {
RegexBuilder::new(&pattern_regex)
.case_insensitive(!config.case_sensitive)
.dot_matches_new_line(true)
.build()
@ -475,20 +562,5 @@ fn run() -> Result<ExitCode> {
also use the '--glob' option to match on a glob pattern.",
e.to_string()
)
})?;
walk::scan(&search_paths, Arc::new(re), Arc::new(config))
}
fn main() {
let result = run();
match result {
Ok(exit_code) => {
process::exit(exit_code.into());
}
Err(err) => {
eprintln!("[fd error]: {:#}", err);
process::exit(ExitCode::GeneralError.into());
}
}
})
}

View File

@ -6,10 +6,10 @@ use std::sync::Arc;
use lscolors::{LsColors, Style};
use crate::config::Config;
use crate::error::print_error;
use crate::exit_codes::ExitCode;
use crate::filesystem::strip_current_dir;
use crate::options::Options;
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
@ -19,7 +19,7 @@ fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
pub fn print_entry(
stdout: &mut StdoutLock,
entry: &Path,
config: &Options,
config: &Config,
wants_to_quit: &Arc<AtomicBool>,
) {
let path = if entry.is_absolute() {
@ -49,7 +49,7 @@ pub fn print_entry(
fn print_entry_colorized(
stdout: &mut StdoutLock,
path: &Path,
config: &Options,
config: &Config,
ls_colors: &LsColors,
wants_to_quit: &Arc<AtomicBool>,
) -> io::Result<()> {
@ -85,7 +85,7 @@ fn print_entry_colorized(
fn print_entry_uncolorized_base(
stdout: &mut StdoutLock,
path: &Path,
config: &Options,
config: &Config,
) -> io::Result<()> {
let separator = if config.null_separator { "\0" } else { "\n" };
@ -100,7 +100,7 @@ fn print_entry_uncolorized_base(
fn print_entry_uncolorized(
stdout: &mut StdoutLock,
path: &Path,
config: &Options,
config: &Config,
) -> io::Result<()> {
print_entry_uncolorized_base(stdout, path, config)
}
@ -109,7 +109,7 @@ fn print_entry_uncolorized(
fn print_entry_uncolorized(
stdout: &mut StdoutLock,
path: &Path,
config: &Options,
config: &Config,
) -> io::Result<()> {
use std::os::unix::ffi::OsStrExt;

View File

@ -13,13 +13,14 @@ use std::time;
use anyhow::{anyhow, Result};
use ignore::overrides::OverrideBuilder;
use ignore::{self, WalkBuilder};
use once_cell::unsync::OnceCell;
use regex::bytes::Regex;
use crate::config::Config;
use crate::error::print_error;
use crate::exec;
use crate::exit_codes::{merge_exitcodes, ExitCode};
use crate::filesystem;
use crate::options::Options;
use crate::output;
/// The receiver thread can either be buffering results or directly streaming to the console.
@ -40,13 +41,15 @@ pub enum WorkerResult {
/// Maximum size of the output buffer before flushing results to the console
pub const MAX_BUFFER_LENGTH: usize = 1000;
/// Default duration until output buffering switches to streaming.
pub const DEFAULT_MAX_BUFFER_TIME: time::Duration = time::Duration::from_millis(100);
/// Recursively scan the given search path for files / pathnames matching the pattern.
///
/// If the `--exec` argument was supplied, this will create a thread pool for executing
/// jobs in parallel from a given command line and the discovered paths. Otherwise, each
/// path will simply be written to standard output.
pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<Options>) -> Result<ExitCode> {
pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> Result<ExitCode> {
let mut path_iter = path_vec.iter();
let first_path_buf = path_iter
.next()
@ -68,7 +71,7 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<Options>) ->
walker
.hidden(config.ignore_hidden)
.ignore(config.read_fdignore)
.parents(config.read_fdignore || config.read_vcsignore)
.parents(config.read_parent_ignore)
.git_ignore(config.read_vcsignore)
.git_global(config.read_vcsignore)
.git_exclude(config.read_vcsignore)
@ -161,7 +164,7 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<Options>) ->
}
fn spawn_receiver(
config: &Arc<Options>,
config: &Arc<Config>,
wants_to_quit: &Arc<AtomicBool>,
rx: Receiver<WorkerResult>,
) -> thread::JoinHandle<ExitCode> {
@ -176,7 +179,13 @@ fn spawn_receiver(
// This will be set to `Some` if the `--exec` argument was supplied.
if let Some(ref cmd) = config.command {
if cmd.in_batch_mode() {
exec::batch(rx, cmd, show_filesystem_errors, enable_output_buffering)
exec::batch(
rx,
cmd,
show_filesystem_errors,
enable_output_buffering,
config.batch_size,
)
} else {
let shared_rx = Arc::new(Mutex::new(rx));
@ -205,12 +214,11 @@ fn spawn_receiver(
}
// Wait for all threads to exit before exiting the program.
let mut results: Vec<ExitCode> = Vec::new();
for h in handles {
results.push(h.join().unwrap());
}
merge_exitcodes(&results)
let exit_codes = handles
.into_iter()
.map(|handle| handle.join().unwrap())
.collect::<Vec<_>>();
merge_exitcodes(exit_codes)
}
} else {
let start = time::Instant::now();
@ -221,9 +229,7 @@ fn spawn_receiver(
let mut mode = ReceiverMode::Buffering;
// Maximum time to wait before we start streaming to the console.
let max_buffer_time = config
.max_buffer_time
.unwrap_or_else(|| time::Duration::from_millis(100));
let max_buffer_time = config.max_buffer_time.unwrap_or(DEFAULT_MAX_BUFFER_TIME);
let stdout = io::stdout();
let mut stdout = stdout.lock();
@ -243,7 +249,7 @@ fn spawn_receiver(
// Have we reached the maximum buffer size or maximum buffering time?
if buffer.len() > MAX_BUFFER_LENGTH
|| time::Instant::now() - start > max_buffer_time
|| start.elapsed() > max_buffer_time
{
// Flush the buffer
for v in &buffer {
@ -266,6 +272,11 @@ fn spawn_receiver(
}
num_results += 1;
if let Some(max_results) = config.max_results {
if num_results >= max_results {
break;
}
}
}
WorkerResult::Error(err) => {
if show_filesystem_errors {
@ -273,21 +284,13 @@ fn spawn_receiver(
}
}
}
if let Some(max_results) = config.max_results {
if num_results >= max_results {
break;
}
}
}
// If we have finished fast enough (faster than max_buffer_time), we haven't streamed
// anything to the console, yet. In this case, sort the results and print them:
if !buffer.is_empty() {
buffer.sort();
for value in buffer {
output::print_entry(&mut stdout, &value, &config, &wants_to_quit);
}
buffer.sort();
for value in buffer {
output::print_entry(&mut stdout, &value, &config, &wants_to_quit);
}
if config.quiet {
@ -299,45 +302,64 @@ fn spawn_receiver(
})
}
pub enum DirEntry {
enum DirEntryInner {
Normal(ignore::DirEntry),
BrokenSymlink(PathBuf),
}
pub struct DirEntry {
inner: DirEntryInner,
metadata: OnceCell<Option<Metadata>>,
}
impl DirEntry {
fn normal(e: ignore::DirEntry) -> Self {
Self {
inner: DirEntryInner::Normal(e),
metadata: OnceCell::new(),
}
}
fn broken_symlink(path: PathBuf) -> Self {
Self {
inner: DirEntryInner::BrokenSymlink(path),
metadata: OnceCell::new(),
}
}
pub fn path(&self) -> &Path {
match self {
DirEntry::Normal(e) => e.path(),
DirEntry::BrokenSymlink(pathbuf) => pathbuf.as_path(),
match &self.inner {
DirEntryInner::Normal(e) => e.path(),
DirEntryInner::BrokenSymlink(pathbuf) => pathbuf.as_path(),
}
}
pub fn file_type(&self) -> Option<FileType> {
match self {
DirEntry::Normal(e) => e.file_type(),
DirEntry::BrokenSymlink(pathbuf) => {
pathbuf.symlink_metadata().map(|m| m.file_type()).ok()
}
match &self.inner {
DirEntryInner::Normal(e) => e.file_type(),
DirEntryInner::BrokenSymlink(_) => self.metadata().map(|m| m.file_type()),
}
}
pub fn metadata(&self) -> Option<Metadata> {
match self {
DirEntry::Normal(e) => e.metadata().ok(),
DirEntry::BrokenSymlink(_) => None,
}
pub fn metadata(&self) -> Option<&Metadata> {
self.metadata
.get_or_init(|| match &self.inner {
DirEntryInner::Normal(e) => e.metadata().ok(),
DirEntryInner::BrokenSymlink(path) => path.symlink_metadata().ok(),
})
.as_ref()
}
pub fn depth(&self) -> Option<usize> {
match self {
DirEntry::Normal(e) => Some(e.depth()),
DirEntry::BrokenSymlink(_) => None,
match &self.inner {
DirEntryInner::Normal(e) => Some(e.depth()),
DirEntryInner::BrokenSymlink(_) => None,
}
}
}
fn spawn_senders(
config: &Arc<Options>,
config: &Arc<Config>,
wants_to_quit: &Arc<AtomicBool>,
pattern: Arc<Regex>,
parallel_walker: ignore::WalkParallel,
@ -359,7 +381,7 @@ fn spawn_senders(
// Skip the root directory entry.
return ignore::WalkState::Continue;
}
Ok(e) => DirEntry::Normal(e),
Ok(e) => DirEntry::normal(e),
Err(ignore::Error::WithPath {
path,
err: inner_err,
@ -371,7 +393,7 @@ fn spawn_senders(
.ok()
.map_or(false, |m| m.file_type().is_symlink()) =>
{
DirEntry::BrokenSymlink(path)
DirEntry::broken_symlink(path)
}
_ => {
return match tx_thread.send(WorkerResult::Error(ignore::Error::WithPath {
@ -440,7 +462,7 @@ fn spawn_senders(
#[cfg(unix)]
{
if let Some(ref owner_constraint) = config.owner_constraint {
if let Ok(ref metadata) = entry_path.metadata() {
if let Some(metadata) = entry.metadata() {
if !owner_constraint.matches(metadata) {
return ignore::WalkState::Continue;
}
@ -453,7 +475,7 @@ fn spawn_senders(
// Filter out unwanted sizes if it is a file and we have been given size constraints.
if !config.size_constraints.is_empty() {
if entry_path.is_file() {
if let Ok(metadata) = entry_path.metadata() {
if let Some(metadata) = entry.metadata() {
let file_size = metadata.len();
if config
.size_constraints
@ -473,7 +495,7 @@ fn spawn_senders(
// Filter out unwanted modification times
if !config.time_constraints.is_empty() {
let mut matched = false;
if let Ok(metadata) = entry_path.metadata() {
if let Some(metadata) = entry.metadata() {
if let Ok(modified) = metadata.modified() {
matched = config
.time_constraints

View File

@ -499,6 +499,78 @@ fn test_gitignore_and_fdignore() {
);
}
/// Ignore parent ignore files (--no-ignore-parent)
#[test]
fn test_no_ignore_parent() {
let dirs = &["inner"];
let files = &[
"inner/parent-ignored",
"inner/child-ignored",
"inner/not-ignored",
];
let te = TestEnv::new(dirs, files);
// Ignore 'parent-ignored' in root
fs::File::create(te.test_root().join(".gitignore"))
.unwrap()
.write_all(b"parent-ignored")
.unwrap();
// Ignore 'child-ignored' in inner
fs::File::create(te.test_root().join("inner/.gitignore"))
.unwrap()
.write_all(b"child-ignored")
.unwrap();
te.assert_output_subdirectory("inner", &[], "not-ignored");
te.assert_output_subdirectory(
"inner",
&["--no-ignore-parent"],
"parent-ignored
not-ignored",
);
}
/// Ignore parent ignore files (--no-ignore-parent) with an inner git repo
#[test]
fn test_no_ignore_parent_inner_git() {
let dirs = &["inner"];
let files = &[
"inner/parent-ignored",
"inner/child-ignored",
"inner/not-ignored",
];
let te = TestEnv::new(dirs, files);
// Make the inner folder also appear as a git repo
fs::create_dir_all(te.test_root().join("inner/.git")).unwrap();
// Ignore 'parent-ignored' in root
fs::File::create(te.test_root().join(".gitignore"))
.unwrap()
.write_all(b"parent-ignored")
.unwrap();
// Ignore 'child-ignored' in inner
fs::File::create(te.test_root().join("inner/.gitignore"))
.unwrap()
.write_all(b"child-ignored")
.unwrap();
te.assert_output_subdirectory(
"inner",
&[],
"not-ignored
parent-ignored",
);
te.assert_output_subdirectory(
"inner",
&["--no-ignore-parent"],
"not-ignored
parent-ignored",
);
}
/// Precedence of .fdignore files
#[test]
fn test_custom_ignore_precedence() {
@ -1347,6 +1419,48 @@ fn test_exec_batch() {
}
}
#[test]
fn test_exec_batch_with_limit() {
// TODO Test for windows
if cfg!(windows) {
return;
}
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
te.assert_output(
&["foo", "--batch-size", "0", "--exec-batch", "echo", "{}"],
"a.foo one/b.foo one/two/C.Foo2 one/two/c.foo one/two/three/d.foo one/two/three/directory_foo",
);
let output = te.assert_success_and_get_output(
".",
&["foo", "--batch-size=2", "--exec-batch", "echo", "{}"],
);
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
assert_eq!(2, line.split_whitespace().count());
}
let mut paths: Vec<_> = stdout
.lines()
.flat_map(|line| line.split_whitespace())
.collect();
paths.sort_unstable();
assert_eq!(
&paths,
&[
"a.foo",
"one/b.foo",
"one/two/C.Foo2",
"one/two/c.foo",
"one/two/three/d.foo",
"one/two/three/directory_foo"
],
);
}
/// Shell script execution (--exec) with a custom --path-separator
#[test]
fn test_exec_with_separator() {
@ -1581,9 +1695,22 @@ fn create_file_with_modified<P: AsRef<Path>>(path: P, duration_in_secs: u64) {
filetime::set_file_times(&path, ft, ft).expect("time modification failed");
}
#[cfg(test)]
fn remove_symlink<P: AsRef<Path>>(path: P) {
#[cfg(unix)]
fs::remove_file(path).expect("remove symlink");
// On Windows, symlinks remember whether they point to files or directories, so try both
#[cfg(windows)]
fs::remove_file(path.as_ref())
.or_else(|_| fs::remove_dir(path.as_ref()))
.expect("remove symlink");
}
#[test]
fn test_modified_relative() {
let te = TestEnv::new(&[], &[]);
remove_symlink(te.test_root().join("symlink"));
create_file_with_modified(te.test_root().join("foo_0_now"), 0);
create_file_with_modified(te.test_root().join("bar_1_min"), 60);
create_file_with_modified(te.test_root().join("foo_10_min"), 600);
@ -1621,8 +1748,9 @@ fn change_file_modified<P: AsRef<Path>>(path: P, iso_date: &str) {
}
#[test]
fn test_modified_asolute() {
fn test_modified_absolute() {
let te = TestEnv::new(&[], &["15mar2018", "30dec2017"]);
remove_symlink(te.test_root().join("symlink"));
change_file_modified(te.test_root().join("15mar2018"), "2018-03-15T12:00:00Z");
change_file_modified(te.test_root().join("30dec2017"), "2017-12-30T23:59:00Z");