diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index 08ac8585cda..2d17cf3105b 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -19,7 +19,7 @@ doctest = false clap = { workspace = true } fluent = { workspace = true } thiserror = { workspace = true } -uucore = { workspace = true, features = ["signals"] } +uucore = { workspace = true, features = ["quoting-style", "signals"] } [target.'cfg(unix)'.dependencies] nix = { workspace = true, features = ["process", "signal"] } diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 61efcbcaf99..893dae9c79c 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -41,6 +41,7 @@ use std::io::stderr; use std::mem::zeroed; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; +use uucore::i18n::UEncoding; use uucore::display::{Quotable, print_all_env_vars}; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError, strip_errno}; @@ -817,7 +818,8 @@ impl EnvAppData { apply_change_directory(&opts)?; if opts.program.is_empty() { // no program provided, so just dump all env vars to stdout - print_all_env_vars(opts.line_ending)?; + // `encoding` is irrelevant when `quoting_style` is None + print_all_env_vars(opts.line_ending, None, UEncoding::Ascii)?; } else { return self.run_program(&opts, self.do_debug_printing); } diff --git a/src/uu/printenv/Cargo.toml b/src/uu/printenv/Cargo.toml index a60b5bda9b2..43548ad4dea 100644 --- a/src/uu/printenv/Cargo.toml +++ b/src/uu/printenv/Cargo.toml @@ -19,7 +19,7 @@ doctest = false [dependencies] clap = { workspace = true } fluent = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["quoting-style"] } [lints] workspace = true diff --git a/src/uu/printenv/locales/en-US.ftl b/src/uu/printenv/locales/en-US.ftl index 20c1a81f8dc..3b8be3bbff5 100644 --- a/src/uu/printenv/locales/en-US.ftl +++ b/src/uu/printenv/locales/en-US.ftl @@ -2,3 +2,4 @@ printenv-about = Display the values of the specified environment VARIABLE(s), or printenv-usage = printenv [OPTION]... [VARIABLE]... printenv-help-null = end each output line with 0 byte rather than newline +printenv-help-quoting-style = set quoting style diff --git a/src/uu/printenv/locales/fr-FR.ftl b/src/uu/printenv/locales/fr-FR.ftl index 3ed5b3904f9..a0f5e98498b 100644 --- a/src/uu/printenv/locales/fr-FR.ftl +++ b/src/uu/printenv/locales/fr-FR.ftl @@ -2,3 +2,4 @@ printenv-about = Afficher les valeurs des VARIABLE(s) d'environnement spécifié printenv-usage = printenv [OPTION]... [VARIABLE]... printenv-help-null = terminer chaque ligne de sortie avec un octet 0 plutôt qu'un saut de ligne +printenv-help-quoting-style = indiquer le style d'encapsulation à utiliser diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index 0005577c159..2647f051b06 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -4,17 +4,19 @@ // file that was distributed with this source code. use std::env; -use std::io::Write; +use std::io::{IsTerminal, Write}; use clap::{Arg, ArgAction, Command}; use uucore::display::{OsWrite, print_all_env_vars}; use uucore::error::UResult; +use uucore::i18n::get_ctype_encoding; use uucore::line_ending::LineEnding; +use uucore::quoting_style::{QuotingStyle, escape_name, quoting_style_from_env}; use uucore::{format_usage, translate}; static OPT_NULL: &str = "null"; - +static OPT_QUOTING_STYLE: &str = "quoting-style"; static ARG_VARIABLES: &str = "variables"; #[uucore::main(no_signals)] @@ -26,10 +28,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); + let maybe_quoting_style = matches + .get_one::(OPT_QUOTING_STYLE) + .and_then(QuotingStyle::parse) + .or_else(|| { + std::io::stdout() + .is_terminal() // quoting style from env var is only used in TTY + .then(quoting_style_from_env) + .flatten() + }); + let separator = LineEnding::from_zero_flag(matches.get_flag(OPT_NULL)); if variables.is_empty() { - print_all_env_vars(separator)?; + print_all_env_vars(separator, maybe_quoting_style, get_ctype_encoding())?; return Ok(()); } @@ -42,6 +54,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } if let Some(var) = env::var_os(env_var) { let mut stdout = std::io::stdout().lock(); + + let var = if let Some(qs) = maybe_quoting_style { + escape_name(&var, qs, get_ctype_encoding()) + } else { + var + }; + stdout.write_all_os(&var)?; write!(stdout, "{separator}")?; } else { @@ -66,6 +85,12 @@ pub fn uu_app() -> Command { .help(translate!("printenv-help-null")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(OPT_QUOTING_STYLE) + .long(OPT_QUOTING_STYLE) + .help(translate!("printenv-help-quoting-style")) + .action(ArgAction::Set), + ) .arg( Arg::new(ARG_VARIABLES) .action(ArgAction::Append) diff --git a/src/uucore/src/lib/features/quoting_style/mod.rs b/src/uucore/src/lib/features/quoting_style/mod.rs index 75551692b5b..a07ded921e9 100644 --- a/src/uucore/src/lib/features/quoting_style/mod.rs +++ b/src/uucore/src/lib/features/quoting_style/mod.rs @@ -157,8 +157,8 @@ impl QuotingStyle { /// /// Used for e.g., the `QUOTING_STYLE` environment variable and the /// `--quoting-style` option of `ls`. - pub fn parse(s: &str) -> Option { - Some(match s { + pub fn parse(s: impl AsRef) -> Option { + Some(match s.as_ref() { "literal" => Self::LITERAL, "shell" => Self::SHELL, "shell-always" => Self::SHELL_ALWAYS, diff --git a/src/uucore/src/lib/mods/display.rs b/src/uucore/src/lib/mods/display.rs index a011e355d0d..19e82410e58 100644 --- a/src/uucore/src/lib/mods/display.rs +++ b/src/uucore/src/lib/mods/display.rs @@ -25,15 +25,23 @@ //! # Ok::<(), std::io::Error>(()) //! ``` -use std::env; use std::ffi::OsStr; -use std::fmt; +#[cfg(feature = "quoting-style")] +use std::ffi::OsString; use std::fs::File; use std::io::{self, BufWriter, Stdout, StdoutLock, Write as _}; // These used to be defined here, but they live in their own crate now. pub use os_display::{Quotable, Quoted}; +#[cfg(feature = "quoting-style")] +use crate::{ + i18n::UEncoding, + quoting_style::{QuotingStyle, escape_name}, +}; +#[cfg(feature = "quoting-style")] +use std::{env, fmt}; + /// Print a path (or `OsStr`-like object) directly to stdout, with a trailing newline, /// without losing any information if its encoding is invalid. /// @@ -120,12 +128,24 @@ impl OsWrite for Box { /// /// This function handles non-UTF-8 environment variable names and values correctly by using /// raw bytes on Unix systems. -pub fn print_all_env_vars(line_ending: T) -> io::Result<()> { +#[cfg(feature = "quoting-style")] +pub fn print_all_env_vars( + line_ending: T, + quoting_style: Option, + encoding: UEncoding, +) -> io::Result<()> { let mut stdout = io::stdout().lock(); for (name, value) in env::vars_os() { - stdout.write_all_os(&name)?; + let escape = |s: OsString| { + quoting_style + .map(|qs| escape_name(&s, qs, encoding)) + .unwrap_or(s) + }; + + stdout.write_all_os(&escape(name))?; stdout.write_all(b"=")?; - stdout.write_all_os(&value)?; + + stdout.write_all_os(&escape(value))?; write!(stdout, "{line_ending}")?; } Ok(()) diff --git a/tests/by-util/test_printenv.rs b/tests/by-util/test_printenv.rs index 89b9140fc21..739c854bf4b 100644 --- a/tests/by-util/test_printenv.rs +++ b/tests/by-util/test_printenv.rs @@ -133,3 +133,148 @@ fn test_non_utf8_env_vars() { .succeeds() .stdout_contains_bytes(b"NON_UTF8_VAR=hello\x80world"); } + +mod quoting_style { + use uutests::new_ucmd; + + use rstest::rstest; + use rstest_reuse::{apply, template}; + + #[template] + #[rstest] + #[case::literal("literal", "a b", "c d")] + #[case::escape("escape", "a\\ b", "c\\ d")] + #[case::shell("shell", "'a b'", "'c d'")] + #[case::c("c", "\"a b\"", "\"c d\"")] + fn test_qs(#[case] qs: &str, #[case] key: &str, #[case] val: &str) {} + + #[apply(test_qs)] + fn test_qs_arg(qs: &str, key: &str, val: &str) { + new_ucmd!() + .env("a b", "c d") + .args(&["--quoting-style", qs]) + .succeeds() + .stdout_contains(format!("{key}={val}")); + + new_ucmd!() + .env("a b", "c d") + .arg("a b") + .args(&["--quoting-style", qs]) + .succeeds() + .stdout_is(format!("{val}\n")); + } + + #[cfg(unix)] + #[apply(test_qs)] + fn test_qs_env(qs: &str, key: &str, val: &str) { + new_ucmd!() + .env("QUOTING_STYLE", qs) + .env("a b", "c d") + .terminal_simulation(true) + .succeeds() + .stdout_contains(format!("{key}={val}")); + + new_ucmd!() + .env("QUOTING_STYLE", qs) + .env("a b", "c d") + .arg("a b") + .terminal_simulation(true) + .succeeds() + .stdout_is(format!("{val}\r\n")); + } + + #[template] + #[rstest] + #[case::locale_c("C", "locale", "'a b'", "'c d'")] + #[case::clocale_c("C", "clocale", "\"a b\"", "\"c d\"")] + #[case::locale_utf8("en_US.UTF-8", "locale", "\u{2018}a b\u{2019}", "\u{2018}c d\u{2019}")] + #[case::clocale_utf8("en_US.UTF-8", "clocale", "\u{2018}a b\u{2019}", "\u{2018}c d\u{2019}")] + fn test_qs_locale( + #[case] locale: &str, + #[case] qs: &str, + #[case] key: &str, + #[case] val: &str, + ) { + } + + #[apply(test_qs_locale)] + fn test_qs_locale_arg(locale: &str, qs: &str, #[case] key: &str, #[case] val: &str) { + new_ucmd!() + .env("LC_ALL", locale) + .env("a b", "c d") + .args(&["--quoting-style", qs]) + .succeeds() + .stdout_contains(format!("{key}={val}")); + + new_ucmd!() + .env("LC_ALL", locale) + .env("a b", "c d") + .arg("a b") + .args(&["--quoting-style", qs]) + .succeeds() + .stdout_is(format!("{val}\n")); + } + + #[cfg(unix)] + #[apply(test_qs_locale)] + fn test_qs_locale_env(locale: &str, qs: &str, #[case] key: &str, #[case] val: &str) { + new_ucmd!() + .env("QUOTING_STYLE", qs) + .env("LC_ALL", locale) + .env("a b", "c d") + .terminal_simulation(true) + .succeeds() + .stdout_contains(format!("{key}={val}")); + + new_ucmd!() + .env("QUOTING_STYLE", qs) + .env("LC_ALL", locale) + .env("a b", "c d") + .arg("a b") + .terminal_simulation(true) + .succeeds() + .stdout_is(format!("{val}\r\n")); + } + + // Check that the QUOTING_STYLE environment variable is ignored when + // stdout is not a terminal. + #[test] + fn test_qs_env_no_effect_without_tty() { + new_ucmd!() + .env("QUOTING_STYLE", "c") + .env("a b", "c d") + .arg("a b") + // .terminal_simulation(false) // stdout is NOT a terminal + .succeeds() + .stdout_is("c d\n") + .stdout_does_not_contain("\"c d\""); + } + + // Check that --quoting-style option takes precedence + // over the QUOTING_STYLE environment variable + #[cfg(unix)] + #[test] + fn test_qs_arg_precedence() { + new_ucmd!() + .env("QUOTING_STYLE", "c") + .env("a b", "c d") + .arg("a b") + .arg("--quoting-style=escape") + .terminal_simulation(true) + .succeeds() + .stdout_is("c\\ d\r\n") + .stdout_does_not_contain("\"c d\""); + } + + #[cfg(unix)] + #[test] + fn test_qs_env_invalid() { + new_ucmd!() + .env("QUOTING_STYLE", "invalid") + .terminal_simulation(true) + .succeeds() + .stderr_is( + "printenv: ignoring invalid value of environment variable QUOTING_STYLE: 'invalid'\r\n", + ); + } +}