diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index b5278386f4..c65a3d758f 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -3788,7 +3788,9 @@ pub const Interpreter = struct { args_slice: ?[]const [:0]const u8 = null, cwd: bun.FileDescriptor, - impl: union(Kind) { + impl: RealImpl, + + const RealImpl = union(Kind) { cat: Cat, touch: Touch, mkdir: Mkdir, @@ -3800,7 +3802,8 @@ pub const Interpreter = struct { rm: Rm, mv: Mv, ls: Ls, - }, + exit: Exit, + }; const Result = @import("../result.zig").Result; @@ -3816,6 +3819,7 @@ pub const Interpreter = struct { rm, mv, ls, + exit, pub fn parentType(this: Kind) type { _ = this; @@ -3834,6 +3838,7 @@ pub const Interpreter = struct { .rm => "usage: rm [-f | -i] [-dIPRrvWx] file ...\n unlink [--] file\n", .mv => "usage: mv [-f | -i | -n] [-hv] source target\n mv [-f | -i | -n] [-v] source ... directory\n", .ls => "usage: ls [-@ABCFGHILOPRSTUWabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]\n", + .exit => "usage: exit [n]\n", }; } @@ -3850,6 +3855,7 @@ pub const Interpreter = struct { .rm => "rm", .mv => "mv", .ls => "ls", + .exit => "exit", }; } @@ -4011,6 +4017,7 @@ pub const Interpreter = struct { .pwd => this.callImplWithType(Pwd, Ret, "pwd", field, args_), .mv => this.callImplWithType(Mv, Ret, "mv", field, args_), .ls => this.callImplWithType(Ls, Ret, "ls", field, args_), + .exit => this.callImplWithType(Exit, Ret, "exit", field, args_), }; } @@ -4080,26 +4087,6 @@ pub const Interpreter = struct { }; switch (kind) { - .cat => { - cmd.exec.bltn.impl = .{ - .cat = Cat{ .bltn = &cmd.exec.bltn }, - }; - }, - .touch => { - cmd.exec.bltn.impl = .{ - .touch = Touch{ .bltn = &cmd.exec.bltn }, - }; - }, - .mkdir => { - cmd.exec.bltn.impl = .{ - .mkdir = Mkdir{ .bltn = &cmd.exec.bltn }, - }; - }, - .@"export" => { - cmd.exec.bltn.impl = .{ - .@"export" = Export{ .bltn = &cmd.exec.bltn }, - }; - }, .rm => { cmd.exec.bltn.impl = .{ .rm = Rm{ @@ -4116,36 +4103,10 @@ pub const Interpreter = struct { }, }; }, - .cd => { - cmd.exec.bltn.impl = .{ - .cd = Cd{ - .bltn = &cmd.exec.bltn, - }, - }; - }, - .which => { - cmd.exec.bltn.impl = .{ - .which = Which{ - .bltn = &cmd.exec.bltn, - }, - }; - }, - .pwd => { - cmd.exec.bltn.impl = .{ - .pwd = Pwd{ .bltn = &cmd.exec.bltn }, - }; - }, - .mv => { - cmd.exec.bltn.impl = .{ - .mv = Mv{ .bltn = &cmd.exec.bltn }, - }; - }, - .ls => { - cmd.exec.bltn.impl = .{ - .ls = Ls{ - .bltn = &cmd.exec.bltn, - }, - }; + inline else => |tag| { + cmd.exec.bltn.impl = @unionInit(RealImpl, @tagName(tag), .{ + .bltn = &cmd.exec.bltn, + }); }, } @@ -8525,6 +8486,80 @@ pub const Interpreter = struct { } }; }; + + pub const Exit = struct { + bltn: *Builtin, + state: enum { + idle, + waiting_io, + err, + done, + } = .idle, + + pub fn start(this: *Exit) Maybe(void) { + const args = this.bltn.argsSlice(); + switch (args.len) { + 0 => { + this.bltn.done(0); + return Maybe(void).success; + }, + 1 => { + const first_arg = args[0][0..std.mem.len(args[0]) :0]; + const exit_code = std.fmt.parseInt(ExitCode, first_arg, 10) catch { + @panic("TODO"); + }; + this.bltn.done(exit_code); + return Maybe(void).success; + }, + else => { + const msg = "exit: too many arguments"; + if (this.bltn.stderr.needsIO()) { + this.state = .waiting_io; + this.bltn.stderr.enqueue(this, msg); + return Maybe(void).success; + } + _ = this.bltn.writeNoIO(.stderr, msg); + this.bltn.done(1); + return Maybe(void).success; + }, + } + } + + pub fn next(this: *Exit) void { + while (!(this.state == .err or this.state == .done)) { + switch (this.state) { + .waiting_io => return, + .idle, .done, .err => unreachable, + } + } + if (this.state == .done) { + this.bltn.done(1); + return; + } + if (this.state == .err) { + this.bltn.done(1); + return; + } + } + + pub fn onIOWriterChunk(this: *Exit, _: usize, maybe_e: ?JSC.SystemError) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.state == .waiting_io); + } + if (maybe_e) |e| { + defer e.deref(); + this.state = .err; + this.next(); + return; + } + this.state = .done; + this.next(); + } + + pub fn deinit(this: *Exit) void { + _ = this; + } + }; }; /// This type is reference counted, but deinitialization is queued onto the event loop @@ -9519,6 +9554,7 @@ pub const IOWriterChildPtr = struct { Interpreter.Builtin.Touch, Interpreter.Builtin.Touch.ShellTouchOutputTask, Interpreter.Builtin.Cat, + Interpreter.Builtin.Exit, shell.subproc.PipeReader.CapturedWriter, }); diff --git a/test/js/bun/shell/commands/exit.test.ts b/test/js/bun/shell/commands/exit.test.ts new file mode 100644 index 0000000000..08885b1d1f --- /dev/null +++ b/test/js/bun/shell/commands/exit.test.ts @@ -0,0 +1,17 @@ +import { $ } from "bun"; +import { describe, test, expect } from "bun:test"; +import { TestBuilder } from "../test_builder"; +import { sortedShellOutput } from "../util"; +import { join } from "path"; + +describe("exit", async () => { + TestBuilder.command`exit`.exitCode(0).runAsTest("works"); + + describe("argument sets exit code", async () => { + for (const arg of [0, 2, 11]) { + TestBuilder.command`exit ${arg}`.exitCode(arg).runAsTest(`${arg}`); + } + }); + + TestBuilder.command`exit 3 5`.exitCode(1).stderr("exit: too many arguments").runAsTest("too many arguments"); +});