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
|
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.
|
a way to opt out of it. And you can easily get similar behavior by adding `.git/` to your global fdignore file.
|
||||||
See #1457.
|
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
|
## Bugfixes
|
||||||
|
|
||||||
|
|
37
src/cli.rs
37
src/cli.rs
|
@ -776,6 +776,11 @@ impl clap::FromArgMatches for Exec {
|
||||||
.get_occurrences::<String>("exec_batch")
|
.get_occurrences::<String>("exec_batch")
|
||||||
.map(CommandSet::new_batch)
|
.map(CommandSet::new_batch)
|
||||||
})
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
matches
|
||||||
|
.get_occurrences::<String>("filter")
|
||||||
|
.map(CommandSet::new_filter)
|
||||||
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?;
|
.map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?;
|
||||||
Ok(Exec { command })
|
Ok(Exec { command })
|
||||||
|
@ -797,7 +802,7 @@ impl clap::Args for Exec {
|
||||||
.allow_hyphen_values(true)
|
.allow_hyphen_values(true)
|
||||||
.value_terminator(";")
|
.value_terminator(";")
|
||||||
.value_name("cmd")
|
.value_name("cmd")
|
||||||
.conflicts_with("list_details")
|
.conflicts_with_all(["list_details", "exec_batch"])
|
||||||
.help("Execute a command for each search result")
|
.help("Execute a command for each search result")
|
||||||
.long_help(
|
.long_help(
|
||||||
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
|
"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 \
|
fd -g 'test_*.py' -X vim\n\n \
|
||||||
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
|
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
|
||||||
fd -e rs -X wc -l\
|
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>,
|
stdout: Vec<u8>,
|
||||||
stderr: Vec<u8>,
|
stderr: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used to print the results of commands that run on results in a thread-safe way
|
||||||
struct OutputBuffer<'a> {
|
struct OutputBuffer<'a> {
|
||||||
output_permission: &'a Mutex<()>,
|
output_permission: &'a Mutex<()>,
|
||||||
outputs: Vec<Outputs>,
|
outputs: Vec<Outputs>,
|
||||||
|
@ -67,8 +69,9 @@ pub fn execute_commands<I: Iterator<Item = io::Result<Command>>>(
|
||||||
let output = if enable_output_buffering {
|
let output = if enable_output_buffering {
|
||||||
cmd.output()
|
cmd.output()
|
||||||
} else {
|
} else {
|
||||||
// If running on only one thread, don't buffer output
|
// If running on only one thread, don't buffer output; instead just
|
||||||
// Allows for viewing and interacting with intermediate command output
|
// write directly to stdout. Allows for viewing and interacting with
|
||||||
|
// intermediate command output
|
||||||
cmd.spawn().and_then(|c| c.wait_with_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 {
|
if enable_output_buffering {
|
||||||
output_buffer.push(output.stdout, output.stderr);
|
output_buffer.push(output.stdout, output.stderr);
|
||||||
}
|
}
|
||||||
if output.status.code() != Some(0) {
|
if !output.status.success() {
|
||||||
output_buffer.write();
|
output_buffer.write();
|
||||||
return ExitCode::GeneralError;
|
return ExitCode::GeneralError;
|
||||||
}
|
}
|
||||||
|
@ -93,6 +96,59 @@ pub fn execute_commands<I: Iterator<Item = io::Result<Command>>>(
|
||||||
ExitCode::Success
|
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 {
|
pub fn handle_cmd_error(cmd: Option<&Command>, err: io::Error) -> ExitCode {
|
||||||
match (cmd, err) {
|
match (cmd, err) {
|
||||||
(Some(cmd), err) if err.kind() == io::ErrorKind::NotFound => {
|
(Some(cmd), err) if err.kind() == io::ErrorKind::NotFound => {
|
||||||
|
|
|
@ -15,6 +15,7 @@ pub fn job(
|
||||||
cmd: &CommandSet,
|
cmd: &CommandSet,
|
||||||
out_perm: &Mutex<()>,
|
out_perm: &Mutex<()>,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
filter: bool,
|
||||||
) -> ExitCode {
|
) -> ExitCode {
|
||||||
// Output should be buffered when only running a single thread
|
// Output should be buffered when only running a single thread
|
||||||
let buffer_output: bool = config.threads > 1;
|
let buffer_output: bool = config.threads > 1;
|
||||||
|
@ -39,7 +40,9 @@ pub fn job(
|
||||||
config.path_separator.as_deref(),
|
config.path_separator.as_deref(),
|
||||||
out_perm,
|
out_perm,
|
||||||
buffer_output,
|
buffer_output,
|
||||||
|
filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
ret = merge_exitcodes([ret, code]);
|
ret = merge_exitcodes([ret, code]);
|
||||||
}
|
}
|
||||||
// Returns error in case of any error.
|
// Returns error in case of any error.
|
||||||
|
|
|
@ -16,7 +16,7 @@ use argmax::Command;
|
||||||
|
|
||||||
use crate::exit_codes::{merge_exitcodes, ExitCode};
|
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};
|
use self::input::{basename, dirname, remove_extension};
|
||||||
pub use self::job::{batch, job};
|
pub use self::job::{batch, job};
|
||||||
use self::token::{tokenize, Token};
|
use self::token::{tokenize, Token};
|
||||||
|
@ -28,6 +28,8 @@ pub enum ExecutionMode {
|
||||||
OneByOne,
|
OneByOne,
|
||||||
/// Command is run for a batch of results at once
|
/// Command is run for a batch of results at once
|
||||||
Batch,
|
Batch,
|
||||||
|
/// Command is executed for each search result to determine if it should be filtered
|
||||||
|
FilterResults,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[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 {
|
pub fn in_batch_mode(&self) -> bool {
|
||||||
self.mode == ExecutionMode::Batch
|
self.mode == ExecutionMode::Batch
|
||||||
}
|
}
|
||||||
|
@ -86,12 +107,18 @@ impl CommandSet {
|
||||||
path_separator: Option<&str>,
|
path_separator: Option<&str>,
|
||||||
out_perm: &Mutex<()>,
|
out_perm: &Mutex<()>,
|
||||||
buffer_output: bool,
|
buffer_output: bool,
|
||||||
|
filter: bool,
|
||||||
) -> ExitCode {
|
) -> ExitCode {
|
||||||
let commands = self
|
let commands = self
|
||||||
.commands
|
.commands
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.generate(input, path_separator));
|
.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
|
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 {
|
pub fn merge_exitcodes(results: impl IntoIterator<Item = ExitCode>) -> ExitCode {
|
||||||
if results.into_iter().any(ExitCode::is_error) {
|
if results.into_iter().any(ExitCode::is_error) {
|
||||||
return ExitCode::GeneralError;
|
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.
|
// This will be set to `Some` if the `--exec` argument was supplied.
|
||||||
if let Some(ref cmd) = config.command {
|
if let Some(ref cmd) = config.command {
|
||||||
if cmd.in_batch_mode() {
|
match cmd.get_mode() {
|
||||||
exec::batch(rx.into_iter().flatten(), cmd, config)
|
exec::ExecutionMode::Batch => exec::batch(rx.into_iter().flatten(), cmd, config),
|
||||||
} else {
|
exec::ExecutionMode::OneByOne | exec::ExecutionMode::FilterResults => {
|
||||||
let out_perm = Mutex::new(());
|
let out_perm = Mutex::new(());
|
||||||
|
let filter = cmd.get_mode() == exec::ExecutionMode::FilterResults;
|
||||||
|
|
||||||
thread::scope(|scope| {
|
thread::scope(|scope| {
|
||||||
// Each spawned job will store its thread handle in here.
|
// Each spawned job will store its thread handle in here.
|
||||||
let threads = config.threads;
|
let threads = config.threads;
|
||||||
let mut handles = Vec::with_capacity(threads);
|
let mut handles = Vec::with_capacity(threads);
|
||||||
for _ in 0..threads {
|
for _ in 0..threads {
|
||||||
let rx = rx.clone();
|
let rx = rx.clone();
|
||||||
|
|
||||||
// Spawn a job thread that will listen for and execute inputs.
|
// Spawn a job thread that will listen for and execute inputs.
|
||||||
let handle = scope
|
let handle = scope.spawn(|| {
|
||||||
.spawn(|| exec::job(rx.into_iter().flatten(), cmd, &out_perm, config));
|
exec::job(rx.into_iter().flatten(), cmd, &out_perm, config, filter)
|
||||||
|
});
|
||||||
|
|
||||||
// Push the handle of the spawned thread into the vector for later joining.
|
// Push the handle of the spawned thread into the vector for later joining.
|
||||||
handles.push(handle);
|
handles.push(handle);
|
||||||
}
|
}
|
||||||
let exit_codes = handles.into_iter().map(|handle| handle.join().unwrap());
|
let exit_codes = handles.into_iter().map(|handle| handle.join().unwrap());
|
||||||
merge_exitcodes(exit_codes)
|
merge_exitcodes(exit_codes)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let stdout = io::stdout().lock();
|
let stdout = io::stdout().lock();
|
||||||
|
|
Loading…
Reference in New Issue