mirror of
https://github.com/sharkdp/fd.git
synced 2026-08-30 17:07:43 +08:00
Add support for batch execution of command
This commit is contained in:
+22
@@ -121,6 +121,16 @@ pub fn build_app() -> App<'static, 'static> {
|
||||
.value_terminator(";")
|
||||
.value_name("cmd"),
|
||||
)
|
||||
.arg(
|
||||
arg("exec-batch")
|
||||
.long("exec-batch")
|
||||
.short("X")
|
||||
.min_values(1)
|
||||
.allow_hyphen_values(true)
|
||||
.value_terminator(";")
|
||||
.value_name("cmd")
|
||||
.conflicts_with("exec"),
|
||||
)
|
||||
.arg(
|
||||
arg("exclude")
|
||||
.long("exclude")
|
||||
@@ -277,6 +287,18 @@ fn usage() -> HashMap<&'static str, Help> {
|
||||
'{//}': parent directory\n \
|
||||
'{.}': path without file extension\n \
|
||||
'{/.}': basename without file extension");
|
||||
doc!(h, "exec-batch"
|
||||
, "Execute a command with all search results at once"
|
||||
, "Execute a command with all search results at once.\n\
|
||||
All arguments following --exec-batch are taken to be arguments to the command until the \
|
||||
argument ';' is encountered.\n\
|
||||
A single occurence of the following placeholders is authorized and substituted by the paths derived from the \
|
||||
search results before the command is executed:\n \
|
||||
'{}': path\n \
|
||||
'{/}': basename\n \
|
||||
'{//}': parent directory\n \
|
||||
'{.}': path without file extension\n \
|
||||
'{/.}': basename without file extension");
|
||||
doc!(h, "exclude"
|
||||
, "Exclude entries that match the given glob pattern"
|
||||
, "Exclude files/directories that match the given glob pattern. This overrides any \
|
||||
|
||||
+2
-2
@@ -9,10 +9,10 @@
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Executes a command.
|
||||
pub fn execute_command(mut cmd: Command, out_perm: Arc<Mutex<()>>) {
|
||||
pub fn execute_command(mut cmd: Command, out_perm: &Mutex<()>) {
|
||||
// Spawn the supplied command.
|
||||
let output = cmd.output();
|
||||
|
||||
|
||||
@@ -44,3 +44,16 @@ pub fn job(
|
||||
cmd.generate_and_execute(&value, Arc::clone(&out_perm));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn batch(rx: Receiver<WorkerResult>, cmd: &CommandTemplate, show_filesystem_errors: bool) {
|
||||
let paths = rx.iter().filter_map(|value| match value {
|
||||
WorkerResult::Entry(val) => Some(val),
|
||||
WorkerResult::Error(err) => {
|
||||
if show_filesystem_errors {
|
||||
print_error!("{}", err);
|
||||
}
|
||||
None
|
||||
}
|
||||
});
|
||||
cmd.generate_and_execute_batch(paths);
|
||||
}
|
||||
|
||||
+110
-9
@@ -13,7 +13,7 @@ mod job;
|
||||
mod token;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -21,9 +21,18 @@ use regex::Regex;
|
||||
|
||||
use self::command::execute_command;
|
||||
use self::input::{basename, dirname, remove_extension};
|
||||
pub use self::job::job;
|
||||
pub use self::job::{batch, job};
|
||||
use self::token::Token;
|
||||
|
||||
/// Execution mode of the command
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ExecutionMode {
|
||||
/// Command is executed for each path found
|
||||
OneByOne,
|
||||
/// Command is run for a batch of results at once
|
||||
Batch,
|
||||
}
|
||||
|
||||
/// Represents a template that is utilized to generate command strings.
|
||||
///
|
||||
/// The template is meant to be coupled with an input in order to generate a command. The
|
||||
@@ -31,10 +40,31 @@ use self::token::Token;
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CommandTemplate {
|
||||
args: Vec<ArgumentTemplate>,
|
||||
mode: ExecutionMode,
|
||||
}
|
||||
|
||||
impl CommandTemplate {
|
||||
pub fn new<I, S>(input: I) -> CommandTemplate
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
Self::build(input, ExecutionMode::OneByOne)
|
||||
}
|
||||
|
||||
pub fn new_batch<I, S>(input: I) -> Result<CommandTemplate, &'static str>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let cmd = Self::build(input, ExecutionMode::Batch);
|
||||
if cmd.tokens_number() > 1 {
|
||||
return Err("Only one placeholder allowed for batch commands");
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
fn build<I, S>(input: I, mode: ExecutionMode) -> CommandTemplate
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
@@ -91,7 +121,19 @@ impl CommandTemplate {
|
||||
args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder]));
|
||||
}
|
||||
|
||||
CommandTemplate { args }
|
||||
CommandTemplate { args, mode }
|
||||
}
|
||||
|
||||
fn tokens_number(&self) -> usize {
|
||||
self.args.iter().filter(|arg| arg.has_tokens()).count()
|
||||
}
|
||||
|
||||
fn prepare_path(input: &Path) -> String {
|
||||
input
|
||||
.strip_prefix(".")
|
||||
.unwrap_or(input)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Generates and executes a command.
|
||||
@@ -99,18 +141,44 @@ impl CommandTemplate {
|
||||
/// Using the internal `args` field, and a supplied `input` variable, a `Command` will be
|
||||
/// build. Once all arguments have been processed, the command is executed.
|
||||
pub fn generate_and_execute(&self, input: &Path, out_perm: Arc<Mutex<()>>) {
|
||||
let input = input
|
||||
.strip_prefix(".")
|
||||
.unwrap_or(input)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let input = Self::prepare_path(input);
|
||||
|
||||
let mut cmd = Command::new(self.args[0].generate(&input).as_ref());
|
||||
for arg in &self.args[1..] {
|
||||
cmd.arg(arg.generate(&input).as_ref());
|
||||
}
|
||||
|
||||
execute_command(cmd, out_perm)
|
||||
execute_command(cmd, &out_perm)
|
||||
}
|
||||
|
||||
pub fn is_batch(&self) -> bool {
|
||||
self.mode == ExecutionMode::Batch
|
||||
}
|
||||
|
||||
pub fn generate_and_execute_batch<I>(&self, paths: I)
|
||||
where
|
||||
I: Iterator<Item = PathBuf>,
|
||||
{
|
||||
let mut cmd = Command::new(self.args[0].generate("").as_ref());
|
||||
let mut paths = paths.map(|p| Self::prepare_path(&p));
|
||||
let mut has_path = false;
|
||||
|
||||
for arg in &self.args[1..] {
|
||||
if arg.has_tokens() {
|
||||
// A single `Tokens` is expected
|
||||
// So we can directy consume the iterator once and for all
|
||||
for path in &mut paths {
|
||||
cmd.arg(arg.generate(&path).as_ref());
|
||||
has_path = true;
|
||||
}
|
||||
} else {
|
||||
cmd.arg(arg.generate("").as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
if has_path {
|
||||
execute_command(cmd, &Mutex::new(()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +193,14 @@ enum ArgumentTemplate {
|
||||
}
|
||||
|
||||
impl ArgumentTemplate {
|
||||
pub fn has_tokens(&self) -> bool {
|
||||
if let ArgumentTemplate::Tokens(_) = self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate<'a>(&'a self, path: &str) -> Cow<'a, str> {
|
||||
use self::Token::*;
|
||||
|
||||
@@ -162,6 +238,7 @@ mod tests {
|
||||
ArgumentTemplate::Text("${SHELL}:".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -175,6 +252,7 @@ mod tests {
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -188,6 +266,7 @@ mod tests {
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Basename]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -201,6 +280,7 @@ mod tests {
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Parent]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -214,6 +294,7 @@ mod tests {
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -231,7 +312,27 @@ mod tests {
|
||||
Token::Text(".ext".into())
|
||||
]),
|
||||
],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_single_batch() {
|
||||
assert_eq!(
|
||||
CommandTemplate::new_batch(&["echo", "{.}"]).unwrap(),
|
||||
CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
mode: ExecutionMode::Batch,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_multiple_batch() {
|
||||
assert!(CommandTemplate::new_batch(&["echo", "{.}", "{}"]).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -142,7 +142,16 @@ fn main() {
|
||||
None
|
||||
};
|
||||
|
||||
let command = matches.values_of("exec").map(CommandTemplate::new);
|
||||
let command = matches
|
||||
.values_of("exec")
|
||||
.map(CommandTemplate::new)
|
||||
.or_else(|| {
|
||||
matches.values_of("exec-batch").map(|m| {
|
||||
CommandTemplate::new_batch(m).unwrap_or_else(|e| {
|
||||
print_error_and_exit!("{}", e);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
let size_limits: Vec<SizeFilter> = matches
|
||||
.values_of("size")
|
||||
|
||||
+26
-22
@@ -126,34 +126,38 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<FdOptions>) {
|
||||
let receiver_thread = thread::spawn(move || {
|
||||
// This will be set to `Some` if the `--exec` argument was supplied.
|
||||
if let Some(ref cmd) = rx_config.command {
|
||||
let shared_rx = Arc::new(Mutex::new(rx));
|
||||
if cmd.is_batch() {
|
||||
exec::batch(rx, cmd, show_filesystem_errors);
|
||||
} else {
|
||||
let shared_rx = Arc::new(Mutex::new(rx));
|
||||
|
||||
let out_perm = Arc::new(Mutex::new(()));
|
||||
let out_perm = Arc::new(Mutex::new(()));
|
||||
|
||||
// TODO: the following line is a workaround to replace the `unsafe` block that was
|
||||
// previously used here to avoid the (unnecessary?) cloning of the command. The
|
||||
// `unsafe` block caused problems on some platforms (SIGILL instructions on Linux) and
|
||||
// therefore had to be removed.
|
||||
let cmd = Arc::new(cmd.clone());
|
||||
// TODO: the following line is a workaround to replace the `unsafe` block that was
|
||||
// previously used here to avoid the (unnecessary?) cloning of the command. The
|
||||
// `unsafe` block caused problems on some platforms (SIGILL instructions on Linux) and
|
||||
// therefore had to be removed.
|
||||
let cmd = Arc::new(cmd.clone());
|
||||
|
||||
// Each spawned job will store it's thread handle in here.
|
||||
let mut handles = Vec::with_capacity(threads);
|
||||
for _ in 0..threads {
|
||||
let rx = Arc::clone(&shared_rx);
|
||||
let cmd = Arc::clone(&cmd);
|
||||
let out_perm = Arc::clone(&out_perm);
|
||||
// Each spawned job will store it's thread handle in here.
|
||||
let mut handles = Vec::with_capacity(threads);
|
||||
for _ in 0..threads {
|
||||
let rx = Arc::clone(&shared_rx);
|
||||
let cmd = Arc::clone(&cmd);
|
||||
let out_perm = Arc::clone(&out_perm);
|
||||
|
||||
// Spawn a job thread that will listen for and execute inputs.
|
||||
let handle =
|
||||
thread::spawn(move || exec::job(rx, cmd, out_perm, show_filesystem_errors));
|
||||
// Spawn a job thread that will listen for and execute inputs.
|
||||
let handle =
|
||||
thread::spawn(move || exec::job(rx, cmd, out_perm, show_filesystem_errors));
|
||||
|
||||
// Push the handle of the spawned thread into the vector for later joining.
|
||||
handles.push(handle);
|
||||
}
|
||||
// Push the handle of the spawned thread into the vector for later joining.
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all threads to exit before exiting the program.
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
// Wait for all threads to exit before exiting the program.
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let start = time::Instant::now();
|
||||
|
||||
+60
-8
@@ -32,6 +32,9 @@ pub struct TestEnv {
|
||||
|
||||
/// Path to the *fd* executable.
|
||||
fd_exe: PathBuf,
|
||||
|
||||
/// Normalize each line by splitting and sorting by whitespace as well
|
||||
normalize_line: bool,
|
||||
}
|
||||
|
||||
/// Create the working directory and the test files.
|
||||
@@ -121,19 +124,24 @@ fn format_output_error(args: &[&str], expected: &str, actual: &str) -> String {
|
||||
}
|
||||
|
||||
/// Normalize the output for comparison.
|
||||
fn normalize_output(s: &str, trim_left: bool) -> String {
|
||||
fn normalize_output(s: &str, trim_left: bool, normalize_line: bool) -> String {
|
||||
// Split into lines and normalize separators.
|
||||
let mut lines = s
|
||||
.replace('\0', "NULL\n")
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let line = if trim_left { line.trim_left() } else { line };
|
||||
line.replace('/', &std::path::MAIN_SEPARATOR.to_string())
|
||||
let line = line.replace('/', &std::path::MAIN_SEPARATOR.to_string());
|
||||
if normalize_line {
|
||||
let mut worlds: Vec<_> = line.split_whitespace().collect();
|
||||
worlds.sort();
|
||||
return worlds.join(" ");
|
||||
}
|
||||
line
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
lines.sort_by_key(|s| s.clone());
|
||||
|
||||
lines.sort();
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
@@ -143,8 +151,17 @@ impl TestEnv {
|
||||
let fd_exe = find_fd_exe();
|
||||
|
||||
TestEnv {
|
||||
temp_dir: temp_dir,
|
||||
fd_exe: fd_exe,
|
||||
temp_dir,
|
||||
fd_exe,
|
||||
normalize_line: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_line(self, normalize: bool) -> TestEnv {
|
||||
TestEnv {
|
||||
temp_dir: self.temp_dir,
|
||||
fd_exe: self.fd_exe,
|
||||
normalize_line: normalize,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,12 +203,47 @@ impl TestEnv {
|
||||
}
|
||||
|
||||
// Normalize both expected and actual output.
|
||||
let expected = normalize_output(expected, true);
|
||||
let actual = normalize_output(&String::from_utf8_lossy(&output.stdout), false);
|
||||
let expected = normalize_output(expected, true, self.normalize_line);
|
||||
let actual = normalize_output(
|
||||
&String::from_utf8_lossy(&output.stdout),
|
||||
false,
|
||||
self.normalize_line,
|
||||
);
|
||||
|
||||
// Compare actual output to expected output.
|
||||
if expected != actual {
|
||||
panic!(format_output_error(args, &expected, &actual));
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that calling *fd* with the specified arguments produces the expected error.
|
||||
pub fn assert_error(&self, args: &[&str], expected: &str) {
|
||||
self.assert_error_subdirectory(".", args, expected)
|
||||
}
|
||||
|
||||
/// Assert that calling *fd* in the specified path under the root working directory,
|
||||
/// and with the specified arguments produces an error with the expected message.
|
||||
fn assert_error_subdirectory<P: AsRef<Path>>(&self, path: P, args: &[&str], expected: &str) {
|
||||
// Setup *fd* command.
|
||||
let mut cmd = process::Command::new(&self.fd_exe);
|
||||
cmd.current_dir(self.temp_dir.path().join(path));
|
||||
cmd.args(args);
|
||||
|
||||
// Run *fd*.
|
||||
let output = cmd.output().expect("fd output");
|
||||
|
||||
// Check for exit status.
|
||||
if output.status.success() {
|
||||
panic!(
|
||||
"fd exited successfully. Expected error {} did not occur.",
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
// Compare actual output to expected output.
|
||||
let actual = String::from_utf8_lossy(&output.stderr);
|
||||
if expected.len() <= actual.len() && expected != &actual[..expected.len()] {
|
||||
panic!(format_output_error(args, &expected, &actual));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -987,6 +987,57 @@ fn assert_exec_output(exec_style: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shell script execution using -exec
|
||||
#[test]
|
||||
fn test_exec_batch() {
|
||||
assert_exec_batch_output("--exec-batch");
|
||||
}
|
||||
|
||||
// Shell script execution using -x
|
||||
#[test]
|
||||
fn test_exec_batch_short_arg() {
|
||||
assert_exec_batch_output("-X");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn assert_exec_batch_output(exec_style: &str) {
|
||||
let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES);
|
||||
let te = te.normalize_line(true);
|
||||
|
||||
// TODO Windows tests: D:file.txt \file.txt \\server\share\file.txt ...
|
||||
if !cfg!(windows) {
|
||||
te.assert_output(
|
||||
&["--absolute-path", "foo", exec_style, "echo"],
|
||||
&format!(
|
||||
"{abs_path}/a.foo {abs_path}/one/b.foo {abs_path}/one/two/C.Foo2 {abs_path}/one/two/c.foo {abs_path}/one/two/three/d.foo {abs_path}/one/two/three/directory_foo",
|
||||
abs_path = &abs_path
|
||||
),
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["foo", exec_style, "echo", "{}"],
|
||||
"a.foo one/b.foo one/two/C.Foo2 one/two/c.foo one/two/three/d.foo one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["foo", exec_style, "echo", "{/}"],
|
||||
"a.foo b.foo C.Foo2 c.foo d.foo directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(&["no_match", exec_style, "echo", "Matched: ", "{/}"], "");
|
||||
|
||||
te.assert_error(
|
||||
&["foo", exec_style, "echo", "{}", "{}"],
|
||||
"[fd error]: Only one placeholder allowed for batch commands",
|
||||
);
|
||||
|
||||
te.assert_error(
|
||||
&["foo", exec_style, "echo", "{/}", ";", "-x", "echo"],
|
||||
"error: The argument '--exec <cmd>' cannot be used with '--exec-batch <cmd>'",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Literal search (--fixed-strings)
|
||||
#[test]
|
||||
fn test_fixed_strings() {
|
||||
|
||||
Reference in New Issue
Block a user