diff --git a/CHANGELOG.md b/CHANGELOG.md index 89ff83964..c8a0db027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Features +- Added flag `--print-exec` for printing commands before they are ran. + ## Bugfixes diff --git a/src/app.rs b/src/app.rs index a9eba45f9..51529cd86 100644 --- a/src/app.rs +++ b/src/app.rs @@ -441,6 +441,16 @@ pub fn build_app() -> Command<'static> { " ), ) + .arg( + Arg::new("print-exec") + .long("print-exec") + .help("Print each command ran with -x or -X.") + .requires("exec-batch") + .requires("exec") + .long_help( + "Print each command before it is executed. Must be ran with -x or -X." + ), + ) .arg( Arg::new("batch-size") .long("batch-size") diff --git a/src/exec/command.rs b/src/exec/command.rs index a642a405b..66d4dbb82 100644 --- a/src/exec/command.rs +++ b/src/exec/command.rs @@ -55,8 +55,16 @@ pub fn execute_commands>>( cmds: I, out_perm: &Mutex<()>, enable_output_buffering: bool, + cmd_display_result: Option, ) -> ExitCode { let mut output_buffer = OutputBuffer::new(out_perm); + + if let Some(ref d) = cmd_display_result { + // Print command. + let cmd_bytes: Vec = d.clone().as_bytes().to_vec(); + output_buffer.push(cmd_bytes, vec![]); + } + for result in cmds { let mut cmd = match result { Ok(cmd) => cmd, diff --git a/src/exec/job.rs b/src/exec/job.rs index 9b95ac24b..b080299a9 100644 --- a/src/exec/job.rs +++ b/src/exec/job.rs @@ -1,12 +1,13 @@ use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex}; +use crate::config::Config; use crate::dir_entry::DirEntry; use crate::error::print_error; use crate::exit_codes::{merge_exitcodes, ExitCode}; use crate::walk::WorkerResult; -use super::CommandSet; +use super::{CommandSet, CommandSetDisplay}; /// 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 @@ -17,6 +18,7 @@ pub fn job( out_perm: Arc>, show_filesystem_errors: bool, buffer_output: bool, + config: &Config, ) -> ExitCode { let mut results: Vec = Vec::new(); loop { @@ -38,9 +40,20 @@ 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.execute(dir_entry.path(), Arc::clone(&out_perm), buffer_output)) + results.push(cmd.execute( + dir_entry.path(), + Arc::clone(&out_perm), + buffer_output, + if cmd.should_print() { + Some(CommandSetDisplay::new(&cmd, &dir_entry, config)) + } else { + None + }, + )) } + // Returns error in case of any error. merge_exitcodes(results) } @@ -50,11 +63,12 @@ pub fn batch( cmd: &CommandSet, show_filesystem_errors: bool, limit: usize, + config: &Config, ) -> ExitCode { - let paths = rx + let entries = rx .into_iter() .filter_map(|worker_result| match worker_result { - WorkerResult::Entry(dir_entry) => Some(dir_entry.into_path()), + WorkerResult::Entry(dir_entry) => Some(dir_entry), WorkerResult::Error(err) => { if show_filesystem_errors { print_error(err.to_string()); @@ -63,5 +77,13 @@ pub fn batch( } }); - cmd.execute_batch(paths, limit) + cmd.execute_batch( + entries, + limit, + if cmd.should_print() { + Some(config) + } else { + None + }, + ) } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index fc26da2a6..be9c9cb71 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -5,9 +5,10 @@ mod token; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; -use std::io; +use std::fmt::{Debug, Display}; +use std::io::{self, Write}; use std::iter; -use std::path::{Component, Path, PathBuf, Prefix}; +use std::path::{Component, Path, Prefix}; use std::process::Stdio; use std::sync::{Arc, Mutex}; @@ -16,7 +17,10 @@ use argmax::Command; use once_cell::sync::Lazy; use regex::Regex; +use crate::config::Config; +use crate::dir_entry::DirEntry; use crate::exit_codes::ExitCode; +use crate::output; use self::command::{execute_commands, handle_cmd_error}; use self::input::{basename, dirname, remove_extension}; @@ -36,11 +40,51 @@ pub enum ExecutionMode { pub struct CommandSet { mode: ExecutionMode, path_separator: Option, + print: bool, commands: Vec, } +// Wrapper for displaying Commands. +pub struct CommandSetDisplay<'a> { + command_set: &'a CommandSet, + entry: &'a DirEntry, + config: &'a Config, +} + +impl<'a> CommandSetDisplay<'a> { + pub fn new( + command_set: &'a CommandSet, + entry: &'a DirEntry, + config: &'a Config, + ) -> CommandSetDisplay<'a> { + CommandSetDisplay { + command_set, + entry, + config, + } + } +} + +impl CommandSetDisplay<'_> { + fn get_command_string(&self, path_separator: Option<&str>) -> Option { + let mut res = String::new(); + for (i, c) in self.command_set.commands.iter().enumerate() { + if i > 0 { + res.push_str("; "); + } + let cmd_hl = c.generate_highlighted_string(self.entry, self.config, path_separator); + match cmd_hl { + None => return None, + Some(cmd_hl_str) => res.push_str(&cmd_hl_str), + } + } + res.push('\n'); + Some(res) + } +} + impl CommandSet { - pub fn new(input: I, path_separator: Option) -> Result + pub fn new(input: I, path_separator: Option, print: bool) -> Result where I: IntoIterator>, S: AsRef, @@ -48,6 +92,7 @@ impl CommandSet { Ok(CommandSet { mode: ExecutionMode::OneByOne, path_separator, + print, commands: input .into_iter() .map(CommandTemplate::new) @@ -55,7 +100,11 @@ impl CommandSet { }) } - pub fn new_batch(input: I, path_separator: Option) -> Result + pub fn new_batch( + input: I, + path_separator: Option, + print: bool, + ) -> Result where I: IntoIterator>, S: AsRef, @@ -63,6 +112,7 @@ impl CommandSet { Ok(CommandSet { mode: ExecutionMode::Batch, path_separator, + print, commands: input .into_iter() .map(|args| { @@ -83,34 +133,57 @@ impl CommandSet { self.mode == ExecutionMode::Batch } - pub fn execute(&self, input: &Path, out_perm: Arc>, buffer_output: bool) -> ExitCode { + pub fn should_print(&self) -> bool { + self.print + } + + pub fn execute( + &self, + input: &Path, + out_perm: Arc>, + buffer_output: bool, + cmd_display: Option, + ) -> ExitCode { let path_separator = self.path_separator.as_deref(); + + let printed_command = cmd_display.and_then(|cd| cd.get_command_string(path_separator)); + let commands = self .commands .iter() .map(|c| c.generate(input, path_separator)); - execute_commands(commands, &out_perm, buffer_output) + execute_commands(commands, &out_perm, buffer_output, printed_command) } - pub fn execute_batch(&self, paths: I, limit: usize) -> ExitCode + pub fn execute_batch( + &self, + entries: I, + limit: usize, + print_with_config: Option<&Config>, + ) -> ExitCode where - I: Iterator, + I: Iterator, { let path_separator = self.path_separator.as_deref(); let builders: io::Result> = self .commands .iter() - .map(|c| CommandBuilder::new(c, limit)) + .map(|c| CommandBuilder::new(c, limit, print_with_config.is_some())) .collect(); match builders { Ok(mut builders) => { - for path in paths { + for entry in entries { for builder in &mut builders { - if let Err(e) = builder.push(&path, path_separator) { + if let Err(e) = builder.push(entry.path(), path_separator) { return handle_cmd_error(Some(&builder.cmd), e); } + + // If provided print config, print command arguments. + if let Some(config) = print_with_config { + builder.push_display_entry(&entry, config, path_separator) + } } } @@ -136,10 +209,12 @@ struct CommandBuilder { cmd: Command, count: usize, limit: usize, + print: bool, + entries_to_print: OsString, } impl CommandBuilder { - fn new(template: &CommandTemplate, limit: usize) -> io::Result { + fn new(template: &CommandTemplate, limit: usize, print: bool) -> io::Result { let mut pre_args = vec![]; let mut path_arg = None; let mut post_args = vec![]; @@ -163,6 +238,8 @@ impl CommandBuilder { cmd, count: 0, limit, + print, + entries_to_print: OsString::new(), }) } @@ -175,6 +252,20 @@ impl CommandBuilder { Ok(cmd) } + fn push_display_entry( + &mut self, + entry: &DirEntry, + config: &Config, + path_separator: Option<&str>, + ) { + self.entries_to_print + .push( + self.path_arg + .generate_with_highlight(entry, path_separator, config), + ); + self.entries_to_print.push(" "); + } + fn push(&mut self, path: &Path, separator: Option<&str>) -> io::Result<()> { if self.limit > 0 && self.count >= self.limit { self.finish()?; @@ -194,6 +285,33 @@ impl CommandBuilder { } fn finish(&mut self) -> io::Result<()> { + if self.print { + let mut to_print = OsString::new(); + + for arg in &self.pre_args { + to_print.push(arg); + to_print.push(" "); + } + + to_print.push(&self.entries_to_print); + + for (i, arg) in self.post_args.iter().enumerate() { + if i > 0 { + to_print.push(" "); + } + to_print.push(arg); + } + + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + if let Some(to_print_str) = to_print.to_str() { + writeln!(stdout, "{}", to_print_str)?; + } + + stdout.flush()?; + self.entries_to_print = OsString::new(); + } if self.count > 0 { self.cmd.try_args(&self.post_args)?; self.cmd.status()?; @@ -215,6 +333,17 @@ struct CommandTemplate { args: Vec, } +impl Display for CommandTemplate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for arg in self.args.iter() { + if let Err(e) = arg.fmt(f) { + return Err(e); + } + } + Ok(()) + } +} + impl CommandTemplate { fn new(input: I) -> Result where @@ -299,6 +428,20 @@ impl CommandTemplate { } Ok(cmd) } + + fn generate_highlighted_string( + &self, + entry: &DirEntry, + config: &Config, + path_separator: Option<&str>, + ) -> Option { + let mut res: OsString = self.args[0].generate(entry.path(), path_separator); + for arg in &self.args[1..] { + res.push(" "); + res.push(arg.generate_with_highlight(entry, path_separator, config)); + } + return res.to_str().map(String::from); + } } /// Represents a template for a single command argument. @@ -319,29 +462,74 @@ impl ArgumentTemplate { /// Generate an argument from this template. If path_separator is Some, then it will replace /// the path separator in all placeholder tokens. Text arguments and tokens are not affected by /// path separator substitution. - pub fn generate(&self, path: impl AsRef, path_separator: Option<&str>) -> OsString { + /// Apply a callback `cb` to each token's result where templates are substituted to allow further + /// processing. Eg. function `generate_with_highlight` will apply text highlighting via the + /// callback to highlight found entries in the generated string. + fn generate_with_callback( + &self, + path: impl AsRef, + path_separator: Option<&str>, + cb: Option<&dyn Fn(&OsStr, &Token) -> OsString>, + ) -> OsString { use self::Token::*; let path = path.as_ref(); + enum TokenGen<'a> { + Template(Cow<'a, OsStr>), + TextOnly(&'a String), + } + match *self { ArgumentTemplate::Tokens(ref tokens) => { let mut s = OsString::new(); + let mut push_token_text = |token_text: TokenGen, tkn: &Token| match token_text { + TokenGen::Template(tmpl_text) => { + if let Some(callback) = cb { + s.push(callback(&tmpl_text, tkn)) + } else { + s.push(tmpl_text) + } + } + TokenGen::TextOnly(text) => s.push(text), + }; for token in tokens { match *token { - Basename => s.push(Self::replace_separator(basename(path), path_separator)), - BasenameNoExt => s.push(Self::replace_separator( - &remove_extension(basename(path).as_ref()), - path_separator, - )), - NoExt => s.push(Self::replace_separator( - &remove_extension(path), - path_separator, - )), - Parent => s.push(Self::replace_separator(&dirname(path), path_separator)), - Placeholder => { - s.push(Self::replace_separator(path.as_ref(), path_separator)) - } - Text(ref string) => s.push(string), + Basename => push_token_text( + TokenGen::Template(Self::replace_separator( + basename(path), + path_separator, + )), + token, + ), + BasenameNoExt => push_token_text( + TokenGen::Template(Self::replace_separator( + &remove_extension(basename(path).as_ref()), + path_separator, + )), + token, + ), + NoExt => push_token_text( + TokenGen::Template(Self::replace_separator( + &remove_extension(path), + path_separator, + )), + token, + ), + Parent => push_token_text( + TokenGen::Template(Self::replace_separator( + &dirname(path), + path_separator, + )), + token, + ), + Placeholder => push_token_text( + TokenGen::Template(Self::replace_separator( + path.as_ref(), + path_separator, + )), + token, + ), + Text(ref string) => push_token_text(TokenGen::TextOnly(string), token), } } s @@ -350,6 +538,36 @@ impl ArgumentTemplate { } } + /// Generate an argument from this template. If path_separator is Some, then it will replace + /// the path separator in all placeholder tokens. Text arguments and tokens are not affected by + /// path separator substitution. + pub fn generate(&self, path: impl AsRef, path_separator: Option<&str>) -> OsString { + self.generate_with_callback(path, path_separator, None) + } + + // Same functionality as `generate` but result will have highlighted entires in the generated + // string. + fn generate_with_highlight( + &self, + entry: &DirEntry, + path_separator: Option<&str>, + config: &Config, + ) -> OsString { + use self::Token::*; + let path = entry.path(); + + let apply_highlight = |token_text: &OsStr, token: &Token| -> OsString { + let path_from_token = Path::new(token_text); + match *token { + Basename => output::paint_entry(entry, path_from_token, config), + NoExt => output::paint_entry(entry, path_from_token, config), + Placeholder => output::paint_entry(entry, path_from_token, config), + _ => OsString::from(token_text), + } + }; + self.generate_with_callback(path, path_separator, Some(&apply_highlight)) + } + /// Replace the path separator in the input with the custom separator string. If path_separator /// is None, simply return a borrowed Cow of the input. Otherwise, the input is /// interpreted as a Path and its components are iterated through and re-joined into a new @@ -413,7 +631,7 @@ mod tests { #[test] fn tokens_with_placeholder() { assert_eq!( - CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]], None).unwrap(), + CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]], None, false).unwrap(), CommandSet { commands: vec![CommandTemplate { args: vec![ @@ -422,6 +640,7 @@ mod tests { ArgumentTemplate::Tokens(vec![Token::Placeholder]), ] }], + print: false, mode: ExecutionMode::OneByOne, path_separator: None, } @@ -431,7 +650,7 @@ mod tests { #[test] fn tokens_with_no_extension() { assert_eq!( - CommandSet::new(vec![vec!["echo", "{.}"]], None).unwrap(), + CommandSet::new(vec![vec!["echo", "{.}"]], None, false).unwrap(), CommandSet { commands: vec![CommandTemplate { args: vec![ @@ -439,6 +658,7 @@ mod tests { ArgumentTemplate::Tokens(vec![Token::NoExt]), ], }], + print: false, mode: ExecutionMode::OneByOne, path_separator: None, } @@ -448,7 +668,7 @@ mod tests { #[test] fn tokens_with_basename() { assert_eq!( - CommandSet::new(vec![vec!["echo", "{/}"]], None).unwrap(), + CommandSet::new(vec![vec!["echo", "{/}"]], None, false).unwrap(), CommandSet { commands: vec![CommandTemplate { args: vec![ @@ -456,6 +676,7 @@ mod tests { ArgumentTemplate::Tokens(vec![Token::Basename]), ], }], + print: false, mode: ExecutionMode::OneByOne, path_separator: None, } @@ -465,7 +686,7 @@ mod tests { #[test] fn tokens_with_parent() { assert_eq!( - CommandSet::new(vec![vec!["echo", "{//}"]], None).unwrap(), + CommandSet::new(vec![vec!["echo", "{//}"]], None, false).unwrap(), CommandSet { commands: vec![CommandTemplate { args: vec![ @@ -473,6 +694,7 @@ mod tests { ArgumentTemplate::Tokens(vec![Token::Parent]), ], }], + print: false, mode: ExecutionMode::OneByOne, path_separator: None, } @@ -482,7 +704,7 @@ mod tests { #[test] fn tokens_with_basename_no_extension() { assert_eq!( - CommandSet::new(vec![vec!["echo", "{/.}"]], None).unwrap(), + CommandSet::new(vec![vec!["echo", "{/.}"]], None, false).unwrap(), CommandSet { commands: vec![CommandTemplate { args: vec![ @@ -490,6 +712,7 @@ mod tests { ArgumentTemplate::Tokens(vec![Token::BasenameNoExt]), ], }], + print: false, mode: ExecutionMode::OneByOne, path_separator: None, } @@ -499,7 +722,7 @@ mod tests { #[test] fn tokens_multiple() { assert_eq!( - CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]], None).unwrap(), + CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]], None, false).unwrap(), CommandSet { commands: vec![CommandTemplate { args: vec![ @@ -511,6 +734,7 @@ mod tests { ]), ], }], + print: false, mode: ExecutionMode::OneByOne, path_separator: None, } @@ -520,7 +744,7 @@ mod tests { #[test] fn tokens_single_batch() { assert_eq!( - CommandSet::new_batch(vec![vec!["echo", "{.}"]], None).unwrap(), + CommandSet::new_batch(vec![vec!["echo", "{.}"]], None, false).unwrap(), CommandSet { commands: vec![CommandTemplate { args: vec![ @@ -528,6 +752,7 @@ mod tests { ArgumentTemplate::Tokens(vec![Token::NoExt]), ], }], + print: false, mode: ExecutionMode::Batch, path_separator: None, } @@ -536,7 +761,7 @@ mod tests { #[test] fn tokens_multiple_batch() { - assert!(CommandSet::new_batch(vec![vec!["echo", "{.}", "{}"]], None).is_err()); + assert!(CommandSet::new_batch(vec![vec!["echo", "{.}", "{}"]], None, false).is_err()); } #[test] @@ -546,7 +771,7 @@ mod tests { #[test] fn command_set_no_args() { - assert!(CommandSet::new(vec![vec!["echo"], vec![]], None).is_err()); + assert!(CommandSet::new(vec![vec!["echo"], vec![]], None, false).is_err()); } #[test] diff --git a/src/main.rs b/src/main.rs index 9c16ff012..a8a23c29e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -397,14 +397,22 @@ fn extract_command( colored_output: bool, ) -> Result> { None.or_else(|| { - matches - .grouped_values_of("exec") - .map(|args| CommandSet::new(args, path_separator.map(str::to_string))) + matches.grouped_values_of("exec").map(|args| { + CommandSet::new( + args, + path_separator.map(str::to_string), + matches.is_present("print-exec"), + ) + }) }) .or_else(|| { - matches - .grouped_values_of("exec-batch") - .map(|args| CommandSet::new_batch(args, path_separator.map(str::to_string))) + matches.grouped_values_of("exec-batch").map(|args| { + CommandSet::new_batch( + args, + path_separator.map(str::to_string), + matches.is_present("print-exec"), + ) + }) }) .or_else(|| { if !matches.is_present("list-details") { @@ -414,8 +422,9 @@ 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| CommandSet::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), false).unwrap() + }); Some(res) }) diff --git a/src/output.rs b/src/output.rs index 261dbf329..1f095dfad 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::ffi::OsString; use std::io::{self, Write}; use std::path::Path; @@ -42,6 +43,47 @@ pub fn print_entry(stdout: &mut W, entry: &DirEntry, config: &Config) } } +pub fn paint_entry(entry: &DirEntry, path: &Path, config: &Config) -> OsString { + if let Some(ls_colors) = &config.ls_colors { + let mut offset = 0; + let path_str = path.to_string_lossy(); + let mut result = OsString::new(); + + if let Some(parent) = path.parent() { + offset = parent.to_string_lossy().len(); + for c in path_str[offset..].chars() { + if std::path::is_separator(c) { + offset += c.len_utf8(); + } else { + break; + } + } + } + + if offset > 0 { + let mut parent_str = Cow::from(&path_str[..offset]); + if let Some(ref separator) = config.path_separator { + *parent_str.to_mut() = replace_path_separator(&parent_str, separator); + } + + let style = ls_colors + .style_for_indicator(Indicator::Directory) + .map(Style::to_ansi_term_style) + .unwrap_or_default(); + result.push(style.paint(parent_str).to_string()); + } + + let style = ls_colors + .style_for_path_with_metadata(path, entry.metadata()) + .map(Style::to_ansi_term_style) + .unwrap_or_default(); + result.push(style.paint(&path_str[offset..]).to_string()); + result + } else { + OsString::from(path) + } +} + // Display a trailing slash if the path is a directory and the config option is enabled. // If the path_separator option is set, display that instead. // The trailing slash will not be colored. diff --git a/src/walk.rs b/src/walk.rs index 463417e65..cea8fc5b3 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -349,10 +349,9 @@ fn spawn_receiver( // 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, cmd, show_filesystem_errors, config.batch_size) + exec::batch(rx, cmd, show_filesystem_errors, config.batch_size, &config) } else { let shared_rx = Arc::new(Mutex::new(rx)); - let out_perm = Arc::new(Mutex::new(())); // Each spawned job will store it's thread handle in here. @@ -361,6 +360,7 @@ fn spawn_receiver( let rx = Arc::clone(&shared_rx); let cmd = Arc::clone(cmd); let out_perm = Arc::clone(&out_perm); + let config = Arc::clone(&config); // Spawn a job thread that will listen for and execute inputs. let handle = thread::spawn(move || { @@ -370,6 +370,7 @@ fn spawn_receiver( out_perm, show_filesystem_errors, enable_output_buffering, + &config, ) }); diff --git a/tests/tests.rs b/tests/tests.rs index 72154dd9c..37c71d726 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1654,6 +1654,158 @@ fn test_exec_with_separator() { ); } +/// Shell script execution (--exec and --exec-batch) with printing +#[test] +fn test_print_exec() { + 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( + &["foo", "--print-exec", "--exec", "echo", "{}"], + "echo ./a.foo + echo ./one/b.foo + echo ./one/two/C.Foo2 + echo ./one/two/c.foo + echo ./one/two/three/d.foo + echo ./one/two/three/directory_foo + ./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", "--print-exec", "--exec", "echo", "{.}"], + "echo a + echo one/b + echo one/two/C + echo one/two/c + echo one/two/three/d + echo one/two/three/directory_foo + a + one/b + one/two/C + one/two/c + one/two/three/d + one/two/three/directory_foo", + ); + te.assert_output( + &["foo", "--print-exec", "--exec", "echo", "{/}"], + "echo a.foo + echo b.foo + echo C.Foo2 + echo c.foo + echo d.foo + echo directory_foo + a.foo + b.foo + C.Foo2 + c.foo + d.foo + directory_foo", + ); + + te.assert_output( + &[ + "--path-separator=#", + "--print-exec", + "--absolute-path", + "foo", + "--exec", + "echo", + ], + &format!( + "echo {abs_path}#a.foo + echo {abs_path}#one#b.foo + echo {abs_path}#one#two#C.Foo2 + echo {abs_path}#one#two#c.foo + echo {abs_path}#one#two#three#d.foo + echo {abs_path}#one#two#three#directory_foo + {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.replace(std::path::MAIN_SEPARATOR, "#"), + ), + ); + + te.assert_output( + &["foo", "--print-exec", "--exec-batch", "echo", "beginarg", "{}", "endarg"], + "echo beginarg ./a.foo ./one/b.foo ./one/two/C.Foo2 ./one/two/c.foo ./one/two/three/d.foo ./one/two/three/directory_foo endarg + beginarg ./a.foo ./one/b.foo ./one/two/C.Foo2 ./one/two/c.foo ./one/two/three/d.foo ./one/two/three/directory_foo endarg", + ); + + te.assert_output( + &[ + "--absolute-path", + "foo", + "--print-exec", + "--exec", + "echo", + ";", + "--exec", + "echo", + "test", + "{/}", + ], + &format!( + "echo {abs_path}/a.foo; echo test a.foo + echo {abs_path}/one/b.foo; echo test b.foo + echo {abs_path}/one/two/C.Foo2; echo test C.Foo2 + echo {abs_path}/one/two/c.foo; echo test c.foo + echo {abs_path}/one/two/three/d.foo; echo test d.foo + echo {abs_path}/one/two/three/directory_foo; echo test directory_foo + {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 + test a.foo + test b.foo + test C.Foo2 + test c.foo + test d.foo + test directory_foo", + abs_path = &abs_path + ), + ); + + te.assert_output( + &[ + "foo", + "--print-exec", + "--exec", + "echo", + "-n", + "{/}: ", + ";", + "--exec", + "echo", + "{//}", + ], + "echo -n a.foo: ; echo . + echo -n b.foo: ; echo ./one + echo -n C.Foo2: ; echo ./one/two + echo -n c.foo: ; echo ./one/two + echo -n d.foo: ; echo ./one/two/three + echo -n directory_foo: ; echo ./one/two/three + a.foo: . + b.foo: ./one + C.Foo2: ./one/two + c.foo: ./one/two + d.foo: ./one/two/three + directory_foo: ./one/two/three", + ); + } +} + /// Non-zero exit code (--quiet) #[test] fn test_quiet() {