From bdea188fde590252f185d8d9834b5bd1096b47c4 Mon Sep 17 00:00:00 2001 From: MsfPablo Date: Fri, 7 Aug 2026 11:39:10 +0200 Subject: [PATCH] -printf: warn on unrecognized directives/escapes instead of dropping or erroring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two divergences from GNU find: - An unrecognized %X directive silently dropped the '%' and printed only the following character. - An unrecognized \X escape was a hard parse error (exit 1) instead of a warning. Both now print a "find: warning: ..." message to stderr and emit the directive/escape literally (with its '%' or '\' prefix), matching GNU find's behavior and exit code (0). Also fixes advance_one(), which sliced the remaining format string at a fixed 1-byte offset and panicked on multibyte characters (e.g. '€') immediately after '%' or '\' — using char::len_utf8() instead. Fixes #815 --- src/find/matchers/printf.rs | 23 ++++++++++++++++++----- tests/test_find.rs | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/find/matchers/printf.rs b/src/find/matchers/printf.rs index 31151db2..ac68d145 100644 --- a/src/find/matchers/printf.rs +++ b/src/find/matchers/printf.rs @@ -152,7 +152,7 @@ impl FormatStringParser<'_> { fn advance_one(&mut self) -> Result> { let c = self.front()?; - self.string = &self.string[1..]; + self.string = &self.string[c.len_utf8()..]; Ok(c) } @@ -203,7 +203,10 @@ impl FormatStringParser<'_> { 'v' => "\x0B", '0' => "\0", '\\' => "\\", - c => return Err(format!("Invalid escape sequence: \\{c}").into()), + c => { + eprintln!("find: warning: unrecognized escape '\\{c}'"); + return Ok(FormatComponent::Literal(format!("\\{c}"))); + } }; Ok(FormatComponent::Literal(c.to_string())) @@ -312,7 +315,10 @@ impl FormatStringParser<'_> { }, 'Y' => FormatDirective::Type { follow_links: true }, // TODO: %Z - _ => return Ok(FormatComponent::Literal(first.to_string())), + _ => { + eprintln!("find: warning: unrecognized format directive '%{first}'"); + return Ok(FormatComponent::Literal(format!("%{first}"))); + } }; Ok(FormatComponent::Directive { @@ -698,7 +704,12 @@ mod tests { ] ); - assert!(FormatString::parse("\\X").is_err()); + // An unrecognized escape is a warning, not an error: it's printed + // literally (backslash included), matching GNU find. + assert_eq!( + FormatString::parse("\\X").unwrap().components, + vec![FormatComponent::Literal("\\X".to_owned())] + ); assert!(FormatString::parse("\\").is_err()); } @@ -798,7 +809,9 @@ mod tests { follow_links: false }), unaligned_directive(FormatDirective::Type { follow_links: true }), - FormatComponent::Literal("?".to_owned()), + // An unrecognized directive is a warning, not a silent drop of + // the '%': it's printed literally, matching GNU find. + FormatComponent::Literal("%?".to_owned()), ] ); diff --git a/tests/test_find.rs b/tests/test_find.rs index ce223d56..f5efdd00 100644 --- a/tests/test_find.rs +++ b/tests/test_find.rs @@ -578,6 +578,24 @@ fn find_printf_octal_escape_before_multibyte_char() { .stdout_only("\0€\n"); } +#[test] +fn find_printf_unrecognized_directive_prints_literally() { + ucmd() + .args(&["./test_data/simple", "-maxdepth", "0", "-printf", "%€|\n"]) + .succeeds() + .stdout_contains("%€|\n") + .stderr_contains("unrecognized format directive '%€'"); +} + +#[test] +fn find_printf_unrecognized_escape_prints_literally() { + ucmd() + .args(&["./test_data/simple", "-maxdepth", "0", "-printf", "\\€|\n"]) + .succeeds() + .stdout_contains("\\€|\n") + .stderr_contains("unrecognized escape '\\€'"); +} + #[test] fn find_printf_width_too_large() { ucmd()