diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index c56fb76dc0bbb..d51a1e1609216 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -18,6 +18,7 @@ //! use undefined memory. const std = @import("std"); const builtin = @import("builtin"); +const string = []const u8; const bun = @import("root").bun; const os = std.os; const Arena = std.heap.ArenaAllocator; @@ -504,6 +505,7 @@ pub const CowEnvMap = Cow(EnvMap, struct { pub const EnvMap = struct { map: MapType, + pub const Iterator = MapType.Iterator; const MapType = std.ArrayHashMap(EnvStr, EnvStr, struct { @@ -3909,7 +3911,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, @@ -3921,7 +3925,8 @@ pub const Interpreter = struct { rm: Rm, mv: Mv, ls: Ls, - }, + exit: Exit, + }; const Result = @import("../result.zig").Result; @@ -3937,6 +3942,7 @@ pub const Interpreter = struct { rm, mv, ls, + exit, pub fn parentType(this: Kind) type { _ = this; @@ -3955,6 +3961,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", }; } @@ -3971,6 +3978,7 @@ pub const Interpreter = struct { .rm => "rm", .mv => "mv", .ls => "ls", + .exit => "exit", }; } @@ -4132,6 +4140,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_), }; } @@ -4201,26 +4210,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{ @@ -4237,36 +4226,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, + }); }, } @@ -8669,6 +8632,84 @@ 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: ExitCode = std.fmt.parseInt(u8, first_arg, 10) catch |err| switch (err) { + error.Overflow => @intCast((std.fmt.parseInt(usize, first_arg, 10) catch return this.fail("exit: numeric argument required")) % 256), + error.InvalidCharacter => return this.fail("exit: numeric argument required"), + }; + this.bltn.done(exit_code); + return Maybe(void).success; + }, + else => { + return this.fail("exit: too many arguments"); + }, + } + } + + fn fail(this: *Exit, msg: string) Maybe(void) { + 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 { + switch (this.state) { + .idle => unreachable, + .waiting_io => { + return; + }, + .err => { + this.bltn.done(1); + return; + }, + .done => { + 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 @@ -9677,6 +9718,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 0000000000000..592b885f28a4e --- /dev/null +++ b/test/js/bun/shell/commands/exit.test.ts @@ -0,0 +1,22 @@ +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"); + + TestBuilder.command`exit 62757836`.exitCode(204).runAsTest("exit code wraps u8"); + + // prettier-ignore + TestBuilder.command`exit abc`.exitCode(1).stderr("exit: numeric argument required").runAsTest("numeric argument required"); +});