From 6ae16c65c73f2b61303124652c16cc2d3ce256f6 Mon Sep 17 00:00:00 2001 From: Umberto Puddu <125773480+upuddu@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:17:28 -0500 Subject: [PATCH] fix: expand format placeholder followed by a literal '}' A placeholder immediately followed by a literal '}' (e.g. '{}}' or '{.}}') was treated as fixed text instead of being expanded. The parser detected the placeholder pattern but, when the next character was '}', fell through to a branch that copied the whole sequence verbatim and dropped the trailing '}'. Remove that branch so the placeholder is always emitted and the following '}' is handled as ordinary literal text. This affects both --format and --exec, which share the template parser. --- CHANGELOG.md | 1 + src/exec/mod.rs | 7 +++---- src/fmt/mod.rs | 39 +++++++++++++++++++++++++++++++-------- tests/tests.rs | 7 +++++++ 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b09d9f3..6e6895937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Fire the "search pattern contains a path separator" diagnostic for any pattern containing `/`, not just patterns that happen to name an existing directory. Preserves the legacy Windows behaviour that also flags native `\` separators when the pattern resolves to a real directory. See #1873. - Also fire the "search pattern contains a path separator" diagnostic for `--and` patterns, not only the primary positional pattern. `--and` patterns are matched against the file name just like the primary pattern, so a path separator in them silently returned zero results. See #1873. - Fix bug where passing "-" as a directory argument didn't actually search that directory, see #849 (@Sean-Kenneth-Doherty). +- Fix `--format`/`--exec` placeholders that are immediately followed by a literal `}` (e.g. `{}}` or `{.}}`). The placeholder is now expanded with the `}` kept as trailing literal text, instead of the whole sequence being treated as fixed text. # 10.4.2 diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 03860e864..126256c2b 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -369,10 +369,9 @@ mod tests { fn tokens_with_literal_braces() { let template = CommandTemplate::new(vec!["{{}}", "{{", "{.}}"], ExecutionMode::OneByOne).unwrap(); - assert_eq!( - generate_str(&template, "foo"), - vec!["{}", "{", "{.}", "foo"] - ); + // `{{}}` and `{{` are literal-brace escapes, while `{.}}` expands the + // `{.}` placeholder and keeps the trailing `}` as a literal. + assert_eq!(generate_str(&template, "foo"), vec!["{}", "{", "foo}"]); } #[test] diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index 87ee41923..2eb6e9a9d 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -73,7 +73,12 @@ impl FormatTemplate { buf += &remaining[..m.start() + BRACE_LEN]; remaining = &remaining[m.end()..]; } - id if !remaining[m.end()..].starts_with('}') => { + id => { + // We found a placeholder. Add any preceding text to the + // buffer, flush it, then push the placeholder token. A + // literal '}' following the placeholder (as in `{}}`) is + // left in `remaining` and handled as ordinary text on the + // next iteration. buf += &remaining[..m.start()]; if !buf.is_empty() { tokens.push(Token::Text(std::mem::take(&mut buf))); @@ -81,13 +86,6 @@ impl FormatTemplate { tokens.push(token_from_pattern_id(id)); remaining = &remaining[m.end()..]; } - _ => { - // We got a normal pattern, but the final "}" - // is escaped, so add up to that to the buffer, then - // skip the final } - buf += &remaining[..m.end()]; - remaining = &remaining[m.end() + BRACE_LEN..]; - } } } // Add the rest of the string to the buffer, and add the final buffer to the tokens @@ -233,6 +231,31 @@ mod fmt_tests { ); } + #[test] + fn parse_placeholder_followed_by_literal_brace() { + use Token::*; + + // A placeholder immediately followed by a literal '}' should still be + // expanded, with the '}' kept as trailing literal text. + assert_eq!( + FormatTemplate::parse("{}}"), + FormatTemplate::Tokens(vec![Placeholder, Text("}".into())]) + ); + assert_eq!( + FormatTemplate::parse("{/}}"), + FormatTemplate::Tokens(vec![Basename, Text("}".into())]) + ); + + let mut path = PathBuf::new(); + path.push("a"); + path.push("file.txt"); + let expanded = FormatTemplate::parse("{}}") + .generate(&path, Some("/")) + .into_string() + .unwrap(); + assert_eq!(expanded, "a/file.txt}"); + } + #[test] fn all_placeholders() { use Token::*; diff --git a/tests/tests.rs b/tests/tests.rs index 191447b5e..cdca57a30 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1755,6 +1755,13 @@ fn format() { parent=one/two/three parent=one/two/three", ); + + // A placeholder immediately followed by a literal '}' is still expanded, + // with the '}' kept as trailing literal text (see issue with `{}}`). + te.assert_output( + &["^a\\.foo$", "--format", "{}}", "--path-separator=/"], + "a.foo}", + ); } /// Shell script execution (--exec)