From 7279bc1741c0ee7678a4a66d26392a1ec7dcfed9 Mon Sep 17 00:00:00 2001 From: Zhuoxun Yang Date: Thu, 9 Nov 2023 10:16:53 +0800 Subject: [PATCH 1/3] printf.md: support %q --- src/uu/printf/printf.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/uu/printf/printf.md b/src/uu/printf/printf.md index 60b50354c6f..9ce2957701e 100644 --- a/src/uu/printf/printf.md +++ b/src/uu/printf/printf.md @@ -78,6 +78,9 @@ Fields second parameter is min-width, integer output below that width is padded with leading zeroes +* `%q`: ARGUMENT is printed in a format that can be reused as shell input, escaping non-printable + characters with the proposed POSIX $'' syntax. + * `%f` or `%F`: decimal floating point value * `%e` or `%E`: scientific notation floating point value * `%g` or `%G`: shorter of specially interpreted decimal or SciNote floating point value. @@ -181,6 +184,11 @@ All string fields have a 'max width' parameter still be interpreted and not throw a warning, you will have problems if you use this for a literal whose code begins with zero, as it will be viewed as in `\\0NNN` form.) +* `%q`: escaped string - the string in a format that can be reused as input by most shells. + Non-printable characters are escaped with the POSIX proposed ‘$''’ syntax, + and shell meta-characters are quoted appropriately. + This is an equivalent format to ls --quoting=shell-escape output. + #### CHAR SUBSTITUTIONS The character field does not have a secondary parameter. From e3ec12233b04f2846769a585aa0f73b64af08833 Mon Sep 17 00:00:00 2001 From: Zhuoxun Yang Date: Thu, 9 Nov 2023 10:17:44 +0800 Subject: [PATCH 2/3] printf: support %q --- src/uucore/src/lib/features/tokenize/sub.rs | 22 ++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/uucore/src/lib/features/tokenize/sub.rs b/src/uucore/src/lib/features/tokenize/sub.rs index c65a37a689b..0ae966fc332 100644 --- a/src/uucore/src/lib/features/tokenize/sub.rs +++ b/src/uucore/src/lib/features/tokenize/sub.rs @@ -10,6 +10,7 @@ //! Subs which have numeric field chars make use of the num_format //! submodule use crate::error::{UError, UResult}; +use crate::quoting_style::{escape_name, QuotingStyle}; use itertools::{put_back_n, PutBackN}; use std::error::Error; use std::fmt::Display; @@ -91,7 +92,7 @@ impl Sub { // for more dry printing, field characters are grouped // in initialization of token. let field_type = match field_char { - 's' | 'b' => FieldType::Strf, + 's' | 'b' | 'q' => FieldType::Strf, 'd' | 'i' | 'u' | 'o' | 'x' | 'X' => FieldType::Intf, 'f' | 'F' => FieldType::Floatf, 'a' | 'A' => FieldType::CninetyNineHexFloatf, @@ -189,7 +190,7 @@ impl SubParser { let mut legal_fields = [ // 'a', 'A', //c99 hex float implementation not yet complete - 'b', 'c', 'd', 'e', 'E', 'f', 'F', 'g', 'G', 'i', 'o', 's', 'u', 'x', 'X', + 'b', 'c', 'd', 'e', 'E', 'f', 'F', 'g', 'G', 'i', 'o', 'q', 's', 'u', 'x', 'X', ]; let mut specifiers = ['h', 'j', 'l', 'L', 't', 'z']; legal_fields.sort_unstable(); @@ -260,7 +261,6 @@ impl SubParser { } x if legal_fields.binary_search(&x).is_ok() => { self.field_char = Some(ch); - self.text_so_far.push(ch); break; } x if specifiers.binary_search(&x).is_ok() => { @@ -331,7 +331,7 @@ impl SubParser { if (field_char == 's' && self.min_width_tmp == Some(String::from("0"))) || (field_char == 'c' && (self.min_width_tmp == Some(String::from("0")) || self.past_decimal)) - || (field_char == 'b' + || ((field_char == 'b' || field_char == 'q') && (self.min_width_tmp.is_some() || self.past_decimal || self.second_field_tmp.is_some())) @@ -391,6 +391,7 @@ impl Sub { // if %s just return arg // if %b use UnescapedText module's unescape-fn // if %c return first char of arg + // if %q return arg which non-printable characters are escaped FieldType::Strf | FieldType::Charf => { match pf_arg { Some(arg_string) => { @@ -404,11 +405,18 @@ impl Sub { UnescapedText::from_it_core(writer, &mut a_it, true); None } - // for 'c': get iter of string vals, + 'q' => Some(escape_name( + arg_string.as_ref(), + &QuotingStyle::Shell { + escape: true, + always_quote: false, + show_control: false, + }, + )), // get opt of first val // and map it to opt - /* 'c' | */ - _ => arg_string.chars().next().map(|x| x.to_string()), + 'c' => arg_string.chars().next().map(|x| x.to_string()), + _ => unreachable!(), } } None => None, From fb414ed9179a985de0e545182395a3bd6effbb02 Mon Sep 17 00:00:00 2001 From: Zhuoxun Yang Date: Thu, 9 Nov 2023 10:18:27 +0800 Subject: [PATCH 3/3] tests/printf: support %q --- tests/by-util/test_printf.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index d7ba5679ecf..a297dbf6833 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -112,6 +112,15 @@ fn sub_b_string_handle_escapes() { .stdout_only("hello \tworld"); } +#[test] +fn sub_b_string_validate_field_params() { + new_ucmd!() + .args(&["hello %7b", "world"]) + .run() + .stdout_is("hello ") + .stderr_is("printf: %7b: invalid conversion specification\n"); +} + #[test] fn sub_b_string_ignore_subs() { new_ucmd!() @@ -120,6 +129,31 @@ fn sub_b_string_ignore_subs() { .stdout_only("hello world %% %i"); } +#[test] +fn sub_q_string_non_printable() { + new_ucmd!() + .args(&["non-printable: %q", "\"$test\""]) + .succeeds() + .stdout_only("non-printable: '\"$test\"'"); +} + +#[test] +fn sub_q_string_validate_field_params() { + new_ucmd!() + .args(&["hello %7q", "world"]) + .run() + .stdout_is("hello ") + .stderr_is("printf: %7q: invalid conversion specification\n"); +} + +#[test] +fn sub_q_string_special_non_printable() { + new_ucmd!() + .args(&["non-printable: %q", "test~"]) + .succeeds() + .stdout_only("non-printable: test~"); +} + #[test] fn sub_char() { new_ucmd!()