Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,15 @@ pub struct Opts {
)]
pub show_errors: bool,

/// Print a message to stderr when the search completes with no matches.
#[arg(
long,
hide_short_help = true,
help = "Print a message when no matches are found",
long_help
)]
pub no_matches_message: bool,

/// Change the current working directory of fd to the provided path. This
/// means that search results will be shown with respect to the given base
/// path. Note that relative paths which are passed to fd via the positional
Expand Down
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ pub struct Config {
/// Whether or not to display filesystem errors
pub show_filesystem_errors: bool,

/// Whether to print a message when the search completes with no matches.
pub no_matches_message: bool,

/// The search pattern supplied on the command line (for status messages).
pub search_pattern: String,

/// The separator used to print file paths.
pub path_separator: Option<String>,

Expand Down
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
pub fn print_error(msg: impl Into<String>) {
eprintln!("[fd error]: {}", msg.into());
}

pub fn print_status(msg: impl Into<String>) {
eprintln!("[fd]: {}", msg.into());
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
#[cfg(unix)]
owner_constraint,
show_filesystem_errors: opts.show_errors,
no_matches_message: opts.no_matches_message,
search_pattern: opts.pattern.clone(),
path_separator,
actual_path_separator,
max_results: opts.max_results(),
Expand Down
13 changes: 13 additions & 0 deletions src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,19 @@ impl<'a, W: Write> ReceiverBuffer<'a, W> {
self.stream()?;
}

if self.num_results == 0
&& self.config.no_matches_message
&& self.config.is_printing()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove printing-mode gate for no-match status

--no-matches-message is described as a search-level status flag, but this condition requires self.config.is_printing(), which is false whenever --exec/--exec-batch is used. That makes the new flag silently ineffective in command-execution workflows (e.g., fd --no-matches-message -x ... PATTERN with zero matches), even though those workflows still perform a search and can end with no results. This should either be emitted regardless of print mode (except quiet) or explicitly documented as unsupported for exec modes.

Useful? React with 👍 / 👎.

&& !self.config.quiet
{
let pattern = &self.config.search_pattern;
if pattern.is_empty() {
crate::error::print_status("No matches found");
} else {
crate::error::print_status(format!("No matches found for pattern: '{pattern}'"));
}
}

if self.config.quiet {
Err(ExitCode::HasResults(self.num_results > 0))
} else {
Expand Down
40 changes: 40 additions & 0 deletions tests/testenv/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,46 @@ impl TestEnv {
}
}

/// Assert that stderr contains the expected substring and stdout is empty.
pub fn assert_stderr_contains(&self, args: &[&str], expected: &str) {
let output = self.run_command(Path::new("."), args);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(expected),
"expected stderr to contain '{expected}', got:\n{stderr}"
);
assert!(
output.stdout.is_empty(),
"expected empty stdout, got:\n{}",
String::from_utf8_lossy(&output.stdout)
);
}

/// Assert that stderr is empty.
pub fn assert_stderr_empty(&self, args: &[&str]) {
let output = self.run_command(Path::new("."), args);
assert!(
output.stderr.is_empty(),
"expected empty stderr, got:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}

/// Assert that both stdout and stderr are empty (exit status is not checked).
pub fn assert_empty_stdout_and_stderr(&self, args: &[&str]) {
let output = self.run_command(Path::new("."), args);
assert!(
output.stdout.is_empty(),
"expected empty stdout, got:\n{}",
String::from_utf8_lossy(&output.stdout)
);
assert!(
output.stderr.is_empty(),
"expected empty stderr, got:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}

/// Assert that calling *fd* with the specified arguments produces the expected error.
pub fn assert_error(&self, args: &[&str], expected: &str) -> process::ExitStatus {
self.assert_error_subdirectory(".", args, Some(expected))
Expand Down
22 changes: 22 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ fn test_simple() {
);
}

/// See: https://github.com/sharkdp/fd/issues/1819
#[test]
fn test_no_matches_message() {
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);

te.assert_output(&["--no-matches-message", "no_such_file"], "");
te.assert_stderr_contains(
&["--no-matches-message", "no_such_file"],
"[fd]: No matches found for pattern: 'no_such_file'",
);

te.assert_output(&["no_such_file"], "");
te.assert_stderr_empty(&["no_such_file"]);
}

#[test]
fn test_no_matches_message_respects_quiet() {
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);

te.assert_empty_stdout_and_stderr(&["-q", "--no-matches-message", "no_such_file"]);
}

static AND_EXTRA_FILES: &[&str] = &[
"a.foo",
"one/b.foo",
Expand Down
Loading