Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/uu/env/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
4 changes: 3 additions & 1 deletion src/uu/env/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/uu/printenv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/uu/printenv/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/uu/printenv/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 28 additions & 3 deletions src/uu/printenv/src/printenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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::<String>(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(());
}

Expand All @@ -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 {
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/uucore/src/lib/features/quoting_style/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
Some(match s {
pub fn parse(s: impl AsRef<str>) -> Option<Self> {
Some(match s.as_ref() {
"literal" => Self::LITERAL,
"shell" => Self::SHELL,
"shell-always" => Self::SHELL_ALWAYS,
Expand Down
30 changes: 25 additions & 5 deletions src/uucore/src/lib/mods/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -120,12 +128,24 @@ impl OsWrite for Box<dyn OsWrite> {
///
/// 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<T: fmt::Display>(line_ending: T) -> io::Result<()> {
#[cfg(feature = "quoting-style")]
pub fn print_all_env_vars<T: fmt::Display>(
line_ending: T,
quoting_style: Option<QuotingStyle>,
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(())
Expand Down
145 changes: 145 additions & 0 deletions tests/by-util/test_printenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
}
}
Loading