diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index 1f32567c41b..12ee4723aac 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -18,7 +18,7 @@ doctest = false [dependencies] clap = { workspace = true } thiserror = { workspace = true } -uucore = { workspace = true, features = ["signals"] } +uucore = { workspace = true, features = ["quoting-style", "signals"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 02505edbed5..24fd44745ba 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}; @@ -820,7 +821,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/ls/locales/en-US.ftl b/src/uu/ls/locales/en-US.ftl index dda2379fa1a..075ec37bf86 100644 --- a/src/uu/ls/locales/en-US.ftl +++ b/src/uu/ls/locales/en-US.ftl @@ -129,7 +129,6 @@ ls-help-full-time = like -l --time-style=full-iso ls-help-context = print any security context of each file ls-help-group-directories-first = group directories before files; can be augmented with a --sort option, but any use of --sort=none (-U) disables grouping -ls-invalid-quoting-style = {$program}: Ignoring invalid value of environment variable QUOTING_STYLE: '{$style}' ls-invalid-columns-width = ignoring invalid width in environment variable COLUMNS: {$width} ls-invalid-ignore-pattern = Invalid pattern for ignore: {$pattern} ls-invalid-hide-pattern = Invalid pattern for hide: {$pattern} diff --git a/src/uu/ls/locales/fr-FR.ftl b/src/uu/ls/locales/fr-FR.ftl index 1ee9d7dfaac..06a1a6aa6d4 100644 --- a/src/uu/ls/locales/fr-FR.ftl +++ b/src/uu/ls/locales/fr-FR.ftl @@ -127,7 +127,6 @@ ls-help-full-time = comme -l --time-style=full-iso ls-help-context = afficher tout contexte de sécurité de chaque fichier ls-help-group-directories-first = grouper les répertoires avant les fichiers ; peut être augmenté avec une option --sort, mais toute utilisation de --sort=none (-U) désactive le groupement -ls-invalid-quoting-style = {$program} : Ignorer la valeur invalide de la variable d'environnement QUOTING_STYLE : '{$style}' ls-invalid-columns-width = ignorer la largeur invalide dans la variable d'environnement COLUMNS : {$width} ls-invalid-ignore-pattern = Motif invalide pour ignore : {$pattern} ls-invalid-hide-pattern = Motif invalide pour hide : {$pattern} diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs index 5985bf12a40..db141a3c152 100644 --- a/src/uu/ls/src/config.rs +++ b/src/uu/ls/src/config.rs @@ -9,7 +9,7 @@ use std::{ borrow::Cow, ffi::{OsStr, OsString}, - io::{self, IsTerminal, Write as _, stdout}, + io::{IsTerminal, stdout}, num::IntErrorKind, }; @@ -18,16 +18,23 @@ use lscolors::LsColors; use term_grid::SPACES_IN_TAB; use uucore::{ - diagnostics::OptionValue, display::Quotable, error::UResult, format::human::SizeFormat, - fsext::MetadataTimeField, line_ending::LineEnding, parser::parse_block_size, - parser::parse_glob, parser::parse_size::parse_size_non_zero_u64, quoting_style::QuotingStyle, - show_error, show_warning, time::format, translate, + diagnostics::OptionValue, + display::Quotable, + error::UResult, + format::human::SizeFormat, + fsext::MetadataTimeField, + line_ending::LineEnding, + parser::{parse_block_size, parse_glob, parse_size::parse_size_non_zero_u64}, + quoting_style::{QuotingStyle, quoting_style_from_env}, + show_error, show_warning, + time::format, + translate, }; use crate::{ LsError, colors::{LsColorsParseError, validate_ls_colors_env}, - display::{Format, IndicatorStyle, LocaleQuoting, LongFormat}, + display::{Format, IndicatorStyle, LongFormat}, options::QUOTING_STYLE, }; @@ -226,7 +233,6 @@ pub struct Config { // Dir and vdir needs access to this field pub quoting_style: QuotingStyle, pub(crate) show_control_chars: bool, - pub(crate) locale_quoting: Option, pub(crate) indicator_style: Option, pub(crate) time_format_recent: String, // Time format for recent dates pub(crate) time_format_older: Option, // Time format for older dates (optional, if not present, time_format_recent is used) @@ -497,73 +503,6 @@ fn extract_hyperlink(options: &clap::ArgMatches) -> bool { } } -/// Match the argument given to --quoting-style or the [`QUOTING_STYLE`] env variable. -/// -/// # Arguments -/// -/// * `style`: the actual argument string -/// * `show_control` - A boolean value representing whether to show control characters. -/// -/// # Returns -/// -/// * An option with None if the style string is invalid, or a `QuotingStyle` wrapped in `Some`. -struct QuotingStyleSpec { - style: QuotingStyle, - fixed_control: bool, - locale: Option, -} - -impl QuotingStyleSpec { - fn new(style: QuotingStyle) -> Self { - Self { - style, - fixed_control: false, - locale: None, - } - } - - fn with_locale(style: QuotingStyle, locale: LocaleQuoting) -> Self { - Self { - style, - fixed_control: true, - locale: Some(locale), - } - } -} -fn match_quoting_style_name( - style: &str, - show_control: bool, -) -> Option<(QuotingStyle, Option)> { - let spec = match style { - "literal" => QuotingStyleSpec::new(QuotingStyle::Literal { - show_control: false, - }), - "shell" => QuotingStyleSpec::new(QuotingStyle::SHELL), - "shell-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_QUOTE), - "shell-escape" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE), - "shell-escape-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE_QUOTE), - "c" => QuotingStyleSpec::new(QuotingStyle::C_DOUBLE), - "escape" => QuotingStyleSpec::new(QuotingStyle::C_NO_QUOTES), - "locale" => QuotingStyleSpec { - style: QuotingStyle::Literal { - show_control: false, - }, - fixed_control: true, - locale: Some(LocaleQuoting::Single), - }, - "clocale" => QuotingStyleSpec::with_locale(QuotingStyle::C_DOUBLE, LocaleQuoting::Double), - _ => return None, - }; - - let style = if spec.fixed_control { - spec.style - } else { - spec.style.show_control(show_control) - }; - - Some((style, spec.locale)) -} - /// Extracts the quoting style to use based on the options provided. /// If no options are given, it looks if a default quoting style is provided /// through the [`QUOTING_STYLE`] environment variable. @@ -580,39 +519,32 @@ fn extract_quoting_style( options: &clap::ArgMatches, show_control: bool, mode: ProgramMode, -) -> (QuotingStyle, Option) { +) -> QuotingStyle { let opt_quoting_style = options.get_one::(QUOTING_STYLE); if let Some(style) = opt_quoting_style { - match match_quoting_style_name(style, show_control) { - Some(pair) => pair, + match QuotingStyle::parse(style) { + Some(qs) => qs.show_control(show_control), None => unreachable!("Should have been caught by Clap"), } } else if options.get_flag(options::quoting::LITERAL) { - (QuotingStyle::Literal { show_control }, None) + QuotingStyle::Literal { show_control } } else if options.get_flag(options::quoting::ESCAPE) { - (QuotingStyle::C_NO_QUOTES, None) + QuotingStyle::C_NO_QUOTES } else if options.get_flag(options::quoting::C) { - (QuotingStyle::C_DOUBLE, None) + QuotingStyle::C_DOUBLE } else { // If set, the QUOTING_STYLE environment variable specifies a default style. - if let Ok(style) = std::env::var("QUOTING_STYLE") { - if let Some(pair) = match_quoting_style_name(style.as_str(), show_control) { - return pair; - } - let _ = writeln!( - io::stderr(), - "{}", - translate!("ls-invalid-quoting-style", "program" => std::env::args().next().unwrap_or_else(|| "ls".to_string()), "style" => style) - ); + if let Some(qs) = quoting_style_from_env() { + return qs.show_control(show_control); } match mode { - ProgramMode::Dir | ProgramMode::Vdir => (QuotingStyle::C_NO_QUOTES, None), + ProgramMode::Dir | ProgramMode::Vdir => QuotingStyle::C_NO_QUOTES, ProgramMode::Ls if stdout().is_terminal() => { - (QuotingStyle::SHELL_ESCAPE.show_control(show_control), None) + QuotingStyle::SHELL_ESCAPE.show_control(show_control) } - ProgramMode::Ls => (QuotingStyle::Literal { show_control }, None), + ProgramMode::Ls => QuotingStyle::Literal { show_control }, } } } @@ -860,8 +792,7 @@ impl Config { || !stdout().is_terminal() }; - let (mut quoting_style, mut locale_quoting) = - extract_quoting_style(options, show_control, mode); + let mut quoting_style = extract_quoting_style(options, show_control, mode); let indicator_style = extract_indicator_style(options); let mut ignore_patterns: Vec = Vec::new(); @@ -930,52 +861,29 @@ impl Config { options::quoting::LITERAL, ]; let get_last = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - if get_last(options::ZERO) - > zero_formats_opts - .into_iter() - .map(get_last) - .max() + (options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine)) + .then(|| options.index_of(flag)) + .flatten() .unwrap_or(0) - { - format = if explicit_long { - format - } else { - Format::OneLine - }; + }; + let zero_idx = get_last(options::ZERO); + let last_of = + |flag_list: &[&str]| flag_list.iter().copied().map(get_last).max().unwrap_or(0); + + if zero_idx > last_of(&zero_formats_opts) && !explicit_long { + format = Format::OneLine; } - if get_last(options::ZERO) - > zero_colors_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { + + if zero_idx > last_of(&zero_colors_opts) { needs_color = false; } - if get_last(options::ZERO) - > zero_show_control_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { + + if zero_idx > last_of(&zero_show_control_opts) { show_control = true; } - if get_last(options::ZERO) - > zero_quoting_style_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { + + if zero_idx > last_of(&zero_quoting_style_opts) { quoting_style = QuotingStyle::Literal { show_control }; - locale_quoting = None; } if needs_color && let Err(err) = validate_ls_colors_env() { @@ -1062,7 +970,6 @@ impl Config { width, quoting_style, show_control_chars: options.get_flag(options::SHOW_CONTROL_CHARS), - locale_quoting, indicator_style, time_format_recent, time_format_older, diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index d43bb2aad84..d9dc301792b 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -47,7 +47,6 @@ use uucore::{ format::human::human_readable, fs::display_permissions, fsext::metadata_get_time, - i18n::{UEncoding, get_ctype_encoding}, line_ending::LineEnding, os_str_as_bytes_lossy, os_string_from_vec, quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}, @@ -132,12 +131,6 @@ pub(crate) enum IndicatorStyle { Classify, } -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum LocaleQuoting { - Single, - Double, -} - #[derive(PartialEq, Eq, Debug)] pub enum Format { Columns, @@ -170,7 +163,8 @@ pub fn show_dir_name( out: &mut BufWriter, config: &Config, ) -> std::io::Result { - let escaped_name = escape_dir_name_with_locale(path_data.path().as_os_str(), config); + let escaped_name = + locale_aware_escape_dir_name(path_data.path().as_os_str(), config.quoting_style); let name = if config.hyperlink && !config.dired { create_hyperlink(&escaped_name, path_data) @@ -183,48 +177,24 @@ pub fn show_dir_name( Ok(name.len()) } -fn escape_with_locale(name: &OsStr, config: &Config, fallback: F) -> OsString -where - F: FnOnce(&OsStr, QuotingStyle) -> OsString, -{ - if let Some(locale) = config.locale_quoting { - locale_quote(name, locale) - } else { - fallback(name, config.quoting_style) - } -} - -fn escape_dir_name_with_locale(name: &OsStr, config: &Config) -> OsString { - escape_with_locale(name, config, locale_aware_escape_dir_name) -} - fn escape_name_with_locale(name: &OsStr, config: &Config) -> OsString { let comma_separated = config.format == Format::Commas && os_str_as_bytes_lossy(name).contains(&b','); - let style = match config.quoting_style { - QuotingStyle::Shell { - escape, - show_control, - .. - } if comma_separated => QuotingStyle::Shell { - escape, - always_quote: true, - show_control, - }, - style => style, + + // Format::Commas forces quoting. + let style = if comma_separated { + config.quoting_style.always_quote(true) + } else { + config.quoting_style }; - let escaped = escape_with_locale(name, config, |name, _| { - locale_aware_escape_name(name, style) - }); + let escaped = locale_aware_escape_name(name, style); - let escaped = - if comma_separated && config.locale_quoting.is_none() && style == QuotingStyle::C_NO_QUOTES - { - escaped.to_string_lossy().replace(',', "\\,").into() - } else { - escaped - }; + let escaped = if comma_separated && style == QuotingStyle::C_NO_QUOTES { + escaped.to_string_lossy().replace(',', "\\,").into() + } else { + escaped + }; if config.format == Format::Commas && config.line_ending == LineEnding::Newline @@ -246,86 +216,6 @@ fn escape_name_with_locale(name: &OsStr, config: &Config) -> OsString { escaped } -fn locale_quote(name: &OsStr, style: LocaleQuoting) -> OsString { - let bytes = os_str_as_bytes_lossy(name); - - // In a UTF-8 locale GNU's locale/clocale quoting uses Unicode quotation - // marks U+2018 (LEFT) and U+2019 (RIGHT) as delimiters for both styles, - // keyed off LC_CTYPE. Since the delimiters differ from any ASCII quote, - // embedded apostrophes and double quotes are left untouched; only control - // characters, backslashes and invalid bytes are escaped. - if get_ctype_encoding() == UEncoding::Utf8 { - let mut quoted = String::with_capacity(name.len() + 6); - quoted.push('\u{2018}'); - for chunk in bytes.utf8_chunks() { - for c in chunk.valid().chars() { - if c == '\\' { - quoted.push_str("\\\\"); - } else if c.is_ascii() && c.is_control() { - push_basic_escape(&mut quoted, c as u8); - } else if c.is_control() { - // Non-ASCII control characters (the C1 range, e.g. - // U+0085 NEL) are not printable; octal-escape their - // UTF-8 bytes like GNU does for non-printable chars. - let mut buf = [0u8; 4]; - for &byte in c.encode_utf8(&mut buf).as_bytes() { - let _ = write!(quoted, "\\{byte:03o}"); - } - } else { - quoted.push(c); - } - } - for &byte in chunk.invalid() { - let _ = write!(quoted, "\\{byte:03o}"); - } - } - quoted.push('\u{2019}'); - return OsString::from(quoted); - } - - let mut quoted = String::with_capacity(name.len() + 2); - match style { - LocaleQuoting::Single => quoted.push('\''), - LocaleQuoting::Double => quoted.push('"'), - } - for &byte in bytes.as_ref() { - push_locale_byte(&mut quoted, byte, style); - } - match style { - LocaleQuoting::Single => quoted.push('\''), - LocaleQuoting::Double => quoted.push('"'), - } - OsString::from(quoted) -} - -fn push_locale_byte(buf: &mut String, byte: u8, style: LocaleQuoting) { - match (style, byte) { - (LocaleQuoting::Single, b'\'') => buf.push_str("'\\''"), - (LocaleQuoting::Double, b'"') => buf.push_str("\\\""), - (_, b'\\') => buf.push_str("\\\\"), - _ => push_basic_escape(buf, byte), - } -} - -fn push_basic_escape(buf: &mut String, byte: u8) { - match byte { - b'\x07' => buf.push_str("\\a"), - b'\x08' => buf.push_str("\\b"), - b'\t' => buf.push_str("\\t"), - b'\n' => buf.push_str("\\n"), - b'\x0b' => buf.push_str("\\v"), - b'\x0c' => buf.push_str("\\f"), - b'\r' => buf.push_str("\\r"), - b'\x1b' => buf.push_str("\\e"), - b'"' => buf.push('"'), - b'\'' => buf.push('\''), - b if (0x20..=0x7e).contains(&b) => buf.push(b as char), - _ => { - let _ = write!(buf, "\\{byte:03o}"); - } - } -} - pub fn should_display(entry: &DirEntry, config: &Config) -> bool { // check if hidden if config.files == Files::Normal && is_hidden(entry) { diff --git a/src/uu/printenv/Cargo.toml b/src/uu/printenv/Cargo.toml index cb6a009b1dc..eb8d951a25e 100644 --- a/src/uu/printenv/Cargo.toml +++ b/src/uu/printenv/Cargo.toml @@ -18,7 +18,7 @@ doctest = false [dependencies] clap = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["quoting-style"] } fluent = { workspace = true } [lints] diff --git a/src/uu/printenv/src/printenv.rs b/src/uu/printenv/src/printenv.rs index 0005577c159..b0351d41764 100644 --- a/src/uu/printenv/src/printenv.rs +++ b/src/uu/printenv/src/printenv.rs @@ -10,7 +10,9 @@ 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::{escape_name, quoting_style_from_env}; use uucore::{format_usage, translate}; static OPT_NULL: &str = "null"; @@ -26,10 +28,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); + let maybe_quoting_style = quoting_style_from_env(); + 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 +46,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 { diff --git a/src/uu/stat/locales/en-US.ftl b/src/uu/stat/locales/en-US.ftl index f962eaf3db9..0bf0e40c4ab 100644 --- a/src/uu/stat/locales/en-US.ftl +++ b/src/uu/stat/locales/en-US.ftl @@ -56,7 +56,6 @@ stat-after-help = Valid format sequences for files (without `--file-system`): ## Error messages -stat-warning-invalid-env-quoting-style = ignoring invalid value of environment variable QUOTING_STYLE: '{$style}' stat-error-invalid-directive = {$directive}: invalid directive stat-error-cannot-read-filesystem = cannot read table of mounted file systems: {$error} stat-error-stdin-filesystem-mode = using '-' to denote standard input does not work in file system mode diff --git a/src/uu/stat/locales/fr-FR.ftl b/src/uu/stat/locales/fr-FR.ftl index d7f48e82439..2f5c7737e40 100644 --- a/src/uu/stat/locales/fr-FR.ftl +++ b/src/uu/stat/locales/fr-FR.ftl @@ -94,7 +94,6 @@ stat-word-birth = Créé ## Messages d'erreur -stat-warning-invalid-env-quoting-style = valeur invalide de la variable d'environnement QUOTING_STYLE ignorée : '{$style}' stat-error-invalid-directive = {$directive} : directive invalide stat-error-cannot-read-filesystem = impossible de lire la table des systèmes de fichiers montés : {$error} stat-error-stdin-filesystem-mode = utiliser '-' pour désigner l'entrée standard ne fonctionne pas en mode système de fichiers diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 49cbbe5d720..4c5398ecfee 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -9,7 +9,7 @@ use std::ops::Range; use uucore::diagnostics::OptionValue; use uucore::error::{UError, UResult, USimpleError}; use uucore::i18n::get_ctype_encoding; -use uucore::quoting_style::{Quotes, QuotingStyle, escape_name}; +use uucore::quoting_style::{QuotingStyle, escape_name, quoting_style_from_env}; use uucore::translate; use clap::builder::ValueParser; @@ -247,23 +247,6 @@ pub enum OutputType<'a> { Unknown, } -/// Match a `QUOTING_STYLE` value to a quoting style. -fn parse_quoting_style(style: &str) -> Option { - Some(match style { - "literal" => QuotingStyle::Literal { show_control: true }, - "shell" => QuotingStyle::SHELL.show_control(true), - "shell-always" => QuotingStyle::SHELL_QUOTE.show_control(true), - "shell-escape" => QuotingStyle::SHELL_ESCAPE, - "shell-escape-always" => QuotingStyle::SHELL_ESCAPE_QUOTE, - "c" | "clocale" => QuotingStyle::C_DOUBLE, - "escape" => QuotingStyle::C_NO_QUOTES, - "locale" => QuotingStyle::C { - quotes: Quotes::Single, - }, - _ => return None, - }) -} - #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum Precision { NotSpecified, @@ -521,22 +504,6 @@ fn quote_file_name(file_name: &str, quoting_style: QuotingStyle) -> String { .to_string() } -/// Get the quoting style from the `QUOTING_STYLE` environment variable. -fn env_quoting_style() -> QuotingStyle { - match env::var("QUOTING_STYLE") { - Ok(style) => parse_quoting_style(&style).unwrap_or_else(|| { - // Warn when QUOTING_STYLE is set to a value we don't understand, - // then fall back to the default. - show_error!( - "{}", - translate!("stat-warning-invalid-env-quoting-style", "style" => style.clone()) - ); - QuotingStyle::SHELL_ESCAPE - }), - Err(_) => QuotingStyle::SHELL_ESCAPE, - } -} - fn get_quoted_file_name( display_name: &str, file: &OsString, @@ -1112,7 +1079,11 @@ impl Stater { } fn quoting_style(&self) -> QuotingStyle { - *self.quoting_style.get_or_init(env_quoting_style) + *self.quoting_style.get_or_init(|| { + quoting_style_from_env() + .unwrap_or(QuotingStyle::SHELL_ESCAPE) + .show_control(true) + }) } fn find_mount_point>(&self, p: P) -> Option<&OsString> { diff --git a/src/uucore/locales/errors/en-US.ftl b/src/uucore/locales/errors/en-US.ftl index 76d73436aeb..585ef18a4a3 100644 --- a/src/uucore/locales/errors/en-US.ftl +++ b/src/uucore/locales/errors/en-US.ftl @@ -54,3 +54,6 @@ size-diag-help-syntax = a size is a number and an optional unit: K, M, G and so # in its own words. range-diag-label-too-large = this number is too large range-diag-label-inverted = this range ends before it starts + +# Invalid QUOTING_STYLE env var +invalid-quoting-style-env-var = ignoring invalid value of environment variable QUOTING_STYLE: { $invalid } diff --git a/src/uucore/locales/errors/fr-FR.ftl b/src/uucore/locales/errors/fr-FR.ftl index 03b9eff60fe..1ac3b8acb76 100644 --- a/src/uucore/locales/errors/fr-FR.ftl +++ b/src/uucore/locales/errors/fr-FR.ftl @@ -54,3 +54,6 @@ size-diag-help-syntax = une taille est un nombre suivi d'une unité facultative # compte, donc chaque utilitaire le dit avec ses propres mots. range-diag-label-too-large = ce nombre est trop grand range-diag-label-inverted = cet intervalle se termine avant de commencer + +# variable d'environnement QUOTING_STYLE invalide +invalid-quoting-style-env-var = valeur invalide de la variable d'environnement QUOTING_STYLE ignorée : { $invalid } diff --git a/src/uucore/src/lib/features/quoting_style/c_quoter.rs b/src/uucore/src/lib/features/quoting_style/c_quoter.rs index 47a21571985..1083ca59835 100644 --- a/src/uucore/src/lib/features/quoting_style/c_quoter.rs +++ b/src/uucore/src/lib/features/quoting_style/c_quoter.rs @@ -3,11 +3,42 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use super::{EscapedChar, Quoter, Quotes}; +use super::{EscapedChar, Quoter}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CQuotes { + pub(super) opening: char, + pub(super) closing: char, +} + +impl CQuotes { + pub const SINGLE: Self = Self { + opening: '\'', + closing: '\'', + }; + + pub const DOUBLE: Self = Self { + opening: '"', + closing: '"', + }; + + pub const LOCALE_UTF8: Self = Self { + opening: '\u{2018}', + closing: '\u{2019}', + }; + + pub(super) fn opening_as_utf8(self, buf: &mut [u8]) -> &[u8] { + self.opening.encode_utf8(buf).as_bytes() + } + + pub(super) fn closing_as_utf8(self, buf: &mut [u8]) -> &[u8] { + self.closing.encode_utf8(buf).as_bytes() + } +} pub(super) struct CQuoter { - /// The type of quotes to use. - quotes: Quotes, + /// The type of quotes to use, if any. + quotes: Option, dirname: bool, @@ -15,12 +46,12 @@ pub(super) struct CQuoter { } impl CQuoter { - pub fn new(quotes: Quotes, dirname: bool, size_hint: usize) -> Self { + pub(super) fn new(quotes: Option, dirname: bool, size_hint: usize) -> Self { let mut buffer = Vec::with_capacity(size_hint); - match quotes { - Quotes::None => (), - Quotes::Single => buffer.push(b'\''), - Quotes::Double => buffer.push(b'"'), + + if let Some(quotes) = quotes { + let mut quote_buf = [0; 4]; + buffer.extend_from_slice(quotes.opening_as_utf8(&mut quote_buf)); } Self { @@ -46,12 +77,15 @@ impl Quoter for CQuoter { } } - fn finalize(mut self: Box) -> Vec { - match self.quotes { - Quotes::None => (), - Quotes::Single => self.buffer.push(b'\''), - Quotes::Double => self.buffer.push(b'"'), + fn finalize(self: Box) -> Vec { + let Self { + quotes, mut buffer, .. + } = *self; + if let Some(quotes) = quotes { + let mut quote_buf = [0; 4]; + buffer.extend_from_slice(quotes.closing_as_utf8(&mut quote_buf)); } - self.buffer + + buffer } } diff --git a/src/uucore/src/lib/features/quoting_style/escaped_char.rs b/src/uucore/src/lib/features/quoting_style/escaped_char.rs index e9a14ca737a..56040cba5a3 100644 --- a/src/uucore/src/lib/features/quoting_style/escaped_char.rs +++ b/src/uucore/src/lib/features/quoting_style/escaped_char.rs @@ -5,7 +5,8 @@ use std::char::from_digit; -use super::Quotes; +use super::c_quoter::CQuotes; +use super::shell_quoter::ShellQuotes; // PR#6559 : Remove `]{}` from special shell chars. const SPECIAL_SHELL_CHARS: &str = "`$&*()|[;\\'\"<>?! "; @@ -107,21 +108,25 @@ impl EscapeOctal { } impl EscapedChar { - pub fn new_literal(c: char) -> Self { + pub(super) fn new_literal(c: char) -> Self { Self { state: EscapeState::Char(c), } } - pub fn new_octal(b: u8) -> Self { + pub(super) fn new_octal(b: u8) -> Self { Self { state: EscapeState::Octal(EscapeOctal::from_byte(b)), } } - pub fn new_c(c: char, quotes: Quotes, dirname: bool) -> Self { + /// Escape a character for C or locale quoting style. + pub(super) fn new_c(c: char, quotes: Option, dirname: bool) -> Self { use EscapeState::*; let init_state = match c { + // When using locale quoting style, only escape the closing + // character, not the opening one. + c if quotes.is_some_and(|q| q.closing == c) => Backslash(c), '\x07' => Backslash('a'), '\x08' => Backslash('b'), '\t' => Backslash('t'), @@ -130,18 +135,7 @@ impl EscapedChar { '\x0C' => Backslash('f'), '\r' => Backslash('r'), '\\' => Backslash('\\'), - '\'' => match quotes { - Quotes::Single => Backslash('\''), - _ => Char('\''), - }, - '"' => match quotes { - Quotes::Double => Backslash('"'), - _ => Char('"'), - }, - ' ' if !dirname => match quotes { - Quotes::None => Backslash(' '), - _ => Char(' '), - }, + ' ' if !dirname && quotes.is_none() => Backslash(' '), ':' if dirname => Backslash(':'), _ if c.is_control() => Octal(EscapeOctal::from_char(c)), _ => Char(c), @@ -149,7 +143,7 @@ impl EscapedChar { Self { state: init_state } } - pub fn new_shell(c: char, escape: bool, quotes: Quotes) -> Self { + pub(super) fn new_shell(c: char, escape: bool, quotes: ShellQuotes) -> Self { use EscapeState::*; let init_state = match c { _ if !escape && c.is_control() => Char(c), @@ -160,10 +154,7 @@ impl EscapedChar { '\x0B' => Backslash('v'), '\x0C' => Backslash('f'), '\r' => Backslash('r'), - '\'' => match quotes { - Quotes::Single => Backslash('\''), - _ => Char('\''), - }, + '\'' if matches!(quotes, ShellQuotes::Single) => Backslash('\''), _ if c.is_control() => Octal(EscapeOctal::from_char(c)), _ if SPECIAL_SHELL_CHARS.contains(c) => ForceQuote(c), _ => Char(c), diff --git a/src/uucore/src/lib/features/quoting_style/mod.rs b/src/uucore/src/lib/features/quoting_style/mod.rs index e4913cde2d4..3c38500c67d 100644 --- a/src/uucore/src/lib/features/quoting_style/mod.rs +++ b/src/uucore/src/lib/features/quoting_style/mod.rs @@ -12,6 +12,7 @@ use crate::i18n::{self, UEncoding}; use crate::quoting_style::c_quoter::CQuoter; use crate::quoting_style::literal_quoter::LiteralQuoter; use crate::quoting_style::shell_quoter::{EscapedShellQuoter, NonEscapedShellQuoter}; +use crate::{show_error, translate}; mod escaped_char; pub use escaped_char::{EscapeState, EscapedChar}; @@ -20,6 +21,9 @@ mod c_quoter; mod literal_quoter; mod shell_quoter; +pub use c_quoter::CQuotes; +use os_display::Quotable; + /// The quoting style to use when escaping a name. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum QuotingStyle { @@ -41,7 +45,7 @@ pub enum QuotingStyle { /// Used in, e.g., `ls --quote-name`. C { /// The type of quotes to use. - quotes: Quotes, + quotes: Option, }, /// Do not escape the string. @@ -50,10 +54,18 @@ pub enum QuotingStyle { /// Whether to show control and non-unicode characters, or replace them with `?`. show_control: bool, }, + + /// Escape using locale. + Locale, + CLocale, } /// Provide sane defaults for quoting styles. impl QuotingStyle { + pub const LITERAL: Self = Self::Literal { + show_control: false, + }; + pub const SHELL: Self = Self::Shell { escape: false, always_quote: false, @@ -78,12 +90,10 @@ impl QuotingStyle { show_control: false, }; - pub const C_NO_QUOTES: Self = Self::C { - quotes: Quotes::None, - }; + pub const C_NO_QUOTES: Self = Self::C { quotes: None }; pub const C_DOUBLE: Self = Self::C { - quotes: Quotes::Double, + quotes: Some(CQuotes::DOUBLE), }; /// Set the `show_control` field of the quoting style. @@ -101,9 +111,87 @@ impl QuotingStyle { show_control, }, Literal { .. } => Literal { show_control }, - C { .. } => self, + C { .. } | Locale | CLocale => self, + } + } + + /// Set the `always_quote` field of the quoting style. + /// Note: this is a no-op for all variants except `Shell`. + pub fn always_quote(self, always_quote: bool) -> Self { + match self { + Self::Shell { + escape, + show_control, + .. + } => Self::Shell { + escape, + show_control, + always_quote, + }, + _ => self, } } + + pub fn parse(s: &str) -> Option { + Some(match s { + "literal" => Self::LITERAL, + "shell" => Self::SHELL, + "shell-always" => Self::SHELL_QUOTE, + "shell-escape" => Self::SHELL_ESCAPE, + "shell-escape-always" => Self::SHELL_ESCAPE_QUOTE, + "c" => Self::C_DOUBLE, + "escape" => Self::C_NO_QUOTES, + "locale" => Self::Locale, + "clocale" => Self::CLocale, + _ => return None, + }) + } +} + +impl fmt::Display for QuotingStyle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Self::Shell { + escape, + always_quote, + show_control, + } => { + let mut style = "shell".to_string(); + if escape { + style.push_str("-escape"); + } + if always_quote { + style.push_str("-always"); + } + if show_control { + style.push_str("-show"); + } + f.write_str(&style) + } + Self::C { .. } => f.write_str("c"), + Self::Locale => f.write_str("locale"), + Self::CLocale => f.write_str("clocale"), + Self::Literal { .. } => f.write_str("literal"), + } + } +} + +pub fn quoting_style_from_env() -> Option { + let Some(style) = std::env::var_os("QUOTING_STYLE") else { + // Variable absent, return None quietly. + return None; + }; + + let Some(qs) = style.to_str().and_then(QuotingStyle::parse) else { + // Variable is present but invalid, report an error. + show_error!( + "{}", + translate!("invalid-quoting-style-env-var", "invalid" => style.to_string_lossy().quote()) + ); + return None; + }; + + Some(qs) } /// Common interface of quoting mechanisms. @@ -126,20 +214,6 @@ trait Quoter { fn finalize(self: Box) -> Vec; } -/// The type of quotes to use when escaping a name as a C string. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Quotes { - /// Do not use quotes. - None, - - /// Use single quotes. - Single, - - /// Use double quotes. - Double, - // TODO: Locale -} - /// Escape a name according to the given quoting style. /// /// This inner function provides an additional flag `dirname` which @@ -158,6 +232,20 @@ fn escape_name_inner( let mut quoter: Box = match style { QuotingStyle::Literal { .. } => Box::new(LiteralQuoter::new(name.len())), QuotingStyle::C { quotes } => Box::new(CQuoter::new(quotes, dirname, name.len())), + QuotingStyle::CLocale => { + let quotes = match encoding { + UEncoding::Ascii => CQuotes::DOUBLE, + UEncoding::Utf8 => CQuotes::LOCALE_UTF8, + }; + Box::new(CQuoter::new(Some(quotes), dirname, name.len())) + } + QuotingStyle::Locale => { + let quotes = match encoding { + UEncoding::Ascii => CQuotes::SINGLE, + UEncoding::Utf8 => CQuotes::LOCALE_UTF8, + }; + Box::new(CQuoter::new(Some(quotes), dirname, name.len())) + } QuotingStyle::Shell { escape: true, always_quote, @@ -237,86 +325,35 @@ pub fn locale_aware_escape_dir_name(name: &OsStr, style: QuotingStyle) -> OsStri escape_dir_name(name, style, i18n::get_locale_encoding()) } -impl fmt::Display for QuotingStyle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - Self::Shell { - escape, - always_quote, - show_control, - } => { - let mut style = "shell".to_string(); - if escape { - style.push_str("-escape"); - } - if always_quote { - style.push_str("-always-quote"); - } - if show_control { - style.push_str("-show-control"); - } - f.write_str(&style) - } - Self::C { .. } => f.write_str("C"), - Self::Literal { .. } => f.write_str("literal"), - } - } -} - -impl fmt::Display for Quotes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - Self::None => f.write_str("None"), - Self::Single => f.write_str("Single"), - Self::Double => f.write_str("Double"), - } - } -} - #[cfg(test)] mod tests { use crate::{ i18n::UEncoding, - quoting_style::{Quotes, QuotingStyle, escape_name_inner}, + quoting_style::{QuotingStyle, escape_name_inner}, }; // spell-checker:ignore (tests/words) one\'two one'two + // Custom quoting style parsing for testing purposes (a -show might be appended) fn get_style(s: &str) -> QuotingStyle { - match s { - "literal" => QuotingStyle::Literal { - show_control: false, - }, - "literal-show" => QuotingStyle::Literal { show_control: true }, - "escape" => QuotingStyle::C_NO_QUOTES, - "c" => QuotingStyle::C_DOUBLE, - "shell" => QuotingStyle::SHELL, - "shell-show" => QuotingStyle::SHELL.show_control(true), - "shell-always" => QuotingStyle::SHELL_QUOTE, - "shell-always-show" => QuotingStyle::SHELL_QUOTE.show_control(true), - "shell-escape" => QuotingStyle::SHELL_ESCAPE, - "shell-escape-always" => QuotingStyle::SHELL_ESCAPE_QUOTE, - _ => panic!("Invalid name!"), - } - } - - fn check_names_inner(encoding: UEncoding, name: &[u8], map: &[(T, &str)]) -> Vec> { - map.iter() - .map(|(_, style)| escape_name_inner(name, get_style(style), false, encoding)) - .collect() + let show_control = s.ends_with("-show"); + let s = s.trim_end_matches("-show"); + QuotingStyle::parse(s).unwrap().show_control(show_control) } fn check_names_encoding(encoding: UEncoding, name: &str, map: &[(&str, &str)]) { - assert_eq!( - map.iter() - .map(|(correct, _)| *correct) - .collect::>(), - check_names_inner(encoding, name.as_bytes(), map) - .iter() - .map(|bytes| std::str::from_utf8(bytes) - .expect("valid str goes in, valid str comes out")) - .collect::>() - ); + for (expected, style) in map { + assert_eq!( + *expected, + std::str::from_utf8(&escape_name_inner( + name.as_bytes(), + get_style(style), + false, + encoding + )) + .expect("valid str goes in, valid str comes out") + ); + } } fn check_names_both(name: &str, map: &[(&str, &str)]) { @@ -325,12 +362,12 @@ mod tests { } fn check_names_encoding_raw(encoding: UEncoding, name: &[u8], map: &[(&[u8], &str)]) { - assert_eq!( - map.iter() - .map(|(correct, _)| *correct) - .collect::>(), - check_names_inner(encoding, name, map) - ); + for (expected, style) in map { + assert_eq!( + *expected, + escape_name_inner(name, get_style(style), false, encoding) + ); + } } fn check_names_raw_both(name: &[u8], map: &[(&[u8], &str)]) { @@ -1067,6 +1104,82 @@ mod tests { ); } + #[test] + fn test_locale_styles() { + fn test_case(name: &str, utf8: &str, ascii_locale: &str, ascii_clocale: &str) { + check_names_encoding( + UEncoding::Utf8, + name, + &[(utf8, "locale"), (utf8, "clocale")], + ); + check_names_encoding( + UEncoding::Ascii, + name, + &[(ascii_locale, "locale"), (ascii_clocale, "clocale")], + ); + } + fn test_case_raw(name: &[u8], utf8: &[u8], ascii_locale: &[u8], ascii_clocale: &[u8]) { + check_names_encoding_raw( + UEncoding::Utf8, + name, + &[(utf8, "locale"), (utf8, "clocale")], + ); + check_names_encoding_raw( + UEncoding::Ascii, + name, + &[(ascii_locale, "locale"), (ascii_clocale, "clocale")], + ); + } + + // Simple name + + test_case( + "one_two", + "\u{2018}one_two\u{2019}", + "'one_two'", + "\"one_two\"", + ); + + // Space + + test_case(" ", "\u{2018} \u{2019}", "' '", "\" \""); + + // Quotes + + test_case("'", "\u{2018}'\u{2019}", "'\\''", "\"'\""); + test_case("\"", "\u{2018}\"\u{2019}", "'\"'", "\"\\\"\""); + test_case( + "\u{2018}", + "\u{2018}\u{2018}\u{2019}", // opening quote is not escaped + "'\\342\\200\\230'", + "\"\\342\\200\\230\"", + ); + test_case( + "\u{2019}", + "\u{2018}\\\u{2019}\u{2019}", // closing quote is escaped + "'\\342\\200\\231'", + "\"\\342\\200\\231\"", + ); + + // ASCII control + + test_case( + "\x00\n", + "\u{2018}\\000\\n\u{2019}", + "'\\000\\n'", + "\"\\000\\n\"", + ); + + // Non-Unicode + + test_case_raw( + b"\xEF", + "\u{2018}\\357\u{2019}".as_bytes(), + b"'\\357'", + b"\"\\357\"", + ); + } + #[test] fn test_quoting_style_display() { let style = QuotingStyle::SHELL_ESCAPE; @@ -1079,18 +1192,17 @@ mod tests { assert_eq!(format!("{style}"), "shell-show-control"); let style = QuotingStyle::C_DOUBLE; - assert_eq!(format!("{style}"), "C"); + assert_eq!(format!("{style}"), "c"); let style = QuotingStyle::Literal { show_control: false, }; assert_eq!(format!("{style}"), "literal"); - } - #[test] - fn test_quotes_display() { - assert_eq!(format!("{}", Quotes::None), "None"); - assert_eq!(format!("{}", Quotes::Single), "Single"); - assert_eq!(format!("{}", Quotes::Double), "Double"); + let style = QuotingStyle::Locale; + assert_eq!(format!("{style}"), "locale"); + + let style = QuotingStyle::CLocale; + assert_eq!(format!("{style}"), "clocale"); } } diff --git a/src/uucore/src/lib/features/quoting_style/shell_quoter.rs b/src/uucore/src/lib/features/quoting_style/shell_quoter.rs index bb531b50d16..7d9fd240e6e 100644 --- a/src/uucore/src/lib/features/quoting_style/shell_quoter.rs +++ b/src/uucore/src/lib/features/quoting_style/shell_quoter.rs @@ -3,13 +3,28 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use super::{EscapeState, EscapedChar, Quoter, Quotes}; +use super::{EscapeState, EscapedChar, Quoter}; // These are characters with special meaning in the shell (e.g. bash). The // first const contains characters that only have a special meaning when they // appear at the beginning of a name. const SPECIAL_SHELL_CHARS_START: &[u8] = b"~#"; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ShellQuotes { + Single, + Double, +} + +impl ShellQuotes { + pub fn as_u8(self) -> u8 { + match self { + Self::Single => b'\'', + Self::Double => b'"', + } + } +} + // Escaped and NonEscaped shell quoting strategies are very different. // Therefore, we are using separate Quoter structures for each of them. @@ -19,7 +34,7 @@ pub(super) struct NonEscapedShellQuoter<'a> { reference: &'a [u8], /// The quotes to be used if necessary - quotes: Quotes, + quotes: ShellQuotes, /// Whether to show control and non-unicode characters, or replace them /// with `?`. @@ -94,7 +109,7 @@ pub(super) struct EscapedShellQuoter<'a> { reference: &'a [u8], /// The quotes to be used if necessary - quotes: Quotes, + quotes: ShellQuotes, // INTERNAL STATE /// Whether the name should be quoted. @@ -190,19 +205,19 @@ fn initial_quoting( dirname: bool, always_quote: bool, check_control_chars: bool, -) -> (Quotes, bool) { +) -> (ShellQuotes, bool) { let has_special_chars = input.iter().any(|c| { shell_escaped_char_set(dirname).contains(c) || (check_control_chars && c.is_ascii_control()) }); if has_special_chars { - (Quotes::Single, true) + (ShellQuotes::Single, true) } else if input.contains(&b'\'') { - (Quotes::Double, true) + (ShellQuotes::Double, true) } else if always_quote || input.is_empty() { - (Quotes::Single, true) + (ShellQuotes::Single, true) } else { - (Quotes::Single, false) + (ShellQuotes::Single, false) } } @@ -226,17 +241,13 @@ fn finalize_shell_quoter( buffer: Vec, reference: &[u8], must_quote: bool, - quotes: Quotes, + quotes: ShellQuotes, ) -> Vec { let contains_quote_chars = must_quote || bytes_start_with(reference, SPECIAL_SHELL_CHARS_START); - if must_quote | contains_quote_chars && quotes != Quotes::None { + if must_quote | contains_quote_chars { let mut quoted = Vec::::with_capacity(buffer.len() + 2); - let quote = if quotes == Quotes::Single { - b'\'' - } else { - b'"' - }; + let quote = quotes.as_u8(); quoted.push(quote); quoted.extend(buffer); quoted.push(quote); @@ -255,63 +266,63 @@ mod tests { // Control chars (0-31 and 0x7F) force single quotes in escape mode assert_eq!( initial_quoting(b"\x01", false, false, true), - (Quotes::Single, true) + (ShellQuotes::Single, true) ); // Control + quote uses single quotes (segmented) in escape mode assert_eq!( initial_quoting(b"\x01'\x01", false, false, true), - (Quotes::Single, true) + (ShellQuotes::Single, true) ); // Simple quote uses double quotes in escape mode assert_eq!( initial_quoting(b"a'b", false, false, true), - (Quotes::Double, true) + (ShellQuotes::Double, true) ); // Shell special chars force single quotes in escape mode assert_eq!( initial_quoting(b"test$var", false, false, true), - (Quotes::Single, true) + (ShellQuotes::Single, true) ); assert_eq!( initial_quoting(b"test\nline", false, false, true), - (Quotes::Single, true) + (ShellQuotes::Single, true) ); // Empty string forces quotes in escape mode assert_eq!( initial_quoting(b"", false, false, true), - (Quotes::Single, true) + (ShellQuotes::Single, true) ); // Always quote flag works in escape mode assert_eq!( initial_quoting(b"normal", false, true, true), - (Quotes::Single, true) + (ShellQuotes::Single, true) ); // Normal text doesn't need quoting in escape mode assert_eq!( initial_quoting(b"hello", false, false, true), - (Quotes::Single, false) + (ShellQuotes::Single, false) ); // Dirname affects colon handling in escape mode assert_eq!( initial_quoting(b"dir:name", true, false, true), - (Quotes::Single, true) + (ShellQuotes::Single, true) ); assert_eq!( initial_quoting(b"file:name", false, false, true), - (Quotes::Single, false) + (ShellQuotes::Single, false) ); // Control chars ignored in non-escape mode assert_eq!( initial_quoting(b"\x01", false, false, false), - (Quotes::Single, false) + (ShellQuotes::Single, false) ); } } 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 9b5c027aafd..0bac303c8aa 100644 --- a/tests/by-util/test_printenv.rs +++ b/tests/by-util/test_printenv.rs @@ -131,3 +131,83 @@ fn test_non_utf8_env_vars() { .succeeds() .stdout_contains_bytes(b"NON_UTF8_VAR=hello\x80world"); } + +#[test] +fn test_quoting_style() { + for (qs, key, val) in [ + ("literal", "a b", "c d"), + ("escape", "a\\ b", "c\\ d"), + ("shell", "'a b'", "'c d'"), + ("c", "\"a b\"", "\"c d\""), + ] { + new_ucmd!() + .env("QUOTING_STYLE", qs) + .env("a b", "c d") + .succeeds() + .stdout_contains(format!("{key}={val}")); + + new_ucmd!() + .env("QUOTING_STYLE", qs) + .env("a b", "c d") + .arg("a b") + .succeeds() + .stdout_is(format!("{val}\n")); + } + + // ASCII Locale + for (qs, key, val) in [ + ("locale", "'a b'", "'c d'"), + ("clocale", "\"a b\"", "\"c d\""), + ] { + new_ucmd!() + .env("LC_ALL", "C") + .env("QUOTING_STYLE", qs) + .env("a b", "c d") + .succeeds() + .stdout_contains(format!("{key}={val}")); + + new_ucmd!() + .env("LC_ALL", "C") + .env("QUOTING_STYLE", qs) + .env("a b", "c d") + .arg("a b") + .succeeds() + .stdout_is(format!("{val}\n")); + } + + // UTF-8 Locale + for (qs, key, val) in [ + ("locale", "\u{2018}a b\u{2019}", "\u{2018}c d\u{2019}"), + ("clocale", "\u{2018}a b\u{2019}", "\u{2018}c d\u{2019}"), + ] { + new_ucmd!() + .env("LC_ALL", "en_US.UTF-8") + .env("QUOTING_STYLE", qs) + .env("a b", "c d") + .succeeds() + .stdout_contains(format!("{key}={val}")); + + new_ucmd!() + .env("LC_ALL", "en_US.UTF-8") + .env("QUOTING_STYLE", qs) + .env("a b", "c d") + .arg("a b") + .succeeds() + .stdout_is(format!("{val}\n")); + } + + // Env var name + new_ucmd!() + .env("QUOTING_STYLE", "shell") + .env("a b", "c d") + .succeeds() + .stdout_contains("'a b'='c d'"); + + // Invalid case + new_ucmd!() + .env("QUOTING_STYLE", "invalid") + .succeeds() + .stderr_is( + "printenv: ignoring invalid value of environment variable QUOTING_STYLE: 'invalid'\n", + ); +}