diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 145477211c2..8164d28f769 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -17,12 +17,12 @@ use std::os::fd::AsFd; use std::path::Path; use std::path::PathBuf; use uucore::diagnostics::OptionValue; -use uucore::display::{Quotable, print_verbatim}; +use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError, strip_errno}; use uucore::line_ending::LineEnding; use uucore::parser::parse_signed_num::number_offset; use uucore::parser::parse_size::ParseSizeError; -use uucore::quoting_style::{QuotingStyle, locale_aware_escape_name}; +use uucore::quoting_style::locale_aware_shell_escape; use uucore::show; use uucore::translate; @@ -449,11 +449,8 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { if !first { writeln!(stdout)?; } - let name = locale_aware_escape_name( - translate!("head-name-stdin").as_ref(), - QuotingStyle::SHELL_ESCAPE, - ); - writeln!(stdout, "==> {} <==", name.to_string_lossy())?; + let name = locale_aware_shell_escape(translate!("head-name-stdin")); + writeln!(stdout, "==> {name} <==")?; } let stdin = io::stdin(); @@ -503,12 +500,8 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { if !first { writeln!(stdout)?; } - write!(stdout, "==> ")?; - print_verbatim(locale_aware_escape_name( - file.as_ref(), - QuotingStyle::SHELL_ESCAPE, - ))?; - writeln!(stdout, " <==")?; + let name = locale_aware_shell_escape(file); + writeln!(stdout, "==> {name} <==")?; first = false; } Ok(()) 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 810d701aec7..5fdd4a622cd 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,45 +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::Escape } 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. - // Value may not be valid UTF-8. - if let Some(os_style) = std::env::var_os("QUOTING_STYLE") { - let style = os_style.to_string_lossy(); - if let Some(pair) = match_quoting_style_name(&style, 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::Escape, 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 }, } } } @@ -866,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(); @@ -959,7 +884,6 @@ impl Config { 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() { @@ -1046,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/dired.rs b/src/uu/ls/src/dired.rs index b2b1e878a42..ce96dbd3b82 100644 --- a/src/uu/ls/src/dired.rs +++ b/src/uu/ls/src/dired.rs @@ -35,12 +35,10 @@ /// Overall, the module ensures each entry in the DIRED output has the correct /// byte position, considering additional lines or padding affecting positions. /// -use crate::display::LocaleQuoting; use crate::{Config, LsError}; use std::fmt; use std::io::{BufWriter, Stdout, Write}; use uucore::error::UResult; -use uucore::quoting_style::{Quotes, QuotingStyle}; #[derive(Debug, Clone, PartialEq)] pub struct BytePosition { @@ -128,42 +126,12 @@ pub fn print_dired_output( writeln!( out, "//DIRED-OPTIONS// --quoting-style={}", - dired_quoting_style_name(config) + config.quoting_style ) .map_err(LsError::WriteError)?; Ok(()) } -/// The canonical `--quoting-style` name for the `//DIRED-OPTIONS//` trailer. -/// `QuotingStyle`'s `Display` impl collapses distinct style names together and adds suffixes GNU's trailer never uses, so this maps the config directly instead. -fn dired_quoting_style_name(config: &Config) -> &'static str { - match config.locale_quoting { - Some(LocaleQuoting::Single) => return "locale", - Some(LocaleQuoting::Double) => return "clocale", - None => {} - } - match config.quoting_style { - QuotingStyle::Literal { .. } => "literal", - QuotingStyle::C { - quotes: Quotes::None, - } => "escape", - // `Quotes::Single` is never produced by ls's own option parsing - // (only reachable through `locale_quoting`, handled above), but - // `c` is the closest canonical name if that ever changes. - QuotingStyle::C { .. } => "c", - QuotingStyle::Shell { - escape, - always_quote, - .. - } => match (escape, always_quote) { - (false, false) => "shell", - (false, true) => "shell-always", - (true, false) => "shell-escape", - (true, true) => "shell-escape-always", - }, - } -} - /// Helper function to print positions with a given prefix. fn print_positions( out: &mut BufWriter, diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index d43bb2aad84..a6ec8066f48 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::Escape { + 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/numfmt/src/options.rs b/src/uu/numfmt/src/options.rs index cecb3917439..009d1492351 100644 --- a/src/uu/numfmt/src/options.rs +++ b/src/uu/numfmt/src/options.rs @@ -3,7 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::ffi::OsStr; use std::fmt::Display; use std::iter::Peekable; // `Range` alone is the field range from uucore, so byte ranges are named apart. @@ -11,7 +10,7 @@ use std::ops::Range as ByteRange; use std::str::{CharIndices, FromStr}; use crate::units::Unit; -use uucore::quoting_style::{Quotes, QuotingStyle, locale_aware_escape_name}; +use uucore::quoting_style::locale_aware_c_single_escape; use uucore::ranges::Range; use uucore::translate; @@ -26,20 +25,6 @@ fn at(iter: &mut Peekable>, s: &str) -> ByteRange { .map_or(s.len()..s.len(), |&(i, c)| i..i + c.len_utf8()) } -/// The format quoted the way an error message shows it: in single quotes, with -/// control characters written as C escapes. Without this a format holding a -/// newline or a tab would split the message across lines or move the cursor. -fn quote_format(s: &str) -> String { - locale_aware_escape_name( - OsStr::new(s), - QuotingStyle::C { - quotes: Quotes::Single, - }, - ) - .to_string_lossy() - .into_owned() -} - pub const DEBUG: &str = "debug"; pub const DELIMITER: &str = "delimiter"; pub const FIELD: &str = "field"; @@ -239,7 +224,7 @@ impl FromStr for FormatOptions { // can say where in the format it gave up. let mut iter = s.char_indices().peekable(); let mut options = Self::default(); - let quoted = quote_format(s); + let quoted = locale_aware_c_single_escape(s); let error = |message: String, span: ByteRange, kind: FormatErrorKind| FormatError { message, span, diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index d606b300de8..63e23f30f62 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -343,7 +343,7 @@ fn handle_parse_error(e: clap::Error, args: &[OsString]) -> Box { // The path is shell-escaped (quoted only if needed), the file name is // always quoted, matching GNU's two quoting styles. let path = locale_aware_escape_name(file, QuotingStyle::SHELL_ESCAPE); - let quoted = locale_aware_escape_name(file, QuotingStyle::SHELL_ESCAPE_QUOTE); + let quoted = locale_aware_escape_name(file, QuotingStyle::SHELL_ESCAPE_ALWAYS); let _ = writeln!( io::stderr(), "{}", 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 1f9bad2a313..c030437f656 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; @@ -246,23 +246,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, @@ -520,22 +503,6 @@ fn quote_file_name(file_name: &OsStr, 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, @@ -1108,7 +1075,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> { @@ -1337,7 +1308,7 @@ impl Stater { fn do_stat(&self, file: &OsStr, stdin_is_fifo: bool) -> UResult { let display_name = file.to_string_lossy(); - let quoted_name = || quote_file_name(file, QuotingStyle::SHELL_ESCAPE_QUOTE); + let quoted_name = || quote_file_name(file, QuotingStyle::SHELL_ESCAPE_ALWAYS); let file = if cfg!(unix) && display_name == "-" { if self.show_fs { show_error!("{}", StatError::StdinFilesystemMode); @@ -1788,13 +1759,13 @@ mod tests { fn test_quote_file_name() { let file_name = "nice' file"; assert_eq!( - quote_file_name(OsStr::new(file_name), QuotingStyle::SHELL_ESCAPE_QUOTE), + quote_file_name(OsStr::new(file_name), QuotingStyle::SHELL_ESCAPE_ALWAYS), "\"nice' file\"" ); let file_name = "nice\" file"; assert_eq!( - quote_file_name(OsStr::new(file_name), QuotingStyle::SHELL_ESCAPE_QUOTE), + quote_file_name(OsStr::new(file_name), QuotingStyle::SHELL_ESCAPE_ALWAYS), "\'nice\" file\'" ); } diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 611cb18ef80..42791a1e8d9 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -14,7 +14,7 @@ use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path::{Path, PathBuf}; #[cfg(not(target_os = "wasi"))] use uucore::error::UResult; -use uucore::quoting_style::{QuotingStyle, locale_aware_escape_name}; +use uucore::quoting_style::locale_aware_shell_escape; use uucore::translate; #[derive(Debug, Clone)] @@ -134,12 +134,11 @@ impl HeaderPrinter { pub fn print(&mut self, string: &str) { if self.verbose { - // GNU quotes the name shown in the header when it needs it. - let name = locale_aware_escape_name(string.as_ref(), QuotingStyle::SHELL_ESCAPE); println!( "{}==> {} <==", if self.first_header { "" } else { "\n" }, - name.to_string_lossy(), + // GNU quotes the name shown in the header when it needs it. + locale_aware_shell_escape(string), ); self.first_header = false; } diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 64d39074843..60d586c873e 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -25,7 +25,7 @@ use std::{ use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser}; use unicode_width::UnicodeWidthChar; use utf8::{BufReadDecoder, BufReadDecoderError}; -use uucore::{display::Quotable, translate}; +use uucore::{display::Quotable, quoting_style::locale_aware_shell_escape, translate}; use uucore::{ error::{FromIo, UError, UResult}, @@ -270,7 +270,7 @@ impl<'a> Input<'a> { /// Converts input into the form that appears in errors. fn path_display(&self) -> String { match self { - Self::Path(path) => escape_name_wrapper(path.as_os_str()), + Self::Path(path) => locale_aware_shell_escape(path.as_os_str()), Self::Stdin(_) => translate!("wc-standard-input"), } } @@ -360,7 +360,7 @@ impl WcError { Some((input, idx)) => { let path = match input { Input::Stdin(_) => STDIN_REPR.into(), - Input::Path(path) => escape_name_wrapper(path.as_os_str()).into(), + Input::Path(path) => locale_aware_shell_escape(path.as_os_str()).into(), }; Self::ZeroLengthFileNameCtx { path, idx } } @@ -778,7 +778,7 @@ fn files0_iter_file<'a>(path: &Path) -> UResult quoting_style::locale_aware_escape_name( path.as_os_str(), - QuotingStyle::SHELL_ESCAPE_QUOTE, + QuotingStyle::SHELL_ESCAPE_ALWAYS, ) .into_string() .expect("All escaped names with the escaping option return valid strings.") @@ -813,7 +813,7 @@ fn files0_iter<'a>( } } Err(e) => Err(e.map_err_context( - || translate!("wc-error-read-error", "path" => escape_name_wrapper(&err_path)), + || translate!("wc-error-read-error", "path" => locale_aware_shell_escape(&err_path)), ) as Box), }), ); @@ -827,12 +827,6 @@ fn files0_iter<'a>( }) } -fn escape_name_wrapper(name: &OsStr) -> String { - quoting_style::locale_aware_escape_name(name, QuotingStyle::SHELL_ESCAPE) - .into_string() - .expect("All escaped names with the escaping option return valid strings.") -} - fn hardware_feature_label(feature: HardwareFeature) -> &'static str { match feature { HardwareFeature::Avx512 => "AVX512F", diff --git a/src/uucore/locales/errors/en-US.ftl b/src/uucore/locales/errors/en-US.ftl index 1f1b9230a26..5720f768ecd 100644 --- a/src/uucore/locales/errors/en-US.ftl +++ b/src/uucore/locales/errors/en-US.ftl @@ -53,3 +53,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 57dc56a6a93..28642d75ea7 100644 --- a/src/uucore/locales/errors/fr-FR.ftl +++ b/src/uucore/locales/errors/fr-FR.ftl @@ -53,3 +53,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/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 048b027c16f..645b80495ae 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -19,7 +19,7 @@ use crate::checksum::{ digest_reader, parse_blake_length, unescape_filename, }; use crate::error::{FromIo, UError, UResult, USimpleError, strip_errno}; -use crate::quoting_style::{QuotingStyle, locale_aware_escape_name}; +use crate::quoting_style::locale_aware_shell_escape; use crate::sum::{self, Blake2b, Blake3, DigestOutput}; use crate::{ os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_warning_caps, translate, @@ -245,9 +245,8 @@ fn write_file_report( verbose: ChecksumVerbose, ) -> io::Result<()> { if result.can_display(verbose) { - let filename = locale_aware_escape_name(filename, QuotingStyle::SHELL_ESCAPE); - // Here, .to_string_lossy() is lossless thanks to the escaping. - writeln!(w, "{}: {result}", filename.to_string_lossy())?; + let filename = locale_aware_shell_escape(filename); + writeln!(w, "{filename}: {result}")?; } Ok(()) } @@ -553,12 +552,7 @@ fn get_file_to_check( ); }; let print_error = |err: io::Error| { - show!(err.map_err_context(|| { - locale_aware_escape_name(filename, QuotingStyle::SHELL_ESCAPE) - // This is non destructive thanks to the escaping - .to_string_lossy() - .to_string() - })); + show!(err.map_err_context(|| { locale_aware_shell_escape(filename) })); }; match File::open(filename) { Ok(f) => { @@ -697,11 +691,9 @@ fn compute_and_check_digest_from_file( match digest_reader(&mut digest, &mut file_reader, ReadingMode::Binary) { Ok(result) => result, Err(err) => { - show!(err.map_err_context(|| { - locale_aware_escape_name(real_filename_to_check, QuotingStyle::SHELL_ESCAPE) - .to_string_lossy() - .to_string() - })); + show!( + err.map_err_context(|| { locale_aware_shell_escape(real_filename_to_check) }) + ); let _ = write_file_report( io::stdout(), diff --git a/src/uucore/src/lib/features/format/argument.rs b/src/uucore/src/lib/features/format/argument.rs index 96a843165dc..449735245cf 100644 --- a/src/uucore/src/lib/features/format/argument.rs +++ b/src/uucore/src/lib/features/format/argument.rs @@ -208,7 +208,7 @@ fn extract_value( match p { Ok(v) => v, Err(e) => { - let input = locale_aware_escape_name(OsStr::new(input), QuotingStyle::C_NO_QUOTES); + let input = locale_aware_escape_name(OsStr::new(input), QuotingStyle::Escape); match e { ExtendedParserError::Overflow(v) | ExtendedParserError::Underflow(v) => { set_exit_code(1); diff --git a/src/uucore/src/lib/features/format/spec.rs b/src/uucore/src/lib/features/format/spec.rs index 47966f59601..069d3069ad0 100644 --- a/src/uucore/src/lib/features/format/spec.rs +++ b/src/uucore/src/lib/features/format/spec.rs @@ -13,11 +13,7 @@ use super::{ }, parse_escape_only, }; -use crate::{ - format::FormatArguments, - os_str_as_bytes, - quoting_style::{QuotingStyle, locale_aware_escape_name}, -}; +use crate::{format::FormatArguments, os_str_as_bytes, quoting_style::locale_aware_shell_escape}; use std::{io::Write, num::NonZero, ops::ControlFlow}; /// A parsed specification for formatting a value @@ -406,12 +402,8 @@ impl Spec { writer.write_all(&parsed).map_err(FormatError::IoError) } Self::QuotedString { position } => { - let s = locale_aware_escape_name( - args.next_string(*position), - QuotingStyle::SHELL_ESCAPE, - ); - let bytes = os_str_as_bytes(&s)?; - writer.write_all(bytes).map_err(FormatError::IoError) + let s = locale_aware_shell_escape(args.next_string(*position)); + writer.write_all(s.as_bytes()).map_err(FormatError::IoError) } Self::SignedInt { width, 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 73105e75f0b..b4850511127 100644 --- a/src/uucore/src/lib/features/quoting_style/mod.rs +++ b/src/uucore/src/lib/features/quoting_style/mod.rs @@ -8,10 +8,13 @@ use std::ffi::{OsStr, OsString}; use std::fmt; +use os_display::Quotable; + 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,9 +23,18 @@ mod c_quoter; mod literal_quoter; mod shell_quoter; -/// The quoting style to use when escaping a name. +pub use c_quoter::CQuotes; + +/// Enum representing a quoting and escaping strategy to print a string. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum QuotingStyle { + /// Do not escape the string. + /// Used in, e.g., `ls --literal`. + Literal { + /// If false, replaces control and non-Unicode characters with `?`. + show_control: bool, + }, + /// Escape the name as a shell string. /// Used in, e.g., `ls --quoting-style=shell`. Shell { @@ -33,27 +45,31 @@ pub enum QuotingStyle { /// Whether to always quote the name. always_quote: bool, - /// Whether to show control and non-unicode characters, or replace them with `?`. + /// If false, replaces control and non-Unicode characters with `?`. show_control: bool, }, /// Escape the name as a C string. /// Used in, e.g., `ls --quote-name`. C { - /// The type of quotes to use. - quotes: Quotes, + quotes: CQuotes, }, - /// Do not escape the string. - /// Used in, e.g., `ls --literal`. - Literal { - /// Whether to show control and non-unicode characters, or replace them with `?`. - show_control: bool, - }, + /// Simply escape characters without quoting + /// Used in, e.g., `ls --quoting-style=escape`. + Escape, + + /// 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, @@ -66,28 +82,30 @@ impl QuotingStyle { show_control: false, }; - pub const SHELL_QUOTE: Self = Self::Shell { + pub const SHELL_ALWAYS: Self = Self::Shell { escape: false, always_quote: true, show_control: false, }; - pub const SHELL_ESCAPE_QUOTE: Self = Self::Shell { + pub const SHELL_ESCAPE_ALWAYS: Self = Self::Shell { escape: true, always_quote: true, show_control: false, }; - pub const C_NO_QUOTES: Self = Self::C { - quotes: Quotes::None, + pub const C_DOUBLE: Self = Self::C { + quotes: CQuotes::DOUBLE, }; - pub const C_DOUBLE: Self = Self::C { - quotes: Quotes::Double, + pub const C_SINGLE: Self = Self::C { + quotes: CQuotes::SINGLE, }; /// Set the `show_control` field of the quoting style. - /// Note: this is a no-op for the `C` variant. + /// + /// > This is a no-op for variants others than [`QuotingStyle::Shell`] + /// > and [`QuotingStyle::Literal`]. pub fn show_control(self, show_control: bool) -> Self { use QuotingStyle::*; match self { @@ -101,9 +119,89 @@ impl QuotingStyle { show_control, }, Literal { .. } => Literal { show_control }, - C { .. } => self, + C { .. } | Escape | Locale | CLocale => self, + } + } + + /// Set the `always_quote` field of the quoting style. + /// + /// > This is a no-op for all variants except [`QuotingStyle::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, } } + + /// Parse a [`QuotingStyle`] from a string. + /// + /// 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 { + "literal" => Self::LITERAL, + "shell" => Self::SHELL, + "shell-always" => Self::SHELL_ALWAYS, + "shell-escape" => Self::SHELL_ESCAPE, + "shell-escape-always" => Self::SHELL_ESCAPE_ALWAYS, + "c" => Self::C_DOUBLE, + "escape" => Self::Escape, + "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, + .. + } => { + write!( + f, + "shell{}{}", + if escape { "-escape" } else { "" }, + if always_quote { "-always" } else { "" } + ) + } + Self::C { .. } => f.write_str("c"), + Self::Escape => f.write_str("escape"), + Self::Literal { .. } => f.write_str("literal"), + Self::Locale => f.write_str("locale"), + Self::CLocale => f.write_str("clocale"), + } + } +} + +/// Retrieve the `QUOTING_STYLE` environment variable. If present, parse it +/// through [`QuotingStyle::parse`], and write a standard error message to +/// stderr in case of an invalid value. +pub fn quoting_style_from_env() -> Option { + // If variable is not set, return None quietly. + let style = std::env::var_os("QUOTING_STYLE")?; + + 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,21 +224,7 @@ 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. +/// Escape a name according to the given [`QuotingStyle`] and [`UEncoding`]. /// /// This inner function provides an additional flag `dirname` which /// is meant for ls' directory name display. @@ -157,7 +241,22 @@ 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::C { quotes } => Box::new(CQuoter::new(Some(quotes), dirname, name.len())), + QuotingStyle::Escape => Box::new(CQuoter::new(None, 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, @@ -203,16 +302,17 @@ fn escape_name_inner( quoter.finalize() } -/// Escape a filename with respect to the given style. +/// Escape a filename with respect to the given [`QuotingStyle`]. pub fn escape_name(name: &OsStr, style: QuotingStyle, encoding: UEncoding) -> OsString { let name = crate::os_str_as_bytes_lossy(name); crate::os_string_from_vec(escape_name_inner(&name, style, false, encoding)) .expect("all byte sequences should be valid for platform, or already replaced in name") } -/// Retrieve the encoding from the locale and pass it to `escape_name`. +/// Retrieve the encoding from the locale and pass it to [`escape_name`]. +#[inline(always)] pub fn locale_aware_escape_name(name: &OsStr, style: QuotingStyle) -> OsString { - escape_name(name, style, i18n::get_locale_encoding()) + escape_name(name, style, i18n::get_ctype_encoding()) } /// Shorthand function for [`locale_aware_escape_name`] @@ -224,6 +324,15 @@ pub fn locale_aware_shell_escape(name: impl AsRef) -> String { .unwrap() // SAFETY: string was just escaped } +/// Shorthand function for [`locale_aware_escape_name`] +/// Useful for quoting in error messages. +#[inline(always)] +pub fn locale_aware_c_single_escape(name: impl AsRef) -> String { + locale_aware_escape_name(name.as_ref(), QuotingStyle::C_SINGLE) + .into_string() + .unwrap() // SAFETY: string was just escaped +} + /// Escape a directory name with respect to the given style. /// This is mainly meant to be used for ls' directory name printing and is not /// likely to be used elsewhere. @@ -233,91 +342,40 @@ pub fn escape_dir_name(dir_name: &OsStr, style: QuotingStyle, encoding: UEncodin .expect("all byte sequences should be valid for platform, or already replaced in name") } -/// Retrieve the encoding from the locale and pass it to `escape_dir_name`. +/// Retrieve the encoding from the locale and pass it to [`escape_dir_name`]. pub fn locale_aware_escape_dir_name(name: &OsStr, style: QuotingStyle) -> OsString { - 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"), - } - } + escape_dir_name(name, style, i18n::get_ctype_encoding()) } #[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)]) { @@ -326,12 +384,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)]) { @@ -1076,30 +1134,108 @@ 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; assert_eq!(format!("{style}"), "shell-escape"); - let style = QuotingStyle::SHELL_QUOTE; - assert_eq!(format!("{style}"), "shell-always-quote"); + let style = QuotingStyle::SHELL_ALWAYS; + assert_eq!(format!("{style}"), "shell-always"); let style = QuotingStyle::SHELL.show_control(true); - assert_eq!(format!("{style}"), "shell-show-control"); + assert_eq!(format!("{style}"), "shell"); let style = QuotingStyle::C_DOUBLE; - assert_eq!(format!("{style}"), "C"); + assert_eq!(format!("{style}"), "c"); + + let style = QuotingStyle::Escape; + assert_eq!(format!("{style}"), "escape"); 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 29b1db3bac3..441f1e54042 100644 --- a/src/uucore/src/lib/features/quoting_style/shell_quoter.rs +++ b/src/uucore/src/lib/features/quoting_style/shell_quoter.rs @@ -3,14 +3,30 @@ // 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 crate::i18n::UEncoding; +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. @@ -20,7 +36,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 `?`. @@ -95,7 +111,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. @@ -117,13 +133,13 @@ impl<'a> EscapedShellQuoter<'a> { ) -> Self { let (mut quotes, must_quote) = initial_quoting(reference, dirname, always_quote, true); // Dollar-quoted byte escapes cannot be nested in double quotes. - if quotes == Quotes::Double { + if quotes == ShellQuotes::Double { let invalid = match encoding { UEncoding::Ascii => !reference.is_ascii(), UEncoding::Utf8 => std::str::from_utf8(reference).is_err(), }; if invalid { - quotes = Quotes::Single; + quotes = ShellQuotes::Single; } } Self { @@ -207,19 +223,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) } } @@ -243,17 +259,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); @@ -272,63 +284,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/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 6ee2b34bf20..d518238a190 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -4338,7 +4338,7 @@ fn test_ls_invalid_quoting_style_env_var_warns() { .arg("alpha") .succeeds() .stdout_is("alpha\n") - .stderr_contains("Ignoring invalid value of environment variable QUOTING_STYLE"); + .stderr_contains("ignoring invalid value of environment variable QUOTING_STYLE"); } #[cfg(unix)] @@ -4356,7 +4356,7 @@ fn test_ls_invalid_quoting_style_env_var_non_utf8() { .arg("alpha") .succeeds() .stdout_is("alpha\n") - .stderr_contains("Ignoring invalid value of environment variable QUOTING_STYLE"); + .stderr_contains("ignoring invalid value of environment variable QUOTING_STYLE"); } #[test]