Merge branch 'master' into no-buffer-single-thread

This commit is contained in:
Thayne McCombs 2021-08-09 01:00:06 -06:00
commit fcaebd2295
11 changed files with 192 additions and 71 deletions

6
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "monthly"

View File

@ -5,6 +5,9 @@
## Bugfixes
- Set default path separator to `/` in MSYS, see #537 and #730 (@aswild)
- fd cannot search files under a RAM disk, see #752
- fd doesn't show substituted drive on Windows, see #365
- Properly handle write errors to devices that are full, see #737
## Changes

72
Cargo.lock generated
View File

@ -1,10 +1,12 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.15"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
@ -29,9 +31,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.35"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4"
checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
[[package]]
name = "atty"
@ -112,9 +114,9 @@ dependencies = [
[[package]]
name = "ctrlc"
version = "3.1.7"
version = "3.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b57a92e9749e10f25a171adcebfafe72991d45e7ec2dcb853e8f83d9dafaeb08"
checksum = "232295399409a8b7ae41276757b5a1cc21032848d42bff2352261f958b3ca29a"
dependencies = [
"nix",
"winapi",
@ -166,6 +168,7 @@ dependencies = [
"lazy_static",
"libc",
"lscolors",
"normpath",
"num_cpus",
"regex",
"regex-syntax",
@ -176,13 +179,13 @@ dependencies = [
[[package]]
name = "filetime"
version = "0.2.13"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c122a393ea57648015bf06fbd3d372378992e86b9ff5a7a497b076a28c79efe"
checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"redox_syscall 0.2.10",
"winapi",
]
@ -217,9 +220,9 @@ dependencies = [
[[package]]
name = "globset"
version = "0.4.6"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a"
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
dependencies = [
"aho-corasick",
"bstr",
@ -290,9 +293,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.80"
version = "0.2.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
[[package]]
name = "log"
@ -314,22 +317,31 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.3.4"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "nix"
version = "0.18.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055"
checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a"
dependencies = [
"bitflags",
"cc",
"cfg-if 0.1.10",
"cfg-if 1.0.0",
"libc",
]
[[package]]
name = "normpath"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27e6e8f70e9fbbe3752d330d769e3424f24b9458ce266df93a3b456902fd696a"
dependencies = [
"winapi",
]
[[package]]
name = "num_cpus"
version = "1.13.0"
@ -383,6 +395,15 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.3.5"
@ -390,26 +411,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
dependencies = [
"getrandom",
"redox_syscall",
"redox_syscall 0.1.57",
]
[[package]]
name = "regex"
version = "1.4.2"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"thread_local",
]
[[package]]
name = "regex-syntax"
version = "0.6.21"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "remove_dir_all"
@ -498,9 +518,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walkdir"

View File

@ -39,7 +39,7 @@ atty = "0.2"
ignore = "0.4.3"
lazy_static = "1.1.0"
num_cpus = "1.8"
regex = "1.0.0"
regex = "1.5.4"
regex-syntax = "0.6"
ctrlc = "3.1"
humantime = "2.0"
@ -47,6 +47,7 @@ lscolors = "0.7"
globset = "0.4"
anyhow = "1.0"
dirs-next = "2.0"
normpath = "0.3"
[dependencies.clap]
version = "2.31.2"
@ -67,7 +68,7 @@ jemallocator = "0.3.0"
[dev-dependencies]
diff = "0.1"
tempdir = "0.3"
filetime = "0.2.1"
filetime = "0.2.14"
[profile.release]
lto = true

View File

@ -646,6 +646,7 @@ cargo install --path .
- [sharkdp](https://github.com/sharkdp)
- [tmccombs](https://github.com/tmccombs)
- [tavianator](https://github.com/tavianator)
## License

54
doc/fd.1 vendored
View File

@ -259,12 +259,22 @@ Provide paths to search as an alternative to the positional \fIpath\fR argument.
\'fd [FLAGS/OPTIONS] \-\-search\-path PATH \-\-search\-path PATH2 [PATTERN]\'
.TP
.BI "\-x, \-\-exec " command
.RS
Execute
.I command
for each search result in parallel (use --threads=1 for sequential command execution). The following placeholders are substituted by a path derived from the current search result:
for each search result in parallel (use --threads=1 for sequential command execution).
Note that all subsequent positional arguments are considered to be arguments to the
.I command
- not to fd.
It is therefore recommended to place the \-x/\-\-exec option last. Alternatively, you can supply
a ';' argument to end the argument list and continue with more fd options.
Most shells require ';' to be escaped: '\\;'.
The following placeholders are substituted before the command is executed:
.RS
.IP {}
path
path (of the current search result)
.IP {/}
basename
.IP {//}
@ -274,15 +284,33 @@ path without file extension
.IP {/.}
basename without file extension
.RE
If no placeholder is present, an implicit "{}" at the end is assumed.
Examples:
- find all *.zip files and unzip them:
fd -e zip -x unzip
- find *.h and *.cpp files and run "clang-format -i .." for each of them:
fd -e h -e cpp -x clang-format -i
- Convert all *.jpg files to *.png files:
fd -e jpg -x convert {} {.}.png
.RE
.TP
.BI "\-X, \-\-exec-batch " command
.RS
Execute
.I command
with all search results at once.
A single occurence of the following placeholders is authorized and substituted by the paths derived from the search results before the command is executed:
once, with all search results as arguments.
One of the following placeholders is substituted before the command is executed:
.RS
.IP {}
path
path (of all search results)
.IP {/}
basename
.IP {//}
@ -292,6 +320,22 @@ path without file extension
.IP {/.}
basename without file extension
.RE
If no placeholder is present, an implicit "{}" at the end is assumed.
Examples:
- Find all test_*.py files and open them in your favorite editor:
fd -g 'test_*.py' -X vim
Note that this executes a single "vim" process with all search results as arguments.
- Find all *.rs files and count the lines with "wc -l ...":
fd -e rs -X wc -l
.RE
.SH PATTERN SYNTAX
The regular expression syntax used by fd is documented here:

View File

@ -298,16 +298,24 @@ pub fn build_app() -> App<'static, 'static> {
.conflicts_with("list-details")
.help("Execute a command for each search result")
.long_help(
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution).\n\
All arguments following --exec are taken to be arguments to the command until the \
argument ';' is encountered.\n\
Each occurrence of the following placeholders is substituted by a path derived from the \
current search result before the command is executed:\n \
'{}': path\n \
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
All positional arguments following --exec are considered to be arguments to the command - not to fd. \
It is therefore recommended to place the '-x'/'--exec' option last.\n\
The following placeholders are substituted before the command is executed:\n \
'{}': path (of the current search result)\n \
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension",
'{/.}': basename without file extension\n\n\
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
Examples:\n\n \
- find all *.zip files and unzip them:\n\n \
fd -e zip -x unzip\n\n \
- find *.h and *.cpp files and run \"clang-format -i ..\" for each of them:\n\n \
fd -e h -e cpp -x clang-format -i\n\n \
- Convert all *.jpg files to *.png files:\n\n \
fd -e jpg -x convert {} {.}.png\
",
),
)
.arg(
@ -321,16 +329,20 @@ pub fn build_app() -> App<'static, 'static> {
.conflicts_with_all(&["exec", "list-details"])
.help("Execute a command with all search results at once")
.long_help(
"Execute a command with all search results at once.\n\
All arguments following --exec-batch are taken to be arguments to the command until the \
argument ';' is encountered.\n\
A single occurrence of the following placeholders is authorized and substituted by the paths derived from the \
search results before the command is executed:\n \
'{}': path\n \
"Execute the given command once, with all search results as arguments.\n\
One of the following placeholders is substituted before the command is executed:\n \
'{}': path (of all search results)\n \
'{/}': basename\n \
'{//}': parent directory\n \
'{.}': path without file extension\n \
'{/.}': basename without file extension",
'{/.}': basename without file extension\n\n\
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
Examples:\n\n \
- Find all test_*.py files and open them in your favorite editor:\n\n \
fd -g 'test_*.py' -X vim\n\n \
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
fd -e rs -X wc -l\
"
),
)
.arg(

View File

@ -7,6 +7,8 @@ use std::io;
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
use std::path::{Path, PathBuf};
use normpath::PathExt;
use crate::walk;
pub fn path_absolute_form(path: &Path) -> io::Result<PathBuf> {
@ -33,10 +35,10 @@ pub fn absolute_path(path: &Path) -> io::Result<PathBuf> {
Ok(path_buf)
}
// Path::is_dir() is not guaranteed to be intuitively correct for "." and ".."
// See: https://github.com/rust-lang/rust/issues/45302
pub fn is_dir(path: &Path) -> bool {
path.is_dir() && (path.file_name().is_some() || path.canonicalize().is_ok())
pub fn is_existing_directory(path: &Path) -> bool {
// Note: we do not use `.exists()` here, as `.` always exists, even if
// the CWD has been deleted.
path.is_dir() && (path.file_name().is_some() || path.normalize().is_ok())
}
#[cfg(any(unix, target_os = "redox"))]

View File

@ -21,6 +21,7 @@ use atty::Stream;
use globset::GlobBuilder;
use lscolors::LsColors;
use regex::bytes::{RegexBuilder, RegexSetBuilder};
use normpath::PathExt;
use crate::error::print_error;
use crate::exec::CommandTemplate;
@ -55,7 +56,7 @@ fn run() -> Result<ExitCode> {
// Set the current working directory of the process
if let Some(base_directory) = matches.value_of_os("base-directory") {
let base_directory = Path::new(base_directory);
if !filesystem::is_dir(base_directory) {
if !filesystem::is_existing_directory(base_directory) {
return Err(anyhow!(
"The '--base-directory' path '{}' is not a directory.",
base_directory.to_string_lossy()
@ -70,7 +71,7 @@ fn run() -> Result<ExitCode> {
}
let current_directory = Path::new(".");
if !filesystem::is_dir(current_directory) {
if !filesystem::is_existing_directory(current_directory) {
return Err(anyhow!(
"Could not retrieve current directory (has it been deleted?)."
));
@ -95,7 +96,7 @@ fn run() -> Result<ExitCode> {
let mut directories = vec![];
for path in paths {
let path_buffer = PathBuf::from(path);
if filesystem::is_dir(&path_buffer) {
if filesystem::is_existing_directory(&path_buffer) {
directories.push(path_buffer);
} else {
print_error(format!(
@ -120,7 +121,7 @@ fn run() -> Result<ExitCode> {
.iter()
.map(|path_buffer| {
path_buffer
.canonicalize()
.normalize()
.and_then(|pb| filesystem::absolute_path(pb.as_path()))
.unwrap()
})
@ -130,7 +131,7 @@ fn run() -> Result<ExitCode> {
// Detect if the user accidentally supplied a path instead of a search pattern
if !matches.is_present("full-path")
&& pattern.contains(std::path::MAIN_SEPARATOR)
&& filesystem::is_dir(Path::new(pattern))
&& Path::new(pattern).is_dir()
{
return Err(anyhow!(
"The search pattern '{pattern}' contains a path-separation character ('{sep}') \
@ -177,6 +178,22 @@ fn run() -> Result<ExitCode> {
.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 {

View File

@ -6,6 +6,7 @@ use std::sync::Arc;
use lscolors::{LsColors, Style};
use crate::error::print_error;
use crate::exit_codes::ExitCode;
use crate::filesystem::strip_current_dir;
use crate::options::Options;
@ -33,9 +34,14 @@ pub fn print_entry(
print_entry_uncolorized(stdout, path, config)
};
if r.is_err() {
// Probably a broken pipe. Exit gracefully.
process::exit(ExitCode::GeneralError.into());
if let Err(e) = r {
if e.kind() == ::std::io::ErrorKind::BrokenPipe {
// Exit gracefully in case of a broken pipe (e.g. 'fd ... | head -n 3').
process::exit(0);
} else {
print_error(format!("Could not write to output: {}", e));
process::exit(ExitCode::GeneralError.into());
}
}
}

View File

@ -5,6 +5,7 @@ use std::io::Write;
use std::path::Path;
use std::time::{Duration, SystemTime};
use normpath::PathExt;
use regex::escape;
use crate::testenv::TestEnv;
@ -23,11 +24,13 @@ static DEFAULT_FILES: &[&str] = &[
"e1 e2",
];
#[allow(clippy::let_and_return)]
fn get_absolute_root_path(env: &TestEnv) -> String {
let path = env
.test_root()
.canonicalize()
.normalize()
.expect("absolute path")
.as_path()
.to_str()
.expect("string")
.to_string();
@ -1090,16 +1093,19 @@ fn test_symlink_as_root() {
fn test_symlink_and_absolute_path() {
let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES);
let expected_path = if cfg!(windows) { "symlink" } else { "one/two" };
te.assert_output_subdirectory(
"symlink",
&["--absolute-path"],
&format!(
"{abs_path}/one/two/c.foo
{abs_path}/one/two/C.Foo2
{abs_path}/one/two/three
{abs_path}/one/two/three/d.foo
{abs_path}/one/two/three/directory_foo",
abs_path = &abs_path
"{abs_path}/{expected_path}/c.foo
{abs_path}/{expected_path}/C.Foo2
{abs_path}/{expected_path}/three
{abs_path}/{expected_path}/three/d.foo
{abs_path}/{expected_path}/three/directory_foo",
abs_path = &abs_path,
expected_path = expected_path
),
);
}
@ -1127,6 +1133,8 @@ fn test_symlink_and_full_path() {
let root = te.system_root();
let prefix = escape(&root.to_string_lossy());
let expected_path = if cfg!(windows) { "symlink" } else { "one/two" };
te.assert_output_subdirectory(
"symlink",
&[
@ -1135,10 +1143,11 @@ fn test_symlink_and_full_path() {
&format!("^{prefix}.*three", prefix = prefix),
],
&format!(
"{abs_path}/one/two/three
{abs_path}/one/two/three/d.foo
{abs_path}/one/two/three/directory_foo",
abs_path = &abs_path
"{abs_path}/{expected_path}/three
{abs_path}/{expected_path}/three/d.foo
{abs_path}/{expected_path}/three/directory_foo",
abs_path = &abs_path,
expected_path = expected_path
),
);
}
@ -1663,7 +1672,7 @@ fn test_base_directory() {
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);
te.assert_output(
&["--base-directory", &abs_base_dir, "foo", &abs_path],
&["--base-directory", abs_base_dir, "foo", &abs_path],
&format!(
"{abs_path}/a.foo
{abs_path}/one/b.foo