mirror of https://github.com/sharkdp/fd.git
Merge 8360c9eaf4
into 8acd7722f0
This commit is contained in:
commit
6dd11c17d5
|
@ -8,6 +8,7 @@
|
|||
was often useful, it also broke some existing workflows, and there wasn't a good way to opt out of it. And there isn't really a good way for us to add
|
||||
a way to opt out of it. And you can easily get similar behavior by adding `.git/` to your global fdignore file.
|
||||
See #1457.
|
||||
- Add `-f` \ `--filter <command>` argument that allows you to filter results based on the output of a command, as requested in #400.
|
||||
|
||||
## Bugfixes
|
||||
|
||||
|
|
37
src/cli.rs
37
src/cli.rs
|
@ -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')
|
||||
.num_args(1..)
|
||||
.allow_hyphen_values(true)
|
||||
.value_terminator(";")
|
||||
.value_name("cmd")
|
||||
.conflicts_with_all(["exec", "exec_batch", "list_details"])
|
||||
.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. \
|
||||
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\
|
||||
"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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);
|
||||
|
||||
// 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 => {
|
||||
|
|
|
@ -15,6 +15,7 @@ pub fn job(
|
|||
cmd: &CommandSet,
|
||||
out_perm: &Mutex<()>,
|
||||
config: &Config,
|
||||
filter: bool,
|
||||
) -> ExitCode {
|
||||
// Output should be buffered when only running a single thread
|
||||
let buffer_output: bool = config.threads > 1;
|
||||
|
@ -39,7 +40,9 @@ pub fn job(
|
|||
config.path_separator.as_deref(),
|
||||
out_perm,
|
||||
buffer_output,
|
||||
filter,
|
||||
);
|
||||
|
||||
ret = merge_exitcodes([ret, code]);
|
||||
}
|
||||
// Returns error in case of any error.
|
||||
|
|
|
@ -16,7 +16,7 @@ use argmax::Command;
|
|||
|
||||
use crate::exit_codes::{merge_exitcodes, ExitCode};
|
||||
|
||||
use self::command::{execute_commands, handle_cmd_error};
|
||||
use self::command::{execute_commands, execute_commands_filtering, handle_cmd_error};
|
||||
use self::input::{basename, dirname, remove_extension};
|
||||
pub use self::job::{batch, job};
|
||||
use self::token::{tokenize, Token};
|
||||
|
@ -28,6 +28,8 @@ pub enum ExecutionMode {
|
|||
OneByOne,
|
||||
/// Command is run for a batch of results at once
|
||||
Batch,
|
||||
/// Command is executed for each search result to determine if it should be filtered
|
||||
FilterResults,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
@ -76,6 +78,25 @@ impl CommandSet {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn new_filter<I, T, S>(input: I) -> Result<CommandSet>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
Ok(CommandSet {
|
||||
mode: ExecutionMode::FilterResults,
|
||||
commands: input
|
||||
.into_iter()
|
||||
.map(CommandTemplate::new)
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_mode(&self) -> ExecutionMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn in_batch_mode(&self) -> bool {
|
||||
self.mode == ExecutionMode::Batch
|
||||
}
|
||||
|
@ -86,12 +107,18 @@ impl CommandSet {
|
|||
path_separator: Option<&str>,
|
||||
out_perm: &Mutex<()>,
|
||||
buffer_output: bool,
|
||||
filter: bool,
|
||||
) -> ExitCode {
|
||||
let commands = self
|
||||
.commands
|
||||
.iter()
|
||||
.map(|c| c.generate(input, path_separator));
|
||||
execute_commands(commands, out_perm, buffer_output)
|
||||
|
||||
if filter {
|
||||
execute_commands_filtering(input, commands, out_perm, buffer_output)
|
||||
} else {
|
||||
execute_commands(commands, out_perm, buffer_output)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_batch<I>(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode
|
||||
|
|
|
@ -43,6 +43,7 @@ impl ExitCode {
|
|||
}
|
||||
}
|
||||
|
||||
/// If any of the exit codes was an error, this returns a GeneralError
|
||||
pub fn merge_exitcodes(results: impl IntoIterator<Item = ExitCode>) -> ExitCode {
|
||||
if results.into_iter().any(ExitCode::is_error) {
|
||||
return ExitCode::GeneralError;
|
||||
|
|
41
src/walk.rs
41
src/walk.rs
|
@ -408,28 +408,31 @@ impl WorkerState {
|
|||
|
||||
// This will be set to `Some` if the `--exec` argument was supplied.
|
||||
if let Some(ref cmd) = config.command {
|
||||
if cmd.in_batch_mode() {
|
||||
exec::batch(rx.into_iter().flatten(), cmd, config)
|
||||
} else {
|
||||
let out_perm = Mutex::new(());
|
||||
match cmd.get_mode() {
|
||||
exec::ExecutionMode::Batch => exec::batch(rx.into_iter().flatten(), cmd, config),
|
||||
exec::ExecutionMode::OneByOne | exec::ExecutionMode::FilterResults => {
|
||||
let out_perm = Mutex::new(());
|
||||
let filter = cmd.get_mode() == exec::ExecutionMode::FilterResults;
|
||||
|
||||
thread::scope(|scope| {
|
||||
// Each spawned job will store its thread handle in here.
|
||||
let threads = config.threads;
|
||||
let mut handles = Vec::with_capacity(threads);
|
||||
for _ in 0..threads {
|
||||
let rx = rx.clone();
|
||||
thread::scope(|scope| {
|
||||
// Each spawned job will store its thread handle in here.
|
||||
let threads = config.threads;
|
||||
let mut handles = Vec::with_capacity(threads);
|
||||
for _ in 0..threads {
|
||||
let rx = rx.clone();
|
||||
|
||||
// Spawn a job thread that will listen for and execute inputs.
|
||||
let handle = scope
|
||||
.spawn(|| exec::job(rx.into_iter().flatten(), cmd, &out_perm, config));
|
||||
// Spawn a job thread that will listen for and execute inputs.
|
||||
let handle = scope.spawn(|| {
|
||||
exec::job(rx.into_iter().flatten(), cmd, &out_perm, config, filter)
|
||||
});
|
||||
|
||||
// Push the handle of the spawned thread into the vector for later joining.
|
||||
handles.push(handle);
|
||||
}
|
||||
let exit_codes = handles.into_iter().map(|handle| handle.join().unwrap());
|
||||
merge_exitcodes(exit_codes)
|
||||
})
|
||||
// Push the handle of the spawned thread into the vector for later joining.
|
||||
handles.push(handle);
|
||||
}
|
||||
let exit_codes = handles.into_iter().map(|handle| handle.join().unwrap());
|
||||
merge_exitcodes(exit_codes)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let stdout = io::stdout().lock();
|
||||
|
|
Loading…
Reference in New Issue