1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 19:47:45 +00:00

uucore::display: impl Quotable for Cow<str>, add escape_control

This commit is contained in:
Jan Verbeek 2021-08-31 12:49:22 +02:00
parent b5550bc4dd
commit a93959aa44

View file

@ -20,6 +20,7 @@
/// # Ok::<(), std::io::Error>(()) /// # Ok::<(), std::io::Error>(())
/// ``` /// ```
// spell-checker:ignore Fbar // spell-checker:ignore Fbar
use std::borrow::{Borrow, Cow};
use std::ffi::OsStr; use std::ffi::OsStr;
#[cfg(any(unix, target_os = "wasi", windows))] #[cfg(any(unix, target_os = "wasi", windows))]
use std::fmt::Write as FmtWrite; use std::fmt::Write as FmtWrite;
@ -54,15 +55,32 @@ pub trait Quotable {
fn quote(&self) -> Quoted<'_>; fn quote(&self) -> Quoted<'_>;
} }
impl<T> Quotable for T macro_rules! impl_as_ref {
where ($type: ty) => {
T: AsRef<OsStr>, impl Quotable for $type {
{ fn quote(&self) -> Quoted<'_> {
fn quote(&self) -> Quoted<'_> { Quoted::new(self.as_ref())
Quoted { }
text: self.as_ref(),
force_quote: true,
} }
};
}
impl_as_ref!(&'_ str);
impl_as_ref!(String);
impl_as_ref!(&'_ std::path::Path);
impl_as_ref!(std::path::PathBuf);
impl_as_ref!(std::path::Component<'_>);
impl_as_ref!(std::path::Components<'_>);
impl_as_ref!(std::path::Iter<'_>);
impl_as_ref!(&'_ std::ffi::OsStr);
impl_as_ref!(std::ffi::OsString);
// Cow<'_, str> does not implement AsRef<OsStr> and this is unlikely to be fixed
// for backward compatibility reasons. Otherwise we'd use a blanket impl.
impl Quotable for Cow<'_, str> {
fn quote(&self) -> Quoted<'_> {
// I suspect there's a better way to do this but I haven't found one
Quoted::new(<Cow<'_, str> as Borrow<str>>::borrow(self).as_ref())
} }
} }
@ -71,14 +89,29 @@ where
pub struct Quoted<'a> { pub struct Quoted<'a> {
text: &'a OsStr, text: &'a OsStr,
force_quote: bool, force_quote: bool,
escape_control: bool,
} }
impl Quoted<'_> { impl<'a> Quoted<'a> {
fn new(text: &'a OsStr) -> Self {
Quoted {
text,
force_quote: true,
escape_control: true,
}
}
/// Add quotes even if not strictly necessary. `true` by default. /// Add quotes even if not strictly necessary. `true` by default.
pub fn force_quote(mut self, force: bool) -> Self { pub fn force_quote(mut self, force: bool) -> Self {
self.force_quote = force; self.force_quote = force;
self self
} }
/// Escape control characters. `true` by default.
pub fn escape_control(mut self, escape: bool) -> Self {
self.escape_control = escape;
self
}
} }
impl Display for Quoted<'_> { impl Display for Quoted<'_> {
@ -89,7 +122,7 @@ impl Display for Quoted<'_> {
// % seems obscure enough to ignore, it's for job control only. // % seems obscure enough to ignore, it's for job control only.
// {} were used in a version elsewhere but seem unnecessary, GNU doesn't escape them. // {} were used in a version elsewhere but seem unnecessary, GNU doesn't escape them.
// ! is a common extension. // ! is a common extension.
const SPECIAL_SHELL_CHARS: &[u8] = b"|&;<>()$`\\\"'*?[]=! "; const SPECIAL_SHELL_CHARS: &[u8] = b"|&;<>()$`\\\"'*?[]=! \t\n";
/// Characters with a special meaning at the beginning of a name. /// Characters with a special meaning at the beginning of a name.
const SPECIAL_SHELL_CHARS_START: &[u8] = b"~#"; const SPECIAL_SHELL_CHARS_START: &[u8] = b"~#";
@ -110,7 +143,9 @@ impl Display for Quoted<'_> {
for &ch in text { for &ch in text {
match ch { match ch {
ch if ch.is_ascii_control() => return write_escaped(f, text), ch if self.escape_control && ch.is_ascii_control() => {
return write_escaped(f, text, self.escape_control)
}
b'\'' => is_single_safe = false, b'\'' => is_single_safe = false,
// Unsafe characters according to: // Unsafe characters according to:
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_03 // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_03
@ -122,7 +157,7 @@ impl Display for Quoted<'_> {
} }
} }
let text = match from_utf8(text) { let text = match from_utf8(text) {
Err(_) => return write_escaped(f, text), Err(_) => return write_escaped(f, text, self.escape_control),
Ok(text) => text, Ok(text) => text,
}; };
if !requires_quote && text.find(char::is_whitespace).is_some() { if !requires_quote && text.find(char::is_whitespace).is_some() {
@ -175,7 +210,7 @@ impl Display for Quoted<'_> {
/// - fish /// - fish
/// - dash /// - dash
/// - tcsh /// - tcsh
fn write_escaped(f: &mut Formatter<'_>, text: &[u8]) -> fmt::Result { fn write_escaped(f: &mut Formatter<'_>, text: &[u8], escape_control: bool) -> fmt::Result {
f.write_str("$'")?; f.write_str("$'")?;
for chunk in from_utf8_iter(text) { for chunk in from_utf8_iter(text) {
match chunk { match chunk {
@ -190,7 +225,9 @@ impl Display for Quoted<'_> {
// \0 doesn't work consistently because of the // \0 doesn't work consistently because of the
// octal \nnn syntax, and null bytes can't appear // octal \nnn syntax, and null bytes can't appear
// in filenames anyway. // in filenames anyway.
ch if ch.is_ascii_control() => write!(f, "\\x{:02X}", ch as u8)?, ch if escape_control && ch.is_ascii_control() => {
write!(f, "\\x{:02X}", ch as u8)?
}
'\\' | '\'' => { '\\' | '\'' => {
// '?' and '"' can also be escaped this way // '?' and '"' can also be escaped this way
// but AFAICT there's no reason to do so // but AFAICT there's no reason to do so
@ -229,14 +266,14 @@ impl Display for Quoted<'_> {
// There's the additional wrinkle that Windows has stricter requirements // There's the additional wrinkle that Windows has stricter requirements
// for filenames: I've been testing using a Linux build of PowerShell, but // for filenames: I've been testing using a Linux build of PowerShell, but
// this code doesn't even compile on Linux. // this code doesn't even compile on Linux.
const SPECIAL_SHELL_CHARS: &str = "|&;<>()$`\"'*?[]=!{}@ "; const SPECIAL_SHELL_CHARS: &str = "|&;<>()$`\"'*?[]=!{}@ \t\r\n";
/// Characters with a special meaning at the beginning of a name. /// Characters with a special meaning at the beginning of a name.
const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#']; const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#'];
// Getting the "raw" representation of an OsStr is actually expensive, // Getting the "raw" representation of an OsStr is actually expensive,
// so avoid it if unnecessary. // so avoid it if unnecessary.
let text = match self.text.to_str() { let text = match self.text.to_str() {
None => return write_escaped(f, self.text), None => return write_escaped(f, self.text, self.escape_control),
Some(text) => text, Some(text) => text,
}; };
@ -255,7 +292,9 @@ impl Display for Quoted<'_> {
for ch in text.chars() { for ch in text.chars() {
match ch { match ch {
ch if ch.is_ascii_control() => return write_escaped(f, self.text), ch if self.escape_control && ch.is_ascii_control() => {
return write_escaped(f, self.text, self.escape_control)
}
'\'' => is_single_safe = false, '\'' => is_single_safe = false,
'"' | '`' | '$' => is_double_safe = false, '"' | '`' | '$' => is_double_safe = false,
_ => (), _ => (),
@ -291,7 +330,7 @@ impl Display for Quoted<'_> {
Ok(()) Ok(())
} }
fn write_escaped(f: &mut Formatter<'_>, text: &OsStr) -> fmt::Result { fn write_escaped(f: &mut Formatter<'_>, text: &OsStr, escape_control: bool) -> fmt::Result {
f.write_char('"')?; f.write_char('"')?;
for ch in decode_utf16(text.encode_wide()) { for ch in decode_utf16(text.encode_wide()) {
match ch { match ch {
@ -300,7 +339,9 @@ impl Display for Quoted<'_> {
'\r' => f.write_str("`r")?, '\r' => f.write_str("`r")?,
'\n' => f.write_str("`n")?, '\n' => f.write_str("`n")?,
'\t' => f.write_str("`t")?, '\t' => f.write_str("`t")?,
ch if ch.is_ascii_control() => write!(f, "`u{{{:04X}}}", ch as u8)?, ch if escape_control && ch.is_ascii_control() => {
write!(f, "`u{{{:04X}}}", ch as u8)?
}
'`' => f.write_str("``")?, '`' => f.write_str("``")?,
'$' => f.write_str("`$")?, '$' => f.write_str("`$")?,
'"' => f.write_str("\"\"")?, '"' => f.write_str("\"\"")?,
@ -382,7 +423,7 @@ pub fn println_verbatim<S: AsRef<OsStr>>(text: S) -> io::Result<()> {
mod tests { mod tests {
use super::*; use super::*;
fn verify_quote(cases: &[(impl AsRef<OsStr>, &str)]) { fn verify_quote(cases: &[(impl Quotable, &str)]) {
for (case, expected) in cases { for (case, expected) in cases {
assert_eq!(case.quote().to_string(), *expected); assert_eq!(case.quote().to_string(), *expected);
} }
@ -442,7 +483,9 @@ mod tests {
#[cfg(any(unix, target_os = "wasi"))] #[cfg(any(unix, target_os = "wasi"))]
#[test] #[test]
fn test_utf8_iter() { fn test_utf8_iter() {
const CASES: &[(&[u8], &[Result<&str, u8>])] = &[ type ByteStr = &'static [u8];
type Chunk = Result<&'static str, u8>;
const CASES: &[(ByteStr, &[Chunk])] = &[
(b"", &[]), (b"", &[]),
(b"hello", &[Ok("hello")]), (b"hello", &[Ok("hello")]),
// Immediately invalid // Immediately invalid