From 06bdc144d70329d68c0268943175c7b798afbb47 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 3 Apr 2021 12:43:37 +0200 Subject: [PATCH 1/3] ls: show/hide control chars --- src/uu/ls/src/ls.rs | 41 ++++++++- src/uu/ls/src/quoting_style.rs | 152 ++++++++++++++++++++++++++------- 2 files changed, 158 insertions(+), 35 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index ece497bdb..9bb8b7e63 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -119,7 +119,8 @@ pub mod options { pub static FILE_TYPE: &str = "file-type"; pub static CLASSIFY: &str = "classify"; } - + pub static HIDE_CONTROL_CHARS: &str = "hide-control-chars"; + pub static SHOW_CONTROL_CHARS: &str = "show-control-chars"; pub static WIDTH: &str = "width"; pub static AUTHOR: &str = "author"; pub static NO_GROUP: &str = "no-group"; @@ -366,24 +367,36 @@ impl Config { }) .or_else(|| termsize::get().map(|s| s.cols)); + let show_control = if options.is_present(options::HIDE_CONTROL_CHARS) { + false + } else if options.is_present(options::SHOW_CONTROL_CHARS) { + true + } else { + false // TODO: only if output is a terminal and the program is `ls` + }; + let quoting_style = if let Some(style) = options.value_of(options::QUOTING_STYLE) { match style { - "literal" => QuotingStyle::Literal, + "literal" => QuotingStyle::Literal { show_control }, "shell" => QuotingStyle::Shell { escape: false, always_quote: false, + show_control, }, "shell-always" => QuotingStyle::Shell { escape: false, always_quote: true, + show_control, }, "shell-escape" => QuotingStyle::Shell { escape: true, always_quote: false, + show_control, }, "shell-escape-always" => QuotingStyle::Shell { escape: true, always_quote: true, + show_control, }, "c" => QuotingStyle::C { quotes: quoting_style::Quotes::Double, @@ -394,7 +407,7 @@ impl Config { _ => unreachable!("Should have been caught by Clap"), } } else if options.is_present(options::quoting::LITERAL) { - QuotingStyle::Literal + QuotingStyle::Literal { show_control } } else if options.is_present(options::quoting::ESCAPE) { QuotingStyle::C { quotes: quoting_style::Quotes::None, @@ -408,6 +421,7 @@ impl Config { QuotingStyle::Shell { escape: true, always_quote: false, + show_control, } }; @@ -619,6 +633,27 @@ pub fn uumain(args: impl uucore::Args) -> i32 { ]) ) + // Control characters + .arg( + Arg::with_name(options::HIDE_CONTROL_CHARS) + .short("q") + .long(options::HIDE_CONTROL_CHARS) + .help("Replace control characters with '?' if they are not escaped.") + .overrides_with_all(&[ + options::HIDE_CONTROL_CHARS, + options::SHOW_CONTROL_CHARS, + ]) + ) + .arg( + Arg::with_name(options::SHOW_CONTROL_CHARS) + .long(options::SHOW_CONTROL_CHARS) + .help("Show control characters 'as is' if they are not escaped.") + .overrides_with_all(&[ + options::HIDE_CONTROL_CHARS, + options::SHOW_CONTROL_CHARS, + ]) + ) + // Time arguments .arg( Arg::with_name(options::TIME) diff --git a/src/uu/ls/src/quoting_style.rs b/src/uu/ls/src/quoting_style.rs index 74108094a..ceb54466c 100644 --- a/src/uu/ls/src/quoting_style.rs +++ b/src/uu/ls/src/quoting_style.rs @@ -2,14 +2,22 @@ use std::char::from_digit; const SPECIAL_SHELL_CHARS: &str = "~`#$&*()\\|[]{};'\"<>?! "; -pub(crate) enum QuotingStyle { - Shell { escape: bool, always_quote: bool }, - C { quotes: Quotes }, - Literal, +pub(super) enum QuotingStyle { + Shell { + escape: bool, + always_quote: bool, + show_control: bool, + }, + C { + quotes: Quotes, + }, + Literal { + show_control: bool, + }, } #[derive(Clone, Copy)] -pub(crate) enum Quotes { +pub(super) enum Quotes { None, Single, Double, @@ -81,6 +89,12 @@ impl EscapeOctal { } impl EscapedChar { + fn new_literal(c: char) -> Self { + Self { + state: EscapeState::Char(c), + } + } + fn new_c(c: char, quotes: Quotes) -> Self { use EscapeState::*; let init_state = match c { @@ -110,27 +124,10 @@ impl EscapedChar { Self { state: init_state } } - // fn new_shell(c: char, quotes: Quotes) -> Self { - // use EscapeState::*; - // let init_state = match c { - // // If the string is single quoted, the single quote should be escaped - // '\'' => match quotes { - // Quotes::Single => Backslash('\''), - // _ => Char('\''), - // }, - // // All control characters should be rendered as ?: - // _ if c.is_ascii_control() => Char('?'), - // // Special shell characters must be escaped: - // _ if SPECIAL_SHELL_CHARS.contains(c) => ForceQuote(c), - // _ => Char(c), - // }; - // Self { state: init_state } - // } - fn new_shell(c: char, escape: bool, quotes: Quotes) -> Self { use EscapeState::*; let init_state = match c { - _ if !escape && c.is_control() => Char('?'), + _ if !escape && c.is_control() => Char(c), '\x07' => Backslash('a'), '\x08' => Backslash('b'), '\t' => Backslash('t'), @@ -149,6 +146,15 @@ impl EscapedChar { }; Self { state: init_state } } + + fn hide_control(self) -> Self { + match self.state { + EscapeState::Char(c) if c.is_control() => Self { + state: EscapeState::Char('?'), + }, + _ => self, + } + } } impl Iterator for EscapedChar { @@ -170,12 +176,20 @@ impl Iterator for EscapedChar { } } -fn shell_without_escape(name: String, quotes: Quotes) -> (String, bool) { +fn shell_without_escape(name: String, quotes: Quotes, show_control_chars: bool) -> (String, bool) { let mut must_quote = false; let mut escaped_str = String::with_capacity(name.len()); for c in name.chars() { - let escaped = EscapedChar::new_shell(c, false, quotes); + let escaped = { + let ec = EscapedChar::new_shell(c, false, quotes); + if show_control_chars { + ec + } else { + ec.hide_control() + } + }; + match escaped.state { EscapeState::Backslash('\'') => escaped_str.push_str("'\\''"), EscapeState::ForceQuote(x) => { @@ -242,7 +256,15 @@ fn shell_with_escape(name: String, quotes: Quotes) -> (String, bool) { pub(super) fn escape_name(name: String, style: &QuotingStyle) -> String { match style { - QuotingStyle::Literal => name, + QuotingStyle::Literal { show_control } => { + if !show_control { + name.chars() + .flat_map(|c| EscapedChar::new_literal(c).hide_control()) + .collect() + } else { + name + } + } QuotingStyle::C { quotes } => { let escaped_str: String = name .chars() @@ -258,6 +280,7 @@ pub(super) fn escape_name(name: String, style: &QuotingStyle) -> String { QuotingStyle::Shell { escape, always_quote, + show_control, } => { let (quotes, must_quote) = if name.contains('"') { (Quotes::Single, true) @@ -272,7 +295,7 @@ pub(super) fn escape_name(name: String, style: &QuotingStyle) -> String { let (escaped_str, contains_quote_chars) = if *escape { shell_with_escape(name, quotes) } else { - shell_without_escape(name, quotes) + shell_without_escape(name, quotes, *show_control) }; match (must_quote | contains_quote_chars, quotes) { @@ -289,7 +312,10 @@ mod tests { use crate::quoting_style::{escape_name, Quotes, QuotingStyle}; fn get_style(s: &str) -> QuotingStyle { match s { - "literal" => QuotingStyle::Literal, + "literal" => QuotingStyle::Literal { + show_control: false, + }, + "literal-show" => QuotingStyle::Literal { show_control: true }, "escape" => QuotingStyle::C { quotes: Quotes::None, }, @@ -299,18 +325,32 @@ mod tests { "shell" => QuotingStyle::Shell { escape: false, always_quote: false, + show_control: false, + }, + "shell-show" => QuotingStyle::Shell { + escape: false, + always_quote: false, + show_control: true, }, "shell-always" => QuotingStyle::Shell { escape: false, always_quote: true, + show_control: false, + }, + "shell-always-show" => QuotingStyle::Shell { + escape: false, + always_quote: true, + show_control: true, }, "shell-escape" => QuotingStyle::Shell { escape: true, always_quote: false, + show_control: false, }, "shell-escape-always" => QuotingStyle::Shell { escape: true, always_quote: true, + show_control: false, }, _ => panic!("Invalid name!"), } @@ -333,10 +373,13 @@ mod tests { "one_two", vec![ ("one_two", "literal"), + ("one_two", "literal-show"), ("one_two", "escape"), ("\"one_two\"", "c"), ("one_two", "shell"), + ("one_two", "shell-show"), ("\'one_two\'", "shell-always"), + ("\'one_two\'", "shell-always-show"), ("one_two", "shell-escape"), ("\'one_two\'", "shell-escape-always"), ], @@ -349,10 +392,13 @@ mod tests { "one two", vec![ ("one two", "literal"), + ("one two", "literal-show"), ("one\\ two", "escape"), ("\"one two\"", "c"), ("\'one two\'", "shell"), + ("\'one two\'", "shell-show"), ("\'one two\'", "shell-always"), + ("\'one two\'", "shell-always-show"), ("\'one two\'", "shell-escape"), ("\'one two\'", "shell-escape-always"), ], @@ -362,10 +408,13 @@ mod tests { " one", vec![ (" one", "literal"), + (" one", "literal-show"), ("\\ one", "escape"), ("\" one\"", "c"), ("' one'", "shell"), + ("' one'", "shell-show"), ("' one'", "shell-always"), + ("' one'", "shell-always-show"), ("' one'", "shell-escape"), ("' one'", "shell-escape-always"), ], @@ -379,10 +428,13 @@ mod tests { "one\"two", vec![ ("one\"two", "literal"), + ("one\"two", "literal-show"), ("one\"two", "escape"), ("\"one\\\"two\"", "c"), ("'one\"two'", "shell"), + ("'one\"two'", "shell-show"), ("'one\"two'", "shell-always"), + ("'one\"two'", "shell-always-show"), ("'one\"two'", "shell-escape"), ("'one\"two'", "shell-escape-always"), ], @@ -393,10 +445,13 @@ mod tests { "one\'two", vec![ ("one'two", "literal"), + ("one'two", "literal-show"), ("one'two", "escape"), ("\"one'two\"", "c"), ("\"one'two\"", "shell"), + ("\"one'two\"", "shell-show"), ("\"one'two\"", "shell-always"), + ("\"one'two\"", "shell-always-show"), ("\"one'two\"", "shell-escape"), ("\"one'two\"", "shell-escape-always"), ], @@ -407,10 +462,13 @@ mod tests { "one'two\"three", vec![ ("one'two\"three", "literal"), + ("one'two\"three", "literal-show"), ("one'two\"three", "escape"), ("\"one'two\\\"three\"", "c"), ("'one'\\''two\"three'", "shell"), + ("'one'\\''two\"three'", "shell-show"), ("'one'\\''two\"three'", "shell-always"), + ("'one'\\''two\"three'", "shell-always-show"), ("'one'\\''two\"three'", "shell-escape"), ("'one'\\''two\"three'", "shell-escape-always"), ], @@ -421,10 +479,13 @@ mod tests { "one''two\"\"three", vec![ ("one''two\"\"three", "literal"), + ("one''two\"\"three", "literal-show"), ("one''two\"\"three", "escape"), ("\"one''two\\\"\\\"three\"", "c"), ("'one'\\'''\\''two\"\"three'", "shell"), + ("'one'\\'''\\''two\"\"three'", "shell-show"), ("'one'\\'''\\''two\"\"three'", "shell-always"), + ("'one'\\'''\\''two\"\"three'", "shell-always-show"), ("'one'\\'''\\''two\"\"three'", "shell-escape"), ("'one'\\'''\\''two\"\"three'", "shell-escape-always"), ], @@ -437,11 +498,14 @@ mod tests { check_names( "one\ntwo", vec![ - ("one\ntwo", "literal"), + ("one?two", "literal"), + ("one\ntwo", "literal-show"), ("one\\ntwo", "escape"), ("\"one\\ntwo\"", "c"), ("one?two", "shell"), + ("one\ntwo", "shell-show"), ("'one?two'", "shell-always"), + ("'one\ntwo'", "shell-always-show"), ("'one'$'\\n''two'", "shell-escape"), ("'one'$'\\n''two'", "shell-escape-always"), ], @@ -452,9 +516,10 @@ mod tests { check_names( "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", vec![ + ("????????????????", "literal"), ( "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", - "literal", + "literal-show", ), ( "\\000\\001\\002\\003\\004\\005\\006\\a\\b\\t\\n\\v\\f\\r\\016\\017", @@ -465,7 +530,15 @@ mod tests { "c", ), ("????????????????", "shell"), + ( + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", + "shell-show", + ), ("'????????????????'", "shell-always"), + ( + "'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F'", + "shell-always-show", + ), ( "''$'\\000\\001\\002\\003\\004\\005\\006\\a\\b\\t\\n\\v\\f\\r\\016\\017'", "shell-escape", @@ -481,9 +554,10 @@ mod tests { check_names( "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F", vec![ + ("????????????????", "literal"), ( "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F", - "literal", + "literal-show", ), ( "\\020\\021\\022\\023\\024\\025\\026\\027\\030\\031\\032\\033\\034\\035\\036\\037", @@ -494,7 +568,15 @@ mod tests { "c", ), ("????????????????", "shell"), + ( + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F", + "shell-show", + ), ("'????????????????'", "shell-always"), + ( + "'\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F'", + "shell-always-show", + ), ( "''$'\\020\\021\\022\\023\\024\\025\\026\\027\\030\\031\\032\\033\\034\\035\\036\\037'", "shell-escape", @@ -510,11 +592,14 @@ mod tests { check_names( "\x7F", vec![ - ("\x7F", "literal"), + ("?", "literal"), + ("\x7F", "literal-show"), ("\\177", "escape"), ("\"\\177\"", "c"), ("?", "shell"), + ("\x7F", "shell-show"), ("'?'", "shell-always"), + ("'\x7F'", "shell-always-show"), ("''$'\\177'", "shell-escape"), ("''$'\\177'", "shell-escape-always"), ], @@ -530,10 +615,13 @@ mod tests { "one?two", vec![ ("one?two", "literal"), + ("one?two", "literal-show"), ("one?two", "escape"), ("\"one?two\"", "c"), ("'one?two'", "shell"), + ("'one?two'", "shell-show"), ("'one?two'", "shell-always"), + ("'one?two'", "shell-always-show"), ("'one?two'", "shell-escape"), ("'one?two'", "shell-escape-always"), ], From 9cb0fc2945afc7d7d5e7c96a4763039652c4c46c Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 3 Apr 2021 13:15:19 +0200 Subject: [PATCH 2/3] ls: forgot to push updated tests --- tests/by-util/test_ls.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index d403e5577..b49620eb1 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -1142,9 +1142,9 @@ fn test_ls_quoting_style() { assert_eq!(result.stdout, "'one'$'\\n''two'\n"); for (arg, correct) in &[ - ("--quoting-style=literal", "one\ntwo"), - ("-N", "one\ntwo"), - ("--literal", "one\ntwo"), + ("--quoting-style=literal", "one?two"), + ("-N", "one?two"), + ("--literal", "one?two"), ("--quoting-style=c", "\"one\\ntwo\""), ("-Q", "\"one\\ntwo\""), ("--quote-name", "\"one\\ntwo\""), @@ -1161,6 +1161,24 @@ fn test_ls_quoting_style() { println!("stdout = {:?}", result.stdout); assert_eq!(result.stdout, format!("{}\n", correct)); } + + for (arg, correct) in &[ + ("--quoting-style=literal", "one\ntwo"), + ("-N", "one\ntwo"), + ("--literal", "one\ntwo"), + ("--quoting-style=shell", "one\ntwo"), + ("--quoting-style=shell-always", "'one\ntwo'"), + ] { + let result = scene + .ucmd() + .arg(arg) + .arg("--show-control-chars") + .arg("one\ntwo") + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!(result.stdout, format!("{}\n", correct)); + } } let result = scene.ucmd().arg("one two").succeeds(); From 54e9cb09dac17cdabd9d254c1dfade833d628f1c Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 3 Apr 2021 16:42:29 +0200 Subject: [PATCH 3/3] ls: add tests for --hide-control-chars --- tests/by-util/test_ls.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index b49620eb1..835a40a5e 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -1162,6 +1162,24 @@ fn test_ls_quoting_style() { assert_eq!(result.stdout, format!("{}\n", correct)); } + for (arg, correct) in &[ + ("--quoting-style=literal", "one?two"), + ("-N", "one?two"), + ("--literal", "one?two"), + ("--quoting-style=shell", "one?two"), + ("--quoting-style=shell-always", "'one?two'"), + ] { + let result = scene + .ucmd() + .arg(arg) + .arg("--hide-control-chars") + .arg("one\ntwo") + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!(result.stdout, format!("{}\n", correct)); + } + for (arg, correct) in &[ ("--quoting-style=literal", "one\ntwo"), ("-N", "one\ntwo"),