From 38c41bc9d18eeda7aa13b86a0994b59cbada209c Mon Sep 17 00:00:00 2001 From: pfg Date: Tue, 19 Nov 2024 20:59:15 -0800 Subject: [PATCH] fix '\' handling, still working on ast of "${""}" and ${""} and then have to fix execution of ["echo", "", ""] --- src/shell/shell.zig | 59 +++++++++++++++-------------- test/regression/issue/15189.test.ts | 33 ++++++++++++++-- 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/shell/shell.zig b/src/shell/shell.zig index 3f3bb16b6f..03cf3b3316 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -2711,14 +2711,11 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { } continue; } - // Treat newline preceded by backslash as whitespace + // Ignore newline preceeded by backslash else if (char == '\n') { if (comptime bun.Environment.allow_assert) { assert(input.escaped); } - if (self.chars.state != .Double) { - try self.break_word_impl(true, true, false); - } continue; } @@ -2771,7 +2768,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { } inline fn isImmediatelyEscapedQuote(self: *@This()) bool { - return (self.chars.state == .Double and + return ((self.chars.state == .Double or self.chars.state == .Single) and (self.chars.current != null and !self.chars.current.?.escaped and self.chars.current.?.char == '"') and (self.chars.prev != null and !self.chars.prev.?.escaped and self.chars.prev.?.char == '"')); } @@ -3516,31 +3513,37 @@ pub fn ShellCharIter(comptime encoding: StringEncoding) type { } pub fn read_char(self: *@This()) ?InputChar { - const indexed_value = self.src.index() orelse return null; - var char = indexed_value.char; - if (char != '\\' or self.state == .Single) return .{ .char = char }; + while (true) { + const indexed_value = self.src.index() orelse return null; + var char = indexed_value.char; + if (char != '\\' or self.state == .Single) return .{ .char = char }; - // Handle backslash - switch (self.state) { - .Normal => { - const peeked = self.src.indexNext() orelse return null; - char = peeked.char; - }, - .Double => { - const peeked = self.src.indexNext() orelse return null; - switch (peeked.char) { - // Backslash only applies to these characters - '$', '`', '"', '\\', '\n', '#' => { - char = peeked.char; - }, - else => return .{ .char = char, .escaped = false }, - } - }, - // We checked `self.state == .Single` above so this is impossible - .Single => unreachable, + // Handle backslash + const peeked = self.src.indexNext() orelse return null; + if (peeked.char == '\n') { + // completely ignore backslash newline, don't advance self.prev/self.current + self.src.eat(true); + continue; + } + switch (self.state) { + .Normal => { + char = peeked.char; + }, + .Double => { + switch (peeked.char) { + // Backslash only applies to these characters + '$', '`', '"', '\\', '#' => { + char = peeked.char; + }, + else => return .{ .char = char, .escaped = false }, + } + }, + // We checked `self.state == .Single` above so this is impossible + .Single => unreachable, + } + + return .{ .char = char, .escaped = true }; } - - return .{ .char = char, .escaped = true }; } }; } diff --git a/test/regression/issue/15189.test.ts b/test/regression/issue/15189.test.ts index e0768c9567..f16a25723e 100644 --- a/test/regression/issue/15189.test.ts +++ b/test/regression/issue/15189.test.ts @@ -15,11 +15,20 @@ describe("bun shell", () => { expect(await $({ raw: ["echo " + 'a"a"'.repeat(1000000)] } as any).text()).toBe("aa".repeat(1000000) + "\n"); }); it("passes correct number of arguments with empty string substitutions", async () => { - expect(await $`echo ${"1"} ${""} ${"2"}`.text()).toBe("1 2\n"); + expect(await $`echo 1 ${""} 2`.text()).toBe("1 2\n"); }); - it("passes correct number of arguments with empty string quotes", async () => { + it("passes correct number of arguments with empty string substitutions 2", async () => { + expect(await $`echo 1 "${""}" 2`.text()).toBe("1 2\n"); + }); + it("passes correct number of arguments with empty string substitutions 3", async () => { + expect(await $`echo 1 '${""}' 2`.text()).toBe("1 2\n"); + }); + it("passes correct number of arguments with empty double string quotes", async () => { expect(await $`echo "1" "" "2"`.text()).toBe("1 2\n"); }); + it("passes correct number of arguments with empty single string quotes", async () => { + expect(await $`echo '1' '' '2'`.text()).toBe("1 2\n"); + }); it("doesn't cause invalid js string ref error with a number after a string ref", async () => { expect(await $`echo ${'"'}1`.text()).toBe('"1\n'); }); @@ -72,7 +81,7 @@ describe("bun shell", () => { it("expands tilde as middle argument", async () => { expect(await $`echo a ~ b`.text()).toBe("a " + process.env.HOME + " b\n"); }); - it.todo("expands tilde as middle argument 2", async () => { + it("expands tilde as middle argument 2", async () => { expect( await $`echo a ~\ b`.text(), @@ -118,6 +127,24 @@ describe("bun shell", () => { it("does not expand tilde second", async () => { expect(await $`echo "a"~`.text()).toBe("a~\n"); }); + it("handles backslashed newline", async () => { + expect( + await $`echo a\ +b`.text(), + ).toBe("ab\n"); + }); + it("handles backslashed newline in single quotes", async () => { + expect( + await $`echo 'a\ +b'`.text(), + ).toBe("a\\\nb\n"); + }); + it("handles backslashed newline in double quotes", async () => { + expect( + await $`echo "a\ +b"`.text(), + ).toBe("ab\n"); + }); // TODO: handle username (`~user` -> getpwnam(user) eg /home/user if the accont exists. but only if all unquoted, ie `~user"a"` <- not allowed) it("fails for bad surrogate pairs", async () => {