From eca8130a4ac17fe69647d84c0d3b3c30908a1169 Mon Sep 17 00:00:00 2001 From: Ulrich Hornung Date: Sat, 30 Mar 2024 21:19:35 +0100 Subject: [PATCH] feature: env argv0 overwrite (unix only) --- src/uu/env/src/env.rs | 82 +++++++++++--- tests/by-util/test_env.rs | 233 +++++++++++++++++++++++++++++++++++++- 2 files changed, 297 insertions(+), 18 deletions(-) diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 4f2790dc8..3908e9e78 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -27,7 +27,7 @@ use std::io::{self, Write}; use std::ops::Deref; #[cfg(unix)] -use std::os::unix::process::ExitStatusExt; +use std::os::unix::process::{CommandExt, ExitStatusExt}; use std::process::{self}; use uucore::display::Quotable; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError}; @@ -48,6 +48,7 @@ struct Options<'a> { unsets: Vec<&'a OsStr>, sets: Vec<(Cow<'a, OsStr>, Cow<'a, OsStr>)>, program: Vec<&'a OsStr>, + argv0: Option<&'a OsStr>, } // print name=value env pairs on screen @@ -173,7 +174,7 @@ pub fn uu_app() -> Command { Arg::new("debug") .short('v') .long("debug") - .action(ArgAction::SetTrue) + .action(ArgAction::Count) .help("print verbose information for each processing step"), ) .arg( @@ -184,6 +185,16 @@ pub fn uu_app() -> Command { .action(ArgAction::Set) .value_parser(ValueParser::os_string()) .help("process and split S into separate arguments; used to pass multiple arguments on shebang lines") + ).arg( + Arg::new("argv0") + .overrides_with("argv0") + .short('a') + .long("argv0") + .value_name("a") + .action(ArgAction::Set) + .value_parser(ValueParser::os_string()) + .help("Override the zeroth argument passed to the command being executed.\ + Without this option a default value of `command` is used.") ) .arg( Arg::new("vars") @@ -248,6 +259,7 @@ fn check_and_handle_string_args( #[derive(Default)] struct EnvAppData { do_debug_printing: bool, + do_input_debug_printing: Option, had_string_argument: bool, } @@ -273,14 +285,19 @@ impl EnvAppData { b if check_and_handle_string_args(b, "-S", &mut all_args, None)? => { self.had_string_argument = true; } + b if check_and_handle_string_args(b, "-vS", &mut all_args, None)? => { + self.do_debug_printing = true; + self.had_string_argument = true; + } b if check_and_handle_string_args( b, - "-vS", + "-vvS", &mut all_args, Some(original_args), )? => { self.do_debug_printing = true; + self.do_input_debug_printing = Some(false); // already done self.had_string_argument = true; } _ => { @@ -323,10 +340,15 @@ impl EnvAppData { fn run_env(&mut self, original_args: impl uucore::Args) -> UResult<()> { let (original_args, matches) = self.parse_arguments(original_args)?; - let did_debug_printing_before = self.do_debug_printing; // could have been done already as part of the "-vS" string parsing - let do_debug_printing = self.do_debug_printing || matches.get_flag("debug"); - if do_debug_printing && !did_debug_printing_before { - debug_print_args(&original_args); + self.do_debug_printing = self.do_debug_printing || (0 != matches.get_count("debug")); + self.do_input_debug_printing = self + .do_input_debug_printing + .or(Some(matches.get_count("debug") >= 2)); + if let Some(value) = self.do_input_debug_printing { + if value { + debug_print_args(&original_args); + self.do_input_debug_printing = Some(false); + } } let mut opts = make_options(&matches)?; @@ -349,7 +371,7 @@ impl EnvAppData { // no program provided, so just dump all env vars to stdout print_env(opts.line_ending); } else { - return self.run_program(opts, do_debug_printing); + return self.run_program(opts, self.do_debug_printing); } Ok(()) @@ -361,14 +383,11 @@ impl EnvAppData { do_debug_printing: bool, ) -> Result<(), Box> { let prog = Cow::from(opts.program[0]); + #[cfg(unix)] + let mut arg0 = prog.clone(); + #[cfg(not(unix))] + let arg0 = prog.clone(); let args = &opts.program[1..]; - if do_debug_printing { - eprintln!("executable: {}", prog.quote()); - for (i, arg) in args.iter().enumerate() { - eprintln!("arg[{}]: {}", i, arg.quote()); - } - } - // we need to execute a command /* * On Unix-like systems Command::status either ends up calling either fork or posix_spawnp @@ -376,7 +395,36 @@ impl EnvAppData { * standard library contains many checks and fail-safes to ensure the process ends up being * created. This is much simpler than dealing with the hassles of calling execvp directly. */ - match process::Command::new(&*prog).args(args).status() { + let mut cmd = process::Command::new(&*prog); + cmd.args(args); + + if let Some(_argv0) = opts.argv0 { + #[cfg(unix)] + { + cmd.arg0(_argv0); + arg0 = Cow::Borrowed(_argv0); + if do_debug_printing { + eprintln!("argv0: {}", arg0.quote()); + } + } + + #[cfg(not(unix))] + return Err(USimpleError::new( + 2, + "--argv0 is currently not supported on this platform", + )); + } + + if do_debug_printing { + eprintln!("executing: {}", prog.maybe_quote()); + let arg_prefix = " arg"; + eprintln!("{}[{}]= {}", arg_prefix, 0, arg0.quote()); + for (i, arg) in args.iter().enumerate() { + eprintln!("{}[{}]= {}", arg_prefix, i + 1, arg.quote()); + } + } + + match cmd.status() { Ok(exit) if !exit.success() => { #[cfg(unix)] if let Some(exit_code) = exit.code() { @@ -443,6 +491,7 @@ fn make_options(matches: &clap::ArgMatches) -> UResult> { Some(v) => v.map(|s| s.as_os_str()).collect(), None => Vec::with_capacity(0), }; + let argv0 = matches.get_one::("argv0").map(|s| s.as_os_str()); let mut opts = Options { ignore_env, @@ -452,6 +501,7 @@ fn make_options(matches: &clap::ArgMatches) -> UResult> { unsets, sets: vec![], program: vec![], + argv0, }; let mut begin_prog_opts = false; diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 13535e416..0df1da752 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -8,6 +8,7 @@ use crate::common::util::expected_result; use crate::common::util::TestScenario; use ::env::native_int_str::{Convert, NCvt}; +use regex::Regex; use std::env; use std::path::Path; use tempfile::tempdir; @@ -55,6 +56,99 @@ fn test_if_windows_batch_files_can_be_executed() { assert!(result.stdout_str().contains("Hello Windows World!")); } +#[test] +fn test_debug_1() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .arg("-v") + .arg(&ts.bin_path) + .args(&["echo", "hello"]) + .succeeds(); + result.stderr_matches( + &Regex::new(concat!( + r"executing: [^\n]+(\/|\\)coreutils(\.exe)?\n", + r" arg\[0\]= '[^\n]+(\/|\\)coreutils(\.exe)?'\n", + r" arg\[1\]= 'echo'\n", + r" arg\[2\]= 'hello'" + )) + .unwrap(), + ); +} + +#[test] +fn test_debug_2() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .arg("-vv") + .arg(ts.bin_path) + .args(&["echo", "hello2"]) + .succeeds(); + result.stderr_matches( + &Regex::new(concat!( + r"input args:\n", + r"arg\[0\]: 'env'\n", + r"arg\[1\]: '-vv'\n", + r"arg\[2\]: '[^\n]+(\/|\\)coreutils(.exe)?'\n", + r"arg\[3\]: 'echo'\n", + r"arg\[4\]: 'hello2'\n", + r"executing: [^\n]+(\/|\\)coreutils(.exe)?\n", + r" arg\[0\]= '[^\n]+(\/|\\)coreutils(.exe)?'\n", + r" arg\[1\]= 'echo'\n", + r" arg\[2\]= 'hello2'" + )) + .unwrap(), + ); +} + +#[test] +fn test_debug1_part_of_string_arg() { + let ts = TestScenario::new(util_name!()); + + let result = ts + .ucmd() + .arg("-vS FOO=BAR") + .arg(ts.bin_path) + .args(&["echo", "hello1"]) + .succeeds(); + result.stderr_matches( + &Regex::new(concat!( + r"executing: [^\n]+(\/|\\)coreutils(\.exe)?\n", + r" arg\[0\]= '[^\n]+(\/|\\)coreutils(\.exe)?'\n", + r" arg\[1\]= 'echo'\n", + r" arg\[2\]= 'hello1'" + )) + .unwrap(), + ); +} + +#[test] +fn test_debug2_part_of_string_arg() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .arg("-vvS FOO=BAR") + .arg(ts.bin_path) + .args(&["echo", "hello2"]) + .succeeds(); + result.stderr_matches( + &Regex::new(concat!( + r"input args:\n", + r"arg\[0\]: 'env'\n", + r"arg\[1\]: '-vvS FOO=BAR'\n", + r"arg\[2\]: '[^\n]+(\/|\\)coreutils(.exe)?'\n", + r"arg\[3\]: 'echo'\n", + r"arg\[4\]: 'hello2'\n", + r"executing: [^\n]+(\/|\\)coreutils(.exe)?\n", + r" arg\[0\]= '[^\n]+(\/|\\)coreutils(.exe)?'\n", + r" arg\[1\]= 'echo'\n", + r" arg\[2\]= 'hello2'" + )) + .unwrap(), + ); +} + #[test] fn test_file_option() { let out = new_ucmd!() @@ -345,10 +439,15 @@ fn test_split_string_into_args_debug_output_whitespace_handling() { let out = scene .ucmd() - .args(&["-vS printf x%sx\\n A \t B \x0B\x0C\r\n"]) + .args(&["-vvS printf x%sx\\n A \t B \x0B\x0C\r\n"]) .succeeds(); assert_eq!(out.stdout_str(), "xAx\nxBx\n"); - assert_eq!(out.stderr_str(), "input args:\narg[0]: 'env'\narg[1]: $'-vS printf x%sx\\\\n A \\t B \\x0B\\x0C\\r\\n'\nexecutable: 'printf'\narg[0]: $'x%sx\\n'\narg[1]: 'A'\narg[2]: 'B'\n"); + assert_eq!( + out.stderr_str(), + "input args:\narg[0]: 'env'\narg[1]: $\ + '-vvS printf x%sx\\\\n A \\t B \\x0B\\x0C\\r\\n'\nexecuting: printf\ + \n arg[0]= 'printf'\n arg[1]= $'x%sx\\n'\n arg[2]= 'A'\n arg[3]= 'B'\n" + ); } // FixMe: This test fails on MACOS: @@ -564,6 +663,136 @@ fn test_env_with_gnu_reference_empty_executable_double_quotes() { .stderr_is("env: '': No such file or directory\n"); } +#[test] +#[cfg(unix)] +fn test_env_overwrite_arg0() { + let ts = TestScenario::new(util_name!()); + + let bin = ts.bin_path.clone(); + + ts.ucmd() + .args(&["--argv0", "echo"]) + .arg(&bin) + .args(&["-n", "hello", "world!"]) + .succeeds() + .stdout_is("hello world!") + .stderr_is(""); + + ts.ucmd() + .args(&["-a", "dirname"]) + .arg(bin) + .args(&["aa/bb/cc"]) + .succeeds() + .stdout_is("aa/bb\n") + .stderr_is(""); +} + +#[test] +#[cfg(unix)] +fn test_env_arg_argv0_overwrite() { + let ts = TestScenario::new(util_name!()); + + let bin = ts.bin_path.clone(); + + // overwrite --argv0 by --argv0 + ts.ucmd() + .args(&["--argv0", "dirname"]) + .args(&["--argv0", "echo"]) + .arg(&bin) + .args(&["aa/bb/cc"]) + .succeeds() + .stdout_is("aa/bb/cc\n") + .stderr_is(""); + + // overwrite -a by -a + ts.ucmd() + .args(&["-a", "dirname"]) + .args(&["-a", "echo"]) + .arg(&bin) + .args(&["aa/bb/cc"]) + .succeeds() + .stdout_is("aa/bb/cc\n") + .stderr_is(""); + + // overwrite --argv0 by -a + ts.ucmd() + .args(&["--argv0", "dirname"]) + .args(&["-a", "echo"]) + .arg(&bin) + .args(&["aa/bb/cc"]) + .succeeds() + .stdout_is("aa/bb/cc\n") + .stderr_is(""); + + // overwrite -a by --argv0 + ts.ucmd() + .args(&["-a", "dirname"]) + .args(&["--argv0", "echo"]) + .arg(&bin) + .args(&["aa/bb/cc"]) + .succeeds() + .stdout_is("aa/bb/cc\n") + .stderr_is(""); +} + +#[test] +#[cfg(unix)] +fn test_env_arg_argv0_overwrite_mixed_with_string_args() { + let ts = TestScenario::new(util_name!()); + + let bin = ts.bin_path.clone(); + + // string arg following normal + ts.ucmd() + .args(&["-S--argv0 dirname"]) + .args(&["--argv0", "echo"]) + .arg(&bin) + .args(&["aa/bb/cc"]) + .succeeds() + .stdout_is("aa/bb/cc\n") + .stderr_is(""); + + // normal following string arg + ts.ucmd() + .args(&["-a", "dirname"]) + .args(&["-S-a echo"]) + .arg(&bin) + .args(&["aa/bb/cc"]) + .succeeds() + .stdout_is("aa/bb/cc\n") + .stderr_is(""); + + // one large string arg + ts.ucmd() + .args(&["-S--argv0 dirname -a echo"]) + .arg(&bin) + .args(&["aa/bb/cc"]) + .succeeds() + .stdout_is("aa/bb/cc\n") + .stderr_is(""); + + // two string args + ts.ucmd() + .args(&["-S-a dirname"]) + .args(&["-S--argv0 echo"]) + .arg(&bin) + .args(&["aa/bb/cc"]) + .succeeds() + .stdout_is("aa/bb/cc\n") + .stderr_is(""); + + // three args: normal, string, normal + ts.ucmd() + .args(&["-a", "sleep"]) + .args(&["-S-a dirname"]) + .args(&["-a", "echo"]) + .arg(&bin) + .args(&["aa/bb/cc"]) + .succeeds() + .stdout_is("aa/bb/cc\n") + .stderr_is(""); +} + #[cfg(test)] mod tests_split_iterator {