diff --git a/src/cli.rs b/src/cli.rs index 4dbe547e0..411ede672 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -833,6 +833,8 @@ pub enum HyperlinkWhen { // so we have to use hand-rolled parsing for exec and exec-batch pub struct Exec { pub command: Option, + pub filter_command: Option, + pub reject_command: Option, } impl clap::FromArgMatches for Exec { @@ -847,7 +849,21 @@ impl clap::FromArgMatches for Exec { }) .transpose() .map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?; - Ok(Exec { command }) + let filter_command = matches + .get_occurrences::("filter") + .map(CommandSet::new) + .transpose() + .map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?; + let reject_command = matches + .get_occurrences::("reject") + .map(CommandSet::new) + .transpose() + .map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?; + Ok(Exec { + command, + filter_command, + reject_command, + }) } fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> clap::error::Result<()> { @@ -923,6 +939,47 @@ impl clap::Args for Exec { " ), ) + .arg( + Arg::new("filter") + .action(ArgAction::Append) + .long("filter") + .num_args(1..) + .allow_hyphen_values(true) + .value_terminator(";") + .value_name("cmd") + .help("Filter results by running a command; keep entry if command exits 0") + .long_help( + "Execute a command for each search result and only include the result \ + if the command exits with status 0. All positional arguments following \ + --filter are part of the command (terminated by ';').\n\ + The same placeholders as --exec are supported: '{}', '{/}', '{//}', '{.}', '{/.}'.\n\n\ + Example:\n\n \ + - Find files containing 'TODO':\n\n \ + fd -t f --filter grep -q TODO {}\n\n \ + - Find non-empty directories:\n\n \ + fd -t d --filter test -n \"$(ls -A {})\"\ + " + ), + ) + .arg( + Arg::new("reject") + .action(ArgAction::Append) + .long("reject") + .num_args(1..) + .allow_hyphen_values(true) + .value_terminator(";") + .value_name("cmd") + .help("Filter results by running a command; exclude entry if command exits 0") + .long_help( + "Execute a command for each search result and exclude the result \ + if the command exits with status 0 (inverse of --filter).\n\ + The same placeholders as --exec and --filter are supported.\n\n\ + Example:\n\n \ + - Find files that are NOT symlinks:\n\n \ + fd --reject test -L {}\ + " + ), + ) } fn augment_args_for_update(cmd: Command) -> Command { diff --git a/src/config.rs b/src/config.rs index 708a99333..240f26bff 100644 --- a/src/config.rs +++ b/src/config.rs @@ -93,6 +93,12 @@ pub struct Config { /// If a value is supplied, each item found will be used to generate and execute commands. pub command: Option>, + /// If supplied, run this command for each match; only include results where it exits 0. + pub filter_command: Option>, + + /// If supplied, run this command for each match; exclude results where it exits 0. + pub reject_command: Option>, + /// Maximum number of search results to pass to each `command`. If zero, the number is /// unlimited. pub batch_size: usize, diff --git a/src/exec/mod.rs b/src/exec/mod.rs index c22e0bd2a..6a1e34dc6 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -90,6 +90,29 @@ impl CommandSet { execute_commands(commands, OutputBuffer::new(null_separator), buffer_output) } + /// Run the command for a given path and return true if it exits with status 0. + /// Output is suppressed (redirected to /dev/null). + pub fn matches_filter(&self, input: &Path, path_separator: Option<&str>) -> bool { + for template in &self.commands { + match template.generate(input, path_separator) { + Ok(mut cmd) => { + cmd.stdout(Stdio::null()); + cmd.stderr(Stdio::null()); + match cmd.status() { + Ok(status) => { + if !status.success() { + return false; + } + } + Err(_) => return false, + } + } + Err(_) => return false, + } + } + true + } + pub fn execute_batch(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode where I: Iterator, diff --git a/src/main.rs b/src/main.rs index 80e380fe9..263dfd929 100644 --- a/src/main.rs +++ b/src/main.rs @@ -244,6 +244,8 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result Result