-
-
Notifications
You must be signed in to change notification settings - Fork 824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add --filter argument that filters results based on command exit-code #1545
base: master
Are you sure you want to change the base?
Changes from all commits
a93fa75
30cd3ea
ad9cd5d
ee1de0f
6f61c18
500b851
dc0cc1a
6fe670d
d1b3bf4
aefd6ac
8360c9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -776,6 +776,11 @@ impl clap::FromArgMatches for Exec { | |
.get_occurrences::<String>("exec_batch") | ||
.map(CommandSet::new_batch) | ||
}) | ||
.or_else(|| { | ||
matches | ||
.get_occurrences::<String>("filter") | ||
.map(CommandSet::new_filter) | ||
}) | ||
.transpose() | ||
.map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?; | ||
Ok(Exec { command }) | ||
|
@@ -797,7 +802,7 @@ impl clap::Args for Exec { | |
.allow_hyphen_values(true) | ||
.value_terminator(";") | ||
.value_name("cmd") | ||
.conflicts_with("list_details") | ||
.conflicts_with_all(["list_details", "exec_batch"]) | ||
.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). \ | ||
|
@@ -851,7 +856,35 @@ impl clap::Args for Exec { | |
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( | ||
Arg::new("filter") | ||
.action(ArgAction::Append) | ||
.long("filter") | ||
.short('f') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if we should use a short option for this. After all we don't have a short option for |
||
.num_args(1..) | ||
.allow_hyphen_values(true) | ||
.value_terminator(";") | ||
.value_name("cmd") | ||
.conflicts_with_all(["exec", "exec_batch", "list_details"]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this need to conflict? Logically, it makes sense to allow filtering items first, and then passing the results that succeed through to exec or exec-batch. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this idea, I'll consider how we can make these commands compatible. |
||
.help("Execute a command to determine whether each result should be filtered") | ||
.long_help( | ||
"Execute a command in parallel for each search result, filtering out results where the exit code is non-zero. \ | ||
There is no guarantee of the order commands are executed in, and the order should not be depended upon. \ | ||
All positional arguments following --filter are considered to be arguments to the command - not to fd. \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe mention that you can end with a |
||
It is therefore recommended to place the '-f'/'--filter' 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\n \ | ||
'{{': literal '{' (for escaping)\n \ | ||
'}}': literal '}' (for escaping)\n\n\ | ||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\ | ||
" | ||
), | ||
) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,8 @@ struct Outputs { | |
stdout: Vec<u8>, | ||
stderr: Vec<u8>, | ||
} | ||
|
||
/// Used to print the results of commands that run on results in a thread-safe way | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do weweant to print output from the filter command, or just suppress it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree it would make more sense for the filter command to simply reduce the results that are matched before they're output to the user as usual. |
||
struct OutputBuffer<'a> { | ||
output_permission: &'a Mutex<()>, | ||
outputs: Vec<Outputs>, | ||
|
@@ -67,8 +69,9 @@ pub fn execute_commands<I: Iterator<Item = io::Result<Command>>>( | |
let output = if enable_output_buffering { | ||
cmd.output() | ||
} else { | ||
// If running on only one thread, don't buffer output | ||
// Allows for viewing and interacting with intermediate command output | ||
// If running on only one thread, don't buffer output; instead just | ||
// write directly to stdout. Allows for viewing and interacting with | ||
// intermediate command output | ||
cmd.spawn().and_then(|c| c.wait_with_output()) | ||
}; | ||
|
||
|
@@ -78,7 +81,7 @@ pub fn execute_commands<I: Iterator<Item = io::Result<Command>>>( | |
if enable_output_buffering { | ||
output_buffer.push(output.stdout, output.stderr); | ||
} | ||
if output.status.code() != Some(0) { | ||
if !output.status.success() { | ||
output_buffer.write(); | ||
return ExitCode::GeneralError; | ||
} | ||
|
@@ -93,6 +96,59 @@ pub fn execute_commands<I: Iterator<Item = io::Result<Command>>>( | |
ExitCode::Success | ||
} | ||
|
||
/// Executes a command and pushes the path to the buffer if it succeeded with a | ||
/// non-zero exit code. | ||
pub fn execute_commands_filtering<I: Iterator<Item = io::Result<Command>>>( | ||
path: &std::path::Path, | ||
cmds: I, | ||
out_perm: &Mutex<()>, | ||
enable_output_buffering: bool, | ||
) -> ExitCode { | ||
let mut output_buffer = OutputBuffer::new(out_perm); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Writing to this output buffer skips all the logic in output.rs. The filter code shouldn't be handling output at all. Rather, this should be used as one of the filters that we use to exclude files to be processed in walk.rs in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I based this on how the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, with exec, the output of the command itself is the desired output, but with filter, the desired output is the path (but possibly with some transformations done on it). The question here is what should we do with the output from the filter command, if any (I imagine in most cases there won't be any). Some options are:
I do think if we do print the output it should be to stderr. |
||
|
||
// Convert path to bufferable path string | ||
let path_str = match path.to_str() { | ||
Some(path) => format!("{}\n", path), | ||
None => { | ||
// Probably had non UTF-8 chars in the path somehow | ||
return ExitCode::GeneralError; | ||
} | ||
}; | ||
let path_u8 = path_str.as_bytes().to_vec(); | ||
|
||
for result in cmds { | ||
let mut cmd = match result { | ||
Ok(cmd) => cmd, | ||
Err(e) => return handle_cmd_error(None, e), | ||
}; | ||
|
||
// Spawn the supplied command. | ||
let output = cmd.output(); | ||
|
||
match output { | ||
Ok(output) => { | ||
if output.status.success() { | ||
if enable_output_buffering { | ||
// Push nothing to stderr because, well, there's nothing to push. | ||
output_buffer.push(path_u8.clone(), vec![]); | ||
} else { | ||
print!("{}", path_str); | ||
} | ||
} else { | ||
return ExitCode::GeneralError; | ||
} | ||
} | ||
Err(why) => { | ||
return handle_cmd_error(Some(&cmd), why); | ||
} | ||
} | ||
} | ||
output_buffer.write(); | ||
ExitCode::Success | ||
} | ||
|
||
/// Displays user-friendly error message based on the kind of error that occurred while | ||
/// running a command | ||
pub fn handle_cmd_error(cmd: Option<&Command>, err: io::Error) -> ExitCode { | ||
match (cmd, err) { | ||
(Some(cmd), err) if err.kind() == io::ErrorKind::NotFound => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be implemented in a way that would allow using it along with exec or exec-batch?