From 18dea1fc5940f4b6a01b09b49339eb71e5765469 Mon Sep 17 00:00:00 2001 From: wtcpython <1762993226@qq.com> Date: Sat, 29 Aug 2026 09:52:22 +0800 Subject: [PATCH] env: trace environment changes with -v --- src/uu/env/src/env.rs | 70 +++++++++++++----------------- src/uucore/src/lib/mods/display.rs | 4 +- tests/by-util/test_env.rs | 63 +++++++++++++++++++++------ 3 files changed, 82 insertions(+), 55 deletions(-) diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 02505edbed5..8d6f7096884 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -42,7 +42,7 @@ use std::mem::zeroed; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; -use uucore::display::{Quotable, print_all_env_vars}; +use uucore::display::{OsWrite, Quotable, print_all_env_vars}; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError, strip_errno}; use uucore::line_ending::LineEnding; #[cfg(all(unix, not(target_os = "fuchsia")))] @@ -109,6 +109,7 @@ struct Options<'a> { sets: Vec<(Cow<'a, OsStr>, Cow<'a, OsStr>)>, program: Vec<&'a OsStr>, argv0: Option<&'a OsStr>, + debug: bool, #[cfg(all(unix, not(target_os = "fuchsia")))] ignore_signal: SignalRequest, #[cfg(all(unix, not(target_os = "fuchsia")))] @@ -492,19 +493,10 @@ fn to_error(text: &NativeIntStr, e: &EnvError) -> Box { } } -fn debug_print_args(args: &[OsString]) { - let mut error = stderr().lock(); - let _ = writeln!(error, "input args:"); - for (i, arg) in args.iter().enumerate() { - let _ = writeln!(error, "arg[{i}]: {}", arg.quote()); - } -} - fn check_and_handle_string_args( arg: &OsString, prefix_to_test: &str, all_args: &mut Vec, - do_debug_print_args: Option<&Vec>, require_non_empty_payload: bool, strip_optional_leading_equals: bool, located: Option<(&[OsString], usize)>, @@ -515,10 +507,6 @@ fn check_and_handle_string_args( return Ok(false); } - if let Some(input_args) = do_debug_print_args { - debug_print_args(input_args); // do it here, such that its also printed when we get an error/panic during parsing - } - let remaining_arg = if strip_optional_leading_equals { if let Some(stripped_remaining_arg) = remaining_arg.strip_prefix(&*NCvt::convert("=")) { stripped_remaining_arg @@ -545,12 +533,10 @@ fn check_and_handle_string_args( #[derive(Default)] struct EnvAppData { do_debug_printing: bool, - do_input_debug_printing: Option, had_string_argument: bool, } struct ParsedArguments { - original_args: Vec, matches: clap::ArgMatches, #[cfg(all(unix, not(target_os = "fuchsia")))] signal_apply_all: BTreeSet<&'static str>, @@ -605,7 +591,6 @@ impl EnvAppData { b, "--split-string", &mut all_args, - None, true, true, located, @@ -617,7 +602,6 @@ impl EnvAppData { b, "-S", &mut all_args, - None, true, false, located, @@ -629,7 +613,6 @@ impl EnvAppData { b, "-vS", &mut all_args, - None, true, false, located, @@ -642,14 +625,12 @@ impl EnvAppData { b, "-vvS", &mut all_args, - Some(original_args), true, false, located, )? => { self.do_debug_printing = true; - self.do_input_debug_printing = Some(false); // already done self.had_string_argument = true; } b if b == "--split-string" || b == "-S" || b == "-vS" || b == "-vvS" => { @@ -661,10 +642,6 @@ impl EnvAppData { if b == "-vS" || b == "-vvS" { self.do_debug_printing = true; } - if b == "-vvS" { - debug_print_args(original_args); - self.do_input_debug_printing = Some(false); - } let native_next_arg = NCvt::convert(next_arg); // The string is the argument after this one, detached. @@ -752,7 +729,6 @@ impl EnvAppData { } }; Ok(ParsedArguments { - original_args, matches, #[cfg(all(unix, not(target_os = "fuchsia")))] signal_apply_all, @@ -761,23 +737,16 @@ impl EnvAppData { fn run_env(&mut self, original_args: impl uucore::Args) -> UResult<()> { let ParsedArguments { - original_args, matches, #[cfg(all(unix, not(target_os = "fuchsia")))] signal_apply_all, } = self.parse_arguments(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 Some(true) == self.do_input_debug_printing { - debug_print_args(&original_args); - self.do_input_debug_printing = Some(false); - } let opts = make_options( &matches, + self.do_debug_printing, #[cfg(all(unix, not(target_os = "fuchsia")))] &signal_apply_all, )?; @@ -822,7 +791,7 @@ impl EnvAppData { // no program provided, so just dump all env vars to stdout print_all_env_vars(opts.line_ending)?; } else { - return self.run_program(&opts, self.do_debug_printing); + return self.run_program(&opts); } Ok(()) @@ -837,11 +806,8 @@ impl EnvAppData { /// - 125: if the env command itself fails /// - 126: if the program is found but cannot be invoked /// - 127: if the program cannot be found - fn run_program( - &mut self, - opts: &Options<'_>, - do_debug_printing: bool, - ) -> Result<(), Box> { + fn run_program(&mut self, opts: &Options<'_>) -> Result<(), Box> { + let do_debug_printing = opts.debug; let prog = Cow::from(opts.program[0]); let arg0 = match opts.argv0 { @@ -945,6 +911,10 @@ impl EnvAppData { fn apply_removal_of_all_env_vars(opts: &Options<'_>) { // remove all env vars if told to ignore presets if opts.ignore_env { + if opts.debug { + let mut error = stderr().lock(); + let _ = writeln!(error, "cleaning environ"); + } for (ref name, _) in env::vars_os() { unsafe { env::remove_var(name); @@ -956,6 +926,7 @@ fn apply_removal_of_all_env_vars(opts: &Options<'_>) { #[cfg_attr(not(unix), allow(clippy::elidable_lifetime_names))] fn make_options<'a>( matches: &'a clap::ArgMatches, + debug: bool, #[cfg(all(unix, not(target_os = "fuchsia")))] signal_apply_all: &BTreeSet<&'static str>, ) -> UResult> { let ignore_env = matches.get_flag("ignore-environment"); @@ -988,6 +959,7 @@ fn make_options<'a>( sets: vec![], program: vec![], argv0, + debug, #[cfg(all(unix, not(target_os = "fuchsia")))] ignore_signal, #[cfg(all(unix, not(target_os = "fuchsia")))] @@ -1024,6 +996,12 @@ fn make_options<'a>( fn apply_unset_env_vars(opts: &Options<'_>) -> Result<(), Box> { for name in &opts.unsets { + if opts.debug { + let mut error = stderr().lock(); + let _ = error.write_all(b"unset: "); + let _ = error.write_all_os(name); + let _ = error.write_all(b"\n"); + } let native_name = NativeStr::new(name); if name.is_empty() || native_name.contains('\0').unwrap() @@ -1051,6 +1029,10 @@ fn apply_change_directory(opts: &Options<'_>) -> Result<(), Box> { } if let Some(d) = opts.running_directory { + if opts.debug { + let mut error = stderr().lock(); + let _ = writeln!(error, "chdir: {}", d.quote()); + } match env::set_current_dir(d) { Ok(()) => d, Err(error) => { @@ -1096,6 +1078,14 @@ fn apply_specified_env_vars(opts: &Options<'_>) { ); continue; } + if opts.debug { + let mut error = stderr().lock(); + let _ = error.write_all(b"setenv: "); + let _ = error.write_all_os(name); + let _ = error.write_all(b"="); + let _ = error.write_all_os(val); + let _ = error.write_all(b"\n"); + } unsafe { env::set_var(name, val); } diff --git a/src/uucore/src/lib/mods/display.rs b/src/uucore/src/lib/mods/display.rs index a011e355d0d..029ee308019 100644 --- a/src/uucore/src/lib/mods/display.rs +++ b/src/uucore/src/lib/mods/display.rs @@ -29,7 +29,7 @@ use std::env; use std::ffi::OsStr; use std::fmt; use std::fs::File; -use std::io::{self, BufWriter, Stdout, StdoutLock, Write as _}; +use std::io::{self, BufWriter, Stderr, StderrLock, Stdout, StdoutLock, Write as _}; // These used to be defined here, but they live in their own crate now. pub use os_display::{Quotable, Quoted}; @@ -105,6 +105,8 @@ pub trait OsWrite: io::Write { impl OsWrite for File {} impl OsWrite for Stdout {} impl OsWrite for StdoutLock<'_> {} +impl OsWrite for Stderr {} +impl OsWrite for StderrLock<'_> {} // A future smarter Windows implementation can first flush the BufWriter before // doing a raw write. impl OsWrite for BufWriter {} diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index e550450aa5f..13961986f28 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -212,12 +212,6 @@ fn test_debug_2() { .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", @@ -261,12 +255,6 @@ fn test_debug2_part_of_string_arg() { .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", @@ -276,6 +264,54 @@ fn test_debug2_part_of_string_arg() { ); } +#[test] +fn test_debug_trace_environment_changes() { + let result = new_ucmd!().args(&["-i", "-v", "A=1"]).succeeds(); + assert_eq!(result.stdout_str(), "A=1\n"); + assert_eq!(result.stderr_str(), "cleaning environ\nsetenv: A=1\n"); +} + +#[test] +fn test_debug_2_output_is_debug_1() { + let ts = TestScenario::new(util_name!()); + let v = ts.ucmd().args(&["-i", "-v", "A=1"]).succeeds(); + let vv = ts.ucmd().args(&["-i", "-vv", "A=1"]).succeeds(); + assert_eq!(v.stderr_str(), vv.stderr_str()); + assert_eq!(v.stdout_str(), vv.stdout_str()); +} + +#[test] +fn test_debug_trace_unset() { + let result = new_ucmd!() + .env("ENV_VERBOSE_UNSET_ME", "x") + .args(&["-v", "-u", "ENV_VERBOSE_UNSET_ME"]) + .succeeds(); + assert_eq!(result.stderr_str(), "unset: ENV_VERBOSE_UNSET_ME\n"); + + // unsetting a variable that does not exist is also traced + let result = new_ucmd!() + .args(&["-v", "-u", "ENV_VERBOSE_NOT_SET"]) + .succeeds(); + assert_eq!(result.stderr_str(), "unset: ENV_VERBOSE_NOT_SET\n"); +} + +#[cfg(feature = "echo")] +#[test] +fn test_debug_trace_chdir() { + let ts = TestScenario::new(util_name!()); + let result = ts + .ucmd() + .args(&["-v", "-C", "."]) + .arg(&ts.bin_path) + .args(&["echo", "hello"]) + .succeeds(); + assert!( + result + .stderr_str() + .starts_with("chdir: '.'\nexecuting: ") + ); +} + #[test] fn test_unset_invalid_variables() { use uucore::display::Quotable; @@ -576,8 +612,7 @@ fn test_split_string_into_args_debug_output_whitespace_handling() { assert_eq!(out.stdout_str(), "xAx\nxBx\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\ + "executing: printf\ \n arg[0]= 'printf'\n arg[1]= $'x%sx\\n'\n arg[2]= 'A'\n arg[3]= 'B'\n" ); }