From a51671c52299206dcecb866a37f5f83eb696c133 Mon Sep 17 00:00:00 2001 From: truffle Date: Fri, 26 Jun 2026 03:13:39 +0000 Subject: [PATCH] fix(strings): don't halve backslashes in interpolated string literals String literals in expressions have their escapes decoded once by the lexer (processEscapeCharacters), then interpolate() decoded every backslash pair a second time, halving runs of backslashes (\\ -> \, \\\\ -> \\). Output from an expression diverged from the same string read as input, and from jq. interpolate() only needs to treat a backslash pair as an escape when it guards an interpolation paren (\\( -> literal \(); a standalone pair must pass through unchanged. Guard the skip on a following '(' so plain backslash runs survive, mirroring the lexer's own \\( special case. Fixes #2561 --- pkg/yqlib/operator_strings.go | 10 ++++++++-- pkg/yqlib/operator_strings_test.go | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/pkg/yqlib/operator_strings.go b/pkg/yqlib/operator_strings.go index 3ab79b0d..c0f1656d 100644 --- a/pkg/yqlib/operator_strings.go +++ b/pkg/yqlib/operator_strings.go @@ -62,8 +62,14 @@ func interpolate(d *dataTreeNavigator, context Context, str string) (string, err i++ continue case '\\': - // skip the escaped backslash - i++ + // A backslash pair is only an interpolation escape when it + // guards an opening paren ("\\(" -> literal "\("). The lexer + // (processEscapeCharacters) has already decoded string escapes, + // so a standalone pair must pass through unchanged rather than + // be halved a second time (#2561). + if i+2 < len(runes) && runes[i+2] == '(' { + i++ // skip the second backslash; '(' is emitted as literal text next + } default: log.Debugf("Ignoring non-escaping backslash @ %v[%d]", str, i) } diff --git a/pkg/yqlib/operator_strings_test.go b/pkg/yqlib/operator_strings_test.go index bbcbc483..b20cb54f 100644 --- a/pkg/yqlib/operator_strings_test.go +++ b/pkg/yqlib/operator_strings_test.go @@ -29,6 +29,22 @@ var stringsOperatorScenarios = []expressionScenario{ "D0, P[], (!!str)::\\\n", }, }, + { + skipDoc: true, + description: "Interpolation - backslashes are not halved (#2561)", + expression: `"\\\\"`, + expected: []string{ + "D0, P[], (!!str)::\\\\\n", + }, + }, + { + skipDoc: true, + description: "Interpolation - odd backslash run preserved (#2561)", + expression: `"\\\\\\"`, + expected: []string{ + "D0, P[], (!!str)::\\\\\\\n", + }, + }, { skipDoc: true, description: "Interpolation - nested",