Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

printf: support %q #5514

Merged
merged 3 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/uu/printf/printf.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
22 changes: 15 additions & 7 deletions src/uucore/src/lib/features/tokenize/sub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -91,7 +92,7 @@
// 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,
Expand Down Expand Up @@ -189,7 +190,7 @@

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();
Expand Down Expand Up @@ -260,7 +261,6 @@
}
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() => {
Expand Down Expand Up @@ -331,7 +331,7 @@
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()))
Expand Down Expand Up @@ -391,6 +391,7 @@
// 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) => {
Expand All @@ -404,11 +405,18 @@
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<char> of first val
// and map it to opt<String>
/* 'c' | */
_ => arg_string.chars().next().map(|x| x.to_string()),
'c' => arg_string.chars().next().map(|x| x.to_string()),
_ => unreachable!(),

Check warning on line 419 in src/uucore/src/lib/features/tokenize/sub.rs

View check run for this annotation

Codecov / codecov/patch

src/uucore/src/lib/features/tokenize/sub.rs#L419

Added line #L419 was not covered by tests
}
}
None => None,
Expand Down
34 changes: 34 additions & 0 deletions tests/by-util/test_printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!()
Expand All @@ -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!()
Expand Down
Loading