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",