Skip to content

Commit

Permalink
Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
zackradisic committed Apr 4, 2024
1 parent ca1dbb4 commit 0e24db0
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 32 deletions.
29 changes: 27 additions & 2 deletions src/shell/interpreter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1751,6 +1751,10 @@ pub const Interpreter = struct {
}
};

pub fn format(this: *const Expansion, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
try writer.print("Expansion(0x{x})", .{@intFromPtr(this)});
}

pub fn init(
interpreter: *ThisInterpreter,
shell_state: *ShellState,
Expand Down Expand Up @@ -2115,6 +2119,7 @@ pub const Interpreter = struct {
}

fn onGlobWalkDone(this: *Expansion, task: *ShellGlobTask) void {
log("{} onGlobWalkDone", .{this});
if (comptime bun.Environment.allow_assert) {
std.debug.assert(this.child_state == .glob);
}
Expand All @@ -2133,6 +2138,16 @@ pub const Interpreter = struct {
}

if (task.result.items.len == 0) {
// In variable assignments, a glob that fails to match should not produce an error, but instead expand to just the pattern
if (this.parent.ptr.is(Assigns) or (this.parent.ptr.is(Cmd) and this.parent.ptr.as(Cmd).state == .expanding_assigns)) {
this.pushCurrentOut();
this.child_state.glob.walker.deinit(true);
this.child_state = .idle;
this.state = .done;
this.next();
return;
}

const msg = std.fmt.allocPrint(bun.default_allocator, "no matches found: {s}", .{this.child_state.glob.walker.pattern}) catch bun.outOfMemory();
this.state = .{
.err = bun.shell.ShellErr{
Expand Down Expand Up @@ -2678,18 +2693,28 @@ pub const Interpreter = struct {
} else {
const size = brk: {
var total: usize = 0;
for (expanding.current_expansion_result.items) |slice| {
const last = expanding.current_expansion_result.items.len -| 1;
for (expanding.current_expansion_result.items, 0..) |slice, i| {
total += slice.len;
if (i != last) {
// for space
total += 1;
}
}
break :brk total;
};

const value = brk: {
var merged = bun.default_allocator.allocSentinel(u8, size, 0) catch bun.outOfMemory();
var i: usize = 0;
for (expanding.current_expansion_result.items) |slice| {
const last = expanding.current_expansion_result.items.len -| 1;
for (expanding.current_expansion_result.items, 0..) |slice, j| {
@memcpy(merged[i .. i + slice.len], slice[0..slice.len]);
i += slice.len;
if (j != last) {
merged[i] = ' ';
i += 1;
}
}
break :brk merged;
};
Expand Down
89 changes: 73 additions & 16 deletions src/shell/shell.zig
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,50 @@ pub const AST = struct {

pub const Tag = enum(u8) { simple, compound };

pub fn merge(this: Atom, right: Atom, allocator: Allocator) !Atom {
if (this == .simple and right == .simple) {
var atoms = try allocator.alloc(SimpleAtom, 2);
atoms[0] = this.simple;
atoms[1] = right.simple;
return .{ .compound = .{
.atoms = atoms,
.brace_expansion_hint = this.simple == .brace_begin or this.simple == .brace_end or right.simple == .brace_begin or right.simple == .brace_end,
.glob_hint = this.simple == .asterisk or this.simple == .double_asterisk or right.simple == .asterisk or right.simple == .double_asterisk,
} };
}

if (this == .compound and right == .compound) {
var atoms = try allocator.alloc(SimpleAtom, this.compound.atoms.len + right.compound.atoms.len);
@memcpy(atoms[0..this.compound.atoms.len], this.compound.atoms);
@memcpy(atoms[this.compound.atoms.len .. this.compound.atoms.len + right.compound.atoms.len], right.compound.atoms);
return .{ .compound = .{
.atoms = atoms,
.brace_expansion_hint = this.compound.brace_expansion_hint or right.compound.brace_expansion_hint,
.glob_hint = this.compound.glob_hint or right.compound.glob_hint,
} };
}

if (this == .simple) {
var atoms = try allocator.alloc(SimpleAtom, 1 + right.compound.atoms.len);
atoms[0] = this.simple;
@memcpy(atoms[1 .. right.compound.atoms.len + 1], right.compound.atoms);
return .{ .compound = .{
.atoms = atoms,
.brace_expansion_hint = this.simple == .brace_begin or this.simple == .brace_end or right.compound.brace_expansion_hint,
.glob_hint = this.simple == .asterisk or this.simple == .double_asterisk or right.compound.glob_hint,
} };
}

var atoms = try allocator.alloc(SimpleAtom, 1 + this.compound.atoms.len);
@memcpy(atoms[0..this.compound.atoms.len], this.compound.atoms);
atoms[this.compound.atoms.len] = right.simple;
return .{ .compound = .{
.atoms = atoms,
.brace_expansion_hint = right.simple == .brace_begin or right.simple == .brace_end or this.compound.brace_expansion_hint,
.glob_hint = right.simple == .asterisk or right.simple == .double_asterisk or this.compound.glob_hint,
} };
}

pub fn atomsLen(this: *const Atom) u32 {
return switch (this.*) {
.simple => 1,
Expand Down Expand Up @@ -1104,7 +1148,7 @@ pub const Parser = struct {

fn expectIfClauseTextToken(self: *Parser, comptime if_clause_token: @TypeOf(.EnumLiteral)) Token {
const tagname = comptime extractIfClauseTextToken(if_clause_token);
std.debug.assert(@as(TokenTag, self.peek()) == .Text);
if (bun.Environment.allow_assert) std.debug.assert(@as(TokenTag, self.peek()) == .Text);
if (self.peek() == .Text and
self.delimits(self.peek_n(1)) and
std.mem.eql(u8, self.text(self.peek().Text), tagname))
Expand All @@ -1113,7 +1157,7 @@ pub const Parser = struct {
_ = self.expect_delimit();
return tok;
}
unreachable;
@panic("Expected: " ++ @tagName(if_clause_token));
}

fn isIfClauseTextToken(self: *Parser, comptime if_clause_token: @TypeOf(.EnumLiteral)) bool {
Expand Down Expand Up @@ -1500,7 +1544,7 @@ pub const Parser = struct {
}

if (eq_idx == txt.len - 1) {
if (self.peek() == .Delimit) {
if (self.delimits(self.peek())) {
_ = self.expect_delimit();
break :var_decl .{
.label = label,
Expand All @@ -1518,10 +1562,25 @@ pub const Parser = struct {
}

const txt_value = txt[eq_idx + 1 .. txt.len];
_ = self.expect_delimit();
if (self.delimits(self.peek())) {
_ = self.expect_delimit();
break :var_decl .{
.label = label,
.value = .{ .simple = .{ .Text = txt_value } },
};
}

const right = try self.parse_atom() orelse {
try self.add_error("Expected an atom", .{});
return ParseError.Expected;
};
const left: AST.Atom = .{
.simple = .{ .Text = txt_value },
};
const merged = try AST.Atom.merge(left, right, self.alloc);
break :var_decl .{
.label = label,
.value = .{ .simple = .{ .Text = txt_value } },
.value = merged,
};
}
break :var_decl null;
Expand Down Expand Up @@ -1680,7 +1739,7 @@ pub const Parser = struct {
return switch (atoms.items.len) {
0 => null,
1 => {
std.debug.assert(atoms.capacity == 1);
if (bun.Environment.allow_assert) std.debug.assert(atoms.capacity == 1);
return AST.Atom.new_simple(atoms.items[0]);
},
else => .{ .compound = .{
Expand Down Expand Up @@ -1713,34 +1772,32 @@ pub const Parser = struct {
}

fn expect(self: *Parser, toktag: TokenTag) Token {
std.debug.assert(toktag == @as(TokenTag, self.peek()));
if (bun.Environment.allow_assert) std.debug.assert(toktag == @as(TokenTag, self.peek()));
if (self.check(toktag)) {
return self.advance();
}
unreachable;
@panic("Unexpected token");
}

fn expect_any(self: *Parser, toktags: []const TokenTag) Token {
// std.debug.assert(toktag == @as(TokenTag, self.peek()));

const peeked = self.peek();
for (toktags) |toktag| {
if (toktag == @as(TokenTag, peeked)) return self.advance();
}

unreachable;
@panic("Unexpected token");
}

fn delimits(self: *Parser, tok: Token) bool {
return tok == .Delimit or tok == .Semicolon or tok == .Semicolon or tok == .Eof or tok == .Newline or (self.inside_subshell != null and tok == self.inside_subshell.?.closing_tok());
}

fn expect_delimit(self: *Parser) Token {
std.debug.assert(self.delimits(self.peek()));
if (bun.Environment.allow_assert) std.debug.assert(self.delimits(self.peek()));
if (self.check(.Delimit) or self.check(.Semicolon) or self.check(.Newline) or self.check(.Eof) or (self.inside_subshell != null and self.check(self.inside_subshell.?.closing_tok()))) {
return self.advance();
}
unreachable;
@panic("Expected a delimiter token");
}

fn match_if_clausetok(self: *Parser, toktag: IfClauseTok) bool {
Expand Down Expand Up @@ -2649,6 +2706,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
.Comma,
.BraceEnd,
.CmdSubstEnd,
.Asterisk,
=> true,

.Pipe,
Expand All @@ -2657,7 +2715,6 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
.DoubleAmpersand,
.Redirect,
.Dollar,
.Asterisk,
.DoubleAsterisk,
.Eq,
.Semicolon,
Expand Down Expand Up @@ -3383,7 +3440,7 @@ pub fn ShellCharIter(comptime encoding: StringEncoding) type {
else => return .{ .char = char, .escaped = false },
}
},
else => unreachable,
.Single => unreachable,
}

return .{ .char = char, .escaped = true };
Expand Down Expand Up @@ -4397,7 +4454,7 @@ pub const TestingAPIs = struct {

const script_ast = Interpreter.parse(&arena, script.items[0..], jsobjs.items[0..], jsstrings.items[0..], &out_parser, &out_lex_result) catch |err| {
if (err == ParseError.Lex) {
std.debug.assert(out_lex_result != null);
if (bun.Environment.allow_assert) std.debug.assert(out_lex_result != null);
const str = out_lex_result.?.combineErrors(arena.allocator());
globalThis.throwPretty("{s}", .{str});
return .undefined;
Expand Down
41 changes: 27 additions & 14 deletions test/js/bun/shell/bunshell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,22 +352,35 @@ describe("bunshell", () => {
});

describe("glob expansion", () => {
test("No matches should fail", async () => {
// Issue #8403: https://github.com/oven-sh/bun/issues/8403
await TestBuilder.command`ls *.sdfljsfsdf`.exitCode(1).stderr("bun: no matches found: *.sdfljsfsdf\n").run();
});
// Issue #8403: https://github.com/oven-sh/bun/issues/8403
TestBuilder.command`ls *.sdfljsfsdf`
.exitCode(1)
.stderr("bun: no matches found: *.sdfljsfsdf\n")
.runAsTest("No matches should fail");

TestBuilder.command`FOO=*.lolwut; echo $FOO`
.stdout("*.lolwut\n")
.runAsTest("No matches in assignment position should print out pattern");

test("Should work with a different cwd", async () => {
TestBuilder.command`FOO=hi*; echo $FOO`
.ensureTempDir()
.stdout("hi*\n")
.runAsTest("Trailing asterisk with no matches");

TestBuilder.command`touch hihello; touch hifriends; FOO=hi*; echo $FOO`
.ensureTempDir()
.stdout(s => expect(s).toBeOneOf(["hihello hifriends\n", "hifriends hihello\n"]))
.runAsTest("Trailing asterisk with matches, inline");

TestBuilder.command`ls *.js`
// Calling `ensureTempDir()` changes the cwd here
await TestBuilder.command`ls *.js`
.ensureTempDir()
.file("foo.js", "foo")
.file("bar.js", "bar")
.stdout(out => {
expect(sortedShellOutput(out)).toEqual(sortedShellOutput("foo.js\nbar.js\n"));
})
.run();
});
.ensureTempDir()
.file("foo.js", "foo")
.file("bar.js", "bar")
.stdout(out => {
expect(sortedShellOutput(out)).toEqual(sortedShellOutput("foo.js\nbar.js\n"));
})
.runAsTest("Should work with a different cwd");
});

describe("brace expansion", () => {
Expand Down

0 comments on commit 0e24db0

Please sign in to comment.