From ff1db36aaa5037fec741651afdec7822f5c6f137 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 20 May 2024 19:37:52 +0200 Subject: [PATCH] shell: handle operators and delimiters better (#11165) Co-authored-by: Jarred Sumner Co-authored-by: Jarred-Sumner --- src/shell/shell.zig | 19 ++++++++++++------- test/js/bun/shell/bunshell.test.ts | 8 ++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/shell/shell.zig b/src/shell/shell.zig index 4830a9ffdb..26e20fac80 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -2482,7 +2482,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { if (self.chars.state == .Single) break :escaped; if (self.in_subshell == .backtick) { - try self.break_word(true); + try self.break_word_operator(); if (self.last_tok_tag()) |toktag| { if (toktag != .Delimit) try self.tokens.append(.Delimit); } @@ -2588,7 +2588,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { comptime assertSpecialChar('|'); if (self.chars.state == .Single or self.chars.state == .Double) break :escaped; - try self.break_word(true); + try self.break_word_operator(); const next = self.peek() orelse { self.add_error("Unexpected EOF"); @@ -2610,7 +2610,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { comptime assertSpecialChar('>'); if (self.chars.state == .Single or self.chars.state == .Double) break :escaped; - try self.break_word_impl(true, false, true); + try self.break_word_operator(); const redirect = self.eat_simple_redirect(.out); try self.tokens.append(.{ .Redirect = redirect }); continue; @@ -2619,7 +2619,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { comptime assertSpecialChar('<'); if (self.chars.state == .Single or self.chars.state == .Double) break :escaped; - try self.break_word_impl(true, false, true); + try self.break_word_operator(); const redirect = self.eat_simple_redirect(.in); try self.tokens.append(.{ .Redirect = redirect }); continue; @@ -2628,7 +2628,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { comptime assertSpecialChar('&'); if (self.chars.state == .Single or self.chars.state == .Double) break :escaped; - try self.break_word(true); + try self.break_word_operator(); const next = self.peek() orelse { try self.tokens.append(.Ampersand); @@ -2751,13 +2751,18 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { return try self.break_word_impl(add_delimiter, false, false); } + /// NOTE: this adds a delimiter + fn break_word_operator(self: *@This()) !void { + return try self.break_word_impl(true, false, true); + } + inline fn isImmediatelyEscapedQuote(self: *@This()) bool { return (self.chars.state == .Double 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 == '"')); } - fn break_word_impl(self: *@This(), add_delimiter: bool, in_normal_space: bool, in_redirect_operator: bool) !void { + fn break_word_impl(self: *@This(), add_delimiter: bool, in_normal_space: bool, in_operator: bool) !void { const start: u32 = self.word_start; const end: u32 = self.j; if (start != end or @@ -2773,7 +2778,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { if (add_delimiter) { try self.tokens.append(.Delimit); } - } else if ((in_normal_space or in_redirect_operator) and self.tokens.items.len > 0 and + } else if ((in_normal_space or in_operator) and self.tokens.items.len > 0 and // whether or not to add a delimiter token switch (self.tokens.items[self.tokens.items.len - 1]) { .Var, diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index 59f35c766c..91223fa78c 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -379,6 +379,14 @@ describe("bunshell", () => { expect(stdout.toString()).toEqual("LMAO\n"); }); + describe("operators no spaces", async () => { + TestBuilder.command`echo LMAO|cat`.stdout("LMAO\n").runAsTest("pipeline"); + TestBuilder.command`echo foo&&echo hi`.stdout("foo\nhi\n").runAsTest("&&"); + TestBuilder.command`echo foo||echo hi`.stdout("foo\n").runAsTest("||"); + TestBuilder.command`echo foo>hi.txt`.ensureTempDir().fileEquals("hi.txt", "foo\n").runAsTest("||"); + TestBuilder.command`echo hifriends#lol`.stdout("hifriends#lol\n").runAsTest("#"); + }); + test("cmd subst", async () => { const haha = "noice"; const { stdout } = await $`echo $(echo noice)`;