diff --git a/src/uu/dd/locales/en-US.ftl b/src/uu/dd/locales/en-US.ftl index 1ae3b68cd5d..76a6fa486a1 100644 --- a/src/uu/dd/locales/en-US.ftl +++ b/src/uu/dd/locales/en-US.ftl @@ -90,21 +90,21 @@ dd-error-not-directory = setting flags for '{ $file }': Not a directory dd-error-failed-discard-cache = failed to discard cache for: { $file } # Parse errors -dd-error-unrecognized-operand = unrecognized operand '{ $operand }' +dd-error-unrecognized-operand = unrecognized operand { $operand } dd-error-multiple-format-table = cannot combine any two of {"{"}ascii,ebcdic,ibm{"}"} dd-error-multiple-case = cannot combine lcase and ucase dd-error-multiple-block = cannot combine block and unblock dd-error-multiple-excl = cannot combine excl and nocreat -dd-error-invalid-flag = invalid input flag: '{ $flag }' -dd-error-invalid-output-flag = invalid output flag: '{ $flag }' -dd-error-conv-flag-no-match = invalid conversion: '{ $flag }' -dd-error-multiplier-parse-failure = invalid number: '{ $input }' +dd-error-invalid-flag = invalid input flag: { $flag } +dd-error-invalid-output-flag = invalid output flag: { $flag } +dd-error-conv-flag-no-match = invalid conversion: { $flag } +dd-error-multiplier-parse-failure = invalid number: { $input } dd-error-multiplier-overflow = Multiplier string would overflow on current system -> { $input } dd-error-block-without-cbs = conv=block or conv=unblock specified without cbs=N -dd-error-status-not-recognized = invalid status level: '{ $level }' +dd-error-status-not-recognized = invalid status level: { $level } dd-error-unimplemented = feature not implemented on this system -> { $feature } dd-error-bs-out-of-range = { $param }=N cannot fit into memory -dd-error-invalid-number = invalid number: '{ $input }' +dd-error-invalid-number = invalid number: { $input } # Progress messages dd-progress-records-in = { $complete }+{ $partial } records in diff --git a/src/uu/dd/locales/fr-FR.ftl b/src/uu/dd/locales/fr-FR.ftl index 10c96bc766d..2e418672e1d 100644 --- a/src/uu/dd/locales/fr-FR.ftl +++ b/src/uu/dd/locales/fr-FR.ftl @@ -91,21 +91,21 @@ dd-error-not-directory = définir les indicateurs pour '{ $file }' : N'est pas u dd-error-failed-discard-cache = échec de la suppression du cache pour : { $file } # Parse errors -dd-error-unrecognized-operand = opérande non reconnue '{ $operand }' +dd-error-unrecognized-operand = opérande non reconnue { $operand } dd-error-multiple-format-table = impossible de combiner deux options parmi {"{"}ascii,ebcdic,ibm{"}"} dd-error-multiple-case = impossible de combiner lcase et ucase dd-error-multiple-block = impossible de combiner block et unblock dd-error-multiple-excl = impossible de combiner excl et nocreat -dd-error-invalid-flag = indicateur d'entrée invalide : '{ $flag }' -dd-error-invalid-output-flag = indicateur de sortie invalide : '{ $flag }' -dd-error-conv-flag-no-match = conversion invalide : '{ $flag }' -dd-error-multiplier-parse-failure = nombre invalide : '{ $input }' +dd-error-invalid-flag = indicateur d'entrée invalide : { $flag } +dd-error-invalid-output-flag = indicateur de sortie invalide : { $flag } +dd-error-conv-flag-no-match = conversion invalide : { $flag } +dd-error-multiplier-parse-failure = nombre invalide : { $input } dd-error-multiplier-overflow = La chaîne de multiplicateur déborderait sur le système actuel -> { $input } dd-error-block-without-cbs = conv=block ou conv=unblock spécifié sans cbs=N -dd-error-status-not-recognized = niveau d'état invalide : '{ $level }' +dd-error-status-not-recognized = niveau d'état invalide : { $level } dd-error-unimplemented = fonctionnalité non implémentée sur ce système -> { $feature } dd-error-bs-out-of-range = { $param }=N ne peut pas tenir en mémoire -dd-error-invalid-number = nombre invalide : '{ $input }' +dd-error-invalid-number = nombre invalide : { $input } # Progress messages dd-progress-records-in = { $complete }+{ $partial } enregistrements en entrée diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index d0aa6d88e2e..183f6da8b3c 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -14,13 +14,39 @@ use thiserror::Error; use uucore::display::Quotable; use uucore::error::UError; use uucore::parser::parse_size::{ParseSizeError, Parser as SizeParser}; +use uucore::quoting_style::{Quotes, QuotingStyle, locale_aware_escape_name}; use uucore::show_warning; use uucore::translate; +/// A value in single quotes, with the characters a terminal would not show +/// spelled out, as GNU's `quote` does: `status=$'\1'` is reported as `'\001'` +/// rather than as the byte itself. +fn quote(value: &str) -> String { + escape( + value, + QuotingStyle::C { + quotes: Quotes::Single, + }, + ) +} + +/// The same, in the shell syntax GNU quotes numbers and operands with, where +/// an unprintable byte ends the quoted run rather than being escaped inside +/// it: `''$'\001'`. +fn shell_quote(value: &str) -> String { + escape(value, QuotingStyle::SHELL_ESCAPE_QUOTE) +} + +fn escape(value: &str, style: QuotingStyle) -> String { + locale_aware_escape_name(value.as_ref(), style) + .into_string() + .expect("escaping a str only ever adds ASCII") +} + /// Parser Errors describe errors with parser input #[derive(Debug, PartialEq, Eq, Error)] pub enum ParseError { - #[error("{}", translate!("dd-error-unrecognized-operand", "operand" => .0))] + #[error("{}", translate!("dd-error-unrecognized-operand", "operand" => shell_quote(.0)))] UnrecognizedOperand(String), #[error("{}", translate!("dd-error-multiple-format-table"))] MultipleFmtTable, @@ -30,27 +56,27 @@ pub enum ParseError { MultipleBlockUnblock, #[error("{}", translate!("dd-error-multiple-excl"))] MultipleExclNoCreate, - #[error("{}", translate!("dd-error-invalid-flag", "flag" => .0))] + #[error("{}", translate!("dd-error-invalid-flag", "flag" => quote(.0)))] FlagNoMatch(String), - #[error("{}", translate!("dd-error-invalid-output-flag", "flag" => .0))] + #[error("{}", translate!("dd-error-invalid-output-flag", "flag" => quote(.0)))] OutputFlagNoMatch(String), - #[error("{}", translate!("dd-error-conv-flag-no-match", "flag" => .0))] + #[error("{}", translate!("dd-error-conv-flag-no-match", "flag" => quote(.0)))] ConvFlagNoMatch(String), - #[error("{}", translate!("dd-error-multiplier-parse-failure", "input" => .0))] + #[error("{}", translate!("dd-error-multiplier-parse-failure", "input" => shell_quote(.0)))] MultiplierStringParseFailure(String), #[error("{}", translate!("dd-error-multiplier-overflow", "input" => .0))] MultiplierStringOverflow(String), #[error("{}", translate!("dd-error-block-without-cbs"))] BlockUnblockWithoutCBS, - #[error("{}", translate!("dd-error-status-not-recognized", "level" => .0))] + #[error("{}", translate!("dd-error-status-not-recognized", "level" => quote(.0)))] StatusLevelNotRecognized(String), #[error("{}", translate!("dd-error-unimplemented", "feature" => .0))] Unimplemented(String), #[error("{}", translate!("dd-error-bs-out-of-range", "param" => .0))] BsOutOfRange(String), - #[error("{}", translate!("dd-error-invalid-number", "input" => .0))] + #[error("{}", translate!("dd-error-invalid-number", "input" => shell_quote(.0)))] InvalidNumber(String), - #[error("invalid number: '{0}': {1}")] + #[error("invalid number: {}: {}", shell_quote(.0), .1)] InvalidNumberWithErrMsg(String, String), } diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 943fdd4feaa..a132e5b4bab 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1394,6 +1394,49 @@ fn test_invalid_flag_arg_gnu_compatibility() { } } +/// A value that would not survive being printed is spelled out instead, as +/// GNU does, rather than sending the raw byte to the terminal. +#[test] +fn test_operand_value_is_quoted_like_gnu() { + // The choices GNU quotes plainly: single quotes with C escapes inside. + for (operand, message) in [ + ("status=\u{1}", r"invalid status level: '\001'"), + ("status=a\tb", r"invalid status level: 'a\tb'"), + ("status=a'b", r"invalid status level: 'a\'b'"), + ("status=a\\b", r"invalid status level: 'a\\b'"), + ("status=a b", "invalid status level: 'a b'"), + ("iflag=\u{1}", r"invalid input flag: '\001'"), + ("oflag=\u{1}", r"invalid output flag: '\001'"), + ("conv=\u{1}", r"invalid conversion: '\001'"), + ] { + new_ucmd!().args(&[operand]).fails().usage_error(message); + } + + // Numbers and operands take the shell syntax GNU uses for them, where an + // unprintable byte ends the quoted run rather than being escaped inside + // it, and a quote switches which quotes are used. + new_ucmd!() + .args(&["\u{1}=1"]) + .fails() + .usage_error(r"unrecognized operand ''$'\001''=1'"); + + new_ucmd!() + .args(&["bs=\u{1}"]) + .fails() + .stderr_is("dd: invalid number: ''$'\\001'\n"); + + new_ucmd!() + .args(&["bs=a'b"]) + .fails() + .stderr_is("dd: invalid number: \"a'b\"\n"); + + // An ordinary value is quoted too, which is what it already was. + new_ucmd!() + .args(&["bs=29d"]) + .fails() + .stderr_is("dd: invalid number: '29d'\n"); +} + #[test] fn test_invalid_file_arg_gnu_compatibility() { new_ucmd!()