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
59 changes: 58 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommandSet>,
pub filter_command: Option<CommandSet>,
pub reject_command: Option<CommandSet>,
}

impl clap::FromArgMatches for Exec {
Expand All @@ -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::<String>("filter")
.map(CommandSet::new)
.transpose()
.map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?;
let reject_command = matches
.get_occurrences::<String>("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<()> {
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<CommandSet>>,

/// If supplied, run this command for each match; only include results where it exits 0.
pub filter_command: Option<Arc<CommandSet>>,

/// If supplied, run this command for each match; exclude results where it exits 0.
pub reject_command: Option<Arc<CommandSet>>,

/// Maximum number of search results to pass to each `command`. If zero, the number is
/// unlimited.
pub batch_size: usize,
Expand Down
23 changes: 23 additions & 0 deletions src/exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<I>(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode
where
I: Iterator<Item = PathBuf>,
Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
};
let command = extract_command(&mut opts, colored_output)?;
let has_command = command.is_some();
let filter_command = opts.exec.filter_command.take();
let reject_command = opts.exec.reject_command.take();

Ok(Config {
case_sensitive,
Expand Down Expand Up @@ -314,6 +316,8 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
.as_deref()
.map(crate::fmt::FormatTemplate::parse),
command: command.map(Arc::new),
filter_command: filter_command.map(Arc::new),
reject_command: reject_command.map(Arc::new),
batch_size: opts.batch_size,
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
ignore_files: std::mem::take(&mut opts.ignore_file),
Expand Down
14 changes: 14 additions & 0 deletions src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,20 @@ impl WorkerState {
}
}

// Apply --filter: only include entries where the command exits 0.
if let Some(ref filter_cmd) = config.filter_command
&& !filter_cmd.matches_filter(entry_path, config.path_separator.as_deref())
{
return WalkState::Continue;
}

// Apply --reject: exclude entries where the command exits 0.
if let Some(ref reject_cmd) = config.reject_command
&& reject_cmd.matches_filter(entry_path, config.path_separator.as_deref())
{
return WalkState::Continue;
}

if config.is_printing()
&& let Some(ls_colors) = &config.ls_colors
{
Expand Down