diff --git a/Cargo.toml b/Cargo.toml index c939dc5..6502428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ once_cell = "1.9.0" [dependencies.clap] version = "3.1" -features = ["suggestions", "color", "wrap_help", "cargo"] +features = ["suggestions", "color", "wrap_help", "cargo", "unstable-grouped"] [target.'cfg(unix)'.dependencies] users = "0.11.0" diff --git a/src/app.rs b/src/app.rs index 4ceebcd..01ddc1d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -386,6 +386,7 @@ pub fn build_app() -> Command<'static> { .long("exec") .short('x') .min_values(1) + .multiple_occurrences(true) .allow_hyphen_values(true) .value_terminator(";") .value_name("cmd") @@ -417,6 +418,7 @@ pub fn build_app() -> Command<'static> { .long("exec-batch") .short('X') .min_values(1) + .multiple_occurrences(true) .allow_hyphen_values(true) .value_terminator(";") .value_name("cmd") diff --git a/src/config.rs b/src/config.rs index 5a71714..697744b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::{path::PathBuf, sync::Arc, time::Duration}; use lscolors::LsColors; use regex::bytes::RegexSet; -use crate::exec::CommandTemplate; +use crate::exec::CommandSet; use crate::filetypes::FileTypes; #[cfg(unix)] use crate::filter::OwnerFilter; @@ -83,7 +83,7 @@ pub struct Config { pub extensions: Option, /// If a value is supplied, each item found will be used to generate and execute commands. - pub command: Option>, + pub command: Option>, /// Maximum number of search results to pass to each `command`. If zero, the number is /// unlimited. diff --git a/src/exec/job.rs b/src/exec/job.rs index 85b30f1..feda262 100644 --- a/src/exec/job.rs +++ b/src/exec/job.rs @@ -6,14 +6,14 @@ use crate::error::print_error; use crate::exit_codes::{merge_exitcodes, ExitCode}; use crate::walk::WorkerResult; -use super::CommandTemplate; +use super::CommandSet; /// An event loop that listens for inputs from the `rx` receiver. Each received input will /// generate a command with the supplied command template. The generated command will then /// be executed, and this process will continue until the receiver's sender has closed. pub fn job( rx: Arc>>, - cmd: Arc, + cmd: Arc, out_perm: Arc>, show_filesystem_errors: bool, buffer_output: bool, @@ -39,7 +39,7 @@ pub fn job( // Drop the lock so that other threads can read from the receiver. drop(lock); // Generate a command, execute it and store its exit code. - results.push(cmd.generate_and_execute(&value, Arc::clone(&out_perm), buffer_output)) + results.push(cmd.execute(&value, Arc::clone(&out_perm), buffer_output)) } // Returns error in case of any error. merge_exitcodes(results) @@ -47,7 +47,7 @@ pub fn job( pub fn batch( rx: Receiver, - cmd: &CommandTemplate, + cmd: &CommandSet, show_filesystem_errors: bool, buffer_output: bool, limit: usize, @@ -63,14 +63,14 @@ pub fn batch( }); if limit == 0 { // no limit - return cmd.generate_and_execute_batch(paths, buffer_output); + return cmd.execute_batch(paths, buffer_output); } let mut exit_codes = Vec::new(); let mut peekable = paths.peekable(); while peekable.peek().is_some() { let limited = peekable.by_ref().take(limit); - let exit_code = cmd.generate_and_execute_batch(limited, buffer_output); + let exit_code = cmd.execute_batch(limited, buffer_output); exit_codes.push(exit_code); } merge_exitcodes(exit_codes) diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 3f93acd..e420c81 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -29,44 +29,101 @@ pub enum ExecutionMode { Batch, } +#[derive(Debug, Clone, PartialEq)] +pub struct CommandSet { + mode: ExecutionMode, + path_separator: Option, + commands: Vec, +} + +impl CommandSet { + pub fn new(input: I, path_separator: Option) -> CommandSet + where + I: IntoIterator>, + S: AsRef, + { + CommandSet { + mode: ExecutionMode::OneByOne, + path_separator, + commands: input.into_iter().map(CommandTemplate::new).collect(), + } + } + + pub fn new_batch(input: I, path_separator: Option) -> Result + where + I: IntoIterator>, + S: AsRef, + { + Ok(CommandSet { + mode: ExecutionMode::Batch, + path_separator, + commands: input + .into_iter() + .map(|args| { + let cmd = CommandTemplate::new(args); + if cmd.number_of_tokens() > 1 { + return Err(anyhow!("Only one placeholder allowed for batch commands")); + } + if cmd.args[0].has_tokens() { + return Err(anyhow!( + "First argument of exec-batch is expected to be a fixed executable" + )); + } + Ok(cmd) + }) + .collect::>>()?, + }) + } + + pub fn in_batch_mode(&self) -> bool { + self.mode == ExecutionMode::Batch + } + + pub fn execute( + &self, + input: &Path, + mut out_perm: Arc>, + buffer_output: bool, + ) -> ExitCode { + let path_separator = self.path_separator.as_deref(); + for cmd in &self.commands { + let exit = + cmd.generate_and_execute(input, path_separator, &mut out_perm, buffer_output); + if exit != ExitCode::Success { + return exit; + } + } + ExitCode::Success + } + + pub fn execute_batch(&self, paths: I, buffer_output: bool) -> ExitCode + where + I: Iterator, + { + let path_separator = self.path_separator.as_deref(); + let mut paths = paths.collect::>(); + paths.sort(); + for cmd in &self.commands { + let exit = cmd.generate_and_execute_batch(&paths, path_separator, buffer_output); + if exit != ExitCode::Success { + return exit; + } + } + ExitCode::Success + } +} + /// 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 /// `generate_and_execute()` method will be used to generate a command and execute it. #[derive(Debug, Clone, PartialEq)] -pub struct CommandTemplate { +struct CommandTemplate { args: Vec, - mode: ExecutionMode, - path_separator: Option, } impl CommandTemplate { - pub fn new(input: I, path_separator: Option) -> CommandTemplate - where - I: IntoIterator, - S: AsRef, - { - Self::build(input, ExecutionMode::OneByOne, path_separator) - } - - pub fn new_batch(input: I, path_separator: Option) -> Result - where - I: IntoIterator, - S: AsRef, - { - let cmd = Self::build(input, ExecutionMode::Batch, path_separator); - if cmd.number_of_tokens() > 1 { - return Err(anyhow!("Only one placeholder allowed for batch commands")); - } - if cmd.args[0].has_tokens() { - return Err(anyhow!( - "First argument of exec-batch is expected to be a fixed executable" - )); - } - Ok(cmd) - } - - fn build(input: I, mode: ExecutionMode, path_separator: Option) -> CommandTemplate + fn new(input: I) -> CommandTemplate where I: IntoIterator, S: AsRef, @@ -122,11 +179,7 @@ impl CommandTemplate { args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder])); } - CommandTemplate { - args, - mode, - path_separator, - } + CommandTemplate { args } } fn number_of_tokens(&self) -> usize { @@ -137,44 +190,38 @@ 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( + fn generate_and_execute( &self, input: &Path, - out_perm: Arc>, + path_separator: Option<&str>, + out_perm: &mut Arc>, buffer_output: bool, ) -> ExitCode { - let mut cmd = Command::new(self.args[0].generate(&input, self.path_separator.as_deref())); + let mut cmd = Command::new(self.args[0].generate(&input, path_separator)); for arg in &self.args[1..] { - cmd.arg(arg.generate(&input, self.path_separator.as_deref())); + cmd.arg(arg.generate(&input, path_separator)); } execute_command(cmd, &out_perm, buffer_output) } - pub fn in_batch_mode(&self) -> bool { - self.mode == ExecutionMode::Batch - } - - pub fn generate_and_execute_batch(&self, paths: I, buffer_output: bool) -> ExitCode - where - I: Iterator, - { + fn generate_and_execute_batch( + &self, + paths: &Vec, + path_separator: Option<&str>, + buffer_output: bool, + ) -> ExitCode { let mut cmd = Command::new(self.args[0].generate("", None)); cmd.stdin(Stdio::inherit()); cmd.stdout(Stdio::inherit()); cmd.stderr(Stdio::inherit()); - let mut paths: Vec<_> = paths.collect(); let mut has_path = false; for arg in &self.args[1..] { if arg.has_tokens() { - paths.sort(); - - // A single `Tokens` is expected - // So we can directly consume the iterator once and for all - for path in &mut paths { - cmd.arg(arg.generate(path, self.path_separator.as_deref())); + for path in paths { + cmd.arg(arg.generate(path, path_separator)); has_path = true; } } else { @@ -302,13 +349,15 @@ mod tests { #[test] fn tokens_with_placeholder() { assert_eq!( - CommandTemplate::new(&[&"echo", &"${SHELL}:"], None), - CommandTemplate { - args: vec![ - ArgumentTemplate::Text("echo".into()), - ArgumentTemplate::Text("${SHELL}:".into()), - ArgumentTemplate::Tokens(vec![Token::Placeholder]), - ], + CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]], None), + CommandSet { + commands: vec![CommandTemplate { + args: vec![ + ArgumentTemplate::Text("echo".into()), + ArgumentTemplate::Text("${SHELL}:".into()), + ArgumentTemplate::Tokens(vec![Token::Placeholder]), + ] + }], mode: ExecutionMode::OneByOne, path_separator: None, } @@ -318,12 +367,14 @@ mod tests { #[test] fn tokens_with_no_extension() { assert_eq!( - CommandTemplate::new(&["echo", "{.}"], None), - CommandTemplate { - args: vec![ - ArgumentTemplate::Text("echo".into()), - ArgumentTemplate::Tokens(vec![Token::NoExt]), - ], + CommandSet::new(vec![vec!["echo", "{.}"]], None), + CommandSet { + commands: vec![CommandTemplate { + args: vec![ + ArgumentTemplate::Text("echo".into()), + ArgumentTemplate::Tokens(vec![Token::NoExt]), + ], + }], mode: ExecutionMode::OneByOne, path_separator: None, } @@ -333,12 +384,14 @@ mod tests { #[test] fn tokens_with_basename() { assert_eq!( - CommandTemplate::new(&["echo", "{/}"], None), - CommandTemplate { - args: vec![ - ArgumentTemplate::Text("echo".into()), - ArgumentTemplate::Tokens(vec![Token::Basename]), - ], + CommandSet::new(vec![vec!["echo", "{/}"]], None), + CommandSet { + commands: vec![CommandTemplate { + args: vec![ + ArgumentTemplate::Text("echo".into()), + ArgumentTemplate::Tokens(vec![Token::Basename]), + ], + }], mode: ExecutionMode::OneByOne, path_separator: None, } @@ -348,12 +401,14 @@ mod tests { #[test] fn tokens_with_parent() { assert_eq!( - CommandTemplate::new(&["echo", "{//}"], None), - CommandTemplate { - args: vec![ - ArgumentTemplate::Text("echo".into()), - ArgumentTemplate::Tokens(vec![Token::Parent]), - ], + CommandSet::new(vec![vec!["echo", "{//}"]], None), + CommandSet { + commands: vec![CommandTemplate { + args: vec![ + ArgumentTemplate::Text("echo".into()), + ArgumentTemplate::Tokens(vec![Token::Parent]), + ], + }], mode: ExecutionMode::OneByOne, path_separator: None, } @@ -363,12 +418,14 @@ mod tests { #[test] fn tokens_with_basename_no_extension() { assert_eq!( - CommandTemplate::new(&["echo", "{/.}"], None), - CommandTemplate { - args: vec![ - ArgumentTemplate::Text("echo".into()), - ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]), - ], + CommandSet::new(vec![vec!["echo", "{/.}"]], None), + CommandSet { + commands: vec![CommandTemplate { + args: vec![ + ArgumentTemplate::Text("echo".into()), + ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]), + ], + }], mode: ExecutionMode::OneByOne, path_separator: None, } @@ -378,16 +435,18 @@ mod tests { #[test] fn tokens_multiple() { assert_eq!( - CommandTemplate::new(&["cp", "{}", "{/.}.ext"], None), - CommandTemplate { - args: vec![ - ArgumentTemplate::Text("cp".into()), - ArgumentTemplate::Tokens(vec![Token::Placeholder]), - ArgumentTemplate::Tokens(vec![ - Token::BasenameNoExt, - Token::Text(".ext".into()) - ]), - ], + CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]], None), + CommandSet { + commands: vec![CommandTemplate { + args: vec![ + ArgumentTemplate::Text("cp".into()), + ArgumentTemplate::Tokens(vec![Token::Placeholder]), + ArgumentTemplate::Tokens(vec![ + Token::BasenameNoExt, + Token::Text(".ext".into()) + ]), + ], + }], mode: ExecutionMode::OneByOne, path_separator: None, } @@ -397,12 +456,14 @@ mod tests { #[test] fn tokens_single_batch() { assert_eq!( - CommandTemplate::new_batch(&["echo", "{.}"], None).unwrap(), - CommandTemplate { + CommandSet::new_batch(vec![vec!["echo", "{.}"]], None).unwrap(), + CommandSet { + commands: vec![CommandTemplate { args: vec![ ArgumentTemplate::Text("echo".into()), ArgumentTemplate::Tokens(vec![Token::NoExt]), ], + }], mode: ExecutionMode::Batch, path_separator: None, } @@ -411,7 +472,7 @@ mod tests { #[test] fn tokens_multiple_batch() { - assert!(CommandTemplate::new_batch(&["echo", "{.}", "{}"], None).is_err()); + assert!(CommandSet::new_batch(vec![vec!["echo", "{.}", "{}"]], None).is_err()); } #[test] diff --git a/src/main.rs b/src/main.rs index 7e55da1..88e7aad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ use regex::bytes::{RegexBuilder, RegexSetBuilder}; use crate::config::Config; use crate::error::print_error; -use crate::exec::CommandTemplate; +use crate::exec::CommandSet; use crate::exit_codes::ExitCode; use crate::filetypes::FileTypes; #[cfg(unix)] @@ -390,19 +390,16 @@ fn extract_command( matches: &clap::ArgMatches, path_separator: Option<&str>, colored_output: bool, -) -> Result> { +) -> Result> { None.or_else(|| { - matches.values_of("exec").map(|args| { - Ok(CommandTemplate::new( - args, - path_separator.map(str::to_string), - )) - }) + matches + .grouped_values_of("exec") + .map(|args| Ok(CommandSet::new(args, path_separator.map(str::to_string)))) }) .or_else(|| { matches - .values_of("exec-batch") - .map(|args| CommandTemplate::new_batch(args, path_separator.map(str::to_string))) + .grouped_values_of("exec-batch") + .map(|args| CommandSet::new_batch(args, path_separator.map(str::to_string))) }) .or_else(|| { if !matches.is_present("list-details") { @@ -412,9 +409,8 @@ fn extract_command( let color = matches.value_of("color").unwrap_or("auto"); let color_arg = format!("--color={}", color); - let res = determine_ls_command(&color_arg, colored_output).map(|cmd| { - CommandTemplate::new_batch(cmd, path_separator.map(str::to_string)).unwrap() - }); + let res = determine_ls_command(&color_arg, colored_output) + .map(|cmd| CommandSet::new_batch([cmd], path_separator.map(str::to_string)).unwrap()); Some(res) })