From 9e0c5fd3c9980dad9cd982be1fcf15130468054c Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Tue, 25 Nov 2025 10:15:08 +1100 Subject: [PATCH] Fixing escape charaters again :cry: #2517 --- pkg/yqlib/lib.go | 72 +++++++++++++++++++++---- pkg/yqlib/lib_test.go | 119 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 9 deletions(-) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index c31724af..5a32665e 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -187,15 +187,69 @@ func parseInt(numberString string) (int, error) { } func processEscapeCharacters(original string) string { - value := original - value = strings.ReplaceAll(value, "\\\"", "\"") - value = strings.ReplaceAll(value, "\\n", "\n") - value = strings.ReplaceAll(value, "\\t", "\t") - value = strings.ReplaceAll(value, "\\r", "\r") - value = strings.ReplaceAll(value, "\\f", "\f") - value = strings.ReplaceAll(value, "\\v", "\v") - value = strings.ReplaceAll(value, "\\b", "\b") - value = strings.ReplaceAll(value, "\\a", "\a") + if original == "" { + return original + } + + var result strings.Builder + runes := []rune(original) + + for i := 0; i < len(runes); i++ { + if runes[i] == '\\' && i < len(runes)-1 { + next := runes[i+1] + switch next { + case '\\': + // Check if followed by opening bracket - if so, preserve both backslashes + // this is required for string interpolation to work correctly. + if i+2 < len(runes) && runes[i+2] == '(' { + // Preserve \\ when followed by ( + result.WriteRune('\\') + result.WriteRune('\\') + i++ // Skip the next backslash (we'll process the ( normally on next iteration) + continue + } + // Escaped backslash: \\ -> \ + result.WriteRune('\\') + i++ // Skip the next backslash + continue + case '"': + result.WriteRune('"') + i++ // Skip the quote + continue + case 'n': + result.WriteRune('\n') + i++ // Skip the 'n' + continue + case 't': + result.WriteRune('\t') + i++ // Skip the 't' + continue + case 'r': + result.WriteRune('\r') + i++ // Skip the 'r' + continue + case 'f': + result.WriteRune('\f') + i++ // Skip the 'f' + continue + case 'v': + result.WriteRune('\v') + i++ // Skip the 'v' + continue + case 'b': + result.WriteRune('\b') + i++ // Skip the 'b' + continue + case 'a': + result.WriteRune('\a') + i++ // Skip the 'a' + continue + } + } + result.WriteRune(runes[i]) + } + + value := result.String() if value != original { log.Debug("processEscapeCharacters from [%v] to [%v]", original, value) } diff --git a/pkg/yqlib/lib_test.go b/pkg/yqlib/lib_test.go index a9aada14..a6ad5786 100644 --- a/pkg/yqlib/lib_test.go +++ b/pkg/yqlib/lib_test.go @@ -408,3 +408,122 @@ func TestKindString(t *testing.T) { test.AssertResult(t, "AliasNode", KindString(AliasNode)) test.AssertResult(t, "unknown!", KindString(Kind(999))) // Invalid kind } + +type processEscapeCharactersScenario struct { + input string + expected string +} + +var processEscapeCharactersScenarios = []processEscapeCharactersScenario{ + { + input: "", + expected: "", + }, + { + input: "hello", + expected: "hello", + }, + { + input: "\\\"", + expected: "\"", + }, + { + input: "hello\\\"world", + expected: "hello\"world", + }, + { + input: "\\n", + expected: "\n", + }, + { + input: "line1\\nline2", + expected: "line1\nline2", + }, + { + input: "\\t", + expected: "\t", + }, + { + input: "hello\\tworld", + expected: "hello\tworld", + }, + { + input: "\\r", + expected: "\r", + }, + { + input: "hello\\rworld", + expected: "hello\rworld", + }, + { + input: "\\f", + expected: "\f", + }, + { + input: "hello\\fworld", + expected: "hello\fworld", + }, + { + input: "\\v", + expected: "\v", + }, + { + input: "hello\\vworld", + expected: "hello\vworld", + }, + { + input: "\\b", + expected: "\b", + }, + { + input: "hello\\bworld", + expected: "hello\bworld", + }, + { + input: "\\a", + expected: "\a", + }, + { + input: "hello\\aworld", + expected: "hello\aworld", + }, + { + input: "\\\"\\n\\t\\r\\f\\v\\b\\a", + expected: "\"\n\t\r\f\v\b\a", + }, + { + input: "multiple\\nlines\\twith\\ttabs", + expected: "multiple\nlines\twith\ttabs", + }, + { + input: "quote\\\"here", + expected: "quote\"here", + }, + { + input: "\\\\", + expected: "\\", // Backslash is processed: "\\\\" becomes "\\" + }, + { + input: "\\\"test\\\"", + expected: "\"test\"", + }, + { + input: "a\\\\b", + expected: "a\\b", // Tests roundtrip: "a\\\\b" should become "a\\b" + }, + { + input: "Hi \\\\(.value)", + expected: "Hi \\\\(.value)", + }, + { + input: `a\\b`, + expected: "a\\b", + }, +} + +func TestProcessEscapeCharacters(t *testing.T) { + for _, tt := range processEscapeCharactersScenarios { + actual := processEscapeCharacters(tt.input) + test.AssertResultComplexWithContext(t, tt.expected, actual, fmt.Sprintf("Input: %q", tt.input)) + } +}