diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig
index fe289ed558..4f54bb7b20 100644
--- a/src/bun.js/api/bun/subprocess.zig
+++ b/src/bun.js/api/bun/subprocess.zig
@@ -1,7 +1,6 @@
//! The Subprocess object is returned by `Bun.spawn`. This file also holds the
//! code for `Bun.spawnSync`
-const Subprocess = @This();
-const MaxBuf = @import("../../MaxBuf.zig");
+
pub usingnamespace JSC.Codegen.JSSubprocess;
pub usingnamespace bun.NewRefCounted(@This(), deinit, null);
@@ -1851,7 +1850,7 @@ fn getArgv0(globalThis: *JSC.JSGlobalObject, PATH: []const u8, cwd: []const u8,
if (PATH_to_use.len == 0) {
actual_argv0 = try allocator.dupeZ(u8, argv0_to_use);
} else {
- const resolved = Which.which(path_buf, PATH_to_use, cwd, argv0_to_use) orelse {
+ const resolved = which(path_buf, PATH_to_use, cwd, argv0_to_use) orelse {
return throwCommandNotFound(globalThis, argv0_to_use);
};
actual_argv0 = try allocator.dupeZ(u8, resolved);
@@ -2678,7 +2677,7 @@ const Allocator = std.mem.Allocator;
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
-const Which = @import("../../../which.zig");
+const which = bun.which;
const Async = bun.Async;
const IPC = @import("../../ipc.zig");
const uws = bun.uws;
@@ -2694,3 +2693,6 @@ const Process = bun.posix.spawn.Process;
const WaiterThread = bun.posix.spawn.WaiterThread;
const Stdio = bun.spawn.Stdio;
const StdioResult = if (Environment.isWindows) bun.spawn.WindowsSpawnResult.StdioResult else ?bun.FileDescriptor;
+
+const Subprocess = @This();
+pub const MaxBuf = bun.io.MaxBuf;
diff --git a/src/bun.js/ipc.zig b/src/bun.js/ipc.zig
index 05d20587f6..3e003bb3eb 100644
--- a/src/bun.js/ipc.zig
+++ b/src/bun.js/ipc.zig
@@ -1,4 +1,4 @@
-const uws = @import("../deps/uws.zig");
+const uws = bun.uws;
const bun = @import("root").bun;
const Environment = bun.Environment;
const Global = bun.Global;
diff --git a/src/bun.js/MaxBuf.zig b/src/io/MaxBuf.zig
similarity index 97%
rename from src/bun.js/MaxBuf.zig
rename to src/io/MaxBuf.zig
index d08f0546ff..b6b545cfde 100644
--- a/src/bun.js/MaxBuf.zig
+++ b/src/io/MaxBuf.zig
@@ -1,12 +1,3 @@
-const Subprocess = @import("api/bun/subprocess.zig");
-const MaxBuf = @This();
-const bun = @import("root").bun;
-const std = @import("std");
-
-pub const Kind = enum {
- stdout,
- stderr,
-};
// null after subprocess finalize
owned_by_subprocess: ?*Subprocess,
// null after pipereader finalize
@@ -82,3 +73,13 @@ pub fn onReadBytes(this: *MaxBuf, bytes: u64) void {
}
}
}
+
+pub const Kind = enum {
+ stdout,
+ stderr,
+};
+
+const bun = @import("root").bun;
+const std = @import("std");
+const Subprocess = bun.JSC.Subprocess;
+const MaxBuf = @This();
diff --git a/src/io/PipeReader.zig b/src/io/PipeReader.zig
index d65e489bd0..3cb7dada3f 100644
--- a/src/io/PipeReader.zig
+++ b/src/io/PipeReader.zig
@@ -1,16 +1,3 @@
-const bun = @import("root").bun;
-const std = @import("std");
-const uv = bun.windows.libuv;
-const Source = @import("./source.zig").Source;
-
-const ReadState = @import("./pipes.zig").ReadState;
-const FileType = @import("./pipes.zig").FileType;
-const MaxBuf = @import("../bun.js/MaxBuf.zig");
-
-const PollOrFd = @import("./pipes.zig").PollOrFd;
-
-const Async = bun.Async;
-
// This is a runtime type instead of comptime due to bugs in Zig.
// https://github.com/ziglang/zig/issues/18664
const BufferedReaderVTable = struct {
@@ -1139,3 +1126,16 @@ else if (bun.Environment.isWindows)
WindowsBufferedReader
else
@compileError("Unsupported platform");
+
+const bun = @import("root").bun;
+const std = @import("std");
+const uv = bun.windows.libuv;
+const Source = @import("./source.zig").Source;
+
+const ReadState = @import("./pipes.zig").ReadState;
+const FileType = @import("./pipes.zig").FileType;
+const MaxBuf = @import("./MaxBuf.zig");
+
+const PollOrFd = @import("./pipes.zig").PollOrFd;
+
+const Async = bun.Async;
diff --git a/src/io/io.zig b/src/io/io.zig
index dc46c10658..12f14b11ba 100644
--- a/src/io/io.zig
+++ b/src/io/io.zig
@@ -694,3 +694,4 @@ pub const WriteStatus = @import("./PipeWriter.zig").WriteStatus;
pub const StreamingWriter = @import("./PipeWriter.zig").StreamingWriter;
pub const StreamBuffer = @import("./PipeWriter.zig").StreamBuffer;
pub const FileType = @import("./pipes.zig").FileType;
+pub const MaxBuf = @import("./MaxBuf.zig");
diff --git a/src/shell/Builtin.zig b/src/shell/Builtin.zig
new file mode 100644
index 0000000000..09764b3ac3
--- /dev/null
+++ b/src/shell/Builtin.zig
@@ -0,0 +1,707 @@
+kind: Kind,
+stdin: BuiltinIO.Input,
+stdout: BuiltinIO.Output,
+stderr: BuiltinIO.Output,
+exit_code: ?ExitCode = null,
+
+export_env: *EnvMap,
+cmd_local_env: *EnvMap,
+
+arena: *bun.ArenaAllocator,
+/// The following are allocated with the above arena
+args: *const std.ArrayList(?[*:0]const u8),
+args_slice: ?[]const [:0]const u8 = null,
+cwd: bun.FileDescriptor,
+
+impl: Impl,
+
+pub const Impl = union(Kind) {
+ cat: Cat,
+ touch: Touch,
+ mkdir: Mkdir,
+ @"export": Export,
+ cd: Cd,
+ echo: Echo,
+ pwd: Pwd,
+ which: Which,
+ rm: Rm,
+ mv: Mv,
+ ls: Ls,
+ exit: Exit,
+ true: True,
+ false: False,
+ yes: Yes,
+ seq: Seq,
+ dirname: Dirname,
+ basename: Basename,
+ cp: Cp,
+};
+
+pub const Result = @import("../result.zig").Result;
+
+// Note: this enum uses @tagName, choose wisely!
+pub const Kind = enum {
+ cat,
+ touch,
+ mkdir,
+ @"export",
+ cd,
+ echo,
+ pwd,
+ which,
+ rm,
+ mv,
+ ls,
+ exit,
+ true,
+ false,
+ yes,
+ seq,
+ dirname,
+ basename,
+ cp,
+
+ pub const DISABLED_ON_POSIX: []const Kind = &.{ .cat, .cp };
+
+ pub fn parentType(this: Kind) type {
+ _ = this;
+ }
+
+ pub fn usageString(this: Kind) []const u8 {
+ return switch (this) {
+ .cat => "usage: cat [-belnstuv] [file ...]\n",
+ .touch => "usage: touch [-A [-][[hh]mm]SS] [-achm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]]\n [-d YYYY-MM-DDThh:mm:SS[.frac][tz]] file ...\n",
+ .mkdir => "usage: mkdir [-pv] [-m mode] directory_name ...\n",
+ .@"export" => "",
+ .cd => "",
+ .echo => "",
+ .pwd => "",
+ .which => "",
+ .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",
+ .true => "",
+ .false => "",
+ .yes => "usage: yes [expletive]\n",
+ .seq => "usage: seq [-w] [-f format] [-s string] [-t string] [first [incr]] last\n",
+ .dirname => "usage: dirname string\n",
+ .basename => "usage: basename string\n",
+ .cp => "usage: cp [-R [-H | -L | -P]] [-fi | -n] [-aclpsvXx] source_file target_file\n cp [-R [-H | -L | -P]] [-fi | -n] [-aclpsvXx] source_file ... target_directory\n",
+ };
+ }
+
+ fn forceEnableOnPosix() bool {
+ return bun.getRuntimeFeatureFlag("BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS");
+ }
+
+ pub fn fromStr(str: []const u8) ?Builtin.Kind {
+ const result = std.meta.stringToEnum(Builtin.Kind, str) orelse return null;
+ if (bun.Environment.isWindows) return result;
+ if (forceEnableOnPosix()) return result;
+ inline for (Builtin.Kind.DISABLED_ON_POSIX) |disabled| {
+ if (disabled == result) {
+ log("{s} builtin disabled on posix for now", .{@tagName(disabled)});
+ return null;
+ }
+ }
+ return result;
+ }
+};
+
+pub const BuiltinIO = struct {
+ /// in the case of array buffer we simply need to write to the pointer
+ /// in the case of blob, we write to the file descriptor
+ pub const Output = union(enum) {
+ fd: struct { writer: *IOWriter, captured: ?*bun.ByteList = null },
+ /// array list not owned by this type
+ buf: std.ArrayList(u8),
+ arraybuf: ArrayBuf,
+ blob: *Blob,
+ ignore,
+
+ const FdOutput = struct {
+ writer: *IOWriter,
+ captured: ?*bun.ByteList = null,
+
+ // pub fn
+ };
+
+ pub fn ref(this: *Output) *Output {
+ switch (this.*) {
+ .fd => {
+ this.fd.writer.ref();
+ },
+ .blob => this.blob.ref(),
+ else => {},
+ }
+ return this;
+ }
+
+ pub fn deref(this: *Output) void {
+ switch (this.*) {
+ .fd => {
+ this.fd.writer.deref();
+ },
+ .blob => this.blob.deref(),
+ else => {},
+ }
+ }
+
+ pub fn needsIO(this: *Output) ?OutputNeedsIOSafeGuard {
+ return switch (this.*) {
+ .fd => OutputNeedsIOSafeGuard{
+ .__i_know_what_i_am_doing_it_needs_io_yes = 0,
+ },
+ else => null,
+ };
+ }
+
+ /// You must check that `.needsIO() == true` before calling this!
+ /// e.g.
+ ///
+ /// ```zig
+ /// if (this.stderr.neesdIO()) |safeguard| {
+ /// this.bltn.stderr.enqueueFmtBltn(this, .cd, fmt, args, safeguard);
+ /// }
+ /// ```
+ pub fn enqueueFmtBltn(
+ this: *@This(),
+ ptr: anytype,
+ comptime kind: ?Interpreter.Builtin.Kind,
+ comptime fmt_: []const u8,
+ args: anytype,
+ _: OutputNeedsIOSafeGuard,
+ ) void {
+ this.fd.writer.enqueueFmtBltn(ptr, this.fd.captured, kind, fmt_, args);
+ }
+
+ pub fn enqueue(this: *@This(), ptr: anytype, buf: []const u8, _: OutputNeedsIOSafeGuard) void {
+ this.fd.writer.enqueue(ptr, this.fd.captured, buf);
+ }
+ };
+
+ pub const Input = union(enum) {
+ fd: *IOReader,
+ /// array list not ownedby this type
+ buf: std.ArrayList(u8),
+ arraybuf: ArrayBuf,
+ blob: *Blob,
+ ignore,
+
+ pub fn ref(this: *Input) *Input {
+ switch (this.*) {
+ .fd => {
+ this.fd.ref();
+ },
+ .blob => this.blob.ref(),
+ else => {},
+ }
+ return this;
+ }
+
+ pub fn deref(this: *Input) void {
+ switch (this.*) {
+ .fd => {
+ this.fd.deref();
+ },
+ .blob => this.blob.deref(),
+ else => {},
+ }
+ }
+
+ pub fn needsIO(this: *Input) bool {
+ return switch (this.*) {
+ .fd => true,
+ else => false,
+ };
+ }
+ };
+
+ const ArrayBuf = struct {
+ buf: JSC.ArrayBuffer.Strong,
+ i: u32 = 0,
+ };
+
+ const Blob = struct {
+ ref_count: usize = 1,
+ blob: bun.JSC.WebCore.Blob,
+ pub usingnamespace bun.NewRefCounted(Blob, _deinit, null);
+
+ fn _deinit(this: *Blob) void {
+ this.blob.deinit();
+ bun.destroy(this);
+ }
+ };
+};
+
+pub fn argsSlice(this: *Builtin) []const [*:0]const u8 {
+ const args_raw = this.args.items[1..];
+ const args_len = std.mem.indexOfScalar(?[*:0]const u8, args_raw, null) orelse @panic("bad");
+ if (args_len == 0)
+ return &[_][*:0]const u8{};
+
+ const args_ptr = args_raw.ptr;
+ return @as([*][*:0]const u8, @ptrCast(args_ptr))[0..args_len];
+}
+
+pub inline fn callImpl(this: *Builtin, comptime Ret: type, comptime field: []const u8, args_: anytype) Ret {
+ return switch (this.kind) {
+ .cat => this.callImplWithType(Cat, Ret, "cat", field, args_),
+ .touch => this.callImplWithType(Touch, Ret, "touch", field, args_),
+ .mkdir => this.callImplWithType(Mkdir, Ret, "mkdir", field, args_),
+ .@"export" => this.callImplWithType(Export, Ret, "export", field, args_),
+ .echo => this.callImplWithType(Echo, Ret, "echo", field, args_),
+ .cd => this.callImplWithType(Cd, Ret, "cd", field, args_),
+ .which => this.callImplWithType(Which, Ret, "which", field, args_),
+ .rm => this.callImplWithType(Rm, Ret, "rm", field, args_),
+ .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_),
+ .true => this.callImplWithType(True, Ret, "true", field, args_),
+ .false => this.callImplWithType(False, Ret, "false", field, args_),
+ .yes => this.callImplWithType(Yes, Ret, "yes", field, args_),
+ .seq => this.callImplWithType(Seq, Ret, "seq", field, args_),
+ .dirname => this.callImplWithType(Dirname, Ret, "dirname", field, args_),
+ .basename => this.callImplWithType(Basename, Ret, "basename", field, args_),
+ .cp => this.callImplWithType(Cp, Ret, "cp", field, args_),
+ };
+}
+
+fn callImplWithType(this: *Builtin, comptime BuiltinImpl: type, comptime Ret: type, comptime union_field: []const u8, comptime field: []const u8, args_: anytype) Ret {
+ const self = &@field(this.impl, union_field);
+ const args = brk: {
+ var args: std.meta.ArgsTuple(@TypeOf(@field(BuiltinImpl, field))) = undefined;
+ args[0] = self;
+
+ var i: usize = 1;
+ inline for (args_) |a| {
+ args[i] = a;
+ i += 1;
+ }
+
+ break :brk args;
+ };
+ return @call(.auto, @field(BuiltinImpl, field), args);
+}
+
+pub inline fn allocator(this: *Builtin) Allocator {
+ return this.parentCmd().base.interpreter.allocator;
+}
+
+pub fn init(
+ cmd: *Cmd,
+ interpreter: *Interpreter,
+ kind: Kind,
+ arena: *bun.ArenaAllocator,
+ node: *const ast.Cmd,
+ args: *const std.ArrayList(?[*:0]const u8),
+ export_env: *EnvMap,
+ cmd_local_env: *EnvMap,
+ cwd: bun.FileDescriptor,
+ io: *IO,
+ comptime in_cmd_subst: bool,
+) CoroutineResult {
+ const stdin: BuiltinIO.Input = switch (io.stdin) {
+ .fd => |fd| .{ .fd = fd.refSelf() },
+ .ignore => .ignore,
+ };
+ const stdout: BuiltinIO.Output = switch (io.stdout) {
+ .fd => |val| .{ .fd = .{ .writer = val.writer.refSelf(), .captured = val.captured } },
+ .pipe => .{ .buf = std.ArrayList(u8).init(bun.default_allocator) },
+ .ignore => .ignore,
+ };
+ const stderr: BuiltinIO.Output = switch (io.stderr) {
+ .fd => |val| .{ .fd = .{ .writer = val.writer.refSelf(), .captured = val.captured } },
+ .pipe => .{ .buf = std.ArrayList(u8).init(bun.default_allocator) },
+ .ignore => .ignore,
+ };
+
+ cmd.exec = .{
+ .bltn = Builtin{
+ .kind = kind,
+ .stdin = stdin,
+ .stdout = stdout,
+ .stderr = stderr,
+ .exit_code = null,
+ .arena = arena,
+ .args = args,
+ .export_env = export_env,
+ .cmd_local_env = cmd_local_env,
+ .cwd = cwd,
+ .impl = undefined,
+ },
+ };
+
+ switch (kind) {
+ .rm => {
+ cmd.exec.bltn.impl = .{
+ .rm = Rm{
+ .opts = .{},
+ },
+ };
+ },
+ .echo => {
+ cmd.exec.bltn.impl = .{
+ .echo = Echo{
+ .output = std.ArrayList(u8).init(arena.allocator()),
+ },
+ };
+ },
+ inline else => |tag| {
+ cmd.exec.bltn.impl = @unionInit(Impl, @tagName(tag), .{});
+ },
+ }
+
+ if (node.redirect_file) |file| brk: {
+ if (comptime in_cmd_subst) {
+ if (node.redirect.stdin) {
+ stdin = .ignore;
+ }
+
+ if (node.redirect.stdout) {
+ stdout = .ignore;
+ }
+
+ if (node.redirect.stderr) {
+ stdout = .ignore;
+ }
+
+ break :brk;
+ }
+
+ switch (file) {
+ .atom => {
+ if (cmd.redirection_file.items.len == 0) {
+ cmd.writeFailingError("bun: ambiguous redirect: at `{s}`\n", .{@tagName(kind)});
+ return .yield;
+ }
+
+ // Regular files are not pollable on linux
+ const is_pollable: bool = if (bun.Environment.isLinux) false else true;
+
+ const path = cmd.redirection_file.items[0..cmd.redirection_file.items.len -| 1 :0];
+ log("EXPANDED REDIRECT: {s}\n", .{cmd.redirection_file.items[0..]});
+ const perm = 0o666;
+ const is_nonblocking = false;
+ const flags = node.redirect.toFlags();
+ const redirfd = switch (ShellSyscall.openat(cmd.base.shell.cwd_fd, path, flags, perm)) {
+ .err => |e| {
+ cmd.writeFailingError("bun: {s}: {s}", .{ e.toShellSystemError().message, path });
+ return .yield;
+ },
+ .result => |f| f,
+ };
+ if (node.redirect.stdin) {
+ cmd.exec.bltn.stdin.deref();
+ cmd.exec.bltn.stdin = .{ .fd = IOReader.init(redirfd, cmd.base.eventLoop()) };
+ }
+ if (node.redirect.stdout) {
+ cmd.exec.bltn.stdout.deref();
+ cmd.exec.bltn.stdout = .{ .fd = .{ .writer = IOWriter.init(redirfd, .{ .pollable = is_pollable, .nonblocking = is_nonblocking }, cmd.base.eventLoop()) } };
+ }
+ if (node.redirect.stderr) {
+ cmd.exec.bltn.stderr.deref();
+ cmd.exec.bltn.stderr = .{ .fd = .{ .writer = IOWriter.init(redirfd, .{ .pollable = is_pollable, .nonblocking = is_nonblocking }, cmd.base.eventLoop()) } };
+ }
+ },
+ .jsbuf => |val| {
+ const globalObject = interpreter.event_loop.js.global;
+ if (interpreter.jsobjs[file.jsbuf.idx].asArrayBuffer(globalObject)) |buf| {
+ const arraybuf: BuiltinIO.ArrayBuf = .{ .buf = JSC.ArrayBuffer.Strong{
+ .array_buffer = buf,
+ .held = JSC.Strong.create(buf.value, globalObject),
+ }, .i = 0 };
+
+ if (node.redirect.stdin) {
+ cmd.exec.bltn.stdin.deref();
+ cmd.exec.bltn.stdin = .{ .arraybuf = arraybuf };
+ }
+
+ if (node.redirect.stdout) {
+ cmd.exec.bltn.stdout.deref();
+ cmd.exec.bltn.stdout = .{ .arraybuf = arraybuf };
+ }
+
+ if (node.redirect.stderr) {
+ cmd.exec.bltn.stderr.deref();
+ cmd.exec.bltn.stderr = .{ .arraybuf = arraybuf };
+ }
+ } else if (interpreter.jsobjs[file.jsbuf.idx].as(JSC.WebCore.Body.Value)) |body| {
+ if ((node.redirect.stdout or node.redirect.stderr) and !(body.* == .Blob and !body.Blob.needsToReadFile())) {
+ // TODO: Locked->stream -> file -> blob conversion via .toBlobIfPossible() except we want to avoid modifying the Response/Request if unnecessary.
+ cmd.base.interpreter.event_loop.js.global.throw("Cannot redirect stdout/stderr to an immutable blob. Expected a file", .{}) catch {};
+ return .yield;
+ }
+
+ var original_blob = body.use();
+ defer original_blob.deinit();
+
+ const blob: *BuiltinIO.Blob = bun.new(BuiltinIO.Blob, .{
+ .blob = original_blob.dupe(),
+ });
+
+ if (node.redirect.stdin) {
+ cmd.exec.bltn.stdin.deref();
+ cmd.exec.bltn.stdin = .{ .blob = blob };
+ }
+
+ if (node.redirect.stdout) {
+ cmd.exec.bltn.stdout.deref();
+ cmd.exec.bltn.stdout = .{ .blob = blob };
+ }
+
+ if (node.redirect.stderr) {
+ cmd.exec.bltn.stderr.deref();
+ cmd.exec.bltn.stderr = .{ .blob = blob };
+ }
+ } else if (interpreter.jsobjs[file.jsbuf.idx].as(JSC.WebCore.Blob)) |blob| {
+ if ((node.redirect.stdout or node.redirect.stderr) and !blob.needsToReadFile()) {
+ // TODO: Locked->stream -> file -> blob conversion via .toBlobIfPossible() except we want to avoid modifying the Response/Request if unnecessary.
+ cmd.base.interpreter.event_loop.js.global.throw("Cannot redirect stdout/stderr to an immutable blob. Expected a file", .{}) catch {};
+ return .yield;
+ }
+
+ const theblob: *BuiltinIO.Blob = bun.new(BuiltinIO.Blob, .{ .blob = blob.dupe() });
+
+ if (node.redirect.stdin) {
+ cmd.exec.bltn.stdin.deref();
+ cmd.exec.bltn.stdin = .{ .blob = theblob };
+ } else if (node.redirect.stdout) {
+ cmd.exec.bltn.stdout.deref();
+ cmd.exec.bltn.stdout = .{ .blob = theblob };
+ } else if (node.redirect.stderr) {
+ cmd.exec.bltn.stderr.deref();
+ cmd.exec.bltn.stderr = .{ .blob = theblob };
+ }
+ } else {
+ const jsval = cmd.base.interpreter.jsobjs[val.idx];
+ cmd.base.interpreter.event_loop.js.global.throw("Unknown JS value used in shell: {}", .{jsval.fmtString(globalObject)}) catch {};
+ return .yield;
+ }
+ },
+ }
+ } else if (node.redirect.duplicate_out) {
+ if (node.redirect.stdout) {
+ cmd.exec.bltn.stderr.deref();
+ cmd.exec.bltn.stderr = cmd.exec.bltn.stdout.ref().*;
+ }
+
+ if (node.redirect.stderr) {
+ cmd.exec.bltn.stdout.deref();
+ cmd.exec.bltn.stdout = cmd.exec.bltn.stderr.ref().*;
+ }
+ }
+
+ return .cont;
+}
+
+pub inline fn eventLoop(this: *const Builtin) JSC.EventLoopHandle {
+ return this.parentCmd().base.eventLoop();
+}
+
+pub inline fn throw(this: *const Builtin, err: *const bun.shell.ShellErr) void {
+ this.parentCmd().base.throw(err) catch {};
+}
+
+pub inline fn parentCmd(this: *const Builtin) *const Cmd {
+ const union_ptr: *const Cmd.Exec = @fieldParentPtr("bltn", this);
+ return @fieldParentPtr("exec", union_ptr);
+}
+
+pub inline fn parentCmdMut(this: *Builtin) *Cmd {
+ const union_ptr: *Cmd.Exec = @fieldParentPtr("bltn", this);
+ return @fieldParentPtr("exec", union_ptr);
+}
+
+pub fn done(this: *Builtin, exit_code: anytype) void {
+ const code: ExitCode = switch (@TypeOf(exit_code)) {
+ bun.C.E => @intFromEnum(exit_code),
+ u1, u8, u16 => exit_code,
+ comptime_int => exit_code,
+ else => @compileError("Invalid type: " ++ @typeName(@TypeOf(exit_code))),
+ };
+ this.exit_code = code;
+
+ var cmd = this.parentCmdMut();
+ log("builtin done ({s}: exit={d}) cmd to free: ({x})", .{ @tagName(this.kind), code, @intFromPtr(cmd) });
+ cmd.exit_code = this.exit_code.?;
+
+ // Aggregate output data if shell state is piped and this cmd is piped
+ if (cmd.io.stdout == .pipe and cmd.io.stdout == .pipe and this.stdout == .buf) {
+ cmd.base.shell.buffered_stdout().append(bun.default_allocator, this.stdout.buf.items[0..]) catch bun.outOfMemory();
+ }
+ // Aggregate output data if shell state is piped and this cmd is piped
+ if (cmd.io.stderr == .pipe and cmd.io.stderr == .pipe and this.stderr == .buf) {
+ cmd.base.shell.buffered_stderr().append(bun.default_allocator, this.stderr.buf.items[0..]) catch bun.outOfMemory();
+ }
+
+ cmd.parent.childDone(cmd, this.exit_code.?);
+}
+
+pub fn start(this: *Builtin) Maybe(void) {
+ switch (this.callImpl(Maybe(void), "start", .{})) {
+ .err => |e| return Maybe(void).initErr(e),
+ .result => {},
+ }
+
+ return Maybe(void).success;
+}
+
+pub fn deinit(this: *Builtin) void {
+ this.callImpl(void, "deinit", .{});
+
+ // No need to free it because it belongs to the parent cmd
+ // _ = Syscall.close(this.cwd);
+
+ this.stdout.deref();
+ this.stderr.deref();
+ this.stdin.deref();
+
+ // Parent cmd frees this
+ // this.arena.deinit();
+}
+
+/// If the stdout/stderr is supposed to be captured then get the bytelist associated with that
+pub fn stdBufferedBytelist(this: *Builtin, comptime io_kind: @Type(.enum_literal)) ?*bun.ByteList {
+ if (comptime io_kind != .stdout and io_kind != .stderr) {
+ @compileError("Bad IO" ++ @tagName(io_kind));
+ }
+
+ const io: *BuiltinIO = &@field(this, @tagName(io_kind));
+ return switch (io.*) {
+ .captured => if (comptime io_kind == .stdout) this.parentCmd().base.shell.buffered_stdout() else this.parentCmd().base.shell.buffered_stderr(),
+ else => null,
+ };
+}
+
+pub fn readStdinNoIO(this: *Builtin) []const u8 {
+ return switch (this.stdin) {
+ .arraybuf => |buf| buf.buf.slice(),
+ .buf => |buf| buf.items[0..],
+ .blob => |blob| blob.blob.sharedView(),
+ else => "",
+ };
+}
+
+/// **WARNING** You should make sure that stdout/stderr does not need IO (e.g. `.needsIO(.stderr)` is false before caling `.writeNoIO(.stderr, buf)`)
+pub fn writeNoIO(this: *Builtin, comptime io_kind: @Type(.enum_literal), buf: []const u8) Maybe(usize) {
+ if (comptime io_kind != .stdout and io_kind != .stderr) {
+ @compileError("Bad IO" ++ @tagName(io_kind));
+ }
+
+ if (buf.len == 0) return Maybe(usize).initResult(0);
+
+ var io: *BuiltinIO.Output = &@field(this, @tagName(io_kind));
+
+ switch (io.*) {
+ .fd => @panic("writeNoIO(. " ++ @tagName(io_kind) ++ ", buf) can't write to a file descriptor, did you check that needsIO(." ++ @tagName(io_kind) ++ ") was false?"),
+ .buf => {
+ log("{s} write to buf len={d} str={s}{s}\n", .{ @tagName(this.kind), buf.len, buf[0..@min(buf.len, 16)], if (buf.len > 16) "..." else "" });
+ io.buf.appendSlice(buf) catch bun.outOfMemory();
+ return Maybe(usize).initResult(buf.len);
+ },
+ .arraybuf => {
+ if (io.arraybuf.i >= io.arraybuf.buf.array_buffer.byte_len) {
+ return Maybe(usize).initErr(Syscall.Error.fromCode(bun.C.E.NOSPC, .write));
+ }
+
+ const len = buf.len;
+ if (io.arraybuf.i + len > io.arraybuf.buf.array_buffer.byte_len) {
+ // std.ArrayList(comptime T: type)
+ }
+ const write_len = if (io.arraybuf.i + len > io.arraybuf.buf.array_buffer.byte_len)
+ io.arraybuf.buf.array_buffer.byte_len - io.arraybuf.i
+ else
+ len;
+
+ const slice = io.arraybuf.buf.slice()[io.arraybuf.i .. io.arraybuf.i + write_len];
+ @memcpy(slice, buf[0..write_len]);
+ io.arraybuf.i +|= @truncate(write_len);
+ log("{s} write to arraybuf {d}\n", .{ @tagName(this.kind), write_len });
+ return Maybe(usize).initResult(write_len);
+ },
+ .blob, .ignore => return Maybe(usize).initResult(buf.len),
+ }
+}
+
+/// Error messages formatted to match bash
+pub fn taskErrorToString(this: *Builtin, comptime kind: Kind, err: anytype) []const u8 {
+ switch (@TypeOf(err)) {
+ Syscall.Error => {
+ if (err.getErrorCodeTagName()) |entry| {
+ _, const sys_errno = entry;
+ if (bun.sys.coreutils_error_map.get(sys_errno)) |message| {
+ if (err.path.len > 0) {
+ return this.fmtErrorArena(kind, "{s}: {s}\n", .{ err.path, message });
+ }
+ return this.fmtErrorArena(kind, "{s}\n", .{message});
+ }
+ }
+ return this.fmtErrorArena(kind, "unknown error {d}\n", .{err.errno});
+ },
+ JSC.SystemError => {
+ if (err.path.length() == 0) return this.fmtErrorArena(kind, "{s}\n", .{err.message.byteSlice()});
+ return this.fmtErrorArena(kind, "{s}: {s}\n", .{ err.message.byteSlice(), err.path });
+ },
+ bun.shell.ShellErr => return switch (err) {
+ .sys => this.taskErrorToString(kind, err.sys),
+ .custom => this.fmtErrorArena(kind, "{s}\n", .{err.custom}),
+ .invalid_arguments => this.fmtErrorArena(kind, "{s}\n", .{err.invalid_arguments.val}),
+ .todo => this.fmtErrorArena(kind, "{s}\n", .{err.todo}),
+ },
+ else => @compileError("Bad type: " ++ @typeName(err)),
+ }
+}
+
+pub fn fmtErrorArena(this: *Builtin, comptime kind: ?Kind, comptime fmt_: []const u8, args: anytype) []u8 {
+ const cmd_str = comptime if (kind) |k| @tagName(k) ++ ": " else "";
+ const fmt = cmd_str ++ fmt_;
+ return std.fmt.allocPrint(this.arena.allocator(), fmt, args) catch bun.outOfMemory();
+}
+
+// --- Shell Builtin Commands ---
+pub const Cat = @import("./builtin/cat.zig");
+pub const Touch = @import("./builtin/touch.zig");
+pub const Mkdir = @import("./builtin/mkdir.zig");
+pub const Export = @import("./builtin/export.zig");
+pub const Cd = @import("./builtin/cd.zig");
+pub const Ls = @import("./builtin/ls.zig");
+pub const Pwd = @import("./builtin/pwd.zig");
+pub const Echo = @import("./builtin/echo.zig");
+pub const Which = @import("./builtin/which.zig");
+pub const Rm = @import("./builtin/rm.zig");
+pub const Exit = @import("./builtin/exit.zig");
+pub const True = @import("./builtin/true.zig");
+pub const False = @import("./builtin/false.zig");
+pub const Yes = @import("./builtin/yes.zig");
+pub const Seq = @import("./builtin/seq.zig");
+pub const Dirname = @import("./builtin/dirname.zig");
+pub const Basename = @import("./builtin/basename.zig");
+pub const Cp = @import("./builtin/cp.zig");
+pub const Mv = @import("./builtin/mv.zig");
+// --- End Shell Builtin Commands ---
+
+const std = @import("std");
+const bun = @import("root").bun;
+
+const shell = bun.shell;
+const Interpreter = shell.interpret.Interpreter;
+const Builtin = Interpreter.Builtin;
+
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const ExitCode = shell.interpret.ExitCode;
+const EnvMap = shell.interpret.EnvMap;
+const log = shell.interpret.log;
+const Syscall = bun.sys;
+const IOWriter = Interpreter.IOWriter;
+const IOReader = Interpreter.IOReader;
+const OutputNeedsIOSafeGuard = shell.interpret.OutputNeedsIOSafeGuard;
+const Cmd = Interpreter.Cmd;
+const ShellSyscall = shell.interpret.ShellSyscall;
+const Allocator = std.mem.Allocator;
+const ast = shell.AST;
+const IO = shell.interpret.IO;
+const CoroutineResult = shell.interpret.CoroutineResult;
diff --git a/src/shell/EnvMap.zig b/src/shell/EnvMap.zig
new file mode 100644
index 0000000000..dbee99cce7
--- /dev/null
+++ b/src/shell/EnvMap.zig
@@ -0,0 +1,105 @@
+map: MapType,
+
+pub const Iterator = MapType.Iterator;
+
+const MapType = std.ArrayHashMap(EnvStr, EnvStr, struct {
+ pub fn hash(self: @This(), s: EnvStr) u32 {
+ _ = self;
+ if (bun.Environment.isWindows) {
+ return bun.CaseInsensitiveASCIIStringContext.hash(undefined, s.slice());
+ }
+ return std.array_hash_map.hashString(s.slice());
+ }
+ pub fn eql(self: @This(), a: EnvStr, b: EnvStr, b_index: usize) bool {
+ _ = self;
+ _ = b_index;
+ if (bun.Environment.isWindows) {
+ return bun.CaseInsensitiveASCIIStringContext.eql(undefined, a.slice(), b.slice(), undefined);
+ }
+ return std.array_hash_map.eqlString(a.slice(), b.slice());
+ }
+}, true);
+
+pub fn init(alloc: Allocator) EnvMap {
+ return .{ .map = MapType.init(alloc) };
+}
+
+pub fn initWithCapacity(alloc: Allocator, cap: usize) EnvMap {
+ var map = MapType.init(alloc);
+ map.ensureTotalCapacity(cap) catch bun.outOfMemory();
+ return .{ .map = map };
+}
+
+pub fn deinit(this: *EnvMap) void {
+ this.derefStrings();
+ this.map.deinit();
+}
+
+pub fn insert(this: *EnvMap, key: EnvStr, val: EnvStr) void {
+ const result = this.map.getOrPut(key) catch bun.outOfMemory();
+ if (!result.found_existing) {
+ key.ref();
+ } else {
+ result.value_ptr.deref();
+ }
+ val.ref();
+ result.value_ptr.* = val;
+}
+
+pub fn iterator(this: *EnvMap) MapType.Iterator {
+ return this.map.iterator();
+}
+
+pub fn clearRetainingCapacity(this: *EnvMap) void {
+ this.derefStrings();
+ this.map.clearRetainingCapacity();
+}
+
+pub fn ensureTotalCapacity(this: *EnvMap, new_capacity: usize) void {
+ this.map.ensureTotalCapacity(new_capacity) catch bun.outOfMemory();
+}
+
+/// NOTE: Make sure you deref the string when done!
+pub fn get(this: *EnvMap, key: EnvStr) ?EnvStr {
+ const val = this.map.get(key) orelse return null;
+ val.ref();
+ return val;
+}
+
+pub fn clone(this: *EnvMap) EnvMap {
+ var new: EnvMap = .{
+ .map = this.map.clone() catch bun.outOfMemory(),
+ };
+ new.refStrings();
+ return new;
+}
+
+pub fn cloneWithAllocator(this: *EnvMap, allocator: Allocator) EnvMap {
+ var new: EnvMap = .{
+ .map = this.map.cloneWithAllocator(allocator) catch bun.outOfMemory(),
+ };
+ new.refStrings();
+ return new;
+}
+
+fn refStrings(this: *EnvMap) void {
+ var iter = this.map.iterator();
+ while (iter.next()) |entry| {
+ entry.key_ptr.ref();
+ entry.value_ptr.ref();
+ }
+}
+
+fn derefStrings(this: *EnvMap) void {
+ var iter = this.map.iterator();
+ while (iter.next()) |entry| {
+ entry.key_ptr.deref();
+ entry.value_ptr.deref();
+ }
+}
+
+const EnvMap = @This();
+const bun = @import("root").bun;
+const Allocator = std.mem.Allocator;
+const std = @import("std");
+const EnvStr = bun.shell.EnvStr;
diff --git a/src/shell/EnvStr.zig b/src/shell/EnvStr.zig
new file mode 100644
index 0000000000..72cfe5b0e3
--- /dev/null
+++ b/src/shell/EnvStr.zig
@@ -0,0 +1,91 @@
+/// Environment strings need to be copied a lot
+/// So we make them reference counted
+///
+/// But sometimes we use strings that are statically allocated, or are allocated
+/// with a predetermined lifetime (e.g. strings in the AST). In that case we
+/// don't want to incur the cost of heap allocating them and refcounting them
+///
+/// So environment strings can be ref counted or borrowed slices
+pub const EnvStr = packed struct {
+ ptr: u48,
+ tag: Tag = .empty,
+ len: usize = 0,
+
+ const debug = bun.Output.scoped(.EnvStr, true);
+
+ const Tag = enum(u16) {
+ /// no value
+ empty,
+
+ /// Dealloced by reference counting
+ refcounted,
+
+ /// Memory is managed elsewhere so don't dealloc it
+ slice,
+ };
+
+ pub inline fn initSlice(str: []const u8) EnvStr {
+ if (str.len == 0)
+ // Zero length strings may have invalid pointers, leading to a bad integer cast.
+ return .{ .tag = .empty, .ptr = 0, .len = 0 };
+
+ return .{
+ .ptr = toPtr(str.ptr),
+ .tag = .slice,
+ .len = str.len,
+ };
+ }
+
+ fn toPtr(ptr_val: *const anyopaque) u48 {
+ const num: [8]u8 = @bitCast(@intFromPtr(ptr_val));
+ return @bitCast(num[0..6].*);
+ }
+
+ pub fn initRefCounted(str: []const u8) EnvStr {
+ if (str.len == 0)
+ return .{ .tag = .empty, .ptr = 0, .len = 0 };
+
+ return .{
+ .ptr = toPtr(RefCountedStr.init(str)),
+ .tag = .refcounted,
+ };
+ }
+
+ pub fn slice(this: EnvStr) []const u8 {
+ return switch (this.tag) {
+ .empty => "",
+ .slice => this.castSlice(),
+ .refcounted => this.castRefCounted().byteSlice(),
+ };
+ }
+
+ pub fn ref(this: EnvStr) void {
+ if (this.asRefCounted()) |refc| {
+ refc.ref();
+ }
+ }
+
+ pub fn deref(this: EnvStr) void {
+ if (this.asRefCounted()) |refc| {
+ refc.deref();
+ }
+ }
+
+ inline fn asRefCounted(this: EnvStr) ?*RefCountedStr {
+ if (this.tag == .refcounted) return this.castRefCounted();
+ return null;
+ }
+
+ inline fn castSlice(this: EnvStr) []const u8 {
+ return @as([*]u8, @ptrFromInt(@as(usize, @intCast(this.ptr))))[0..this.len];
+ }
+
+ inline fn castRefCounted(this: EnvStr) *RefCountedStr {
+ return @ptrFromInt(@as(usize, @intCast(this.ptr)));
+ }
+};
+
+const bun = @import("root").bun;
+const interpreter = @import("./interpreter.zig");
+const shell = bun.shell;
+const RefCountedStr = interpreter.RefCountedStr;
diff --git a/src/shell/IOWriter.zig b/src/shell/IOWriter.zig
new file mode 100644
index 0000000000..d3c778e41c
--- /dev/null
+++ b/src/shell/IOWriter.zig
@@ -0,0 +1,491 @@
+writer: WriterImpl = if (bun.Environment.isWindows) .{} else .{
+ .close_fd = false,
+},
+fd: bun.FileDescriptor,
+writers: Writers = .{ .inlined = .{} },
+buf: std.ArrayListUnmanaged(u8) = .{},
+/// quick hack to get windows working
+/// ideally this should be removed
+winbuf: if (bun.Environment.isWindows) std.ArrayListUnmanaged(u8) else u0 = if (bun.Environment.isWindows) .{} else 0,
+__idx: usize = 0,
+total_bytes_written: usize = 0,
+ref_count: u32 = 1,
+err: ?JSC.SystemError = null,
+evtloop: JSC.EventLoopHandle,
+concurrent_task: JSC.EventLoopTask,
+is_writing: if (bun.Environment.isWindows) bool else u0 = if (bun.Environment.isWindows) false else 0,
+async_deinit: AsyncDeinitWriter = .{},
+started: bool = false,
+flags: InitFlags = .{},
+
+const debug = bun.Output.scoped(.IOWriter, true);
+
+const ChildPtr = IOWriterChildPtr;
+
+/// ~128kb
+/// We shrunk the `buf` when we reach the last writer,
+/// but if this never happens, we shrink `buf` when it exceeds this threshold
+const SHRINK_THRESHOLD = 1024 * 128;
+
+pub const auto_poll = false;
+
+pub usingnamespace bun.NewRefCounted(@This(), asyncDeinit, "IOWriterRefCount");
+const This = @This();
+pub const WriterImpl = bun.io.BufferedWriter(
+ This,
+ onWrite,
+ onError,
+ onClose,
+ getBuffer,
+ null,
+);
+pub const Poll = WriterImpl;
+
+pub fn __onClose(_: *This) void {}
+pub fn __flush(_: *This) void {}
+
+pub fn refSelf(this: *This) *This {
+ this.ref();
+ return this;
+}
+
+pub const InitFlags = packed struct(u8) {
+ pollable: bool = false,
+ nonblocking: bool = false,
+ is_socket: bool = false,
+ __unused: u5 = 0,
+};
+
+pub fn init(fd: bun.FileDescriptor, flags: InitFlags, evtloop: JSC.EventLoopHandle) *This {
+ const this = IOWriter.new(.{
+ .fd = fd,
+ .evtloop = evtloop,
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop),
+ });
+
+ this.writer.parent = this;
+ this.flags = flags;
+
+ debug("IOWriter(0x{x}, fd={}) init flags={any}", .{ @intFromPtr(this), fd, flags });
+
+ return this;
+}
+
+pub fn __start(this: *This) Maybe(void) {
+ debug("IOWriter(0x{x}, fd={}) __start()", .{ @intFromPtr(this), this.fd });
+ if (this.writer.start(this.fd, this.flags.pollable).asErr()) |e_| {
+ const e: bun.sys.Error = e_;
+ if (bun.Environment.isPosix) {
+ // We get this if we pass in a file descriptor that is not
+ // pollable, for example a special character device like
+ // /dev/null. If so, restart with polling disabled.
+ //
+ // It's also possible on Linux for EINVAL to be returned
+ // when registering multiple writable/readable polls for the
+ // same file descriptor. The shell code here makes sure to
+ // _not_ run into that case, but it is possible.
+ if (e.getErrno() == .INVAL) {
+ debug("IOWriter(0x{x}, fd={}) got EINVAL", .{ @intFromPtr(this), this.fd });
+ this.flags.pollable = false;
+ this.flags.nonblocking = false;
+ this.flags.is_socket = false;
+ this.writer.handle = .{ .closed = {} };
+ return __start(this);
+ }
+
+ if (bun.Environment.isLinux) {
+ // On linux regular files are not pollable and return EPERM,
+ // so restart if that's the case with polling disabled.
+ if (e.getErrno() == .PERM) {
+ this.flags.pollable = false;
+ this.flags.nonblocking = false;
+ this.flags.is_socket = false;
+ this.writer.handle = .{ .closed = {} };
+ return __start(this);
+ }
+ }
+ }
+
+ if (bun.Environment.isWindows) {
+ // This might happen if the file descriptor points to NUL.
+ // On Windows GetFileType(NUL) returns FILE_TYPE_CHAR, so
+ // `this.writer.start()` will try to open it as a tty with
+ // uv_tty_init, but this returns EBADF. As a workaround,
+ // we'll try opening the file descriptor as a file.
+ if (e.getErrno() == .BADF) {
+ this.flags.pollable = false;
+ this.flags.nonblocking = false;
+ this.flags.is_socket = false;
+ return this.writer.startWithFile(this.fd);
+ }
+ }
+ return .{ .err = e };
+ }
+ if (comptime bun.Environment.isPosix) {
+ if (this.flags.nonblocking) {
+ this.writer.getPoll().?.flags.insert(.nonblocking);
+ }
+
+ if (this.flags.is_socket) {
+ this.writer.getPoll().?.flags.insert(.socket);
+ } else if (this.flags.pollable) {
+ this.writer.getPoll().?.flags.insert(.fifo);
+ }
+ }
+
+ return Maybe(void).success;
+}
+
+pub fn eventLoop(this: *This) JSC.EventLoopHandle {
+ return this.evtloop;
+}
+
+/// Idempotent write call
+pub fn write(this: *This) void {
+ if (!this.started) {
+ log("IOWriter(0x{x}, fd={}) starting", .{ @intFromPtr(this), this.fd });
+ if (this.__start().asErr()) |e| {
+ this.onError(e);
+ return;
+ }
+ this.started = true;
+ if (comptime bun.Environment.isPosix) {
+ if (this.writer.handle == .fd) {} else return;
+ } else return;
+ }
+ if (bun.Environment.isWindows) {
+ log("IOWriter(0x{x}, fd={}) write() is_writing={any}", .{ @intFromPtr(this), this.fd, this.is_writing });
+ if (this.is_writing) return;
+ this.is_writing = true;
+ if (this.writer.startWithCurrentPipe().asErr()) |e| {
+ this.onError(e);
+ return;
+ }
+ return;
+ }
+
+ if (this.writer.handle == .poll) {
+ if (!this.writer.handle.poll.isWatching()) {
+ log("IOWriter(0x{x}, fd={}) calling this.writer.write()", .{ @intFromPtr(this), this.fd });
+ this.writer.write();
+ } else log("IOWriter(0x{x}, fd={}) poll already watching", .{ @intFromPtr(this), this.fd });
+ } else {
+ log("IOWriter(0x{x}, fd={}) no poll, calling write", .{ @intFromPtr(this), this.fd });
+ this.writer.write();
+ }
+}
+
+/// Cancel the chunks enqueued by the given writer by
+/// marking them as dead
+pub fn cancelChunks(this: *This, ptr_: anytype) void {
+ const ptr = switch (@TypeOf(ptr_)) {
+ ChildPtr => ptr_,
+ else => ChildPtr.init(ptr_),
+ };
+ if (this.writers.len() == 0) return;
+ const idx = this.__idx;
+ const slice: []Writer = this.writers.sliceMutable();
+ if (idx >= slice.len) return;
+ for (slice[idx..]) |*w| {
+ if (w.ptr.ptr.repr._ptr == ptr.ptr.repr._ptr) {
+ w.setDead();
+ }
+ }
+}
+
+const Writer = struct {
+ ptr: ChildPtr,
+ len: usize,
+ written: usize = 0,
+ bytelist: ?*bun.ByteList = null,
+
+ pub fn rawPtr(this: Writer) ?*anyopaque {
+ return this.ptr.ptr.ptr();
+ }
+
+ pub fn isDead(this: Writer) bool {
+ return this.ptr.ptr.isNull();
+ }
+
+ pub fn setDead(this: *Writer) void {
+ this.ptr.ptr = ChildPtr.ChildPtrRaw.Null;
+ }
+};
+
+pub const Writers = SmolList(Writer, 2);
+
+/// Skips over dead children and increments `total_bytes_written` by the
+/// amount they would have written so the buf is skipped as well
+pub fn skipDead(this: *This) void {
+ const slice = this.writers.slice();
+ for (slice[this.__idx..]) |*w| {
+ if (w.isDead()) {
+ this.__idx += 1;
+ this.total_bytes_written += w.len - w.written;
+ continue;
+ }
+ return;
+ }
+ return;
+}
+
+pub fn onWrite(this: *This, amount: usize, status: bun.io.WriteStatus) void {
+ this.setWriting(false);
+ debug("IOWriter(0x{x}, fd={}) onWrite({d}, {})", .{ @intFromPtr(this), this.fd, amount, status });
+ if (this.__idx >= this.writers.len()) return;
+ const child = this.writers.get(this.__idx);
+ if (child.isDead()) {
+ this.bump(child);
+ } else {
+ if (child.bytelist) |bl| {
+ const written_slice = this.buf.items[this.total_bytes_written .. this.total_bytes_written + amount];
+ bl.append(bun.default_allocator, written_slice) catch bun.outOfMemory();
+ }
+ this.total_bytes_written += amount;
+ child.written += amount;
+ if (status == .end_of_file) {
+ const not_fully_written = !this.isLastIdx(this.__idx) or child.written < child.len;
+ if (bun.Environment.allow_assert and not_fully_written) {
+ bun.Output.debugWarn("IOWriter(0x{x}, fd={}) received done without fully writing data, check that onError is thrown", .{ @intFromPtr(this), this.fd });
+ }
+ return;
+ }
+
+ if (child.written >= child.len) {
+ this.bump(child);
+ }
+ }
+
+ const wrote_everything: bool = this.total_bytes_written >= this.buf.items.len;
+
+ log("IOWriter(0x{x}, fd={}) wrote_everything={}, idx={d} writers={d} next_len={d}", .{ @intFromPtr(this), this.fd, wrote_everything, this.__idx, this.writers.len(), if (this.writers.len() >= 1) this.writers.get(0).len else 0 });
+ if (!wrote_everything and this.__idx < this.writers.len()) {
+ debug("IOWriter(0x{x}, fd={}) poll again", .{ @intFromPtr(this), this.fd });
+ if (comptime bun.Environment.isWindows) {
+ this.setWriting(true);
+ this.writer.write();
+ } else {
+ if (this.writer.handle == .poll)
+ this.writer.registerPoll()
+ else
+ this.writer.write();
+ }
+ }
+}
+
+pub fn onClose(this: *This) void {
+ this.setWriting(false);
+}
+
+pub fn onError(this: *This, err__: bun.sys.Error) void {
+ this.setWriting(false);
+ const ee = err__.toShellSystemError();
+ this.err = ee;
+ log("IOWriter(0x{x}, fd={}) onError errno={s} errmsg={} errsyscall={}", .{ @intFromPtr(this), this.fd, @tagName(ee.getErrno()), ee.message, ee.syscall });
+ var seen_alloc = std.heap.stackFallback(@sizeOf(usize) * 64, bun.default_allocator);
+ var seen = std.ArrayList(usize).initCapacity(seen_alloc.get(), 64) catch bun.outOfMemory();
+ defer seen.deinit();
+ writer_loop: for (this.writers.slice()) |w| {
+ if (w.isDead()) continue;
+ const ptr = w.ptr.ptr.ptr();
+ if (seen.items.len < 8) {
+ for (seen.items[0..]) |item| {
+ if (item == @intFromPtr(ptr)) {
+ continue :writer_loop;
+ }
+ }
+ } else if (std.mem.indexOfScalar(usize, seen.items[0..], @intFromPtr(ptr)) != null) {
+ continue :writer_loop;
+ }
+
+ w.ptr.onWriteChunk(0, this.err);
+ seen.append(@intFromPtr(ptr)) catch bun.outOfMemory();
+ }
+}
+
+pub fn getBuffer(this: *This) []const u8 {
+ const result = this.getBufferImpl();
+ if (comptime bun.Environment.isWindows) {
+ this.winbuf.clearRetainingCapacity();
+ this.winbuf.appendSlice(bun.default_allocator, result) catch bun.outOfMemory();
+ return this.winbuf.items;
+ }
+ log("IOWriter(0x{x}, fd={}) getBuffer = {d} bytes", .{ @intFromPtr(this), this.fd, result.len });
+ return result;
+}
+
+fn getBufferImpl(this: *This) []const u8 {
+ const writer = brk: {
+ if (this.__idx >= this.writers.len()) {
+ log("IOWriter(0x{x}, fd={}) getBufferImpl all writes done", .{ @intFromPtr(this), this.fd });
+ return "";
+ }
+ log("IOWriter(0x{x}, fd={}) getBufferImpl idx={d} writer_len={d}", .{ @intFromPtr(this), this.fd, this.__idx, this.writers.len() });
+ var writer = this.writers.get(this.__idx);
+ if (!writer.isDead()) break :brk writer;
+ log("IOWriter(0x{x}, fd={}) skipping dead", .{ @intFromPtr(this), this.fd });
+ this.skipDead();
+ if (this.__idx >= this.writers.len()) {
+ log("IOWriter(0x{x}, fd={}) getBufferImpl all writes done", .{ @intFromPtr(this), this.fd });
+ return "";
+ }
+ writer = this.writers.get(this.__idx);
+ break :brk writer;
+ };
+ log("IOWriter(0x{x}, fd={}) getBufferImpl writer_len={} writer_written={}", .{ @intFromPtr(this), this.fd, writer.len, writer.written });
+ const remaining = writer.len - writer.written;
+ if (bun.Environment.allow_assert) {
+ assert(!(writer.len == writer.written));
+ }
+ return this.buf.items[this.total_bytes_written .. this.total_bytes_written + remaining];
+}
+
+pub fn bump(this: *This, current_writer: *Writer) void {
+ log("IOWriter(0x{x}, fd={}) bump(0x{x} {s})", .{ @intFromPtr(this), this.fd, @intFromPtr(current_writer), @tagName(current_writer.ptr.ptr.tag()) });
+
+ const is_dead = current_writer.isDead();
+ const written = current_writer.written;
+ const child_ptr = current_writer.ptr;
+
+ defer {
+ if (!is_dead) child_ptr.onWriteChunk(written, null);
+ }
+
+ if (is_dead) {
+ this.skipDead();
+ } else {
+ if (bun.Environment.allow_assert) {
+ if (!is_dead) assert(current_writer.written == current_writer.len);
+ }
+ this.__idx += 1;
+ }
+
+ if (this.__idx >= this.writers.len()) {
+ log("IOWriter(0x{x}, fd={}) all writers complete: truncating", .{ @intFromPtr(this), this.fd });
+ this.buf.clearRetainingCapacity();
+ this.__idx = 0;
+ this.writers.clearRetainingCapacity();
+ this.total_bytes_written = 0;
+ return;
+ }
+
+ if (this.total_bytes_written >= SHRINK_THRESHOLD) {
+ const slice = this.buf.items[this.total_bytes_written..];
+ const remaining_len = slice.len;
+ log("IOWriter(0x{x}, fd={}) exceeded shrink threshold: truncating (new_len={d}, writer_starting_idx={d})", .{ @intFromPtr(this), this.fd, remaining_len, this.__idx });
+ if (slice.len == 0) {
+ this.buf.clearRetainingCapacity();
+ this.total_bytes_written = 0;
+ } else {
+ bun.copy(u8, this.buf.items[0..remaining_len], slice);
+ this.buf.items.len = remaining_len;
+ this.total_bytes_written = 0;
+ }
+ this.writers.truncate(this.__idx);
+ this.__idx = 0;
+ if (bun.Environment.allow_assert) {
+ if (this.writers.len() > 0) {
+ const first = this.writers.getConst(this.__idx);
+ assert(this.buf.items.len >= first.len);
+ }
+ }
+ }
+}
+
+pub fn enqueue(this: *This, ptr: anytype, bytelist: ?*bun.ByteList, buf: []const u8) void {
+ const childptr = if (@TypeOf(ptr) == ChildPtr) ptr else ChildPtr.init(ptr);
+ if (buf.len == 0) {
+ log("IOWriter(0x{x}, fd={}) enqueue EMPTY", .{ @intFromPtr(this), this.fd });
+ childptr.onWriteChunk(0, null);
+ return;
+ }
+ const writer: Writer = .{
+ .ptr = childptr,
+ .len = buf.len,
+ .bytelist = bytelist,
+ };
+ log("IOWriter(0x{x}, fd={}) enqueue(0x{x} {s}, buf_len={d}, buf={s}, writer_len={d})", .{ @intFromPtr(this), this.fd, @intFromPtr(writer.rawPtr()), @tagName(writer.ptr.ptr.tag()), buf.len, buf[0..@min(128, buf.len)], this.writers.len() + 1 });
+ this.buf.appendSlice(bun.default_allocator, buf) catch bun.outOfMemory();
+ this.writers.append(writer);
+ this.write();
+}
+
+pub fn enqueueFmtBltn(
+ this: *This,
+ ptr: anytype,
+ bytelist: ?*bun.ByteList,
+ comptime kind: ?Interpreter.Builtin.Kind,
+ comptime fmt_: []const u8,
+ args: anytype,
+) void {
+ const cmd_str = comptime if (kind) |k| @tagName(k) ++ ": " else "";
+ const fmt__ = cmd_str ++ fmt_;
+ this.enqueueFmt(ptr, bytelist, fmt__, args);
+}
+
+pub fn enqueueFmt(
+ this: *This,
+ ptr: anytype,
+ bytelist: ?*bun.ByteList,
+ comptime fmt: []const u8,
+ args: anytype,
+) void {
+ var buf_writer = this.buf.writer(bun.default_allocator);
+ const start = this.buf.items.len;
+ buf_writer.print(fmt, args) catch bun.outOfMemory();
+ const end = this.buf.items.len;
+ const writer: Writer = .{
+ .ptr = if (@TypeOf(ptr) == ChildPtr) ptr else ChildPtr.init(ptr),
+ .len = end - start,
+ .bytelist = bytelist,
+ };
+ log("IOWriter(0x{x}, fd={}) enqueue(0x{x} {s}, {s})", .{ @intFromPtr(this), this.fd, @intFromPtr(writer.rawPtr()), @tagName(writer.ptr.ptr.tag()), this.buf.items[start..end] });
+ this.writers.append(writer);
+ this.write();
+}
+
+pub fn asyncDeinit(this: *@This()) void {
+ debug("IOWriter(0x{x}, fd={}) asyncDeinit", .{ @intFromPtr(this), this.fd });
+ this.async_deinit.enqueue();
+}
+
+pub fn __deinit(this: *This) void {
+ debug("IOWriter(0x{x}, fd={}) deinit", .{ @intFromPtr(this), this.fd });
+ if (bun.Environment.allow_assert) assert(this.ref_count == 0);
+ this.buf.deinit(bun.default_allocator);
+ if (comptime bun.Environment.isPosix) {
+ if (this.writer.handle == .poll and this.writer.handle.poll.isRegistered()) {
+ this.writer.handle.closeImpl(null, {}, false);
+ }
+ } else this.winbuf.deinit(bun.default_allocator);
+ if (this.fd != bun.invalid_fd) _ = bun.sys.close(this.fd);
+ this.writer.disableKeepingProcessAlive(this.evtloop);
+ this.destroy();
+}
+
+pub fn isLastIdx(this: *This, idx: usize) bool {
+ return idx == this.writers.len() -| 1;
+}
+
+/// Only does things on windows
+pub inline fn setWriting(this: *This, writing: bool) void {
+ if (bun.Environment.isWindows) {
+ log("IOWriter(0x{x}, fd={}) setWriting({any})", .{ @intFromPtr(this), this.fd, writing });
+ this.is_writing = writing;
+ }
+}
+
+const IOWriter = @This();
+const bun = @import("root").bun;
+const shell = bun.shell;
+const Interpreter = shell.Interpreter;
+const EnvMap = shell.EnvMap;
+const EnvStr = shell.EnvStr;
+const JSC = bun.JSC;
+const std = @import("std");
+const assert = bun.assert;
+const log = bun.Output.scoped(.IOWriter, true);
+const SmolList = shell.SmolList;
+const Maybe = JSC.Maybe;
+const IOWriterChildPtr = shell.interpret.IOWriterChildPtr;
+const AsyncDeinitWriter = shell.Interpreter.AsyncDeinitWriter;
diff --git a/src/shell/ParsedShellScript.zig b/src/shell/ParsedShellScript.zig
new file mode 100644
index 0000000000..4aeac11bb0
--- /dev/null
+++ b/src/shell/ParsedShellScript.zig
@@ -0,0 +1,179 @@
+pub usingnamespace JSC.Codegen.JSParsedShellScript;
+
+args: ?*ShellArgs = null,
+/// allocated with arena in jsobjs
+jsobjs: std.ArrayList(JSValue),
+export_env: ?EnvMap = null,
+quiet: bool = false,
+cwd: ?bun.String = null,
+this_jsvalue: JSValue = .zero,
+
+pub fn take(
+ this: *ParsedShellScript,
+ _: *JSC.JSGlobalObject,
+ out_args: **ShellArgs,
+ out_jsobjs: *std.ArrayList(JSValue),
+ out_quiet: *bool,
+ out_cwd: *?bun.String,
+ out_export_env: *?EnvMap,
+) void {
+ out_args.* = this.args.?;
+ out_jsobjs.* = this.jsobjs;
+ out_quiet.* = this.quiet;
+ out_cwd.* = this.cwd;
+ out_export_env.* = this.export_env;
+
+ this.args = null;
+ this.jsobjs = std.ArrayList(JSValue).init(bun.default_allocator);
+ this.cwd = null;
+ this.export_env = null;
+}
+
+pub fn finalize(
+ this: *ParsedShellScript,
+) void {
+ this.this_jsvalue = .zero;
+
+ if (this.export_env) |*env| env.deinit();
+ if (this.cwd) |*cwd| cwd.deref();
+ for (this.jsobjs.items) |jsobj| {
+ jsobj.unprotect();
+ }
+ if (this.args) |a| a.deinit();
+ bun.destroy(this);
+}
+
+pub fn setCwd(this: *ParsedShellScript, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
+ const arguments_ = callframe.arguments_old(2);
+ var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice());
+ const str_js = arguments.nextEat() orelse {
+ return globalThis.throw("$`...`.cwd(): expected a string argument", .{});
+ };
+ const str = try bun.String.fromJS(str_js, globalThis);
+ this.cwd = str;
+ return .undefined;
+}
+
+pub fn setQuiet(this: *ParsedShellScript, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue {
+ this.quiet = true;
+ return .undefined;
+}
+
+pub fn setEnv(this: *ParsedShellScript, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
+ const value1 = callframe.argument(0).getObject() orelse {
+ return globalThis.throwInvalidArguments("env must be an object", .{});
+ };
+
+ var object_iter = try JSC.JSPropertyIterator(.{
+ .skip_empty_name = false,
+ .include_value = true,
+ }).init(globalThis, value1);
+ defer object_iter.deinit();
+
+ var env: EnvMap = EnvMap.init(bun.default_allocator);
+ env.ensureTotalCapacity(object_iter.len);
+
+ // If the env object does not include a $PATH, it must disable path lookup for argv[0]
+ // PATH = "";
+
+ while (try object_iter.next()) |key| {
+ const keyslice = key.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory();
+ var value = object_iter.value;
+ if (value == .undefined) continue;
+
+ const value_str = try value.getZigString(globalThis);
+ const slice = value_str.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory();
+ const keyref = EnvStr.initRefCounted(keyslice);
+ defer keyref.deref();
+ const valueref = EnvStr.initRefCounted(slice);
+ defer valueref.deref();
+
+ env.insert(keyref, valueref);
+ }
+ if (this.export_env) |*previous| {
+ previous.deinit();
+ }
+ this.export_env = env;
+ return .undefined;
+}
+
+pub fn createParsedShellScript(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue {
+ var shargs = ShellArgs.init();
+
+ const arguments_ = callframe.arguments_old(2);
+ const arguments = arguments_.slice();
+ if (arguments.len < 2) {
+ return globalThis.throwNotEnoughArguments("Bun.$", 2, arguments.len);
+ }
+ const string_args = arguments[0];
+ const template_args_js = arguments[1];
+ var template_args = template_args_js.arrayIterator(globalThis);
+
+ var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, shargs.arena_allocator());
+ var jsstrings = try std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4);
+ defer {
+ for (jsstrings.items[0..]) |bunstr| {
+ bunstr.deref();
+ }
+ jsstrings.deinit();
+ }
+ var jsobjs = std.ArrayList(JSValue).init(shargs.arena_allocator());
+ var script = std.ArrayList(u8).init(shargs.arena_allocator());
+ try bun.shell.shellCmdFromJS(globalThis, string_args, &template_args, &jsobjs, &jsstrings, &script);
+
+ var parser: ?bun.shell.Parser = null;
+ var lex_result: ?shell.LexResult = null;
+ const script_ast = Interpreter.parse(
+ shargs.arena_allocator(),
+ script.items[0..],
+ jsobjs.items[0..],
+ jsstrings.items[0..],
+ &parser,
+ &lex_result,
+ ) catch |err| {
+ if (err == shell.ParseError.Lex) {
+ assert(lex_result != null);
+ const str = lex_result.?.combineErrors(shargs.arena_allocator());
+ return globalThis.throwPretty("{s}", .{str});
+ }
+
+ if (parser) |*p| {
+ if (bun.Environment.allow_assert) {
+ assert(p.errors.items.len > 0);
+ }
+ const errstr = p.combineErrors();
+ return globalThis.throwPretty("{s}", .{errstr});
+ }
+
+ return globalThis.throwError(err, "failed to lex/parse shell");
+ };
+
+ shargs.script_ast = script_ast;
+
+ const parsed_shell_script = bun.new(ParsedShellScript, .{
+ .args = shargs,
+ .jsobjs = jsobjs,
+ });
+ parsed_shell_script.this_jsvalue = JSC.Codegen.JSParsedShellScript.toJS(parsed_shell_script, globalThis);
+
+ bun.Analytics.Features.shell += 1;
+ return parsed_shell_script.this_jsvalue;
+}
+
+const ParsedShellScript = @This();
+const bun = @import("root").bun;
+const shell = bun.shell;
+const Interpreter = shell.Interpreter;
+const interpreter = @import("./interpreter.zig");
+const EnvMap = shell.EnvMap;
+const EnvStr = shell.EnvStr;
+const JSC = bun.JSC;
+const ShellArgs = interpreter.ShellArgs;
+const std = @import("std");
+const JSValue = JSC.JSValue;
+const JSGlobalObject = JSC.JSGlobalObject;
+const CallFrame = JSC.CallFrame;
+const Node = JSC.Node;
+const ArgumentsSlice = JSC.Node.ArgumentsSlice;
+const assert = bun.assert;
+const log = bun.Output.scoped(.ParsedShellScript, true);
diff --git a/src/shell/RefCountedStr.zig b/src/shell/RefCountedStr.zig
new file mode 100644
index 0000000000..d4c5ef7e27
--- /dev/null
+++ b/src/shell/RefCountedStr.zig
@@ -0,0 +1,46 @@
+refcount: u32 = 1,
+len: u32 = 0,
+ptr: [*]const u8 = undefined,
+
+const debug = bun.Output.scoped(.RefCountedEnvStr, true);
+
+pub fn init(slice: []const u8) *RefCountedStr {
+ debug("init: {s}", .{slice});
+ const this = bun.default_allocator.create(RefCountedStr) catch bun.outOfMemory();
+ this.* = .{
+ .refcount = 1,
+ .len = @intCast(slice.len),
+ .ptr = slice.ptr,
+ };
+ return this;
+}
+
+pub fn byteSlice(this: *RefCountedStr) []const u8 {
+ if (this.len == 0) return "";
+ return this.ptr[0..this.len];
+}
+
+pub fn ref(this: *RefCountedStr) void {
+ this.refcount += 1;
+}
+
+pub fn deref(this: *RefCountedStr) void {
+ this.refcount -= 1;
+ if (this.refcount == 0) {
+ this.deinit();
+ }
+}
+
+fn deinit(this: *RefCountedStr) void {
+ debug("deinit: {s}", .{this.byteSlice()});
+ this.freeStr();
+ bun.default_allocator.destroy(this);
+}
+
+fn freeStr(this: *RefCountedStr) void {
+ if (this.len == 0) return;
+ bun.default_allocator.free(this.ptr[0..this.len]);
+}
+
+const RefCountedStr = @This();
+const bun = @import("root").bun;
diff --git a/src/shell/builtin/basename.zig b/src/shell/builtin/basename.zig
new file mode 100644
index 0000000000..eef5599a33
--- /dev/null
+++ b/src/shell/builtin/basename.zig
@@ -0,0 +1,93 @@
+state: enum { idle, waiting_io, err, done } = .idle,
+buf: std.ArrayListUnmanaged(u8) = .{},
+
+pub fn start(this: *@This()) Maybe(void) {
+ const args = this.bltn().argsSlice();
+ var iter = bun.SliceIterator([*:0]const u8).init(args);
+
+ if (args.len == 0) return this.fail(Builtin.Kind.usageString(.basename));
+
+ while (iter.next()) |item| {
+ const arg = bun.sliceTo(item, 0);
+ _ = this.print(bun.path.basename(arg));
+ _ = this.print("\n");
+ }
+
+ this.state = .done;
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.bltn().stdout.enqueue(this, this.buf.items, safeguard);
+ } else {
+ this.bltn().done(0);
+ }
+ return Maybe(void).success;
+}
+
+pub fn deinit(this: *@This()) void {
+ this.buf.deinit(bun.default_allocator);
+ //basename
+}
+
+fn fail(this: *@This(), msg: []const u8) Maybe(void) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .err;
+ this.bltn().stderr.enqueue(this, msg, safeguard);
+ return Maybe(void).success;
+ }
+ _ = this.bltn().writeNoIO(.stderr, msg);
+ this.bltn().done(1);
+ return Maybe(void).success;
+}
+
+fn print(this: *@This(), msg: []const u8) Maybe(void) {
+ if (this.bltn().stdout.needsIO() != null) {
+ this.buf.appendSlice(bun.default_allocator, msg) catch bun.outOfMemory();
+ return Maybe(void).success;
+ }
+ const res = this.bltn().writeNoIO(.stdout, msg);
+ if (res == .err) return Maybe(void).initErr(res.err);
+ return Maybe(void).success;
+}
+
+pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void {
+ if (maybe_e) |e| {
+ defer e.deref();
+ this.state = .err;
+ this.bltn().done(1);
+ return;
+ }
+ switch (this.state) {
+ .done => this.bltn().done(0),
+ .err => this.bltn().done(1),
+ else => {},
+ }
+}
+
+pub inline fn bltn(this: *@This()) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("basename", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Basename = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
diff --git a/src/shell/builtin/cat.zig b/src/shell/builtin/cat.zig
new file mode 100644
index 0000000000..4162ab8386
--- /dev/null
+++ b/src/shell/builtin/cat.zig
@@ -0,0 +1,367 @@
+opts: Opts = .{},
+state: union(enum) {
+ idle,
+ exec_stdin: struct {
+ in_done: bool = false,
+ chunks_queued: usize = 0,
+ chunks_done: usize = 0,
+ errno: ExitCode = 0,
+ },
+ exec_filepath_args: struct {
+ args: []const [*:0]const u8,
+ idx: usize = 0,
+ reader: ?*IOReader = null,
+ chunks_queued: usize = 0,
+ chunks_done: usize = 0,
+ out_done: bool = false,
+ in_done: bool = false,
+
+ pub fn deinit(this: *@This()) void {
+ if (this.reader) |r| r.deref();
+ }
+ },
+ waiting_write_err,
+ done,
+} = .idle,
+
+pub fn writeFailingError(this: *Cat, buf: []const u8, exit_code: ExitCode) Maybe(void) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .waiting_write_err;
+ this.bltn().stderr.enqueue(this, buf, safeguard);
+ return Maybe(void).success;
+ }
+
+ _ = this.bltn().writeNoIO(.stderr, buf);
+
+ this.bltn().done(exit_code);
+ return Maybe(void).success;
+}
+
+pub fn start(this: *Cat) Maybe(void) {
+ const filepath_args = switch (this.opts.parse(this.bltn().argsSlice())) {
+ .ok => |filepath_args| filepath_args,
+ .err => |e| {
+ const buf = switch (e) {
+ .illegal_option => |opt_str| this.bltn().fmtErrorArena(.cat, "illegal option -- {s}\n", .{opt_str}),
+ .show_usage => Builtin.Kind.cat.usageString(),
+ .unsupported => |unsupported| this.bltn().fmtErrorArena(.cat, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
+ };
+
+ _ = this.writeFailingError(buf, 1);
+ return Maybe(void).success;
+ },
+ };
+
+ const should_read_from_stdin = filepath_args == null or filepath_args.?.len == 0;
+
+ if (should_read_from_stdin) {
+ this.state = .{
+ .exec_stdin = .{},
+ };
+ } else {
+ this.state = .{
+ .exec_filepath_args = .{
+ .args = filepath_args.?,
+ },
+ };
+ }
+
+ _ = this.next();
+
+ return Maybe(void).success;
+}
+
+pub fn next(this: *Cat) void {
+ switch (this.state) {
+ .idle => @panic("Invalid state"),
+ .exec_stdin => {
+ if (!this.bltn().stdin.needsIO()) {
+ this.state.exec_stdin.in_done = true;
+ const buf = this.bltn().readStdinNoIO();
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.bltn().stdout.enqueue(this, buf, safeguard);
+ } else {
+ _ = this.bltn().writeNoIO(.stdout, buf);
+ this.bltn().done(0);
+ return;
+ }
+ return;
+ }
+ this.bltn().stdin.fd.addReader(this);
+ this.bltn().stdin.fd.start();
+ return;
+ },
+ .exec_filepath_args => {
+ var exec = &this.state.exec_filepath_args;
+ if (exec.idx >= exec.args.len) {
+ exec.deinit();
+ return this.bltn().done(0);
+ }
+
+ if (exec.reader) |r| r.deref();
+
+ const arg = std.mem.span(exec.args[exec.idx]);
+ exec.idx += 1;
+ const dir = this.bltn().parentCmd().base.shell.cwd_fd;
+ const fd = switch (ShellSyscall.openat(dir, arg, bun.O.RDONLY, 0)) {
+ .result => |fd| fd,
+ .err => |e| {
+ const buf = this.bltn().taskErrorToString(.cat, e);
+ _ = this.writeFailingError(buf, 1);
+ exec.deinit();
+ return;
+ },
+ };
+
+ const reader = IOReader.init(fd, this.bltn().eventLoop());
+ exec.chunks_done = 0;
+ exec.chunks_queued = 0;
+ exec.reader = reader;
+ exec.reader.?.addReader(this);
+ exec.reader.?.start();
+ },
+ .waiting_write_err => return,
+ .done => this.bltn().done(0),
+ }
+}
+
+pub fn onIOWriterChunk(this: *Cat, _: usize, err: ?JSC.SystemError) void {
+ debug("onIOWriterChunk(0x{x}, {s}, had_err={any})", .{ @intFromPtr(this), @tagName(this.state), err != null });
+ const errno: ExitCode = if (err) |e| brk: {
+ defer e.deref();
+ break :brk @as(ExitCode, @intCast(@intFromEnum(e.getErrno())));
+ } else 0;
+ // Writing to stdout errored, cancel everything and write error
+ if (err) |e| {
+ defer e.deref();
+ switch (this.state) {
+ .exec_stdin => {
+ this.state.exec_stdin.errno = errno;
+ // Cancel reader if needed
+ if (!this.state.exec_stdin.in_done) {
+ if (this.bltn().stdin.needsIO()) {
+ this.bltn().stdin.fd.removeReader(this);
+ }
+ this.state.exec_stdin.in_done = true;
+ }
+ this.bltn().done(e.getErrno());
+ },
+ .exec_filepath_args => {
+ var exec = &this.state.exec_filepath_args;
+ if (exec.reader) |r| {
+ r.removeReader(this);
+ }
+ exec.deinit();
+ this.bltn().done(e.getErrno());
+ },
+ .waiting_write_err => this.bltn().done(e.getErrno()),
+ else => @panic("Invalid state"),
+ }
+ return;
+ }
+
+ switch (this.state) {
+ .exec_stdin => {
+ this.state.exec_stdin.chunks_done += 1;
+ if (this.state.exec_stdin.in_done and (this.state.exec_stdin.chunks_done >= this.state.exec_stdin.chunks_queued)) {
+ this.bltn().done(0);
+ return;
+ }
+ // Need to wait for more chunks to be written
+ },
+ .exec_filepath_args => {
+ this.state.exec_filepath_args.chunks_done += 1;
+ if (this.state.exec_filepath_args.chunks_done >= this.state.exec_filepath_args.chunks_queued) {
+ this.state.exec_filepath_args.out_done = true;
+ }
+ if (this.state.exec_filepath_args.in_done and this.state.exec_filepath_args.out_done) {
+ this.next();
+ return;
+ }
+ // Wait for reader to be done
+ return;
+ },
+ .waiting_write_err => this.bltn().done(1),
+ else => @panic("Invalid state"),
+ }
+}
+
+pub fn onIOReaderChunk(this: *Cat, chunk: []const u8) ReadChunkAction {
+ debug("onIOReaderChunk(0x{x}, {s}, chunk_len={d})", .{ @intFromPtr(this), @tagName(this.state), chunk.len });
+ switch (this.state) {
+ .exec_stdin => {
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.state.exec_stdin.chunks_queued += 1;
+ this.bltn().stdout.enqueue(this, chunk, safeguard);
+ return .cont;
+ }
+ _ = this.bltn().writeNoIO(.stdout, chunk);
+ },
+ .exec_filepath_args => {
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.state.exec_filepath_args.chunks_queued += 1;
+ this.bltn().stdout.enqueue(this, chunk, safeguard);
+ return .cont;
+ }
+ _ = this.bltn().writeNoIO(.stdout, chunk);
+ },
+ else => @panic("Invalid state"),
+ }
+ return .cont;
+}
+
+pub fn onIOReaderDone(this: *Cat, err: ?JSC.SystemError) void {
+ const errno: ExitCode = if (err) |e| brk: {
+ defer e.deref();
+ break :brk @as(ExitCode, @intCast(@intFromEnum(e.getErrno())));
+ } else 0;
+ debug("onIOReaderDone(0x{x}, {s}, errno={d})", .{ @intFromPtr(this), @tagName(this.state), errno });
+
+ switch (this.state) {
+ .exec_stdin => {
+ this.state.exec_stdin.errno = errno;
+ this.state.exec_stdin.in_done = true;
+ if (errno != 0) {
+ if ((this.state.exec_stdin.chunks_done >= this.state.exec_stdin.chunks_queued) or this.bltn().stdout.needsIO() == null) {
+ this.bltn().done(errno);
+ return;
+ }
+ this.bltn().stdout.fd.writer.cancelChunks(this);
+ return;
+ }
+ if ((this.state.exec_stdin.chunks_done >= this.state.exec_stdin.chunks_queued) or this.bltn().stdout.needsIO() == null) {
+ this.bltn().done(0);
+ }
+ },
+ .exec_filepath_args => {
+ this.state.exec_filepath_args.in_done = true;
+ if (errno != 0) {
+ if (this.state.exec_filepath_args.out_done or this.bltn().stdout.needsIO() == null) {
+ this.state.exec_filepath_args.deinit();
+ this.bltn().done(errno);
+ return;
+ }
+ this.bltn().stdout.fd.writer.cancelChunks(this);
+ return;
+ }
+ if (this.state.exec_filepath_args.out_done or (this.state.exec_filepath_args.chunks_done >= this.state.exec_filepath_args.chunks_queued) or this.bltn().stdout.needsIO() == null) {
+ this.next();
+ }
+ },
+ .done, .waiting_write_err, .idle => {},
+ }
+}
+
+pub fn deinit(_: *Cat) void {}
+
+pub inline fn bltn(this: *Cat) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("cat", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+const Opts = struct {
+ /// -b
+ ///
+ /// Number the non-blank output lines, starting at 1.
+ number_nonblank: bool = false,
+
+ /// -e
+ ///
+ /// Display non-printing characters and display a dollar sign ($) at the end of each line.
+ show_ends: bool = false,
+
+ /// -n
+ ///
+ /// Number the output lines, starting at 1.
+ number_all: bool = false,
+
+ /// -s
+ ///
+ /// Squeeze multiple adjacent empty lines, causing the output to be single spaced.
+ squeeze_blank: bool = false,
+
+ /// -t
+ ///
+ /// Display non-printing characters and display tab characters as ^I at the end of each line.
+ show_tabs: bool = false,
+
+ /// -u
+ ///
+ /// Disable output buffering.
+ disable_output_buffering: bool = false,
+
+ /// -v
+ ///
+ /// Displays non-printing characters so they are visible.
+ show_nonprinting: bool = false,
+
+ const Parse = FlagParser(*@This());
+
+ pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) {
+ return Parse.parseFlags(opts, args);
+ }
+
+ pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult {
+ _ = this; // autofix
+ _ = flag;
+ return null;
+ }
+
+ pub fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
+ _ = this; // autofix
+ switch (char) {
+ 'b' => {
+ return .{ .unsupported = unsupportedFlag("-b") };
+ },
+ 'e' => {
+ return .{ .unsupported = unsupportedFlag("-e") };
+ },
+ 'n' => {
+ return .{ .unsupported = unsupportedFlag("-n") };
+ },
+ 's' => {
+ return .{ .unsupported = unsupportedFlag("-s") };
+ },
+ 't' => {
+ return .{ .unsupported = unsupportedFlag("-t") };
+ },
+ 'u' => {
+ return .{ .unsupported = unsupportedFlag("-u") };
+ },
+ 'v' => {
+ return .{ .unsupported = unsupportedFlag("-v") };
+ },
+ else => {
+ return .{ .illegal_option = smallflags[1 + i ..] };
+ },
+ }
+
+ return null;
+ }
+};
+
+const debug = bun.Output.scoped(.ShellCat, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Cat = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
diff --git a/src/shell/builtin/cd.zig b/src/shell/builtin/cd.zig
new file mode 100644
index 0000000000..e10c8e7eaf
--- /dev/null
+++ b/src/shell/builtin/cd.zig
@@ -0,0 +1,150 @@
+//! Some additional behaviour beyond basic `cd
`:
+//! - `cd` by itself or `cd ~` will always put the user in their home directory.
+//! - `cd ~username` will put the user in the home directory of the specified user
+//! - `cd -` will put the user in the previous directory
+state: union(enum) {
+ idle,
+ waiting_write_stderr,
+ done,
+ err: Syscall.Error,
+} = .idle,
+
+fn writeStderrNonBlocking(this: *Cd, comptime fmt: []const u8, args: anytype) void {
+ this.state = .waiting_write_stderr;
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.bltn().stderr.enqueueFmtBltn(this, .cd, fmt, args, safeguard);
+ } else {
+ const buf = this.bltn().fmtErrorArena(.cd, fmt, args);
+ _ = this.bltn().writeNoIO(.stderr, buf);
+ this.state = .done;
+ this.bltn().done(1);
+ }
+}
+
+pub fn start(this: *Cd) Maybe(void) {
+ const args = this.bltn().argsSlice();
+ if (args.len > 1) {
+ this.writeStderrNonBlocking("too many arguments\n", .{});
+ // yield execution
+ return Maybe(void).success;
+ }
+
+ if (args.len == 1) {
+ const first_arg = args[0][0..std.mem.len(args[0]) :0];
+ switch (first_arg[0]) {
+ '-' => {
+ switch (this.bltn().parentCmd().base.shell.changePrevCwd(this.bltn().parentCmd().base.interpreter)) {
+ .result => {},
+ .err => |err| {
+ return this.handleChangeCwdErr(err, this.bltn().parentCmd().base.shell.prevCwdZ());
+ },
+ }
+ },
+ '~' => {
+ const homedir = this.bltn().parentCmd().base.shell.getHomedir();
+ homedir.deref();
+ switch (this.bltn().parentCmd().base.shell.changeCwd(this.bltn().parentCmd().base.interpreter, homedir.slice())) {
+ .result => {},
+ .err => |err| return this.handleChangeCwdErr(err, homedir.slice()),
+ }
+ },
+ else => {
+ switch (this.bltn().parentCmd().base.shell.changeCwd(this.bltn().parentCmd().base.interpreter, first_arg)) {
+ .result => {},
+ .err => |err| return this.handleChangeCwdErr(err, first_arg),
+ }
+ },
+ }
+ }
+
+ this.bltn().done(0);
+ return Maybe(void).success;
+}
+
+fn handleChangeCwdErr(this: *Cd, err: Syscall.Error, new_cwd_: []const u8) Maybe(void) {
+ const errno: usize = @intCast(err.errno);
+
+ switch (errno) {
+ @as(usize, @intFromEnum(bun.C.E.NOTDIR)) => {
+ if (this.bltn().stderr.needsIO() == null) {
+ const buf = this.bltn().fmtErrorArena(.cd, "not a directory: {s}\n", .{new_cwd_});
+ _ = this.bltn().writeNoIO(.stderr, buf);
+ this.state = .done;
+ this.bltn().done(1);
+ // yield execution
+ return Maybe(void).success;
+ }
+
+ this.writeStderrNonBlocking("not a directory: {s}\n", .{new_cwd_});
+ return Maybe(void).success;
+ },
+ @as(usize, @intFromEnum(bun.C.E.NOENT)) => {
+ if (this.bltn().stderr.needsIO() == null) {
+ const buf = this.bltn().fmtErrorArena(.cd, "not a directory: {s}\n", .{new_cwd_});
+ _ = this.bltn().writeNoIO(.stderr, buf);
+ this.state = .done;
+ this.bltn().done(1);
+ // yield execution
+ return Maybe(void).success;
+ }
+
+ this.writeStderrNonBlocking("not a directory: {s}\n", .{new_cwd_});
+ return Maybe(void).success;
+ },
+ else => return Maybe(void).success,
+ }
+}
+
+pub fn onIOWriterChunk(this: *Cd, _: usize, e: ?JSC.SystemError) void {
+ if (comptime bun.Environment.allow_assert) {
+ assert(this.state == .waiting_write_stderr);
+ }
+
+ if (e != null) {
+ defer e.?.deref();
+ this.bltn().done(e.?.getErrno());
+ return;
+ }
+
+ this.state = .done;
+ this.bltn().done(1);
+}
+
+pub inline fn bltn(this: *Cd) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("cd", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+pub fn deinit(this: *Cd) void {
+ log("({s}) deinit", .{@tagName(.cd)});
+ _ = this;
+}
+
+// --
+const log = bun.Output.scoped(.Cd, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Cd = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const Syscall = bun.sys;
+const assert = bun.assert;
diff --git a/src/shell/builtin/cp.zig b/src/shell/builtin/cp.zig
new file mode 100644
index 0000000000..445d5edbba
--- /dev/null
+++ b/src/shell/builtin/cp.zig
@@ -0,0 +1,776 @@
+opts: Opts = .{},
+state: union(enum) {
+ idle,
+ exec: struct {
+ target_path: [:0]const u8,
+ paths_to_copy: []const [*:0]const u8,
+ started: bool = false,
+ /// this is thread safe as it is only incremented
+ /// and decremented on the main thread by this struct
+ tasks_count: u32 = 0,
+ output_waiting: u32 = 0,
+ output_done: u32 = 0,
+ err: ?bun.shell.ShellErr = null,
+
+ ebusy: if (bun.Environment.isWindows) EbusyState else struct {} = .{},
+ },
+ ebusy: struct {
+ state: EbusyState,
+ idx: usize = 0,
+ main_exit_code: ExitCode = 0,
+ },
+ waiting_write_err,
+ done,
+} = .idle,
+
+pub fn format(this: *const Cp, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
+ try writer.print("Cp(0x{x})", .{@intFromPtr(this)});
+}
+
+/// On Windows it is possible to get an EBUSY error very simply
+/// by running the following command:
+///
+/// `cp myfile.txt myfile.txt mydir/`
+///
+/// Bearing in mind that the shell cp implementation creates a
+/// ShellCpTask for each source file, it's possible for one of the
+/// tasks to get EBUSY while trying to access the source file or the
+/// destination file.
+///
+/// But it's fine to ignore the EBUSY error since at
+/// least one of them will succeed anyway.
+///
+/// We handle this _after_ all the tasks have been
+/// executed, to avoid complicated synchronization on multiple
+/// threads, because the precise src or dest for each argument is
+/// not known until its corresponding ShellCpTask is executed by the
+/// threadpool.
+const EbusyState = struct {
+ tasks: std.ArrayListUnmanaged(*ShellCpTask) = .{},
+ absolute_targets: bun.StringArrayHashMapUnmanaged(void) = .{},
+ absolute_srcs: bun.StringArrayHashMapUnmanaged(void) = .{},
+
+ pub fn deinit(this: *EbusyState) void {
+ // The tasks themselves are freed in `ignoreEbusyErrorIfPossible()`
+ this.tasks.deinit(bun.default_allocator);
+ for (this.absolute_targets.keys()) |tgt| {
+ bun.default_allocator.free(tgt);
+ }
+ this.absolute_targets.deinit(bun.default_allocator);
+ for (this.absolute_srcs.keys()) |tgt| {
+ bun.default_allocator.free(tgt);
+ }
+ this.absolute_srcs.deinit(bun.default_allocator);
+ }
+};
+
+pub fn start(this: *Cp) Maybe(void) {
+ const maybe_filepath_args = switch (this.opts.parse(this.bltn().argsSlice())) {
+ .ok => |args| args,
+ .err => |e| {
+ const buf = switch (e) {
+ .illegal_option => |opt_str| this.bltn().fmtErrorArena(.cp, "illegal option -- {s}\n", .{opt_str}),
+ .show_usage => Builtin.Kind.cp.usageString(),
+ .unsupported => |unsupported| this.bltn().fmtErrorArena(.cp, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
+ };
+
+ _ = this.writeFailingError(buf, 1);
+ return Maybe(void).success;
+ },
+ };
+
+ if (maybe_filepath_args == null or maybe_filepath_args.?.len <= 1) {
+ _ = this.writeFailingError(Builtin.Kind.cp.usageString(), 1);
+ return Maybe(void).success;
+ }
+
+ const args = maybe_filepath_args orelse unreachable;
+ const paths_to_copy = args[0 .. args.len - 1];
+ const tgt_path = std.mem.span(args[args.len - 1]);
+
+ this.state = .{ .exec = .{
+ .target_path = tgt_path,
+ .paths_to_copy = paths_to_copy,
+ } };
+
+ this.next();
+
+ return Maybe(void).success;
+}
+
+pub fn ignoreEbusyErrorIfPossible(this: *Cp) void {
+ if (!bun.Environment.isWindows) @compileError("dont call this plz");
+
+ if (this.state.ebusy.idx < this.state.ebusy.state.tasks.items.len) {
+ outer_loop: for (this.state.ebusy.state.tasks.items[this.state.ebusy.idx..], 0..) |task_, i| {
+ const task: *ShellCpTask = task_;
+ const failure_src = task.src_absolute.?;
+ const failure_tgt = task.tgt_absolute.?;
+ if (this.state.ebusy.state.absolute_targets.get(failure_tgt)) |_| {
+ task.deinit();
+ continue :outer_loop;
+ }
+ if (this.state.ebusy.state.absolute_srcs.get(failure_src)) |_| {
+ task.deinit();
+ continue :outer_loop;
+ }
+ this.state.ebusy.idx += i + 1;
+ this.printShellCpTask(task);
+ return;
+ }
+ }
+
+ this.state.ebusy.state.deinit();
+ const exit_code = this.state.ebusy.main_exit_code;
+ this.state = .done;
+ this.bltn().done(exit_code);
+}
+
+pub fn next(this: *Cp) void {
+ while (this.state != .done) {
+ switch (this.state) {
+ .idle => @panic("Invalid state for \"Cp\": idle, this indicates a bug in Bun. Please file a GitHub issue"),
+ .exec => {
+ var exec = &this.state.exec;
+ if (exec.started) {
+ if (this.state.exec.tasks_count <= 0 and this.state.exec.output_done >= this.state.exec.output_waiting) {
+ const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0;
+ if (this.state.exec.err != null) {
+ this.state.exec.err.?.deinit(bun.default_allocator);
+ }
+ if (comptime bun.Environment.isWindows) {
+ if (exec.ebusy.tasks.items.len > 0) {
+ this.state = .{ .ebusy = .{ .state = this.state.exec.ebusy, .main_exit_code = exit_code } };
+ continue;
+ }
+ exec.ebusy.deinit();
+ }
+ this.state = .done;
+ this.bltn().done(exit_code);
+ return;
+ }
+ return;
+ }
+
+ exec.started = true;
+ exec.tasks_count = @intCast(exec.paths_to_copy.len);
+
+ const cwd_path = this.bltn().parentCmd().base.shell.cwdZ();
+
+ // Launch a task for each argument
+ for (exec.paths_to_copy) |path_raw| {
+ const path = std.mem.span(path_raw);
+ const cp_task = ShellCpTask.create(this, this.bltn().eventLoop(), this.opts, 1 + exec.paths_to_copy.len, path, exec.target_path, cwd_path);
+ cp_task.schedule();
+ }
+ return;
+ },
+ .ebusy => {
+ if (comptime bun.Environment.isWindows) {
+ this.ignoreEbusyErrorIfPossible();
+ return;
+ } else @panic("Should only be called on Windows");
+ },
+ .waiting_write_err => return,
+ .done => unreachable,
+ }
+ }
+
+ this.bltn().done(0);
+}
+
+pub fn deinit(cp: *Cp) void {
+ assert(cp.state == .done or cp.state == .waiting_write_err);
+}
+
+pub fn writeFailingError(this: *Cp, buf: []const u8, exit_code: ExitCode) Maybe(void) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .waiting_write_err;
+ this.bltn().stderr.enqueue(this, buf, safeguard);
+ return Maybe(void).success;
+ }
+
+ _ = this.bltn().writeNoIO(.stderr, buf);
+
+ this.bltn().done(exit_code);
+ return Maybe(void).success;
+}
+
+pub fn onIOWriterChunk(this: *Cp, _: usize, e: ?JSC.SystemError) void {
+ if (e) |err| err.deref();
+ if (this.state == .waiting_write_err) {
+ return this.bltn().done(1);
+ }
+ this.state.exec.output_done += 1;
+ this.next();
+}
+
+pub inline fn bltn(this: *@This()) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("cp", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+pub fn onShellCpTaskDone(this: *Cp, task: *ShellCpTask) void {
+ assert(this.state == .exec);
+ log("task done: 0x{x} {d}", .{ @intFromPtr(task), this.state.exec.tasks_count });
+ this.state.exec.tasks_count -= 1;
+
+ const err_ = task.err;
+
+ if (comptime bun.Environment.isWindows) {
+ if (err_) |err| {
+ if (err == .sys and
+ err.sys.getErrno() == .BUSY and
+ (task.tgt_absolute != null and
+ err.sys.path.eqlUTF8(task.tgt_absolute.?)) or
+ (task.src_absolute != null and
+ err.sys.path.eqlUTF8(task.src_absolute.?)))
+ {
+ log("{} got ebusy {d} {d}", .{ this, this.state.exec.ebusy.tasks.items.len, this.state.exec.paths_to_copy.len });
+ this.state.exec.ebusy.tasks.append(bun.default_allocator, task) catch bun.outOfMemory();
+ this.next();
+ return;
+ }
+ } else {
+ const tgt_absolute = task.tgt_absolute;
+ task.tgt_absolute = null;
+ if (tgt_absolute) |tgt| this.state.exec.ebusy.absolute_targets.put(bun.default_allocator, tgt, {}) catch bun.outOfMemory();
+ const src_absolute = task.src_absolute;
+ task.src_absolute = null;
+ if (src_absolute) |tgt| this.state.exec.ebusy.absolute_srcs.put(bun.default_allocator, tgt, {}) catch bun.outOfMemory();
+ }
+ }
+
+ this.printShellCpTask(task);
+}
+
+pub fn printShellCpTask(this: *Cp, task: *ShellCpTask) void {
+ // Deinitialize this task as we are starting a new one
+ defer task.deinit();
+
+ const err_ = task.err;
+ var output = task.takeOutput();
+
+ const output_task: *ShellCpOutputTask = bun.new(ShellCpOutputTask, .{
+ .parent = this,
+ .output = .{ .arrlist = output.moveToUnmanaged() },
+ .state = .waiting_write_err,
+ });
+ if (err_) |err| {
+ this.state.exec.err = err;
+ const error_string = this.bltn().taskErrorToString(.cp, err);
+ output_task.start(error_string);
+ return;
+ }
+ output_task.start(null);
+}
+
+pub const ShellCpOutputTask = OutputTask(Cp, .{
+ .writeErr = ShellCpOutputTaskVTable.writeErr,
+ .onWriteErr = ShellCpOutputTaskVTable.onWriteErr,
+ .writeOut = ShellCpOutputTaskVTable.writeOut,
+ .onWriteOut = ShellCpOutputTaskVTable.onWriteOut,
+ .onDone = ShellCpOutputTaskVTable.onDone,
+});
+
+const ShellCpOutputTaskVTable = struct {
+ pub fn writeErr(this: *Cp, childptr: anytype, errbuf: []const u8) CoroutineResult {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state.exec.output_waiting += 1;
+ this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
+ return .yield;
+ }
+ _ = this.bltn().writeNoIO(.stderr, errbuf);
+ return .cont;
+ }
+
+ pub fn onWriteErr(this: *Cp) void {
+ this.state.exec.output_done += 1;
+ }
+
+ pub fn writeOut(this: *Cp, childptr: anytype, output: *OutputSrc) CoroutineResult {
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.state.exec.output_waiting += 1;
+ this.bltn().stdout.enqueue(childptr, output.slice(), safeguard);
+ return .yield;
+ }
+ _ = this.bltn().writeNoIO(.stdout, output.slice());
+ return .cont;
+ }
+
+ pub fn onWriteOut(this: *Cp) void {
+ this.state.exec.output_done += 1;
+ }
+
+ pub fn onDone(this: *Cp) void {
+ this.next();
+ }
+};
+
+pub const ShellCpTask = struct {
+ cp: *Cp,
+
+ opts: Opts,
+ operands: usize = 0,
+ src: [:0]const u8,
+ tgt: [:0]const u8,
+ src_absolute: ?[:0]const u8 = null,
+ tgt_absolute: ?[:0]const u8 = null,
+ cwd_path: [:0]const u8,
+ verbose_output_lock: bun.Mutex = .{},
+ verbose_output: ArrayList(u8) = ArrayList(u8).init(bun.default_allocator),
+
+ task: JSC.WorkPoolTask = .{ .callback = &runFromThreadPool },
+ event_loop: JSC.EventLoopHandle,
+ concurrent_task: JSC.EventLoopTask,
+ err: ?bun.shell.ShellErr = null,
+
+ const debug = bun.Output.scoped(.ShellCpTask, false);
+
+ fn deinit(this: *ShellCpTask) void {
+ debug("deinit", .{});
+ this.verbose_output.deinit();
+ if (this.err) |e| {
+ e.deinit(bun.default_allocator);
+ }
+ if (this.src_absolute) |sc| {
+ bun.default_allocator.free(sc);
+ }
+ if (this.tgt_absolute) |tc| {
+ bun.default_allocator.free(tc);
+ }
+ bun.destroy(this);
+ }
+
+ pub fn schedule(this: *@This()) void {
+ debug("schedule", .{});
+ WorkPool.schedule(&this.task);
+ }
+
+ pub fn create(
+ cp: *Cp,
+ evtloop: JSC.EventLoopHandle,
+ opts: Opts,
+ operands: usize,
+ src: [:0]const u8,
+ tgt: [:0]const u8,
+ cwd_path: [:0]const u8,
+ ) *ShellCpTask {
+ return bun.new(ShellCpTask, ShellCpTask{
+ .cp = cp,
+ .operands = operands,
+ .opts = opts,
+ .src = src,
+ .tgt = tgt,
+ .cwd_path = cwd_path,
+ .event_loop = evtloop,
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop),
+ });
+ }
+
+ fn takeOutput(this: *ShellCpTask) ArrayList(u8) {
+ const out = this.verbose_output;
+ this.verbose_output = ArrayList(u8).init(bun.default_allocator);
+ return out;
+ }
+
+ pub fn ensureDest(nodefs: *JSC.Node.NodeFS, dest: bun.OSPathSliceZ) Maybe(void) {
+ return switch (nodefs.mkdirRecursiveOSPath(dest, JSC.Node.Arguments.Mkdir.DefaultMode, false)) {
+ .err => |err| Maybe(void){ .err = err },
+ .result => Maybe(void).success,
+ };
+ }
+
+ pub fn hasTrailingSep(path: [:0]const u8) bool {
+ if (path.len == 0) return false;
+ return ResolvePath.Platform.auto.isSeparator(path[path.len - 1]);
+ }
+
+ const Kind = enum {
+ file,
+ dir,
+ };
+
+ pub fn isDir(_: *ShellCpTask, path: [:0]const u8) Maybe(bool) {
+ if (bun.Environment.isWindows) {
+ const attributes = bun.sys.getFileAttributes(path[0..path.len]) orelse {
+ const err: Syscall.Error = .{
+ .errno = @intFromEnum(bun.C.SystemErrno.ENOENT),
+ .syscall = .copyfile,
+ .path = path,
+ };
+ return .{ .err = err };
+ };
+
+ return .{ .result = attributes.is_directory };
+ }
+ const stat = switch (Syscall.lstat(path)) {
+ .result => |x| x,
+ .err => |e| {
+ return .{ .err = e };
+ },
+ };
+ return .{ .result = bun.S.ISDIR(stat.mode) };
+ }
+
+ fn enqueueToEventLoop(this: *ShellCpTask) void {
+ if (this.event_loop == .js) {
+ this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
+ } else {
+ this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
+ }
+ }
+
+ pub fn runFromMainThread(this: *ShellCpTask) void {
+ debug("runFromMainThread", .{});
+ this.cp.onShellCpTaskDone(this);
+ }
+
+ pub fn runFromMainThreadMini(this: *ShellCpTask, _: *void) void {
+ this.runFromMainThread();
+ }
+
+ pub fn runFromThreadPool(task: *WorkPoolTask) void {
+ debug("runFromThreadPool", .{});
+ var this: *@This() = @fieldParentPtr("task", task);
+ if (this.runFromThreadPoolImpl()) |e| {
+ this.err = e;
+ this.enqueueToEventLoop();
+ return;
+ }
+ }
+
+ fn runFromThreadPoolImpl(this: *ShellCpTask) ?bun.shell.ShellErr {
+ var buf2: bun.PathBuffer = undefined;
+ var buf3: bun.PathBuffer = undefined;
+ // We have to give an absolute path to our cp
+ // implementation for it to work with cwd
+ const src: [:0]const u8 = brk: {
+ if (ResolvePath.Platform.auto.isAbsolute(this.src)) break :brk this.src;
+ const parts: []const []const u8 = &.{
+ this.cwd_path[0..],
+ this.src[0..],
+ };
+ break :brk ResolvePath.joinZ(parts, .auto);
+ };
+ var tgt: [:0]const u8 = brk: {
+ if (ResolvePath.Platform.auto.isAbsolute(this.tgt)) break :brk this.tgt;
+ const parts: []const []const u8 = &.{
+ this.cwd_path[0..],
+ this.tgt[0..],
+ };
+ break :brk ResolvePath.joinZBuf(buf2[0..bun.MAX_PATH_BYTES], parts, .auto);
+ };
+
+ // Cases:
+ // SRC DEST
+ // ----------------
+ // file -> file
+ // file -> folder
+ // folder -> folder
+ // ----------------
+ // We need to check dest to see what it is
+ // If it doesn't exist we need to create it
+ const src_is_dir = switch (this.isDir(src)) {
+ .result => |x| x,
+ .err => |e| return bun.shell.ShellErr.newSys(e),
+ };
+
+ // Any source directory without -R is an error
+ if (src_is_dir and !this.opts.recursive) {
+ const errmsg = std.fmt.allocPrint(bun.default_allocator, "{s} is a directory (not copied)", .{this.src}) catch bun.outOfMemory();
+ return .{ .custom = errmsg };
+ }
+
+ if (!src_is_dir and bun.strings.eql(src, tgt)) {
+ const errmsg = std.fmt.allocPrint(bun.default_allocator, "{s} and {s} are identical (not copied)", .{ this.src, this.src }) catch bun.outOfMemory();
+ return .{ .custom = errmsg };
+ }
+
+ const tgt_is_dir: bool, const tgt_exists: bool = switch (this.isDir(tgt)) {
+ .result => |is_dir| .{ is_dir, true },
+ .err => |e| brk: {
+ if (e.getErrno() == bun.C.E.NOENT) {
+ // If it has a trailing directory separator, its a directory
+ const is_dir = hasTrailingSep(tgt);
+ break :brk .{ is_dir, false };
+ }
+ return bun.shell.ShellErr.newSys(e);
+ },
+ };
+
+ var copying_many = false;
+
+ // Note:
+ // The following logic is based on the POSIX spec:
+ // https://man7.org/linux/man-pages/man1/cp.1p.html
+
+ // Handle the "1st synopsis": source_file -> target_file
+ if (!src_is_dir and !tgt_is_dir and this.operands == 2) {
+ // Don't need to do anything here
+ }
+ // Handle the "2nd synopsis": -R source_files... -> target
+ else if (this.opts.recursive) {
+ if (tgt_exists) {
+ const basename = ResolvePath.basename(src[0..src.len]);
+ const parts: []const []const u8 = &.{
+ tgt[0..tgt.len],
+ basename,
+ };
+ tgt = ResolvePath.joinZBuf(buf3[0..bun.MAX_PATH_BYTES], parts, .auto);
+ } else if (this.operands == 2) {
+ // source_dir -> new_target_dir
+ } else {
+ const errmsg = std.fmt.allocPrint(bun.default_allocator, "directory {s} does not exist", .{this.tgt}) catch bun.outOfMemory();
+ return .{ .custom = errmsg };
+ }
+ copying_many = true;
+ }
+ // Handle the "3rd synopsis": source_files... -> target
+ else {
+ if (src_is_dir) return .{ .custom = std.fmt.allocPrint(bun.default_allocator, "{s} is a directory (not copied)", .{this.src}) catch bun.outOfMemory() };
+ if (!tgt_exists or !tgt_is_dir) return .{ .custom = std.fmt.allocPrint(bun.default_allocator, "{s} is not a directory", .{this.tgt}) catch bun.outOfMemory() };
+ const basename = ResolvePath.basename(src[0..src.len]);
+ const parts: []const []const u8 = &.{
+ tgt[0..tgt.len],
+ basename,
+ };
+ tgt = ResolvePath.joinZBuf(buf3[0..bun.MAX_PATH_BYTES], parts, .auto);
+ copying_many = true;
+ }
+
+ this.src_absolute = bun.default_allocator.dupeZ(u8, src[0..src.len]) catch bun.outOfMemory();
+ this.tgt_absolute = bun.default_allocator.dupeZ(u8, tgt[0..tgt.len]) catch bun.outOfMemory();
+
+ const args = JSC.Node.Arguments.Cp{
+ .src = JSC.Node.PathLike{ .string = bun.PathString.init(this.src_absolute.?) },
+ .dest = JSC.Node.PathLike{ .string = bun.PathString.init(this.tgt_absolute.?) },
+ .flags = .{
+ .mode = @enumFromInt(0),
+ .recursive = this.opts.recursive,
+ .force = true,
+ .errorOnExist = false,
+ .deinit_paths = false,
+ },
+ };
+
+ debug("Scheduling {s} -> {s}", .{ this.src_absolute.?, this.tgt_absolute.? });
+ if (this.event_loop == .js) {
+ const vm: *JSC.VirtualMachine = this.event_loop.js.getVmImpl();
+ debug("Yoops", .{});
+ _ = JSC.Node.ShellAsyncCpTask.createWithShellTask(
+ vm.global,
+ args,
+ vm,
+ bun.ArenaAllocator.init(bun.default_allocator),
+ this,
+ false,
+ );
+ } else {
+ _ = JSC.Node.ShellAsyncCpTask.createMini(
+ args,
+ this.event_loop.mini,
+ bun.ArenaAllocator.init(bun.default_allocator),
+ this,
+ );
+ }
+
+ return null;
+ }
+
+ fn onSubtaskFinish(this: *ShellCpTask, err: Maybe(void)) void {
+ debug("onSubtaskFinish", .{});
+ if (err.asErr()) |e| {
+ this.err = bun.shell.ShellErr.newSys(e);
+ }
+ this.enqueueToEventLoop();
+ }
+
+ pub fn onCopyImpl(this: *ShellCpTask, src: [:0]const u8, dest: [:0]const u8) void {
+ this.verbose_output_lock.lock();
+ log("onCopy: {s} -> {s}\n", .{ src, dest });
+ defer this.verbose_output_lock.unlock();
+ var writer = this.verbose_output.writer();
+ writer.print("{s} -> {s}\n", .{ src, dest }) catch bun.outOfMemory();
+ }
+
+ pub fn cpOnCopy(this: *ShellCpTask, src_: anytype, dest_: anytype) void {
+ if (!this.opts.verbose) return;
+ if (comptime bun.Environment.isPosix) return this.onCopyImpl(src_, dest_);
+
+ var buf: bun.PathBuffer = undefined;
+ var buf2: bun.PathBuffer = undefined;
+ const src: [:0]const u8 = switch (@TypeOf(src_)) {
+ [:0]const u8, [:0]u8 => src_,
+ [:0]const u16, [:0]u16 => bun.strings.fromWPath(buf[0..], src_),
+ else => @compileError("Invalid type: " ++ @typeName(@TypeOf(src_))),
+ };
+ const dest: [:0]const u8 = switch (@TypeOf(dest_)) {
+ [:0]const u8, [:0]u8 => src_,
+ [:0]const u16, [:0]u16 => bun.strings.fromWPath(buf2[0..], dest_),
+ else => @compileError("Invalid type: " ++ @typeName(@TypeOf(dest_))),
+ };
+ this.onCopyImpl(src, dest);
+ }
+
+ pub fn cpOnFinish(this: *ShellCpTask, result: Maybe(void)) void {
+ this.onSubtaskFinish(result);
+ }
+};
+
+const Opts = packed struct {
+ /// -f
+ ///
+ /// If the destination file cannot be opened, remove it and create a
+ /// new file, without prompting for confirmation regardless of its
+ /// permissions. (The -f option overrides any previous -n option.) The
+ /// target file is not unlinked before the copy. Thus, any existing access
+ /// rights will be retained.
+ remove_and_create_new_file_if_not_found: bool = false,
+
+ /// -H
+ ///
+ /// Take actions based on the type and contents of the file
+ /// referenced by any symbolic link specified as a
+ /// source_file operand.
+ dereference_command_line_symlinks: bool = false,
+
+ /// -i
+ ///
+ /// Write a prompt to standard error before copying to any
+ /// existing non-directory destination file. If the
+ /// response from the standard input is affirmative, the
+ /// copy shall be attempted; otherwise, it shall not.
+ interactive: bool = false,
+
+ /// -L
+ ///
+ /// Take actions based on the type and contents of the file
+ /// referenced by any symbolic link specified as a
+ /// source_file operand or any symbolic links encountered
+ /// during traversal of a file hierarchy.
+ dereference_all_symlinks: bool = false,
+
+ /// -P
+ ///
+ /// Take actions on any symbolic link specified as a
+ /// source_file operand or any symbolic link encountered
+ /// during traversal of a file hierarchy.
+ preserve_symlinks: bool = false,
+
+ /// -p
+ ///
+ /// Duplicate the following characteristics of each source
+ /// file in the corresponding destination file:
+ /// 1. The time of last data modification and time of last
+ /// access.
+ /// 2. The user ID and group ID.
+ /// 3. The file permission bits and the S_ISUID and
+ /// S_ISGID bits.
+ preserve_file_attributes: bool = false,
+
+ /// -R
+ ///
+ /// Copy file hierarchies.
+ recursive: bool = false,
+
+ /// -v
+ ///
+ /// Cause cp to be verbose, showing files as they are copied.
+ verbose: bool = false,
+
+ /// -n
+ ///
+ /// Do not overwrite an existing file. (The -n option overrides any previous -f or -i options.)
+ overwrite_existing_file: bool = true,
+
+ const Parse = FlagParser(*@This());
+
+ pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) {
+ return Parse.parseFlags(opts, args);
+ }
+
+ pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult {
+ _ = this;
+ _ = flag;
+ return null;
+ }
+
+ pub fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
+ switch (char) {
+ 'f' => {
+ return .{ .unsupported = unsupportedFlag("-f") };
+ },
+ 'H' => {
+ return .{ .unsupported = unsupportedFlag("-H") };
+ },
+ 'i' => {
+ return .{ .unsupported = unsupportedFlag("-i") };
+ },
+ 'L' => {
+ return .{ .unsupported = unsupportedFlag("-L") };
+ },
+ 'P' => {
+ return .{ .unsupported = unsupportedFlag("-P") };
+ },
+ 'p' => {
+ return .{ .unsupported = unsupportedFlag("-P") };
+ },
+ 'R' => {
+ this.recursive = true;
+ return .continue_parsing;
+ },
+ 'v' => {
+ this.verbose = true;
+ return .continue_parsing;
+ },
+ 'n' => {
+ this.overwrite_existing_file = true;
+ this.remove_and_create_new_file_if_not_found = false;
+ return .continue_parsing;
+ },
+ else => {
+ return .{ .illegal_option = smallflags[i..] };
+ },
+ }
+
+ return null;
+ }
+};
+
+// --
+const log = bun.Output.scoped(.cp, true);
+const ArrayList = std.ArrayList;
+const Syscall = bun.sys;
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Cp = @This();
+const CoroutineResult = interpreter.CoroutineResult;
+const OutputTask = interpreter.OutputTask;
+const assert = bun.assert;
+
+const OutputSrc = interpreter.OutputSrc;
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const WorkPool = JSC.WorkPool;
+const WorkPoolTask = JSC.WorkPoolTask;
+const ResolvePath = bun.path;
diff --git a/src/shell/builtin/dirname.zig b/src/shell/builtin/dirname.zig
new file mode 100644
index 0000000000..a61ef308a9
--- /dev/null
+++ b/src/shell/builtin/dirname.zig
@@ -0,0 +1,95 @@
+state: enum { idle, waiting_io, err, done } = .idle,
+buf: std.ArrayListUnmanaged(u8) = .{},
+
+pub fn start(this: *@This()) Maybe(void) {
+ const args = this.bltn().argsSlice();
+ var iter = bun.SliceIterator([*:0]const u8).init(args);
+
+ if (args.len == 0) return this.fail(Builtin.Kind.usageString(.dirname));
+
+ while (iter.next()) |item| {
+ const arg = bun.sliceTo(item, 0);
+ _ = this.print(bun.path.dirname(arg, .posix));
+ _ = this.print("\n");
+ }
+
+ this.state = .done;
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.bltn().stdout.enqueue(this, this.buf.items, safeguard);
+ } else {
+ this.bltn().done(0);
+ }
+ return Maybe(void).success;
+}
+
+pub fn deinit(this: *@This()) void {
+ this.buf.deinit(bun.default_allocator);
+ //dirname
+}
+
+fn fail(this: *@This(), msg: []const u8) Maybe(void) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .err;
+ this.bltn().stderr.enqueue(this, msg, safeguard);
+ return Maybe(void).success;
+ }
+ _ = this.bltn().writeNoIO(.stderr, msg);
+ this.bltn().done(1);
+ return Maybe(void).success;
+}
+
+fn print(this: *@This(), msg: []const u8) Maybe(void) {
+ if (this.bltn().stdout.needsIO() != null) {
+ this.buf.appendSlice(bun.default_allocator, msg) catch bun.outOfMemory();
+ return Maybe(void).success;
+ }
+ const res = this.bltn().writeNoIO(.stdout, msg);
+ if (res == .err) return Maybe(void).initErr(res.err);
+ return Maybe(void).success;
+}
+
+pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void {
+ if (maybe_e) |e| {
+ defer e.deref();
+ this.state = .err;
+ this.bltn().done(1);
+ return;
+ }
+ switch (this.state) {
+ .done => this.bltn().done(0),
+ .err => this.bltn().done(1),
+ else => {},
+ }
+}
+
+pub inline fn bltn(this: *@This()) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("dirname", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const debug = bun.Output.scoped(.ShellCat, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Dirname = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
diff --git a/src/shell/builtin/echo.zig b/src/shell/builtin/echo.zig
new file mode 100644
index 0000000000..4eb9fc5b6e
--- /dev/null
+++ b/src/shell/builtin/echo.zig
@@ -0,0 +1,92 @@
+/// Should be allocated with the arena from Builtin
+output: std.ArrayList(u8),
+
+state: union(enum) {
+ idle,
+ waiting,
+ done,
+} = .idle,
+
+pub fn start(this: *Echo) Maybe(void) {
+ const args = this.bltn().argsSlice();
+
+ var has_leading_newline: bool = false;
+ const args_len = args.len;
+ for (args, 0..) |arg, i| {
+ const thearg = std.mem.span(arg);
+ if (i < args_len - 1) {
+ this.output.appendSlice(thearg) catch bun.outOfMemory();
+ this.output.append(' ') catch bun.outOfMemory();
+ } else {
+ if (thearg.len > 0 and thearg[thearg.len - 1] == '\n') {
+ has_leading_newline = true;
+ }
+ this.output.appendSlice(bun.strings.trimSubsequentLeadingChars(thearg, '\n')) catch bun.outOfMemory();
+ }
+ }
+
+ if (!has_leading_newline) this.output.append('\n') catch bun.outOfMemory();
+
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.state = .waiting;
+ this.bltn().stdout.enqueue(this, this.output.items[0..], safeguard);
+ return Maybe(void).success;
+ }
+ _ = this.bltn().writeNoIO(.stdout, this.output.items[0..]);
+ this.state = .done;
+ this.bltn().done(0);
+ return Maybe(void).success;
+}
+
+pub fn onIOWriterChunk(this: *Echo, _: usize, e: ?JSC.SystemError) void {
+ if (comptime bun.Environment.allow_assert) {
+ assert(this.state == .waiting);
+ }
+
+ if (e != null) {
+ defer e.?.deref();
+ this.bltn().done(e.?.getErrno());
+ return;
+ }
+
+ this.state = .done;
+ this.bltn().done(0);
+}
+
+pub fn deinit(this: *Echo) void {
+ log("({s}) deinit", .{@tagName(.echo)});
+ this.output.deinit();
+}
+
+pub inline fn bltn(this: *Echo) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("echo", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const log = bun.Output.scoped(.echo, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Echo = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const assert = bun.assert;
diff --git a/src/shell/builtin/exit.zig b/src/shell/builtin/exit.zig
new file mode 100644
index 0000000000..3e5a008a38
--- /dev/null
+++ b/src/shell/builtin/exit.zig
@@ -0,0 +1,107 @@
+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\n")) % 256),
+ error.InvalidCharacter => return this.fail("exit: numeric argument required\n"),
+ };
+ this.bltn().done(exit_code);
+ return Maybe(void).success;
+ },
+ else => {
+ return this.fail("exit: too many arguments\n");
+ },
+ }
+}
+
+fn fail(this: *Exit, msg: []const u8) Maybe(void) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .waiting_io;
+ this.bltn().stderr.enqueue(this, msg, safeguard);
+ 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 => @panic("Unexpected \"idle\" state in Exit. This indicates a bug in Bun. Please file a GitHub issue."),
+ .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) {
+ 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;
+}
+
+pub inline fn bltn(this: *Exit) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("exit", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const log = bun.Output.scoped(.Exit, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Exit = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const assert = bun.assert;
diff --git a/src/shell/builtin/export.zig b/src/shell/builtin/export.zig
new file mode 100644
index 0000000000..f15203714e
--- /dev/null
+++ b/src/shell/builtin/export.zig
@@ -0,0 +1,146 @@
+printing: bool = false,
+
+const Entry = struct {
+ key: EnvStr,
+ value: EnvStr,
+
+ pub fn compare(context: void, this: @This(), other: @This()) bool {
+ return bun.strings.cmpStringsAsc(context, this.key.slice(), other.key.slice());
+ }
+};
+
+pub fn writeOutput(this: *Export, comptime io_kind: @Type(.enum_literal), comptime fmt: []const u8, args: anytype) Maybe(void) {
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ var output: *BuiltinIO.Output = &@field(this.bltn(), @tagName(io_kind));
+ this.printing = true;
+ output.enqueueFmtBltn(this, .@"export", fmt, args, safeguard);
+ return Maybe(void).success;
+ }
+
+ const buf = this.bltn().fmtErrorArena(.@"export", fmt, args);
+ _ = this.bltn().writeNoIO(io_kind, buf);
+ this.bltn().done(0);
+ return Maybe(void).success;
+}
+
+pub fn onIOWriterChunk(this: *Export, _: usize, e: ?JSC.SystemError) void {
+ if (comptime bun.Environment.allow_assert) {
+ assert(this.printing);
+ }
+
+ const exit_code: ExitCode = if (e != null) brk: {
+ defer e.?.deref();
+ break :brk @intFromEnum(e.?.getErrno());
+ } else 0;
+
+ this.bltn().done(exit_code);
+}
+
+pub fn start(this: *Export) Maybe(void) {
+ const args = this.bltn().argsSlice();
+
+ // Calling `export` with no arguments prints all exported variables lexigraphically ordered
+ if (args.len == 0) {
+ var arena = this.bltn().arena;
+
+ var keys = std.ArrayList(Entry).init(arena.allocator());
+ var iter = this.bltn().export_env.iterator();
+ while (iter.next()) |entry| {
+ keys.append(.{
+ .key = entry.key_ptr.*,
+ .value = entry.value_ptr.*,
+ }) catch bun.outOfMemory();
+ }
+
+ std.mem.sort(Entry, keys.items[0..], {}, Entry.compare);
+
+ const len = brk: {
+ var len: usize = 0;
+ for (keys.items) |entry| {
+ len += std.fmt.count("{s}={s}\n", .{ entry.key.slice(), entry.value.slice() });
+ }
+ break :brk len;
+ };
+ var buf = arena.allocator().alloc(u8, len) catch bun.outOfMemory();
+ {
+ var i: usize = 0;
+ for (keys.items) |entry| {
+ const written_slice = std.fmt.bufPrint(buf[i..], "{s}={s}\n", .{ entry.key.slice(), entry.value.slice() }) catch @panic("This should not happen");
+ i += written_slice.len;
+ }
+ }
+
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.printing = true;
+ this.bltn().stdout.enqueue(this, buf, safeguard);
+
+ return Maybe(void).success;
+ }
+
+ _ = this.bltn().writeNoIO(.stdout, buf);
+ this.bltn().done(0);
+ return Maybe(void).success;
+ }
+
+ for (args) |arg_raw| {
+ const arg_sentinel = arg_raw[0..std.mem.len(arg_raw) :0];
+ const arg = arg_sentinel[0..arg_sentinel.len];
+ if (arg.len == 0) continue;
+
+ const eqsign_idx = std.mem.indexOfScalar(u8, arg, '=') orelse {
+ if (!shell.isValidVarName(arg)) {
+ const buf = this.bltn().fmtErrorArena(.@"export", "`{s}`: not a valid identifier", .{arg});
+ return this.writeOutput(.stderr, "{s}\n", .{buf});
+ }
+ this.bltn().parentCmd().base.shell.assignVar(this.bltn().parentCmd().base.interpreter, EnvStr.initSlice(arg), EnvStr.initSlice(""), .exported);
+ continue;
+ };
+
+ const label = arg[0..eqsign_idx];
+ const value = arg_sentinel[eqsign_idx + 1 .. :0];
+ this.bltn().parentCmd().base.shell.assignVar(this.bltn().parentCmd().base.interpreter, EnvStr.initSlice(label), EnvStr.initSlice(value), .exported);
+ }
+
+ this.bltn().done(0);
+ return Maybe(void).success;
+}
+
+pub fn deinit(this: *Export) void {
+ log("({s}) deinit", .{@tagName(.@"export")});
+ _ = this;
+}
+
+pub inline fn bltn(this: *Export) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("export", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const debug = bun.Output.scoped(.ShellExport, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Export = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = JSC.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+const log = debug;
+const EnvStr = interpreter.EnvStr;
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const BuiltinIO = Interpreter.Builtin.BuiltinIO;
+const assert = bun.assert;
diff --git a/src/shell/builtin/false.zig b/src/shell/builtin/false.zig
new file mode 100644
index 0000000000..f5af97ddad
--- /dev/null
+++ b/src/shell/builtin/false.zig
@@ -0,0 +1,26 @@
+pub fn start(this: *@This()) Maybe(void) {
+ this.bltn().done(1);
+ return Maybe(void).success;
+}
+
+pub fn onIOWriterChunk(_: *@This(), _: usize, _: ?JSC.SystemError) void {
+ // no IO is done
+}
+
+pub fn deinit(this: *@This()) void {
+ _ = this;
+}
+
+pub inline fn bltn(this: *@This()) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("false", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const bun = @import("root").bun;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
diff --git a/src/shell/builtin/ls.zig b/src/shell/builtin/ls.zig
new file mode 100644
index 0000000000..0a93977fff
--- /dev/null
+++ b/src/shell/builtin/ls.zig
@@ -0,0 +1,812 @@
+opts: Opts = .{},
+
+state: union(enum) {
+ idle,
+ exec: struct {
+ err: ?Syscall.Error = null,
+ task_count: std.atomic.Value(usize),
+ tasks_done: usize = 0,
+ output_waiting: usize = 0,
+ output_done: usize = 0,
+ },
+ waiting_write_err,
+ done,
+} = .idle,
+
+pub fn start(this: *Ls) Maybe(void) {
+ this.next();
+ return Maybe(void).success;
+}
+
+pub fn writeFailingError(this: *Ls, buf: []const u8, exit_code: ExitCode) Maybe(void) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .waiting_write_err;
+ this.bltn().stderr.enqueue(this, buf, safeguard);
+ return Maybe(void).success;
+ }
+
+ _ = this.bltn().writeNoIO(.stderr, buf);
+
+ this.bltn().done(exit_code);
+ return Maybe(void).success;
+}
+
+fn next(this: *Ls) void {
+ while (!(this.state == .done)) {
+ switch (this.state) {
+ .idle => {
+ // Will be null if called with no args, in which case we just run once with "." directory
+ const paths: ?[]const [*:0]const u8 = switch (this.parseOpts()) {
+ .ok => |paths| paths,
+ .err => |e| {
+ const buf = switch (e) {
+ .illegal_option => |opt_str| this.bltn().fmtErrorArena(.ls, "illegal option -- {s}\n", .{opt_str}),
+ .show_usage => Builtin.Kind.ls.usageString(),
+ };
+
+ _ = this.writeFailingError(buf, 1);
+ return;
+ },
+ };
+
+ const task_count = if (paths) |p| p.len else 1;
+
+ this.state = .{
+ .exec = .{
+ .task_count = std.atomic.Value(usize).init(task_count),
+ },
+ };
+
+ const cwd = this.bltn().cwd;
+ if (paths) |p| {
+ for (p) |path_raw| {
+ const path = path_raw[0..std.mem.len(path_raw) :0];
+ var task = ShellLsTask.create(this, this.opts, &this.state.exec.task_count, cwd, path, this.bltn().eventLoop());
+ task.schedule();
+ }
+ } else {
+ var task = ShellLsTask.create(this, this.opts, &this.state.exec.task_count, cwd, ".", this.bltn().eventLoop());
+ task.schedule();
+ }
+ },
+ .exec => {
+ // It's done
+ log("Ls(0x{x}, state=exec) Check: tasks_done={d} task_count={d} output_done={d} output_waiting={d}", .{
+ @intFromPtr(this),
+ this.state.exec.tasks_done,
+ this.state.exec.task_count.load(.monotonic),
+ this.state.exec.output_done,
+ this.state.exec.output_waiting,
+ });
+ if (this.state.exec.tasks_done >= this.state.exec.task_count.load(.monotonic) and this.state.exec.output_done >= this.state.exec.output_waiting) {
+ const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0;
+ this.state = .done;
+ this.bltn().done(exit_code);
+ return;
+ }
+ return;
+ },
+ .waiting_write_err => {
+ return;
+ },
+ .done => unreachable,
+ }
+ }
+
+ this.bltn().done(0);
+ return;
+}
+
+pub fn deinit(_: *Ls) void {}
+
+pub fn onIOWriterChunk(this: *Ls, _: usize, e: ?JSC.SystemError) void {
+ if (e) |err| err.deref();
+ if (this.state == .waiting_write_err) {
+ return this.bltn().done(1);
+ }
+ this.state.exec.output_done += 1;
+ this.next();
+}
+
+pub fn onShellLsTaskDone(this: *Ls, task: *ShellLsTask) void {
+ defer task.deinit(true);
+ this.state.exec.tasks_done += 1;
+ var output = task.takeOutput();
+ const err_ = task.err;
+
+ // TODO: Reuse the *ShellLsTask allocation
+ const output_task: *ShellLsOutputTask = bun.new(ShellLsOutputTask, .{
+ .parent = this,
+ .output = .{ .arrlist = output.moveToUnmanaged() },
+ .state = .waiting_write_err,
+ });
+
+ if (err_) |err| {
+ this.state.exec.err = err;
+ const error_string = this.bltn().taskErrorToString(.ls, err);
+ output_task.start(error_string);
+ return;
+ }
+ output_task.start(null);
+}
+
+pub const ShellLsOutputTask = OutputTask(Ls, .{
+ .writeErr = ShellLsOutputTaskVTable.writeErr,
+ .onWriteErr = ShellLsOutputTaskVTable.onWriteErr,
+ .writeOut = ShellLsOutputTaskVTable.writeOut,
+ .onWriteOut = ShellLsOutputTaskVTable.onWriteOut,
+ .onDone = ShellLsOutputTaskVTable.onDone,
+});
+
+const ShellLsOutputTaskVTable = struct {
+ pub fn writeErr(this: *Ls, childptr: anytype, errbuf: []const u8) CoroutineResult {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state.exec.output_waiting += 1;
+ this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
+ return .yield;
+ }
+ _ = this.bltn().writeNoIO(.stderr, errbuf);
+ return .cont;
+ }
+
+ pub fn onWriteErr(this: *Ls) void {
+ this.state.exec.output_done += 1;
+ }
+
+ pub fn writeOut(this: *Ls, childptr: anytype, output: *OutputSrc) CoroutineResult {
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.state.exec.output_waiting += 1;
+ this.bltn().stdout.enqueue(childptr, output.slice(), safeguard);
+ return .yield;
+ }
+ _ = this.bltn().writeNoIO(.stdout, output.slice());
+ return .cont;
+ }
+
+ pub fn onWriteOut(this: *Ls) void {
+ this.state.exec.output_done += 1;
+ }
+
+ pub fn onDone(this: *Ls) void {
+ this.next();
+ }
+};
+
+pub const ShellLsTask = struct {
+ const debug = bun.Output.scoped(.ShellLsTask, true);
+ ls: *Ls,
+ opts: Opts,
+
+ is_root: bool = true,
+ task_count: *std.atomic.Value(usize),
+
+ cwd: bun.FileDescriptor,
+ /// Should be allocated with bun.default_allocator
+ path: [:0]const u8 = &[0:0]u8{},
+ /// Should use bun.default_allocator
+ output: std.ArrayList(u8),
+ is_absolute: bool = false,
+ err: ?Syscall.Error = null,
+ result_kind: enum { file, dir, idk } = .idk,
+
+ event_loop: JSC.EventLoopHandle,
+ concurrent_task: JSC.EventLoopTask,
+ task: JSC.WorkPoolTask = .{
+ .callback = workPoolCallback,
+ },
+
+ pub fn schedule(this: *@This()) void {
+ JSC.WorkPool.schedule(&this.task);
+ }
+
+ pub fn create(ls: *Ls, opts: Opts, task_count: *std.atomic.Value(usize), cwd: bun.FileDescriptor, path: [:0]const u8, event_loop: JSC.EventLoopHandle) *@This() {
+ const task = bun.default_allocator.create(@This()) catch bun.outOfMemory();
+ task.* = @This(){
+ .ls = ls,
+ .opts = opts,
+ .cwd = cwd,
+ .path = bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory(),
+ .output = std.ArrayList(u8).init(bun.default_allocator),
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(event_loop),
+ .event_loop = event_loop,
+ .task_count = task_count,
+ };
+ return task;
+ }
+
+ pub fn enqueue(this: *@This(), path: [:0]const u8) void {
+ debug("enqueue: {s}", .{path});
+ const new_path = this.join(
+ bun.default_allocator,
+ &[_][]const u8{
+ this.path[0..this.path.len],
+ path[0..path.len],
+ },
+ this.is_absolute,
+ );
+
+ var subtask = @This().create(this.ls, this.opts, this.task_count, this.cwd, new_path, this.event_loop);
+ _ = this.task_count.fetchAdd(1, .monotonic);
+ subtask.is_root = false;
+ subtask.schedule();
+ }
+
+ inline fn join(_: *@This(), alloc: Allocator, subdir_parts: []const []const u8, is_absolute: bool) [:0]const u8 {
+ if (!is_absolute) {
+ // If relative paths enabled, stdlib join is preferred over
+ // ResolvePath.joinBuf because it doesn't try to normalize the path
+ return std.fs.path.joinZ(alloc, subdir_parts) catch bun.outOfMemory();
+ }
+
+ const out = alloc.dupeZ(u8, bun.path.join(subdir_parts, .auto)) catch bun.outOfMemory();
+
+ return out;
+ }
+
+ pub fn run(this: *@This()) void {
+ const fd = switch (ShellSyscall.openat(this.cwd, this.path, bun.O.RDONLY | bun.O.DIRECTORY, 0)) {
+ .err => |e| {
+ switch (e.getErrno()) {
+ bun.C.E.NOENT => {
+ this.err = this.errorWithPath(e, this.path);
+ },
+ bun.C.E.NOTDIR => {
+ this.result_kind = .file;
+ this.addEntry(this.path);
+ },
+ else => {
+ this.err = this.errorWithPath(e, this.path);
+ },
+ }
+ return;
+ },
+ .result => |fd| fd,
+ };
+
+ defer {
+ _ = Syscall.close(fd);
+ debug("run done", .{});
+ }
+
+ if (!this.opts.list_directories) {
+ if (!this.is_root) {
+ const writer = this.output.writer();
+ std.fmt.format(writer, "{s}:\n", .{this.path}) catch bun.outOfMemory();
+ }
+
+ var iterator = DirIterator.iterate(fd.asDir(), .u8);
+ var entry = iterator.next();
+
+ while (switch (entry) {
+ .err => |e| {
+ this.err = this.errorWithPath(e, this.path);
+ return;
+ },
+ .result => |ent| ent,
+ }) |current| : (entry = iterator.next()) {
+ this.addEntry(current.name.sliceAssumeZ());
+ if (current.kind == .directory and this.opts.recursive) {
+ this.enqueue(current.name.sliceAssumeZ());
+ }
+ }
+
+ return;
+ }
+
+ const writer = this.output.writer();
+ std.fmt.format(writer, "{s}\n", .{this.path}) catch bun.outOfMemory();
+ return;
+ }
+
+ fn shouldSkipEntry(this: *@This(), name: [:0]const u8) bool {
+ if (this.opts.show_all) return false;
+ if (this.opts.show_almost_all) {
+ if (bun.strings.eqlComptime(name[0..1], ".") or bun.strings.eqlComptime(name[0..2], "..")) return true;
+ }
+ return false;
+ }
+
+ // TODO more complex output like multi-column
+ fn addEntry(this: *@This(), name: [:0]const u8) void {
+ const skip = this.shouldSkipEntry(name);
+ debug("Entry: (skip={}) {s} :: {s}", .{ skip, this.path, name });
+ if (skip) return;
+ this.output.ensureUnusedCapacity(name.len + 1) catch bun.outOfMemory();
+ this.output.appendSlice(name) catch bun.outOfMemory();
+ this.output.append('\n') catch bun.outOfMemory();
+ }
+
+ fn errorWithPath(this: *@This(), err: Syscall.Error, path: [:0]const u8) Syscall.Error {
+ _ = this;
+ return err.withPath(bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory());
+ }
+
+ pub fn workPoolCallback(task: *JSC.WorkPoolTask) void {
+ var this: *@This() = @fieldParentPtr("task", task);
+ this.run();
+ this.doneLogic();
+ }
+
+ fn doneLogic(this: *@This()) void {
+ debug("Done", .{});
+ if (this.event_loop == .js) {
+ this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
+ } else {
+ this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
+ }
+ }
+
+ pub fn takeOutput(this: *@This()) std.ArrayList(u8) {
+ const ret = this.output;
+ this.output = std.ArrayList(u8).init(bun.default_allocator);
+ return ret;
+ }
+
+ pub fn runFromMainThread(this: *@This()) void {
+ debug("runFromMainThread", .{});
+ this.ls.onShellLsTaskDone(this);
+ }
+
+ pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
+ this.runFromMainThread();
+ }
+
+ pub fn deinit(this: *@This(), comptime free_this: bool) void {
+ debug("deinit {s}", .{if (free_this) "free_this=true" else "free_this=false"});
+ bun.default_allocator.free(this.path);
+ this.output.deinit();
+ if (comptime free_this) bun.default_allocator.destroy(this);
+ }
+};
+
+const Opts = struct {
+ /// `-a`, `--all`
+ /// Do not ignore entries starting with .
+ show_all: bool = false,
+
+ /// `-A`, `--almost-all`
+ /// Do not list implied . and ..
+ show_almost_all: bool = true,
+
+ /// `--author`
+ /// With -l, print the author of each file
+ show_author: bool = false,
+
+ /// `-b`, `--escape`
+ /// Print C-style escapes for nongraphic characters
+ escape: bool = false,
+
+ /// `--block-size=SIZE`
+ /// With -l, scale sizes by SIZE when printing them; e.g., '--block-size=M'
+ block_size: ?usize = null,
+
+ /// `-B`, `--ignore-backups`
+ /// Do not list implied entries ending with ~
+ ignore_backups: bool = false,
+
+ /// `-c`
+ /// Sort by, and show, ctime (time of last change of file status information); affects sorting and display based on options
+ use_ctime: bool = false,
+
+ /// `-C`
+ /// List entries by columns
+ list_by_columns: bool = false,
+
+ /// `--color[=WHEN]`
+ /// Color the output; WHEN can be 'always', 'auto', or 'never'
+ color: ?[]const u8 = null,
+
+ /// `-d`, `--directory`
+ /// List directories themselves, not their contents
+ list_directories: bool = false,
+
+ /// `-D`, `--dired`
+ /// Generate output designed for Emacs' dired mode
+ dired_mode: bool = false,
+
+ /// `-f`
+ /// List all entries in directory order
+ unsorted: bool = false,
+
+ /// `-F`, `--classify[=WHEN]`
+ /// Append indicator (one of */=>@|) to entries; WHEN can be 'always', 'auto', or 'never'
+ classify: ?[]const u8 = null,
+
+ /// `--file-type`
+ /// Likewise, except do not append '*'
+ file_type: bool = false,
+
+ /// `--format=WORD`
+ /// Specify format: 'across', 'commas', 'horizontal', 'long', 'single-column', 'verbose', 'vertical'
+ format: ?[]const u8 = null,
+
+ /// `--full-time`
+ /// Like -l --time-style=full-iso
+ full_time: bool = false,
+
+ /// `-g`
+ /// Like -l, but do not list owner
+ no_owner: bool = false,
+
+ /// `--group-directories-first`
+ /// Group directories before files
+ group_directories_first: bool = false,
+
+ /// `-G`, `--no-group`
+ /// In a long listing, don't print group names
+ no_group: bool = false,
+
+ /// `-h`, `--human-readable`
+ /// With -l and -s, print sizes like 1K 234M 2G etc.
+ human_readable: bool = false,
+
+ /// `--si`
+ /// Use powers of 1000 not 1024 for sizes
+ si_units: bool = false,
+
+ /// `-H`, `--dereference-command-line`
+ /// Follow symbolic links listed on the command line
+ dereference_cmd_symlinks: bool = false,
+
+ /// `--dereference-command-line-symlink-to-dir`
+ /// Follow each command line symbolic link that points to a directory
+ dereference_cmd_dir_symlinks: bool = false,
+
+ /// `--hide=PATTERN`
+ /// Do not list entries matching shell PATTERN
+ hide_pattern: ?[]const u8 = null,
+
+ /// `--hyperlink[=WHEN]`
+ /// Hyperlink file names; WHEN can be 'always', 'auto', or 'never'
+ hyperlink: ?[]const u8 = null,
+
+ /// `--indicator-style=WORD`
+ /// Append indicator with style to entry names: 'none', 'slash', 'file-type', 'classify'
+ indicator_style: ?[]const u8 = null,
+
+ /// `-i`, `--inode`
+ /// Print the index number of each file
+ show_inode: bool = false,
+
+ /// `-I`, `--ignore=PATTERN`
+ /// Do not list entries matching shell PATTERN
+ ignore_pattern: ?[]const u8 = null,
+
+ /// `-k`, `--kibibytes`
+ /// Default to 1024-byte blocks for file system usage
+ kibibytes: bool = false,
+
+ /// `-l`
+ /// Use a long listing format
+ long_listing: bool = false,
+
+ /// `-L`, `--dereference`
+ /// Show information for the file the symbolic link references
+ dereference: bool = false,
+
+ /// `-m`
+ /// Fill width with a comma separated list of entries
+ comma_separated: bool = false,
+
+ /// `-n`, `--numeric-uid-gid`
+ /// Like -l, but list numeric user and group IDs
+ numeric_uid_gid: bool = false,
+
+ /// `-N`, `--literal`
+ /// Print entry names without quoting
+ literal: bool = false,
+
+ /// `-o`
+ /// Like -l, but do not list group information
+ no_group_info: bool = false,
+
+ /// `-p`, `--indicator-style=slash`
+ /// Append / indicator to directories
+ slash_indicator: bool = false,
+
+ /// `-q`, `--hide-control-chars`
+ /// Print ? instead of nongraphic characters
+ hide_control_chars: bool = false,
+
+ /// `--show-control-chars`
+ /// Show nongraphic characters as-is
+ show_control_chars: bool = false,
+
+ /// `-Q`, `--quote-name`
+ /// Enclose entry names in double quotes
+ quote_name: bool = false,
+
+ /// `--quoting-style=WORD`
+ /// Use quoting style for entry names
+ quoting_style: ?[]const u8 = null,
+
+ /// `-r`, `--reverse`
+ /// Reverse order while sorting
+ reverse_order: bool = false,
+
+ /// `-R`, `--recursive`
+ /// List subdirectories recursively
+ recursive: bool = false,
+
+ /// `-s`, `--size`
+ /// Print the allocated size of each file, in blocks
+ show_size: bool = false,
+
+ /// `-S`
+ /// Sort by file size, largest first
+ sort_by_size: bool = false,
+
+ /// `--sort=WORD`
+ /// Sort by a specified attribute
+ sort_method: ?[]const u8 = null,
+
+ /// `--time=WORD`
+ /// Select which timestamp to use for display or sorting
+ time_method: ?[]const u8 = null,
+
+ /// `--time-style=TIME_STYLE`
+ /// Time/date format with -l
+ time_style: ?[]const u8 = null,
+
+ /// `-t`
+ /// Sort by time, newest first
+ sort_by_time: bool = false,
+
+ /// `-T`, `--tabsize=COLS`
+ /// Assume tab stops at each specified number of columns
+ tabsize: ?usize = null,
+
+ /// `-u`
+ /// Sort by, and show, access time
+ use_atime: bool = false,
+
+ /// `-U`
+ /// Do not sort; list entries in directory order
+ no_sort: bool = false,
+
+ /// `-v`
+ /// Natural sort of (version) numbers within text
+ natural_sort: bool = false,
+
+ /// `-w`, `--width=COLS`
+ /// Set output width to specified number of columns
+ output_width: ?usize = null,
+
+ /// `-x`
+ /// List entries by lines instead of by columns
+ list_by_lines: bool = false,
+
+ /// `-X`
+ /// Sort alphabetically by entry extension
+ sort_by_extension: bool = false,
+
+ /// `-Z`, `--context`
+ /// Print any security context of each file
+ show_context: bool = false,
+
+ /// `--zero`
+ /// End each output line with NUL, not newline
+ end_with_nul: bool = false,
+
+ /// `-1`
+ /// List one file per line
+ one_file_per_line: bool = false,
+
+ /// `--help`
+ /// Display help and exit
+ show_help: bool = false,
+
+ /// `--version`
+ /// Output version information and exit
+ show_version: bool = false,
+
+ /// Custom parse error for invalid options
+ const ParseError = union(enum) {
+ illegal_option: []const u8,
+ show_usage,
+ };
+};
+
+pub fn parseOpts(this: *Ls) Result(?[]const [*:0]const u8, Opts.ParseError) {
+ return this.parseFlags();
+}
+
+pub fn parseFlags(this: *Ls) Result(?[]const [*:0]const u8, Opts.ParseError) {
+ const args = this.bltn().argsSlice();
+ var idx: usize = 0;
+ if (args.len == 0) {
+ return .{ .ok = null };
+ }
+
+ while (idx < args.len) : (idx += 1) {
+ const flag = args[idx];
+ switch (this.parseFlag(flag[0..std.mem.len(flag)])) {
+ .done => {
+ const filepath_args = args[idx..];
+ return .{ .ok = filepath_args };
+ },
+ .continue_parsing => {},
+ .illegal_option => |opt_str| return .{ .err = .{ .illegal_option = opt_str } },
+ }
+ }
+
+ return .{ .err = .show_usage };
+}
+
+pub fn parseFlag(this: *Ls, flag: []const u8) union(enum) { continue_parsing, done, illegal_option: []const u8 } {
+ if (flag.len == 0) return .done;
+ if (flag[0] != '-') return .done;
+
+ // FIXME windows
+ if (flag.len == 1) return .{ .illegal_option = "-" };
+
+ const small_flags = flag[1..];
+ for (small_flags) |char| {
+ switch (char) {
+ 'a' => {
+ this.opts.show_all = true;
+ },
+ 'A' => {
+ this.opts.show_almost_all = true;
+ },
+ 'b' => {
+ this.opts.escape = true;
+ },
+ 'B' => {
+ this.opts.ignore_backups = true;
+ },
+ 'c' => {
+ this.opts.use_ctime = true;
+ },
+ 'C' => {
+ this.opts.list_by_columns = true;
+ },
+ 'd' => {
+ this.opts.list_directories = true;
+ },
+ 'D' => {
+ this.opts.dired_mode = true;
+ },
+ 'f' => {
+ this.opts.unsorted = true;
+ },
+ 'F' => {
+ this.opts.classify = "always";
+ },
+ 'g' => {
+ this.opts.no_owner = true;
+ },
+ 'G' => {
+ this.opts.no_group = true;
+ },
+ 'h' => {
+ this.opts.human_readable = true;
+ },
+ 'H' => {
+ this.opts.dereference_cmd_symlinks = true;
+ },
+ 'i' => {
+ this.opts.show_inode = true;
+ },
+ 'I' => {
+ this.opts.ignore_pattern = ""; // This will require additional logic to handle patterns
+ },
+ 'k' => {
+ this.opts.kibibytes = true;
+ },
+ 'l' => {
+ this.opts.long_listing = true;
+ },
+ 'L' => {
+ this.opts.dereference = true;
+ },
+ 'm' => {
+ this.opts.comma_separated = true;
+ },
+ 'n' => {
+ this.opts.numeric_uid_gid = true;
+ },
+ 'N' => {
+ this.opts.literal = true;
+ },
+ 'o' => {
+ this.opts.no_group_info = true;
+ },
+ 'p' => {
+ this.opts.slash_indicator = true;
+ },
+ 'q' => {
+ this.opts.hide_control_chars = true;
+ },
+ 'Q' => {
+ this.opts.quote_name = true;
+ },
+ 'r' => {
+ this.opts.reverse_order = true;
+ },
+ 'R' => {
+ this.opts.recursive = true;
+ },
+ 's' => {
+ this.opts.show_size = true;
+ },
+ 'S' => {
+ this.opts.sort_by_size = true;
+ },
+ 't' => {
+ this.opts.sort_by_time = true;
+ },
+ 'T' => {
+ this.opts.tabsize = 8; // Default tab size, needs additional handling for custom sizes
+ },
+ 'u' => {
+ this.opts.use_atime = true;
+ },
+ 'U' => {
+ this.opts.no_sort = true;
+ },
+ 'v' => {
+ this.opts.natural_sort = true;
+ },
+ 'w' => {
+ this.opts.output_width = 0; // Default to no limit, needs additional handling for custom widths
+ },
+ 'x' => {
+ this.opts.list_by_lines = true;
+ },
+ 'X' => {
+ this.opts.sort_by_extension = true;
+ },
+ 'Z' => {
+ this.opts.show_context = true;
+ },
+ '1' => {
+ this.opts.one_file_per_line = true;
+ },
+ else => {
+ return .{ .illegal_option = flag[1..2] };
+ },
+ }
+ }
+
+ return .continue_parsing;
+}
+
+pub inline fn bltn(this: *Ls) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("ls", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+const Ls = @This();
+const log = bun.Output.scoped(.ls, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+const Syscall = bun.sys;
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const Allocator = std.mem.Allocator;
+const DirIterator = bun.DirIterator;
+const builtin = @import("builtin");
+const OutputNeedsIOSafeGuard = interpreter.OutputNeedsIOSafeGuard;
+const OutputTask = interpreter.OutputTask;
+const OutputSrc = interpreter.OutputSrc;
+const CoroutineResult = interpreter.CoroutineResult;
+const assert = bun.assert;
diff --git a/src/shell/builtin/mkdir.zig b/src/shell/builtin/mkdir.zig
new file mode 100644
index 0000000000..e2bd51fad0
--- /dev/null
+++ b/src/shell/builtin/mkdir.zig
@@ -0,0 +1,413 @@
+opts: Opts = .{},
+state: union(enum) {
+ idle,
+ exec: struct {
+ started: bool = false,
+ tasks_count: usize = 0,
+ tasks_done: usize = 0,
+ output_waiting: u16 = 0,
+ output_done: u16 = 0,
+ args: []const [*:0]const u8,
+ err: ?JSC.SystemError = null,
+ },
+ waiting_write_err,
+ done,
+} = .idle,
+
+pub fn onIOWriterChunk(this: *Mkdir, _: usize, e: ?JSC.SystemError) void {
+ if (e) |err| err.deref();
+
+ switch (this.state) {
+ .waiting_write_err => return this.bltn().done(1),
+ .exec => {
+ this.state.exec.output_done += 1;
+ },
+ .idle, .done => @panic("Invalid state"),
+ }
+
+ this.next();
+}
+pub fn writeFailingError(this: *Mkdir, buf: []const u8, exit_code: ExitCode) Maybe(void) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .waiting_write_err;
+ this.bltn().stderr.enqueue(this, buf, safeguard);
+ return Maybe(void).success;
+ }
+
+ _ = this.bltn().writeNoIO(.stderr, buf);
+ // if (this.bltn().writeNoIO(.stderr, buf).asErr()) |e| {
+ // return .{ .err = e };
+ // }
+
+ this.bltn().done(exit_code);
+ return Maybe(void).success;
+}
+
+pub fn start(this: *Mkdir) Maybe(void) {
+ const filepath_args = switch (this.opts.parse(this.bltn().argsSlice())) {
+ .ok => |filepath_args| filepath_args,
+ .err => |e| {
+ const buf = switch (e) {
+ .illegal_option => |opt_str| this.bltn().fmtErrorArena(.mkdir, "illegal option -- {s}\n", .{opt_str}),
+ .show_usage => Builtin.Kind.mkdir.usageString(),
+ .unsupported => |unsupported| this.bltn().fmtErrorArena(.mkdir, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
+ };
+
+ _ = this.writeFailingError(buf, 1);
+ return Maybe(void).success;
+ },
+ } orelse {
+ _ = this.writeFailingError(Builtin.Kind.mkdir.usageString(), 1);
+ return Maybe(void).success;
+ };
+
+ this.state = .{
+ .exec = .{
+ .args = filepath_args,
+ },
+ };
+
+ _ = this.next();
+
+ return Maybe(void).success;
+}
+
+pub fn next(this: *Mkdir) void {
+ switch (this.state) {
+ .idle => @panic("Invalid state"),
+ .exec => {
+ var exec = &this.state.exec;
+ if (exec.started) {
+ if (this.state.exec.tasks_done >= this.state.exec.tasks_count and this.state.exec.output_done >= this.state.exec.output_waiting) {
+ const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0;
+ if (this.state.exec.err) |e| e.deref();
+ this.state = .done;
+ this.bltn().done(exit_code);
+ return;
+ }
+ return;
+ }
+
+ exec.started = true;
+ exec.tasks_count = exec.args.len;
+
+ for (exec.args) |dir_to_mk_| {
+ const dir_to_mk = dir_to_mk_[0..std.mem.len(dir_to_mk_) :0];
+ var task = ShellMkdirTask.create(this, this.opts, dir_to_mk, this.bltn().parentCmd().base.shell.cwdZ());
+ task.schedule();
+ }
+ },
+ .waiting_write_err => return,
+ .done => this.bltn().done(0),
+ }
+}
+
+pub fn onShellMkdirTaskDone(this: *Mkdir, task: *ShellMkdirTask) void {
+ defer task.deinit();
+ this.state.exec.tasks_done += 1;
+ var output = task.takeOutput();
+ const err = task.err;
+ const output_task: *ShellMkdirOutputTask = bun.new(ShellMkdirOutputTask, .{
+ .parent = this,
+ .output = .{ .arrlist = output.moveToUnmanaged() },
+ .state = .waiting_write_err,
+ });
+
+ if (err) |e| {
+ const error_string = this.bltn().taskErrorToString(.mkdir, e);
+ this.state.exec.err = e;
+ output_task.start(error_string);
+ return;
+ }
+ output_task.start(null);
+}
+
+pub const ShellMkdirOutputTask = OutputTask(Mkdir, .{
+ .writeErr = ShellMkdirOutputTaskVTable.writeErr,
+ .onWriteErr = ShellMkdirOutputTaskVTable.onWriteErr,
+ .writeOut = ShellMkdirOutputTaskVTable.writeOut,
+ .onWriteOut = ShellMkdirOutputTaskVTable.onWriteOut,
+ .onDone = ShellMkdirOutputTaskVTable.onDone,
+});
+
+const ShellMkdirOutputTaskVTable = struct {
+ pub fn writeErr(this: *Mkdir, childptr: anytype, errbuf: []const u8) CoroutineResult {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state.exec.output_waiting += 1;
+ this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
+ return .yield;
+ }
+ _ = this.bltn().writeNoIO(.stderr, errbuf);
+ return .cont;
+ }
+
+ pub fn onWriteErr(this: *Mkdir) void {
+ this.state.exec.output_done += 1;
+ }
+
+ pub fn writeOut(this: *Mkdir, childptr: anytype, output: *OutputSrc) CoroutineResult {
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.state.exec.output_waiting += 1;
+ const slice = output.slice();
+ log("THE SLICE: {d} {s}", .{ slice.len, slice });
+ this.bltn().stdout.enqueue(childptr, slice, safeguard);
+ return .yield;
+ }
+ _ = this.bltn().writeNoIO(.stdout, output.slice());
+ return .cont;
+ }
+
+ pub fn onWriteOut(this: *Mkdir) void {
+ this.state.exec.output_done += 1;
+ }
+
+ pub fn onDone(this: *Mkdir) void {
+ this.next();
+ }
+};
+
+pub fn deinit(this: *Mkdir) void {
+ _ = this;
+}
+
+pub const ShellMkdirTask = struct {
+ mkdir: *Mkdir,
+
+ opts: Opts,
+ filepath: [:0]const u8,
+ cwd_path: [:0]const u8,
+ created_directories: ArrayList(u8),
+
+ err: ?JSC.SystemError = null,
+ task: JSC.WorkPoolTask = .{ .callback = &runFromThreadPool },
+ event_loop: JSC.EventLoopHandle,
+ concurrent_task: JSC.EventLoopTask,
+
+ pub fn deinit(this: *ShellMkdirTask) void {
+ this.created_directories.deinit();
+ bun.default_allocator.destroy(this);
+ }
+
+ fn takeOutput(this: *ShellMkdirTask) ArrayList(u8) {
+ const out = this.created_directories;
+ this.created_directories = ArrayList(u8).init(bun.default_allocator);
+ return out;
+ }
+
+ pub fn format(this: *const ShellMkdirTask, comptime fmt_: []const u8, options_: std.fmt.FormatOptions, writer: anytype) !void {
+ _ = fmt_; // autofix
+ _ = options_; // autofix
+ try writer.print("ShellMkdirTask(0x{x}, filepath={s})", .{ @intFromPtr(this), this.filepath });
+ }
+
+ pub fn create(
+ mkdir: *Mkdir,
+ opts: Opts,
+ filepath: [:0]const u8,
+ cwd_path: [:0]const u8,
+ ) *ShellMkdirTask {
+ const task = bun.default_allocator.create(ShellMkdirTask) catch bun.outOfMemory();
+ const evtloop = mkdir.bltn().parentCmd().base.eventLoop();
+ task.* = ShellMkdirTask{
+ .mkdir = mkdir,
+ .opts = opts,
+ .cwd_path = cwd_path,
+ .filepath = filepath,
+ .created_directories = ArrayList(u8).init(bun.default_allocator),
+ .event_loop = evtloop,
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop),
+ };
+ return task;
+ }
+
+ pub fn schedule(this: *@This()) void {
+ debug("{} schedule", .{this});
+ WorkPool.schedule(&this.task);
+ }
+
+ pub fn runFromMainThread(this: *@This()) void {
+ debug("{} runFromJS", .{this});
+ this.mkdir.onShellMkdirTaskDone(this);
+ }
+
+ pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
+ this.runFromMainThread();
+ }
+
+ fn runFromThreadPool(task: *JSC.WorkPoolTask) void {
+ var this: *ShellMkdirTask = @fieldParentPtr("task", task);
+ debug("{} runFromThreadPool", .{this});
+
+ // We have to give an absolute path to our mkdir
+ // implementation for it to work with cwd
+ const filepath: [:0]const u8 = brk: {
+ if (ResolvePath.Platform.auto.isAbsolute(this.filepath)) break :brk this.filepath;
+ const parts: []const []const u8 = &.{
+ this.cwd_path[0..],
+ this.filepath[0..],
+ };
+ break :brk ResolvePath.joinZ(parts, .auto);
+ };
+
+ var node_fs = JSC.Node.NodeFS{};
+ // Recursive
+ if (this.opts.parents) {
+ const args = JSC.Node.Arguments.Mkdir{
+ .path = JSC.Node.PathLike{ .string = bun.PathString.init(filepath) },
+ .recursive = true,
+ .always_return_none = true,
+ };
+
+ var vtable = MkdirVerboseVTable{ .inner = this, .active = this.opts.verbose };
+
+ switch (node_fs.mkdirRecursiveImpl(args, *MkdirVerboseVTable, &vtable)) {
+ .result => {},
+ .err => |e| {
+ this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError();
+ std.mem.doNotOptimizeAway(&node_fs);
+ },
+ }
+ } else {
+ const args = JSC.Node.Arguments.Mkdir{
+ .path = JSC.Node.PathLike{ .string = bun.PathString.init(filepath) },
+ .recursive = false,
+ .always_return_none = true,
+ };
+ switch (node_fs.mkdirNonRecursive(args)) {
+ .result => {
+ if (this.opts.verbose) {
+ this.created_directories.appendSlice(filepath[0..filepath.len]) catch bun.outOfMemory();
+ this.created_directories.append('\n') catch bun.outOfMemory();
+ }
+ },
+ .err => |e| {
+ this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError();
+ std.mem.doNotOptimizeAway(&node_fs);
+ },
+ }
+ }
+
+ if (this.event_loop == .js) {
+ this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
+ } else {
+ this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
+ }
+ }
+
+ const MkdirVerboseVTable = struct {
+ inner: *ShellMkdirTask,
+ active: bool,
+
+ pub fn onCreateDir(vtable: *@This(), dirpath: bun.OSPathSliceZ) void {
+ if (!vtable.active) return;
+ if (bun.Environment.isWindows) {
+ var buf: bun.PathBuffer = undefined;
+ const str = bun.strings.fromWPath(&buf, dirpath[0..dirpath.len]);
+ vtable.inner.created_directories.appendSlice(str) catch bun.outOfMemory();
+ vtable.inner.created_directories.append('\n') catch bun.outOfMemory();
+ } else {
+ vtable.inner.created_directories.appendSlice(dirpath) catch bun.outOfMemory();
+ vtable.inner.created_directories.append('\n') catch bun.outOfMemory();
+ }
+ return;
+ }
+ };
+};
+
+const Opts = struct {
+ /// -m, --mode
+ ///
+ /// set file mode (as in chmod), not a=rwx - umask
+ mode: ?u32 = null,
+
+ /// -p, --parents
+ ///
+ /// no error if existing, make parent directories as needed,
+ /// with their file modes unaffected by any -m option.
+ parents: bool = false,
+
+ /// -v, --verbose
+ ///
+ /// print a message for each created directory
+ verbose: bool = false,
+
+ const Parse = FlagParser(*@This());
+
+ pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) {
+ return Parse.parseFlags(opts, args);
+ }
+
+ pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult {
+ if (bun.strings.eqlComptime(flag, "--mode")) {
+ return .{ .unsupported = "--mode" };
+ } else if (bun.strings.eqlComptime(flag, "--parents")) {
+ this.parents = true;
+ return .continue_parsing;
+ } else if (bun.strings.eqlComptime(flag, "--vebose")) {
+ this.verbose = true;
+ return .continue_parsing;
+ }
+
+ return null;
+ }
+
+ pub fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
+ switch (char) {
+ 'm' => {
+ return .{ .unsupported = "-m " };
+ },
+ 'p' => {
+ this.parents = true;
+ },
+ 'v' => {
+ this.verbose = true;
+ },
+ else => {
+ return .{ .illegal_option = smallflags[1 + i ..] };
+ },
+ }
+
+ return null;
+ }
+};
+
+pub inline fn bltn(this: *Mkdir) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("mkdir", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const debug = bun.Output.scoped(.ShellMkdir, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Cat = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const Mkdir = @This();
+const log = debug;
+const OutputTask = interpreter.OutputTask;
+const CoroutineResult = interpreter.CoroutineResult;
+const OutputSrc = interpreter.OutputSrc;
+const WorkPool = bun.JSC.WorkPool;
+const ResolvePath = bun.path;
+const Syscall = bun.sys;
+const ArrayList = std.ArrayList;
diff --git a/src/shell/builtin/mv.zig b/src/shell/builtin/mv.zig
new file mode 100644
index 0000000000..04916c748a
--- /dev/null
+++ b/src/shell/builtin/mv.zig
@@ -0,0 +1,525 @@
+opts: Opts = .{},
+args: struct {
+ sources: []const [*:0]const u8 = &[_][*:0]const u8{},
+ target: [:0]const u8 = &[0:0]u8{},
+ target_fd: ?bun.FileDescriptor = null,
+} = .{},
+state: union(enum) {
+ idle,
+ check_target: struct {
+ task: ShellMvCheckTargetTask,
+ state: union(enum) {
+ running,
+ done,
+ },
+ },
+ executing: struct {
+ task_count: usize,
+ tasks_done: usize = 0,
+ error_signal: std.atomic.Value(bool),
+ tasks: []ShellMvBatchedTask,
+ err: ?Syscall.Error = null,
+ },
+ done,
+ waiting_write_err: struct {
+ exit_code: ExitCode,
+ },
+ err,
+} = .idle,
+
+pub const ShellMvCheckTargetTask = struct {
+ mv: *Mv,
+
+ cwd: bun.FileDescriptor,
+ target: [:0]const u8,
+ result: ?Maybe(?bun.FileDescriptor) = null,
+
+ task: ShellTask(@This(), runFromThreadPool, runFromMainThread, debug),
+
+ pub fn runFromThreadPool(this: *@This()) void {
+ const fd = switch (ShellSyscall.openat(this.cwd, this.target, bun.O.RDONLY | bun.O.DIRECTORY, 0)) {
+ .err => |e| {
+ switch (e.getErrno()) {
+ bun.C.E.NOTDIR => {
+ this.result = .{ .result = null };
+ },
+ else => {
+ this.result = .{ .err = e };
+ },
+ }
+ return;
+ },
+ .result => |fd| fd,
+ };
+ this.result = .{ .result = fd };
+ }
+
+ pub fn runFromMainThread(this: *@This()) void {
+ this.mv.checkTargetTaskDone(this);
+ }
+
+ pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
+ this.runFromMainThread();
+ }
+};
+
+pub const ShellMvBatchedTask = struct {
+ const BATCH_SIZE = 5;
+
+ mv: *Mv,
+ sources: []const [*:0]const u8,
+ target: [:0]const u8,
+ target_fd: ?bun.FileDescriptor,
+ cwd: bun.FileDescriptor,
+ error_signal: *std.atomic.Value(bool),
+
+ err: ?Syscall.Error = null,
+
+ task: ShellTask(@This(), runFromThreadPool, runFromMainThread, debug),
+ event_loop: JSC.EventLoopHandle,
+
+ pub fn runFromThreadPool(this: *@This()) void {
+ // Moving multiple entries into a directory
+ if (this.sources.len > 1) return this.moveMultipleIntoDir();
+
+ const src = this.sources[0][0..std.mem.len(this.sources[0]) :0];
+ // Moving entry into directory
+ if (this.target_fd) |fd| {
+ _ = fd;
+
+ var buf: bun.PathBuffer = undefined;
+ _ = this.moveInDir(src, &buf);
+ return;
+ }
+
+ switch (Syscall.renameat(this.cwd, src, this.cwd, this.target)) {
+ .err => |e| {
+ if (e.getErrno() == .NOTDIR) {
+ this.err = e.withPath(this.target);
+ } else this.err = e;
+ },
+ else => {},
+ }
+ }
+
+ pub fn moveInDir(this: *@This(), src: [:0]const u8, buf: *bun.PathBuffer) bool {
+ const path_in_dir_ = bun.path.normalizeBuf(ResolvePath.basename(src), buf, .auto);
+ if (path_in_dir_.len + 1 >= buf.len) {
+ this.err = Syscall.Error.fromCode(bun.C.E.NAMETOOLONG, .rename);
+ return false;
+ }
+ buf[path_in_dir_.len] = 0;
+ const path_in_dir = buf[0..path_in_dir_.len :0];
+
+ switch (Syscall.renameat(this.cwd, src, this.target_fd.?, path_in_dir)) {
+ .err => |e| {
+ const target_path = ResolvePath.joinZ(&[_][]const u8{
+ this.target,
+ ResolvePath.basename(src),
+ }, .auto);
+
+ this.err = e.withPath(bun.default_allocator.dupeZ(u8, target_path[0..]) catch bun.outOfMemory());
+ return false;
+ },
+ else => {},
+ }
+
+ return true;
+ }
+
+ fn moveMultipleIntoDir(this: *@This()) void {
+ var buf: bun.PathBuffer = undefined;
+ var fixed_alloc = std.heap.FixedBufferAllocator.init(buf[0..bun.MAX_PATH_BYTES]);
+
+ for (this.sources) |src_raw| {
+ if (this.error_signal.load(.seq_cst)) return;
+ defer fixed_alloc.reset();
+
+ const src = src_raw[0..std.mem.len(src_raw) :0];
+ if (!this.moveInDir(src, &buf)) {
+ return;
+ }
+ }
+ }
+
+ /// From the man pages of `mv`:
+ /// ```txt
+ /// As the rename(2) call does not work across file systems, mv uses cp(1) and rm(1) to accomplish the move. The effect is equivalent to:
+ /// rm -f destination_path && \
+ /// cp -pRP source_file destination && \
+ /// rm -rf source_file
+ /// ```
+ fn moveAcrossFilesystems(this: *@This(), src: [:0]const u8, dest: [:0]const u8) void {
+ _ = this;
+ _ = src;
+ _ = dest;
+
+ // TODO
+ }
+
+ pub fn runFromMainThread(this: *@This()) void {
+ this.mv.batchedMoveTaskDone(this);
+ }
+
+ pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
+ this.runFromMainThread();
+ }
+};
+
+pub fn start(this: *Mv) Maybe(void) {
+ return this.next();
+}
+
+pub fn writeFailingError(this: *Mv, buf: []const u8, exit_code: ExitCode) Maybe(void) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .{ .waiting_write_err = .{ .exit_code = exit_code } };
+ this.bltn().stderr.enqueue(this, buf, safeguard);
+ return Maybe(void).success;
+ }
+
+ _ = this.bltn().writeNoIO(.stderr, buf);
+
+ this.bltn().done(exit_code);
+ return Maybe(void).success;
+}
+
+pub fn next(this: *Mv) Maybe(void) {
+ while (!(this.state == .done or this.state == .err)) {
+ switch (this.state) {
+ .idle => {
+ if (this.parseOpts().asErr()) |e| {
+ const buf = switch (e) {
+ .illegal_option => |opt_str| this.bltn().fmtErrorArena(.mv, "illegal option -- {s}\n", .{opt_str}),
+ .show_usage => Builtin.Kind.mv.usageString(),
+ };
+
+ return this.writeFailingError(buf, 1);
+ }
+ this.state = .{
+ .check_target = .{
+ .task = ShellMvCheckTargetTask{
+ .mv = this,
+ .cwd = this.bltn().parentCmd().base.shell.cwd_fd,
+ .target = this.args.target,
+ .task = .{
+ .event_loop = this.bltn().parentCmd().base.eventLoop(),
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.bltn().parentCmd().base.eventLoop()),
+ },
+ },
+ .state = .running,
+ },
+ };
+ this.state.check_target.task.task.schedule();
+ return Maybe(void).success;
+ },
+ .check_target => {
+ if (this.state.check_target.state == .running) return Maybe(void).success;
+ const check_target = &this.state.check_target;
+
+ if (comptime bun.Environment.allow_assert) {
+ assert(check_target.task.result != null);
+ }
+
+ const maybe_fd: ?bun.FileDescriptor = switch (check_target.task.result.?) {
+ .err => |e| brk: {
+ switch (e.getErrno()) {
+ bun.C.E.NOENT => {
+ // Means we are renaming entry, not moving to a directory
+ if (this.args.sources.len == 1) break :brk null;
+
+ const buf = this.bltn().fmtErrorArena(.mv, "{s}: No such file or directory\n", .{this.args.target});
+ return this.writeFailingError(buf, 1);
+ },
+ else => {
+ const sys_err = e.toShellSystemError();
+ const buf = this.bltn().fmtErrorArena(.mv, "{s}: {s}\n", .{ sys_err.path.byteSlice(), sys_err.message.byteSlice() });
+ return this.writeFailingError(buf, 1);
+ },
+ }
+ },
+ .result => |maybe_fd| maybe_fd,
+ };
+
+ // Trying to move multiple files into a file
+ if (maybe_fd == null and this.args.sources.len > 1) {
+ const buf = this.bltn().fmtErrorArena(.mv, "{s} is not a directory\n", .{this.args.target});
+ return this.writeFailingError(buf, 1);
+ }
+
+ const count_per_task = ShellMvBatchedTask.BATCH_SIZE;
+
+ const task_count = brk: {
+ const sources_len: f64 = @floatFromInt(this.args.sources.len);
+ const batch_size: f64 = @floatFromInt(count_per_task);
+ const task_count: usize = @intFromFloat(@ceil(sources_len / batch_size));
+ break :brk task_count;
+ };
+
+ this.args.target_fd = maybe_fd;
+ const cwd_fd = this.bltn().parentCmd().base.shell.cwd_fd;
+ const tasks = this.bltn().arena.allocator().alloc(ShellMvBatchedTask, task_count) catch bun.outOfMemory();
+ // Initialize tasks
+ {
+ var i: usize = 0;
+ while (i < tasks.len) : (i += 1) {
+ const start_idx = i * count_per_task;
+ const end_idx = @min(start_idx + count_per_task, this.args.sources.len);
+ const sources = this.args.sources[start_idx..end_idx];
+
+ tasks[i] = ShellMvBatchedTask{
+ .mv = this,
+ .cwd = cwd_fd,
+ .target = this.args.target,
+ .target_fd = this.args.target_fd,
+ .sources = sources,
+ // We set this later
+ .error_signal = undefined,
+ .task = .{
+ .event_loop = this.bltn().parentCmd().base.eventLoop(),
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.bltn().parentCmd().base.eventLoop()),
+ },
+ .event_loop = this.bltn().parentCmd().base.eventLoop(),
+ };
+ }
+ }
+
+ this.state = .{
+ .executing = .{
+ .task_count = task_count,
+ .error_signal = std.atomic.Value(bool).init(false),
+ .tasks = tasks,
+ },
+ };
+
+ for (this.state.executing.tasks) |*t| {
+ t.error_signal = &this.state.executing.error_signal;
+ t.task.schedule();
+ }
+
+ return Maybe(void).success;
+ },
+ // Shouldn't happen
+ .executing => {},
+ .waiting_write_err => {
+ return Maybe(void).success;
+ },
+ .done, .err => unreachable,
+ }
+ }
+
+ switch (this.state) {
+ .done => this.bltn().done(0),
+ else => this.bltn().done(1),
+ }
+
+ return Maybe(void).success;
+}
+
+pub fn onIOWriterChunk(this: *Mv, _: usize, e: ?JSC.SystemError) void {
+ defer if (e) |err| err.deref();
+ switch (this.state) {
+ .waiting_write_err => {
+ if (e != null) {
+ this.state = .err;
+ _ = this.next();
+ return;
+ }
+ this.bltn().done(this.state.waiting_write_err.exit_code);
+ return;
+ },
+ else => @panic("Invalid state"),
+ }
+}
+
+pub fn checkTargetTaskDone(this: *Mv, task: *ShellMvCheckTargetTask) void {
+ _ = task;
+
+ if (comptime bun.Environment.allow_assert) {
+ assert(this.state == .check_target);
+ assert(this.state.check_target.task.result != null);
+ }
+
+ this.state.check_target.state = .done;
+ _ = this.next();
+ return;
+}
+
+pub fn batchedMoveTaskDone(this: *Mv, task: *ShellMvBatchedTask) void {
+ if (comptime bun.Environment.allow_assert) {
+ assert(this.state == .executing);
+ assert(this.state.executing.tasks_done < this.state.executing.task_count);
+ }
+
+ var exec = &this.state.executing;
+
+ if (task.err) |err| {
+ exec.error_signal.store(true, .seq_cst);
+ if (exec.err == null) {
+ exec.err = err;
+ } else {
+ bun.default_allocator.free(err.path);
+ }
+ }
+
+ exec.tasks_done += 1;
+ if (exec.tasks_done >= exec.task_count) {
+ if (exec.err) |err| {
+ const e = err.toShellSystemError();
+ const buf = this.bltn().fmtErrorArena(.mv, "{}: {}\n", .{ e.path, e.message });
+ _ = this.writeFailingError(buf, err.errno);
+ return;
+ }
+ this.state = .done;
+
+ _ = this.next();
+ return;
+ }
+}
+
+pub fn deinit(this: *Mv) void {
+ if (this.args.target_fd != null and this.args.target_fd.? != bun.invalid_fd) {
+ _ = Syscall.close(this.args.target_fd.?);
+ }
+}
+
+const Opts = struct {
+ /// `-f`
+ ///
+ /// Do not prompt for confirmation before overwriting the destination path. (The -f option overrides any previous -i or -n options.)
+ force_overwrite: bool = true,
+ /// `-h`
+ ///
+ /// If the target operand is a symbolic link to a directory, do not follow it. This causes the mv utility to rename the file source to the destination path target rather than moving source into the
+ /// directory referenced by target.
+ no_dereference: bool = false,
+ /// `-i`
+ ///
+ /// Cause mv to write a prompt to standard error before moving a file that would overwrite an existing file. If the response from the standard input begins with the character ‘y’ or ‘Y’, the move is
+ /// attempted. (The -i option overrides any previous -f or -n options.)
+ interactive_mode: bool = false,
+ /// `-n`
+ ///
+ /// Do not overwrite an existing file. (The -n option overrides any previous -f or -i options.)
+ no_overwrite: bool = false,
+ /// `-v`
+ ///
+ /// Cause mv to be verbose, showing files after they are moved.
+ verbose_output: bool = false,
+
+ const ParseError = union(enum) {
+ illegal_option: []const u8,
+ show_usage,
+ };
+};
+
+pub fn parseOpts(this: *Mv) Result(void, Opts.ParseError) {
+ const filepath_args = switch (this.parseFlags()) {
+ .ok => |args| args,
+ .err => |e| return .{ .err = e },
+ };
+
+ if (filepath_args.len < 2) {
+ return .{ .err = .show_usage };
+ }
+
+ this.args.sources = filepath_args[0 .. filepath_args.len - 1];
+ this.args.target = std.mem.span(filepath_args[filepath_args.len - 1]);
+
+ return .ok;
+}
+
+pub fn parseFlags(this: *Mv) Result([]const [*:0]const u8, Opts.ParseError) {
+ const args = this.bltn().argsSlice();
+ var idx: usize = 0;
+ if (args.len == 0) {
+ return .{ .err = .show_usage };
+ }
+
+ while (idx < args.len) : (idx += 1) {
+ const flag = args[idx];
+ switch (this.parseFlag(flag[0..std.mem.len(flag)])) {
+ .done => {
+ const filepath_args = args[idx..];
+ return .{ .ok = filepath_args };
+ },
+ .continue_parsing => {},
+ .illegal_option => |opt_str| return .{ .err = .{ .illegal_option = opt_str } },
+ }
+ }
+
+ return .{ .err = .show_usage };
+}
+
+pub fn parseFlag(this: *Mv, flag: []const u8) union(enum) { continue_parsing, done, illegal_option: []const u8 } {
+ if (flag.len == 0) return .done;
+ if (flag[0] != '-') return .done;
+
+ const small_flags = flag[1..];
+ for (small_flags) |char| {
+ switch (char) {
+ 'f' => {
+ this.opts.force_overwrite = true;
+ this.opts.interactive_mode = false;
+ this.opts.no_overwrite = false;
+ },
+ 'h' => {
+ this.opts.no_dereference = true;
+ },
+ 'i' => {
+ this.opts.interactive_mode = true;
+ this.opts.force_overwrite = false;
+ this.opts.no_overwrite = false;
+ },
+ 'n' => {
+ this.opts.no_overwrite = true;
+ this.opts.force_overwrite = false;
+ this.opts.interactive_mode = false;
+ },
+ 'v' => {
+ this.opts.verbose_output = true;
+ },
+ else => {
+ return .{ .illegal_option = "-" };
+ },
+ }
+ }
+
+ return .continue_parsing;
+}
+
+pub inline fn bltn(this: *Mv) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("mv", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const debug = bun.Output.scoped(.ShellCat, true);
+const Mv = @This();
+
+const Syscall = bun.sys;
+const ShellTask = interpreter.ShellTask;
+const assert = bun.assert;
+const std = @import("std");
+const bun = @import("root").bun;
+const shell = bun.shell;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ReadChunkAction = interpreter.ReadChunkAction;
+const FlagParser = interpreter.FlagParser;
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const ResolvePath = bun.path;
diff --git a/src/shell/builtin/pwd.zig b/src/shell/builtin/pwd.zig
new file mode 100644
index 0000000000..de6bdc6f22
--- /dev/null
+++ b/src/shell/builtin/pwd.zig
@@ -0,0 +1,111 @@
+state: union(enum) {
+ idle,
+ waiting_io: struct {
+ kind: enum { stdout, stderr },
+ },
+ err,
+ done,
+} = .idle,
+
+pub fn start(this: *Pwd) Maybe(void) {
+ const args = this.bltn().argsSlice();
+ if (args.len > 0) {
+ const msg = "pwd: too many arguments\n";
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .{ .waiting_io = .{ .kind = .stderr } };
+ this.bltn().stderr.enqueue(this, msg, safeguard);
+ return Maybe(void).success;
+ }
+
+ _ = this.bltn().writeNoIO(.stderr, msg);
+ this.bltn().done(1);
+ return Maybe(void).success;
+ }
+
+ const cwd_str = this.bltn().parentCmd().base.shell.cwd();
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.state = .{ .waiting_io = .{ .kind = .stdout } };
+ this.bltn().stdout.enqueueFmtBltn(this, null, "{s}\n", .{cwd_str}, safeguard);
+ return Maybe(void).success;
+ }
+ const buf = this.bltn().fmtErrorArena(null, "{s}\n", .{cwd_str});
+
+ _ = this.bltn().writeNoIO(.stdout, buf);
+
+ this.state = .done;
+ this.bltn().done(0);
+ return Maybe(void).success;
+}
+
+pub fn next(this: *Pwd) void {
+ while (!(this.state == .err or this.state == .done)) {
+ switch (this.state) {
+ .waiting_io => return,
+ .idle => @panic("Unexpected \"idle\" state in Pwd. This indicates a bug in Bun. Please file a GitHub issue."),
+ .done, .err => unreachable,
+ }
+ }
+
+ switch (this.state) {
+ .done => this.bltn().done(0),
+ .err => this.bltn().done(1),
+ else => {},
+ }
+}
+
+pub fn onIOWriterChunk(this: *Pwd, _: usize, e: ?JSC.SystemError) void {
+ if (comptime bun.Environment.allow_assert) {
+ assert(this.state == .waiting_io);
+ }
+
+ if (e != null) {
+ defer e.?.deref();
+ this.state = .err;
+ this.next();
+ return;
+ }
+
+ this.state = switch (this.state.waiting_io.kind) {
+ .stdout => .done,
+ .stderr => .err,
+ };
+
+ this.next();
+}
+
+pub fn deinit(this: *Pwd) void {
+ _ = this;
+}
+
+pub inline fn bltn(this: *Pwd) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("pwd", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const debug = bun.Output.scoped(.ShellCat, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Pwd = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const assert = bun.assert;
diff --git a/src/shell/builtin/rm.zig b/src/shell/builtin/rm.zig
new file mode 100644
index 0000000000..cd710d0c4c
--- /dev/null
+++ b/src/shell/builtin/rm.zig
@@ -0,0 +1,1227 @@
+opts: Opts,
+state: union(enum) {
+ idle,
+ parse_opts: struct {
+ args_slice: []const [*:0]const u8,
+ idx: u32 = 0,
+ state: union(enum) {
+ normal,
+ wait_write_err,
+ } = .normal,
+ },
+ exec: struct {
+ // task: RmTask,
+ filepath_args: []const [*:0]const u8,
+ total_tasks: usize,
+ err: ?Syscall.Error = null,
+ lock: bun.Mutex = bun.Mutex{},
+ error_signal: std.atomic.Value(bool) = .{ .raw = false },
+ output_done: std.atomic.Value(usize) = .{ .raw = 0 },
+ output_count: std.atomic.Value(usize) = .{ .raw = 0 },
+ state: union(enum) {
+ idle,
+ waiting: struct {
+ tasks_done: usize = 0,
+ },
+
+ pub fn tasksDone(this: *@This()) usize {
+ return switch (this.*) {
+ .idle => 0,
+ .waiting => this.waiting.tasks_done,
+ };
+ }
+ },
+
+ fn incrementOutputCount(this: *@This(), comptime thevar: @Type(.enum_literal)) void {
+ var atomicvar = &@field(this, @tagName(thevar));
+ const result = atomicvar.fetchAdd(1, .seq_cst);
+ log("[rm] {s}: {d} + 1", .{ @tagName(thevar), result });
+ return;
+ }
+
+ fn getOutputCount(this: *@This(), comptime thevar: @Type(.enum_literal)) usize {
+ var atomicvar = &@field(this, @tagName(thevar));
+ return atomicvar.load(.seq_cst);
+ }
+ },
+ done: struct { exit_code: ExitCode },
+ err: ExitCode,
+} = .idle,
+
+pub const Opts = struct {
+ /// `--no-preserve-root` / `--preserve-root`
+ ///
+ /// If set to false, then allow the recursive removal of the root directory.
+ /// Safety feature to prevent accidental deletion of the root directory.
+ preserve_root: bool = true,
+
+ /// `-f`, `--force`
+ ///
+ /// Ignore nonexistent files and arguments, never prompt.
+ force: bool = false,
+
+ /// Configures how the user should be prompted on removal of files.
+ prompt_behaviour: PromptBehaviour = .never,
+
+ /// `-r`, `-R`, `--recursive`
+ ///
+ /// Remove directories and their contents recursively.
+ recursive: bool = false,
+
+ /// `-v`, `--verbose`
+ ///
+ /// Explain what is being done (prints which files/dirs are being deleted).
+ verbose: bool = false,
+
+ /// `-d`, `--dir`
+ ///
+ /// Remove empty directories. This option permits you to remove a directory
+ /// without specifying `-r`/`-R`/`--recursive`, provided that the directory is
+ /// empty.
+ remove_empty_dirs: bool = false,
+
+ const PromptBehaviour = union(enum) {
+ /// `--interactive=never`
+ ///
+ /// Default
+ never,
+
+ /// `-I`, `--interactive=once`
+ ///
+ /// Once before removing more than three files, or when removing recursively.
+ once: struct {
+ removed_count: u32 = 0,
+ },
+
+ /// `-i`, `--interactive=always`
+ ///
+ /// Prompt before every removal.
+ always,
+ };
+};
+
+pub fn start(this: *Rm) Maybe(void) {
+ return this.next();
+}
+
+pub noinline fn next(this: *Rm) Maybe(void) {
+ while (this.state != .done and this.state != .err) {
+ switch (this.state) {
+ .idle => {
+ this.state = .{
+ .parse_opts = .{
+ .args_slice = this.bltn().argsSlice(),
+ },
+ };
+ continue;
+ },
+ .parse_opts => {
+ var parse_opts = &this.state.parse_opts;
+ switch (parse_opts.state) {
+ .normal => {
+ // This means there were no arguments or only
+ // flag arguments meaning no positionals, in
+ // either case we must print the usage error
+ // string
+ if (parse_opts.idx >= parse_opts.args_slice.len) {
+ const error_string = Builtin.Kind.usageString(.rm);
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ parse_opts.state = .wait_write_err;
+ this.bltn().stderr.enqueue(this, error_string, safeguard);
+ return Maybe(void).success;
+ }
+
+ _ = this.bltn().writeNoIO(.stderr, error_string);
+
+ this.bltn().done(1);
+ return Maybe(void).success;
+ }
+
+ const idx = parse_opts.idx;
+
+ const arg_raw = parse_opts.args_slice[idx];
+ const arg = arg_raw[0..std.mem.len(arg_raw)];
+
+ switch (parseFlag(&this.opts, this.bltn(), arg)) {
+ .continue_parsing => {
+ parse_opts.idx += 1;
+ continue;
+ },
+ .done => {
+ if (this.opts.recursive) {
+ this.opts.remove_empty_dirs = true;
+ }
+
+ if (this.opts.prompt_behaviour != .never) {
+ const buf = "rm: \"-i\" is not supported yet";
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ parse_opts.state = .wait_write_err;
+ this.bltn().stderr.enqueue(this, buf, safeguard);
+ continue;
+ }
+
+ _ = this.bltn().writeNoIO(.stderr, buf);
+
+ this.bltn().done(1);
+ return Maybe(void).success;
+ }
+
+ const filepath_args_start = idx;
+ const filepath_args = parse_opts.args_slice[filepath_args_start..];
+
+ // Check that non of the paths will delete the root
+ {
+ var buf: bun.PathBuffer = undefined;
+ const cwd = switch (Syscall.getcwd(&buf)) {
+ .err => |err| {
+ return .{ .err = err };
+ },
+ .result => |cwd| cwd,
+ };
+
+ for (filepath_args) |filepath| {
+ const path = filepath[0..bun.len(filepath)];
+ const resolved_path = if (ResolvePath.Platform.auto.isAbsolute(path)) path else bun.path.join(&[_][]const u8{ cwd, path }, .auto);
+ const is_root = brk: {
+ const normalized = bun.path.normalizeString(resolved_path, false, .auto);
+ const dirname = ResolvePath.dirname(normalized, .auto);
+ const is_root = std.mem.eql(u8, dirname, "");
+ break :brk is_root;
+ };
+
+ if (is_root) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ parse_opts.state = .wait_write_err;
+ this.bltn().stderr.enqueueFmtBltn(this, .rm, "\"{s}\" may not be removed\n", .{resolved_path}, safeguard);
+ return Maybe(void).success;
+ }
+
+ const error_string = this.bltn().fmtErrorArena(.rm, "\"{s}\" may not be removed\n", .{resolved_path});
+
+ _ = this.bltn().writeNoIO(.stderr, error_string);
+
+ this.bltn().done(1);
+ return Maybe(void).success;
+ }
+ }
+ }
+
+ const total_tasks = filepath_args.len;
+ this.state = .{
+ .exec = .{
+ .filepath_args = filepath_args,
+ .total_tasks = total_tasks,
+ .state = .idle,
+ .output_done = std.atomic.Value(usize).init(0),
+ .output_count = std.atomic.Value(usize).init(0),
+ },
+ };
+ // this.state.exec.task.schedule();
+ // return Maybe(void).success;
+ continue;
+ },
+ .illegal_option => {
+ const error_string = "rm: illegal option -- -\n";
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ parse_opts.state = .wait_write_err;
+ this.bltn().stderr.enqueue(this, error_string, safeguard);
+ return Maybe(void).success;
+ }
+
+ _ = this.bltn().writeNoIO(.stderr, error_string);
+
+ this.bltn().done(1);
+ return Maybe(void).success;
+ },
+ .illegal_option_with_flag => {
+ const flag = arg;
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ parse_opts.state = .wait_write_err;
+ this.bltn().stderr.enqueueFmtBltn(this, .rm, "illegal option -- {s}\n", .{flag[1..]}, safeguard);
+ return Maybe(void).success;
+ }
+ const error_string = this.bltn().fmtErrorArena(.rm, "illegal option -- {s}\n", .{flag[1..]});
+
+ _ = this.bltn().writeNoIO(.stderr, error_string);
+
+ this.bltn().done(1);
+ return Maybe(void).success;
+ },
+ }
+ },
+ .wait_write_err => {
+ @panic("Invalid");
+ // // Errored
+ // if (parse_opts.state.wait_write_err.err) |e| {
+ // this.state = .{ .err = e };
+ // continue;
+ // }
+
+ // // Done writing
+ // if (this.state.parse_opts.state.wait_write_err.remain() == 0) {
+ // this.state = .{ .done = .{ .exit_code = 0 } };
+ // continue;
+ // }
+
+ // // yield execution to continue writing
+ // return Maybe(void).success;
+ },
+ }
+ },
+ .exec => {
+ const cwd = this.bltn().parentCmd().base.shell.cwd_fd;
+ // Schedule task
+ if (this.state.exec.state == .idle) {
+ this.state.exec.state = .{ .waiting = .{} };
+ for (this.state.exec.filepath_args) |root_raw| {
+ const root = root_raw[0..std.mem.len(root_raw)];
+ const root_path_string = bun.PathString.init(root[0..root.len]);
+ const is_absolute = ResolvePath.Platform.auto.isAbsolute(root);
+ var task = ShellRmTask.create(root_path_string, this, cwd, &this.state.exec.error_signal, is_absolute);
+ task.schedule();
+ // task.
+ }
+ }
+
+ // do nothing
+ return Maybe(void).success;
+ },
+ .done, .err => unreachable,
+ }
+ }
+
+ switch (this.state) {
+ .done => this.bltn().done(0),
+ .err => this.bltn().done(this.state.err),
+ else => {},
+ }
+
+ return Maybe(void).success;
+}
+
+pub fn onIOWriterChunk(this: *Rm, _: usize, e: ?JSC.SystemError) void {
+ log("Rm(0x{x}).onIOWriterChunk()", .{@intFromPtr(this)});
+ if (comptime bun.Environment.allow_assert) {
+ assert((this.state == .parse_opts and this.state.parse_opts.state == .wait_write_err) or
+ (this.state == .exec and this.state.exec.state == .waiting and this.state.exec.output_count.load(.seq_cst) > 0));
+ }
+
+ if (this.state == .exec and this.state.exec.state == .waiting) {
+ log("Rm(0x{x}) output done={d} output count={d}", .{ @intFromPtr(this), this.state.exec.getOutputCount(.output_done), this.state.exec.getOutputCount(.output_count) });
+ this.state.exec.incrementOutputCount(.output_done);
+ if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) {
+ const code: ExitCode = if (this.state.exec.err != null) 1 else 0;
+ this.bltn().done(code);
+ return;
+ }
+ return;
+ }
+
+ if (e != null) {
+ defer e.?.deref();
+ this.state = .{ .err = @intFromEnum(e.?.getErrno()) };
+ this.bltn().done(e.?.getErrno());
+ return;
+ }
+
+ this.bltn().done(1);
+ return;
+}
+
+pub fn deinit(this: *Rm) void {
+ _ = this;
+}
+
+pub inline fn bltn(this: *Rm) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("rm", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+const ParseFlagsResult = enum {
+ continue_parsing,
+ done,
+ illegal_option,
+ illegal_option_with_flag,
+};
+
+fn parseFlag(this: *Opts, _: *Builtin, flag: []const u8) ParseFlagsResult {
+ if (flag.len == 0) return .done;
+ if (flag[0] != '-') return .done;
+ if (flag.len > 2 and flag[1] == '-') {
+ if (bun.strings.eqlComptime(flag, "--preserve-root")) {
+ this.preserve_root = true;
+ return .continue_parsing;
+ } else if (bun.strings.eqlComptime(flag, "--no-preserve-root")) {
+ this.preserve_root = false;
+ return .continue_parsing;
+ } else if (bun.strings.eqlComptime(flag, "--recursive")) {
+ this.recursive = true;
+ return .continue_parsing;
+ } else if (bun.strings.eqlComptime(flag, "--verbose")) {
+ this.verbose = true;
+ return .continue_parsing;
+ } else if (bun.strings.eqlComptime(flag, "--dir")) {
+ this.remove_empty_dirs = true;
+ return .continue_parsing;
+ } else if (bun.strings.eqlComptime(flag, "--interactive=never")) {
+ this.prompt_behaviour = .never;
+ return .continue_parsing;
+ } else if (bun.strings.eqlComptime(flag, "--interactive=once")) {
+ this.prompt_behaviour = .{ .once = .{} };
+ return .continue_parsing;
+ } else if (bun.strings.eqlComptime(flag, "--interactive=always")) {
+ this.prompt_behaviour = .always;
+ return .continue_parsing;
+ }
+
+ return .illegal_option;
+ }
+
+ const small_flags = flag[1..];
+ for (small_flags) |char| {
+ switch (char) {
+ 'f' => {
+ this.force = true;
+ this.prompt_behaviour = .never;
+ },
+ 'r', 'R' => {
+ this.recursive = true;
+ },
+ 'v' => {
+ this.verbose = true;
+ },
+ 'd' => {
+ this.remove_empty_dirs = true;
+ },
+ 'i' => {
+ this.prompt_behaviour = .{ .once = .{} };
+ },
+ 'I' => {
+ this.prompt_behaviour = .always;
+ },
+ else => {
+ return .illegal_option_with_flag;
+ },
+ }
+ }
+
+ return .continue_parsing;
+}
+
+pub fn onShellRmTaskDone(this: *Rm, task: *ShellRmTask) void {
+ var exec = &this.state.exec;
+ const tasks_done = switch (exec.state) {
+ .idle => @panic("Invalid state"),
+ .waiting => brk: {
+ exec.state.waiting.tasks_done += 1;
+ const amt = exec.state.waiting.tasks_done;
+ if (task.err) |err| {
+ exec.err = err;
+ const error_string = this.bltn().taskErrorToString(.rm, err);
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ log("Rm(0x{x}) task=0x{x} ERROR={s}", .{ @intFromPtr(this), @intFromPtr(task), error_string });
+ exec.incrementOutputCount(.output_count);
+ this.bltn().stderr.enqueue(this, error_string, safeguard);
+ return;
+ } else {
+ _ = this.bltn().writeNoIO(.stderr, error_string);
+ }
+ }
+ break :brk amt;
+ },
+ };
+
+ log("ShellRmTask(0x{x}, task={s})", .{ @intFromPtr(task), task.root_path });
+ // Wait until all tasks done and all output is written
+ if (tasks_done >= this.state.exec.total_tasks and
+ exec.getOutputCount(.output_done) >= exec.getOutputCount(.output_count))
+ {
+ this.state = .{ .done = .{ .exit_code = if (exec.err) |theerr| theerr.errno else 0 } };
+ _ = this.next();
+ return;
+ }
+}
+
+fn writeVerbose(this: *Rm, verbose: *ShellRmTask.DirTask) void {
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ const buf = verbose.takeDeletedEntries();
+ defer buf.deinit();
+ this.bltn().stdout.enqueue(this, buf.items, safeguard);
+ } else {
+ _ = this.bltn().writeNoIO(.stdout, verbose.deleted_entries.items);
+ _ = this.state.exec.incrementOutputCount(.output_done);
+ if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) {
+ this.bltn().done(if (this.state.exec.err != null) @as(ExitCode, 1) else @as(ExitCode, 0));
+ return;
+ }
+ return;
+ }
+}
+
+pub const ShellRmTask = struct {
+ const debug = bun.Output.scoped(.AsyncRmTask, true);
+
+ rm: *Rm,
+ opts: Opts,
+
+ cwd: bun.FileDescriptor,
+ cwd_path: ?CwdPath = if (bun.Environment.isPosix) 0 else null,
+
+ root_task: DirTask,
+ root_path: bun.PathString = bun.PathString.empty,
+ root_is_absolute: bool,
+
+ error_signal: *std.atomic.Value(bool),
+ err_mutex: bun.Mutex = .{},
+ err: ?Syscall.Error = null,
+
+ event_loop: JSC.EventLoopHandle,
+ concurrent_task: JSC.EventLoopTask,
+ task: JSC.WorkPoolTask = .{
+ .callback = workPoolCallback,
+ },
+ join_style: JoinStyle,
+
+ /// On Windows we allow posix path separators
+ /// But this results in weird looking paths if we use our path.join function which uses the platform separator:
+ /// `foo/bar + baz -> foo/bar\baz`
+ ///
+ /// So detect which path separator the user is using and prefer that.
+ /// If both are used, pick the first one.
+ const JoinStyle = union(enum) {
+ posix,
+ windows,
+
+ pub fn fromPath(p: bun.PathString) JoinStyle {
+ if (comptime bun.Environment.isPosix) return .posix;
+ const backslash = std.mem.indexOfScalar(u8, p.slice(), '\\') orelse std.math.maxInt(usize);
+ const forwardslash = std.mem.indexOfScalar(u8, p.slice(), '/') orelse std.math.maxInt(usize);
+ if (forwardslash <= backslash)
+ return .posix;
+ return .windows;
+ }
+ };
+
+ const CwdPath = if (bun.Environment.isWindows) [:0]const u8 else u0;
+
+ const ParentRmTask = @This();
+
+ pub const DirTask = struct {
+ task_manager: *ParentRmTask,
+ parent_task: ?*DirTask,
+ path: [:0]const u8,
+ is_absolute: bool = false,
+ subtask_count: std.atomic.Value(usize),
+ need_to_wait: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
+ deleting_after_waiting_for_children: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
+ kind_hint: EntryKindHint,
+ task: JSC.WorkPoolTask = .{ .callback = runFromThreadPool },
+ deleted_entries: std.ArrayList(u8),
+ concurrent_task: JSC.EventLoopTask,
+
+ const EntryKindHint = enum { idk, dir, file };
+
+ pub fn takeDeletedEntries(this: *DirTask) std.ArrayList(u8) {
+ debug("DirTask(0x{x} path={s}) takeDeletedEntries", .{ @intFromPtr(this), this.path });
+ const ret = this.deleted_entries;
+ this.deleted_entries = std.ArrayList(u8).init(ret.allocator);
+ return ret;
+ }
+
+ pub fn runFromMainThread(this: *DirTask) void {
+ debug("DirTask(0x{x}, path={s}) runFromMainThread", .{ @intFromPtr(this), this.path });
+ this.task_manager.rm.writeVerbose(this);
+ }
+
+ pub fn runFromMainThreadMini(this: *DirTask, _: *void) void {
+ this.runFromMainThread();
+ }
+
+ pub fn runFromThreadPool(task: *JSC.WorkPoolTask) void {
+ var this: *DirTask = @fieldParentPtr("task", task);
+ this.runFromThreadPoolImpl();
+ }
+
+ fn runFromThreadPoolImpl(this: *DirTask) void {
+ defer {
+ if (!this.deleting_after_waiting_for_children.load(.seq_cst)) {
+ this.postRun();
+ }
+ }
+
+ // Root, get cwd path on windows
+ if (bun.Environment.isWindows) {
+ if (this.parent_task == null) {
+ var buf: bun.PathBuffer = undefined;
+ const cwd_path = switch (Syscall.getFdPath(this.task_manager.cwd, &buf)) {
+ .result => |p| bun.default_allocator.dupeZ(u8, p) catch bun.outOfMemory(),
+ .err => |err| {
+ debug("[runFromThreadPoolImpl:getcwd] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path });
+ this.task_manager.err_mutex.lock();
+ defer this.task_manager.err_mutex.unlock();
+ if (this.task_manager.err == null) {
+ this.task_manager.err = err;
+ this.task_manager.error_signal.store(true, .seq_cst);
+ }
+ return;
+ },
+ };
+ this.task_manager.cwd_path = cwd_path;
+ }
+ }
+
+ debug("DirTask: {s}", .{this.path});
+ this.is_absolute = ResolvePath.Platform.auto.isAbsolute(this.path[0..this.path.len]);
+ switch (this.task_manager.removeEntry(this, this.is_absolute)) {
+ .err => |err| {
+ debug("[runFromThreadPoolImpl] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path });
+ this.task_manager.err_mutex.lock();
+ defer this.task_manager.err_mutex.unlock();
+ if (this.task_manager.err == null) {
+ this.task_manager.err = err;
+ this.task_manager.error_signal.store(true, .seq_cst);
+ } else {
+ bun.default_allocator.free(err.path);
+ }
+ },
+ .result => {},
+ }
+ }
+
+ fn handleErr(this: *DirTask, err: Syscall.Error) void {
+ debug("[handleErr] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path });
+ this.task_manager.err_mutex.lock();
+ defer this.task_manager.err_mutex.unlock();
+ if (this.task_manager.err == null) {
+ this.task_manager.err = err;
+ this.task_manager.error_signal.store(true, .seq_cst);
+ } else {
+ bun.default_allocator.free(err.path);
+ }
+ }
+
+ pub fn postRun(this: *DirTask) void {
+ debug("DirTask(0x{x}, path={s}) postRun", .{ @intFromPtr(this), this.path });
+ // // This is true if the directory has subdirectories
+ // // that need to be deleted
+ if (this.need_to_wait.load(.seq_cst)) return;
+
+ // We have executed all the children of this task
+ if (this.subtask_count.fetchSub(1, .seq_cst) == 1) {
+ defer {
+ if (this.task_manager.opts.verbose)
+ this.queueForWrite()
+ else
+ this.deinit();
+ }
+
+ // If we have a parent and we are the last child, now we can delete the parent
+ if (this.parent_task != null) {
+ // It's possible that we queued this subdir task and it finished, while the parent
+ // was still in the `removeEntryDir` function
+ const tasks_left_before_decrement = this.parent_task.?.subtask_count.fetchSub(1, .seq_cst);
+ const parent_still_in_remove_entry_dir = !this.parent_task.?.need_to_wait.load(.monotonic);
+ if (!parent_still_in_remove_entry_dir and tasks_left_before_decrement == 2) {
+ this.parent_task.?.deleteAfterWaitingForChildren();
+ }
+ return;
+ }
+
+ // Otherwise we are root task
+ this.task_manager.finishConcurrently();
+ }
+
+ // Otherwise need to wait
+ }
+
+ pub fn deleteAfterWaitingForChildren(this: *DirTask) void {
+ debug("DirTask(0x{x}, path={s}) deleteAfterWaitingForChildren", .{ @intFromPtr(this), this.path });
+ // `runFromMainThreadImpl` has a `defer this.postRun()` so need to set this to true to skip that
+ this.deleting_after_waiting_for_children.store(true, .seq_cst);
+ this.need_to_wait.store(false, .seq_cst);
+ var do_post_run = true;
+ defer {
+ if (do_post_run) this.postRun();
+ }
+ if (this.task_manager.error_signal.load(.seq_cst)) {
+ return;
+ }
+
+ switch (this.task_manager.removeEntryDirAfterChildren(this)) {
+ .err => |e| {
+ debug("[deleteAfterWaitingForChildren] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(e.getErrno()), e.path });
+ this.task_manager.err_mutex.lock();
+ defer this.task_manager.err_mutex.unlock();
+ if (this.task_manager.err == null) {
+ this.task_manager.err = e;
+ } else {
+ bun.default_allocator.free(e.path);
+ }
+ },
+ .result => |deleted| {
+ if (!deleted) {
+ do_post_run = false;
+ }
+ },
+ }
+ }
+
+ pub fn queueForWrite(this: *DirTask) void {
+ log("DirTask(0x{x}, path={s}) queueForWrite to_write={d}", .{ @intFromPtr(this), this.path, this.deleted_entries.items.len });
+ if (this.deleted_entries.items.len == 0) return;
+ if (this.task_manager.event_loop == .js) {
+ this.task_manager.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
+ } else {
+ this.task_manager.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
+ }
+ }
+
+ pub fn deinit(this: *DirTask) void {
+ this.deleted_entries.deinit();
+ // The root's path string is from Rm's argv so don't deallocate it
+ // And the root task is actually a field on the struct of the AsyncRmTask so don't deallocate it either
+ if (this.parent_task != null) {
+ bun.default_allocator.free(this.path);
+ bun.default_allocator.destroy(this);
+ }
+ }
+ };
+
+ pub fn create(root_path: bun.PathString, rm: *Rm, cwd: bun.FileDescriptor, error_signal: *std.atomic.Value(bool), is_absolute: bool) *ShellRmTask {
+ const task = bun.default_allocator.create(ShellRmTask) catch bun.outOfMemory();
+ task.* = ShellRmTask{
+ .rm = rm,
+ .opts = rm.opts,
+ .cwd = cwd,
+ .root_path = root_path,
+ .root_task = DirTask{
+ .task_manager = task,
+ .parent_task = null,
+ .path = root_path.sliceAssumeZ(),
+ .subtask_count = std.atomic.Value(usize).init(1),
+ .kind_hint = .idk,
+ .deleted_entries = std.ArrayList(u8).init(bun.default_allocator),
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(rm.bltn().eventLoop()),
+ },
+ .event_loop = rm.bltn().parentCmd().base.eventLoop(),
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(rm.bltn().eventLoop()),
+ .error_signal = error_signal,
+ .root_is_absolute = is_absolute,
+ .join_style = JoinStyle.fromPath(root_path),
+ };
+ return task;
+ }
+
+ pub fn schedule(this: *@This()) void {
+ JSC.WorkPool.schedule(&this.task);
+ }
+
+ pub fn enqueue(this: *ShellRmTask, parent_dir: *DirTask, path: [:0]const u8, is_absolute: bool, kind_hint: DirTask.EntryKindHint) void {
+ if (this.error_signal.load(.seq_cst)) {
+ return;
+ }
+ const new_path = this.join(
+ bun.default_allocator,
+ &[_][]const u8{
+ parent_dir.path[0..parent_dir.path.len],
+ path[0..path.len],
+ },
+ is_absolute,
+ );
+ this.enqueueNoJoin(parent_dir, new_path, kind_hint);
+ }
+
+ pub fn enqueueNoJoin(this: *ShellRmTask, parent_task: *DirTask, path: [:0]const u8, kind_hint: DirTask.EntryKindHint) void {
+ defer debug("enqueue: {s} {s}", .{ path, @tagName(kind_hint) });
+
+ if (this.error_signal.load(.seq_cst)) {
+ return;
+ }
+
+ var subtask = bun.default_allocator.create(DirTask) catch bun.outOfMemory();
+ subtask.* = DirTask{
+ .task_manager = this,
+ .path = path,
+ .parent_task = parent_task,
+ .subtask_count = std.atomic.Value(usize).init(1),
+ .kind_hint = kind_hint,
+ .deleted_entries = std.ArrayList(u8).init(bun.default_allocator),
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.event_loop),
+ };
+
+ const count = parent_task.subtask_count.fetchAdd(1, .monotonic);
+ if (comptime bun.Environment.allow_assert) {
+ assert(count > 0);
+ }
+
+ JSC.WorkPool.schedule(&subtask.task);
+ }
+
+ pub fn getcwd(this: *ShellRmTask) bun.FileDescriptor {
+ return this.cwd;
+ }
+
+ pub fn verboseDeleted(this: *@This(), dir_task: *DirTask, path: [:0]const u8) Maybe(void) {
+ debug("deleted: {s}", .{path[0..path.len]});
+ if (!this.opts.verbose) return Maybe(void).success;
+ if (dir_task.deleted_entries.items.len == 0) {
+ debug("DirTask(0x{x}, {s}) Incrementing output count (deleted={s})", .{ @intFromPtr(dir_task), dir_task.path, path });
+ _ = this.rm.state.exec.incrementOutputCount(.output_count);
+ }
+ dir_task.deleted_entries.appendSlice(path[0..path.len]) catch bun.outOfMemory();
+ dir_task.deleted_entries.append('\n') catch bun.outOfMemory();
+ return Maybe(void).success;
+ }
+
+ pub fn finishConcurrently(this: *ShellRmTask) void {
+ debug("finishConcurrently", .{});
+ if (this.event_loop == .js) {
+ this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
+ } else {
+ this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
+ }
+ }
+
+ pub fn bufJoin(this: *ShellRmTask, buf: *bun.PathBuffer, parts: []const []const u8, _: Syscall.Tag) Maybe([:0]const u8) {
+ if (this.join_style == .posix) {
+ return .{ .result = ResolvePath.joinZBuf(buf, parts, .posix) };
+ } else return .{ .result = ResolvePath.joinZBuf(buf, parts, .windows) };
+ }
+
+ pub fn removeEntry(this: *ShellRmTask, dir_task: *DirTask, is_absolute: bool) Maybe(void) {
+ var remove_child_vtable = RemoveFileVTable{
+ .task = this,
+ .child_of_dir = false,
+ };
+ var buf: bun.PathBuffer = undefined;
+ switch (dir_task.kind_hint) {
+ .idk, .file => return this.removeEntryFile(dir_task, dir_task.path, is_absolute, &buf, &remove_child_vtable),
+ .dir => return this.removeEntryDir(dir_task, is_absolute, &buf),
+ }
+ }
+
+ fn removeEntryDir(this: *ShellRmTask, dir_task: *DirTask, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
+ const path = dir_task.path;
+ const dirfd = this.cwd;
+ debug("removeEntryDir({s})", .{path});
+
+ // If `-d` is specified without `-r` then we can just use `rmdirat`
+ if (this.opts.remove_empty_dirs and !this.opts.recursive) out_to_iter: {
+ var delete_state = RemoveFileParent{
+ .task = this,
+ .treat_as_dir = true,
+ .allow_enqueue = false,
+ };
+ while (delete_state.treat_as_dir) {
+ switch (ShellSyscall.rmdirat(dirfd, path)) {
+ .result => return Maybe(void).success,
+ .err => |e| {
+ switch (e.getErrno()) {
+ bun.C.E.NOENT => {
+ if (this.opts.force) return this.verboseDeleted(dir_task, path);
+ return .{ .err = this.errorWithPath(e, path) };
+ },
+ bun.C.E.NOTDIR => {
+ delete_state.treat_as_dir = false;
+ if (this.removeEntryFile(dir_task, dir_task.path, is_absolute, buf, &delete_state).asErr()) |err| {
+ return .{ .err = this.errorWithPath(err, path) };
+ }
+ if (!delete_state.treat_as_dir) return Maybe(void).success;
+ if (delete_state.treat_as_dir) break :out_to_iter;
+ },
+ else => return .{ .err = this.errorWithPath(e, path) },
+ }
+ },
+ }
+ }
+ }
+
+ if (!this.opts.recursive) {
+ return Maybe(void).initErr(Syscall.Error.fromCode(bun.C.E.ISDIR, .TODO).withPath(bun.default_allocator.dupeZ(u8, dir_task.path) catch bun.outOfMemory()));
+ }
+
+ const flags = bun.O.DIRECTORY | bun.O.RDONLY;
+ const fd = switch (ShellSyscall.openat(dirfd, path, flags, 0)) {
+ .result => |fd| fd,
+ .err => |e| {
+ switch (e.getErrno()) {
+ bun.C.E.NOENT => {
+ if (this.opts.force) return this.verboseDeleted(dir_task, path);
+ return .{ .err = this.errorWithPath(e, path) };
+ },
+ bun.C.E.NOTDIR => {
+ return this.removeEntryFile(dir_task, dir_task.path, is_absolute, buf, &DummyRemoveFile.dummy);
+ },
+ else => return .{ .err = this.errorWithPath(e, path) },
+ }
+ },
+ };
+
+ var close_fd = true;
+ defer {
+ // On posix we can close the file descriptor whenever, but on Windows
+ // we need to close it BEFORE we delete
+ if (close_fd) {
+ _ = Syscall.close(fd);
+ }
+ }
+
+ if (this.error_signal.load(.seq_cst)) {
+ return Maybe(void).success;
+ }
+
+ var iterator = DirIterator.iterate(fd.asDir(), .u8);
+ var entry = iterator.next();
+
+ var remove_child_vtable = RemoveFileVTable{
+ .task = this,
+ .child_of_dir = true,
+ };
+
+ var i: usize = 0;
+ while (switch (entry) {
+ .err => |err| {
+ return .{ .err = this.errorWithPath(err, path) };
+ },
+ .result => |ent| ent,
+ }) |current| : (entry = iterator.next()) {
+ debug("dir({s}) entry({s}, {s})", .{ path, current.name.slice(), @tagName(current.kind) });
+ // TODO this seems bad maybe better to listen to kqueue/epoll event
+ if (fastMod(i, 4) == 0 and this.error_signal.load(.seq_cst)) return Maybe(void).success;
+
+ defer i += 1;
+ switch (current.kind) {
+ .directory => {
+ this.enqueue(dir_task, current.name.sliceAssumeZ(), is_absolute, .dir);
+ },
+ else => {
+ const name = current.name.sliceAssumeZ();
+ const file_path = switch (this.bufJoin(
+ buf,
+ &[_][]const u8{
+ path[0..path.len],
+ name[0..name.len],
+ },
+ .unlink,
+ )) {
+ .err => |e| return .{ .err = e },
+ .result => |p| p,
+ };
+
+ switch (this.removeEntryFile(dir_task, file_path, is_absolute, buf, &remove_child_vtable)) {
+ .err => |e| return .{ .err = this.errorWithPath(e, current.name.sliceAssumeZ()) },
+ .result => {},
+ }
+ },
+ }
+ }
+
+ // Need to wait for children to finish
+ if (dir_task.subtask_count.load(.seq_cst) > 1) {
+ close_fd = true;
+ dir_task.need_to_wait.store(true, .seq_cst);
+ return Maybe(void).success;
+ }
+
+ if (this.error_signal.load(.seq_cst)) return Maybe(void).success;
+
+ if (bun.Environment.isWindows) {
+ close_fd = false;
+ _ = Syscall.close(fd);
+ }
+
+ debug("[removeEntryDir] remove after children {s}", .{path});
+ switch (ShellSyscall.unlinkatWithFlags(this.getcwd(), path, std.posix.AT.REMOVEDIR)) {
+ .result => {
+ switch (this.verboseDeleted(dir_task, path)) {
+ .err => |e| return .{ .err = e },
+ else => {},
+ }
+ return Maybe(void).success;
+ },
+ .err => |e| {
+ switch (e.getErrno()) {
+ bun.C.E.NOENT => {
+ if (this.opts.force) {
+ switch (this.verboseDeleted(dir_task, path)) {
+ .err => |e2| return .{ .err = e2 },
+ else => {},
+ }
+ return Maybe(void).success;
+ }
+
+ return .{ .err = this.errorWithPath(e, path) };
+ },
+ else => return .{ .err = e },
+ }
+ },
+ }
+ }
+
+ const DummyRemoveFile = struct {
+ var dummy: @This() = std.mem.zeroes(@This());
+
+ pub fn onIsDir(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
+ _ = this; // autofix
+ _ = parent_dir_task; // autofix
+ _ = path; // autofix
+ _ = is_absolute; // autofix
+ _ = buf; // autofix
+
+ return Maybe(void).success;
+ }
+
+ pub fn onDirNotEmpty(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
+ _ = this; // autofix
+ _ = parent_dir_task; // autofix
+ _ = path; // autofix
+ _ = is_absolute; // autofix
+ _ = buf; // autofix
+
+ return Maybe(void).success;
+ }
+ };
+
+ const RemoveFileVTable = struct {
+ task: *ShellRmTask,
+ child_of_dir: bool,
+
+ pub fn onIsDir(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
+ if (this.child_of_dir) {
+ this.task.enqueueNoJoin(parent_dir_task, bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(), .dir);
+ return Maybe(void).success;
+ }
+ return this.task.removeEntryDir(parent_dir_task, is_absolute, buf);
+ }
+
+ pub fn onDirNotEmpty(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
+ if (this.child_of_dir) return .{ .result = this.task.enqueueNoJoin(parent_dir_task, bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(), .dir) };
+ return this.task.removeEntryDir(parent_dir_task, is_absolute, buf);
+ }
+ };
+
+ const RemoveFileParent = struct {
+ task: *ShellRmTask,
+ treat_as_dir: bool,
+ allow_enqueue: bool = true,
+ enqueued: bool = false,
+
+ pub fn onIsDir(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
+ _ = parent_dir_task; // autofix
+ _ = path; // autofix
+ _ = is_absolute; // autofix
+ _ = buf; // autofix
+
+ this.treat_as_dir = true;
+ return Maybe(void).success;
+ }
+
+ pub fn onDirNotEmpty(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
+ _ = is_absolute; // autofix
+ _ = buf; // autofix
+
+ this.treat_as_dir = true;
+ if (this.allow_enqueue) {
+ this.task.enqueueNoJoin(parent_dir_task, path, .dir);
+ this.enqueued = true;
+ }
+ return Maybe(void).success;
+ }
+ };
+
+ fn removeEntryDirAfterChildren(this: *ShellRmTask, dir_task: *DirTask) Maybe(bool) {
+ debug("remove entry after children: {s}", .{dir_task.path});
+ const dirfd = bun.toFD(this.cwd);
+ var state = RemoveFileParent{
+ .task = this,
+ .treat_as_dir = true,
+ };
+ while (true) {
+ if (state.treat_as_dir) {
+ log("rmdirat({}, {s})", .{ dirfd, dir_task.path });
+ switch (ShellSyscall.rmdirat(dirfd, dir_task.path)) {
+ .result => {
+ _ = this.verboseDeleted(dir_task, dir_task.path);
+ return .{ .result = true };
+ },
+ .err => |e| {
+ switch (e.getErrno()) {
+ bun.C.E.NOENT => {
+ if (this.opts.force) {
+ _ = this.verboseDeleted(dir_task, dir_task.path);
+ return .{ .result = true };
+ }
+ return .{ .err = this.errorWithPath(e, dir_task.path) };
+ },
+ bun.C.E.NOTDIR => {
+ state.treat_as_dir = false;
+ continue;
+ },
+ else => return .{ .err = this.errorWithPath(e, dir_task.path) },
+ }
+ },
+ }
+ } else {
+ var buf: bun.PathBuffer = undefined;
+ if (this.removeEntryFile(dir_task, dir_task.path, dir_task.is_absolute, &buf, &state).asErr()) |e| {
+ return .{ .err = e };
+ }
+ if (state.enqueued) return .{ .result = false };
+ if (state.treat_as_dir) continue;
+ return .{ .result = true };
+ }
+ }
+ }
+
+ fn removeEntryFile(this: *ShellRmTask, parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer, vtable: anytype) Maybe(void) {
+ const VTable = std.meta.Child(@TypeOf(vtable));
+ const Handler = struct {
+ pub fn onIsDir(vtable_: anytype, parent_dir_task_: *DirTask, path_: [:0]const u8, is_absolute_: bool, buf_: *bun.PathBuffer) Maybe(void) {
+ if (@hasDecl(VTable, "onIsDir")) {
+ return VTable.onIsDir(vtable_, parent_dir_task_, path_, is_absolute_, buf_);
+ }
+ return Maybe(void).success;
+ }
+
+ pub fn onDirNotEmpty(vtable_: anytype, parent_dir_task_: *DirTask, path_: [:0]const u8, is_absolute_: bool, buf_: *bun.PathBuffer) Maybe(void) {
+ if (@hasDecl(VTable, "onDirNotEmpty")) {
+ return VTable.onDirNotEmpty(vtable_, parent_dir_task_, path_, is_absolute_, buf_);
+ }
+ return Maybe(void).success;
+ }
+ };
+ const dirfd = bun.toFD(this.cwd);
+ switch (ShellSyscall.unlinkatWithFlags(dirfd, path, 0)) {
+ .result => return this.verboseDeleted(parent_dir_task, path),
+ .err => |e| {
+ debug("unlinkatWithFlags({s}) = {s}", .{ path, @tagName(e.getErrno()) });
+ switch (e.getErrno()) {
+ bun.C.E.NOENT => {
+ if (this.opts.force)
+ return this.verboseDeleted(parent_dir_task, path);
+
+ return .{ .err = this.errorWithPath(e, path) };
+ },
+ bun.C.E.ISDIR => {
+ return Handler.onIsDir(vtable, parent_dir_task, path, is_absolute, buf);
+ },
+ // This might happen if the file is actually a directory
+ bun.C.E.PERM => {
+ switch (builtin.os.tag) {
+ // non-Linux POSIX systems and Windows return EPERM when trying to delete a directory, so
+ // we need to handle that case specifically and translate the error
+ .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos, .windows => {
+ // If we are allowed to delete directories then we can call `unlink`.
+ // If `path` points to a directory, then it is deleted (if empty) or we handle it as a directory
+ // If it's actually a file, we get an error so we don't need to call `stat` to check that.
+ if (this.opts.recursive or this.opts.remove_empty_dirs) {
+ return switch (ShellSyscall.unlinkatWithFlags(this.getcwd(), path, std.posix.AT.REMOVEDIR)) {
+ // it was empty, we saved a syscall
+ .result => return this.verboseDeleted(parent_dir_task, path),
+ .err => |e2| {
+ return switch (e2.getErrno()) {
+ // not empty, process directory as we would normally
+ bun.C.E.NOTEMPTY => {
+ // this.enqueueNoJoin(parent_dir_task, path, .dir);
+ // return Maybe(void).success;
+ return Handler.onDirNotEmpty(vtable, parent_dir_task, path, is_absolute, buf);
+ },
+ // actually a file, the error is a permissions error
+ bun.C.E.NOTDIR => .{ .err = this.errorWithPath(e, path) },
+ else => .{ .err = this.errorWithPath(e2, path) },
+ };
+ },
+ };
+ }
+
+ // We don't know if it was an actual permissions error or it was a directory so we need to try to delete it as a directory
+ return Handler.onIsDir(vtable, parent_dir_task, path, is_absolute, buf);
+ },
+ else => {},
+ }
+
+ return .{ .err = this.errorWithPath(e, path) };
+ },
+ else => return .{ .err = this.errorWithPath(e, path) },
+ }
+ },
+ }
+ }
+
+ fn errorWithPath(this: *ShellRmTask, err: Syscall.Error, path: [:0]const u8) Syscall.Error {
+ _ = this;
+ return err.withPath(bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory());
+ }
+
+ inline fn join(this: *ShellRmTask, alloc: Allocator, subdir_parts: []const []const u8, is_absolute: bool) [:0]const u8 {
+ _ = this;
+ if (!is_absolute) {
+ // If relative paths enabled, stdlib join is preferred over
+ // ResolvePath.joinBuf because it doesn't try to normalize the path
+ return std.fs.path.joinZ(alloc, subdir_parts) catch bun.outOfMemory();
+ }
+
+ const out = alloc.dupeZ(u8, bun.path.join(subdir_parts, .auto)) catch bun.outOfMemory();
+
+ return out;
+ }
+
+ pub fn workPoolCallback(task: *JSC.WorkPoolTask) void {
+ var this: *ShellRmTask = @alignCast(@fieldParentPtr("task", task));
+ this.root_task.runFromThreadPoolImpl();
+ }
+
+ pub fn runFromMainThread(this: *ShellRmTask) void {
+ this.rm.onShellRmTaskDone(this);
+ }
+
+ pub fn runFromMainThreadMini(this: *ShellRmTask, _: *void) void {
+ this.rm.onShellRmTaskDone(this);
+ }
+
+ pub fn deinit(this: *ShellRmTask) void {
+ bun.default_allocator.destroy(this);
+ }
+};
+
+inline fn fastMod(val: anytype, comptime rhs: comptime_int) @TypeOf(val) {
+ const Value = @typeInfo(@TypeOf(val));
+ if (Value != .int) @compileError("LHS of fastMod should be an int");
+ if (Value.int.signedness != .unsigned) @compileError("LHS of fastMod should be unsigned");
+ if (!comptime std.math.isPowerOfTwo(rhs)) @compileError("RHS of fastMod should be power of 2");
+
+ return val & (rhs - 1);
+}
+
+// --
+const log = bun.Output.scoped(.Rm, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Rm = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const Syscall = bun.sys;
+const assert = bun.assert;
+const ResolvePath = bun.path;
+const Allocator = std.mem.Allocator;
+const DirIterator = bun.DirIterator;
+const builtin = @import("builtin");
diff --git a/src/shell/builtin/seq.zig b/src/shell/builtin/seq.zig
new file mode 100644
index 0000000000..f07c842f79
--- /dev/null
+++ b/src/shell/builtin/seq.zig
@@ -0,0 +1,162 @@
+state: enum { idle, waiting_io, err, done } = .idle,
+buf: std.ArrayListUnmanaged(u8) = .{},
+_start: f32 = 1,
+_end: f32 = 1,
+increment: f32 = 1,
+separator: []const u8 = "\n",
+terminator: []const u8 = "",
+fixed_width: bool = false,
+
+pub fn start(this: *@This()) Maybe(void) {
+ const args = this.bltn().argsSlice();
+ var iter = bun.SliceIterator([*:0]const u8).init(args);
+
+ if (args.len == 0) {
+ return this.fail(Builtin.Kind.usageString(.seq));
+ }
+ while (iter.next()) |item| {
+ const arg = bun.sliceTo(item, 0);
+
+ if (std.mem.eql(u8, arg, "-s") or std.mem.eql(u8, arg, "--separator")) {
+ this.separator = bun.sliceTo(iter.next() orelse return this.fail("seq: option requires an argument -- s\n"), 0);
+ continue;
+ }
+ if (std.mem.startsWith(u8, arg, "-s")) {
+ this.separator = arg[2..];
+ continue;
+ }
+
+ if (std.mem.eql(u8, arg, "-t") or std.mem.eql(u8, arg, "--terminator")) {
+ this.terminator = bun.sliceTo(iter.next() orelse return this.fail("seq: option requires an argument -- t\n"), 0);
+ continue;
+ }
+ if (std.mem.startsWith(u8, arg, "-t")) {
+ this.terminator = arg[2..];
+ continue;
+ }
+
+ if (std.mem.eql(u8, arg, "-w") or std.mem.eql(u8, arg, "--fixed-width")) {
+ this.fixed_width = true;
+ continue;
+ }
+
+ iter.index -= 1;
+ break;
+ }
+
+ const maybe1 = iter.next().?;
+ const int1 = std.fmt.parseFloat(f32, bun.sliceTo(maybe1, 0)) catch return this.fail("seq: invalid argument\n");
+ this._end = int1;
+ if (this._start > this._end) this.increment = -1;
+
+ const maybe2 = iter.next();
+ if (maybe2 == null) return this.do();
+ const int2 = std.fmt.parseFloat(f32, bun.sliceTo(maybe2.?, 0)) catch return this.fail("seq: invalid argument\n");
+ this._start = int1;
+ this._end = int2;
+ if (this._start < this._end) this.increment = 1;
+ if (this._start > this._end) this.increment = -1;
+
+ const maybe3 = iter.next();
+ if (maybe3 == null) return this.do();
+ const int3 = std.fmt.parseFloat(f32, bun.sliceTo(maybe3.?, 0)) catch return this.fail("seq: invalid argument\n");
+ this._start = int1;
+ this.increment = int2;
+ this._end = int3;
+
+ if (this.increment == 0) return this.fail("seq: zero increment\n");
+ if (this._start > this._end and this.increment > 0) return this.fail("seq: needs negative decrement\n");
+ if (this._start < this._end and this.increment < 0) return this.fail("seq: needs positive increment\n");
+
+ return this.do();
+}
+
+fn fail(this: *@This(), msg: []const u8) Maybe(void) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .err;
+ this.bltn().stderr.enqueue(this, msg, safeguard);
+ return Maybe(void).success;
+ }
+ _ = this.bltn().writeNoIO(.stderr, msg);
+ this.bltn().done(1);
+ return Maybe(void).success;
+}
+
+fn do(this: *@This()) Maybe(void) {
+ var current = this._start;
+ var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
+ defer arena.deinit();
+
+ while (if (this.increment > 0) current <= this._end else current >= this._end) : (current += this.increment) {
+ const str = std.fmt.allocPrint(arena.allocator(), "{d}", .{current}) catch bun.outOfMemory();
+ defer _ = arena.reset(.retain_capacity);
+ _ = this.print(str);
+ _ = this.print(this.separator);
+ }
+ _ = this.print(this.terminator);
+
+ this.state = .done;
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.bltn().stdout.enqueue(this, this.buf.items, safeguard);
+ } else {
+ this.bltn().done(0);
+ }
+ return Maybe(void).success;
+}
+
+fn print(this: *@This(), msg: []const u8) Maybe(void) {
+ if (this.bltn().stdout.needsIO() != null) {
+ this.buf.appendSlice(bun.default_allocator, msg) catch bun.outOfMemory();
+ return Maybe(void).success;
+ }
+ const res = this.bltn().writeNoIO(.stdout, msg);
+ if (res == .err) return Maybe(void).initErr(res.err);
+ return Maybe(void).success;
+}
+
+pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void {
+ if (maybe_e) |e| {
+ defer e.deref();
+ this.state = .err;
+ this.bltn().done(1);
+ return;
+ }
+ switch (this.state) {
+ .done => this.bltn().done(0),
+ .err => this.bltn().done(1),
+ else => {},
+ }
+}
+
+pub fn deinit(this: *@This()) void {
+ this.buf.deinit(bun.default_allocator);
+ //seq
+}
+
+pub inline fn bltn(this: *@This()) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("seq", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Seq = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
diff --git a/src/shell/builtin/touch.zig b/src/shell/builtin/touch.zig
new file mode 100644
index 0000000000..5b881d65a7
--- /dev/null
+++ b/src/shell/builtin/touch.zig
@@ -0,0 +1,426 @@
+opts: Opts = .{},
+state: union(enum) {
+ idle,
+ exec: struct {
+ started: bool = false,
+ tasks_count: usize = 0,
+ tasks_done: usize = 0,
+ output_done: usize = 0,
+ output_waiting: usize = 0,
+ started_output_queue: bool = false,
+ args: []const [*:0]const u8,
+ err: ?JSC.SystemError = null,
+ },
+ waiting_write_err,
+ done,
+} = .idle,
+
+pub fn format(this: *const Touch, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
+ _ = fmt; // autofix
+ _ = opts; // autofix
+ try writer.print("Touch(0x{x}, state={s})", .{ @intFromPtr(this), @tagName(this.state) });
+}
+
+pub fn deinit(this: *Touch) void {
+ log("{} deinit", .{this});
+}
+
+pub fn start(this: *Touch) Maybe(void) {
+ const filepath_args = switch (this.opts.parse(this.bltn().argsSlice())) {
+ .ok => |filepath_args| filepath_args,
+ .err => |e| {
+ const buf = switch (e) {
+ .illegal_option => |opt_str| this.bltn().fmtErrorArena(.touch, "illegal option -- {s}\n", .{opt_str}),
+ .show_usage => Builtin.Kind.touch.usageString(),
+ .unsupported => |unsupported| this.bltn().fmtErrorArena(.touch, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
+ };
+
+ _ = this.writeFailingError(buf, 1);
+ return Maybe(void).success;
+ },
+ } orelse {
+ _ = this.writeFailingError(Builtin.Kind.touch.usageString(), 1);
+ return Maybe(void).success;
+ };
+
+ this.state = .{
+ .exec = .{
+ .args = filepath_args,
+ },
+ };
+
+ _ = this.next();
+
+ return Maybe(void).success;
+}
+
+pub fn next(this: *Touch) void {
+ switch (this.state) {
+ .idle => @panic("Invalid state"),
+ .exec => {
+ var exec = &this.state.exec;
+ if (exec.started) {
+ if (this.state.exec.tasks_done >= this.state.exec.tasks_count and this.state.exec.output_done >= this.state.exec.output_waiting) {
+ const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0;
+ this.state = .done;
+ this.bltn().done(exit_code);
+ return;
+ }
+ return;
+ }
+
+ exec.started = true;
+ exec.tasks_count = exec.args.len;
+
+ for (exec.args) |dir_to_mk_| {
+ const dir_to_mk = dir_to_mk_[0..std.mem.len(dir_to_mk_) :0];
+ var task = ShellTouchTask.create(this, this.opts, dir_to_mk, this.bltn().parentCmd().base.shell.cwdZ());
+ task.schedule();
+ }
+ },
+ .waiting_write_err => return,
+ .done => this.bltn().done(0),
+ }
+}
+
+pub fn onIOWriterChunk(this: *Touch, _: usize, e: ?JSC.SystemError) void {
+ if (this.state == .waiting_write_err) {
+ return this.bltn().done(1);
+ }
+
+ if (e) |err| err.deref();
+
+ this.next();
+}
+
+pub fn writeFailingError(this: *Touch, buf: []const u8, exit_code: ExitCode) Maybe(void) {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state = .waiting_write_err;
+ this.bltn().stderr.enqueue(this, buf, safeguard);
+ return Maybe(void).success;
+ }
+
+ _ = this.bltn().writeNoIO(.stderr, buf);
+
+ this.bltn().done(exit_code);
+ return Maybe(void).success;
+}
+
+pub fn onShellTouchTaskDone(this: *Touch, task: *ShellTouchTask) void {
+ log("{} onShellTouchTaskDone {} tasks_done={d} tasks_count={d}", .{ this, task, this.state.exec.tasks_done, this.state.exec.tasks_count });
+
+ defer bun.default_allocator.destroy(task);
+ this.state.exec.tasks_done += 1;
+ const err = task.err;
+
+ if (err) |e| {
+ const output_task: *ShellTouchOutputTask = bun.new(ShellTouchOutputTask, .{
+ .parent = this,
+ .output = .{ .arrlist = .{} },
+ .state = .waiting_write_err,
+ });
+ const error_string = this.bltn().taskErrorToString(.touch, e);
+ this.state.exec.err = e;
+ output_task.start(error_string);
+ return;
+ }
+
+ this.next();
+}
+
+pub const ShellTouchOutputTask = OutputTask(Touch, .{
+ .writeErr = ShellTouchOutputTaskVTable.writeErr,
+ .onWriteErr = ShellTouchOutputTaskVTable.onWriteErr,
+ .writeOut = ShellTouchOutputTaskVTable.writeOut,
+ .onWriteOut = ShellTouchOutputTaskVTable.onWriteOut,
+ .onDone = ShellTouchOutputTaskVTable.onDone,
+});
+
+const ShellTouchOutputTaskVTable = struct {
+ pub fn writeErr(this: *Touch, childptr: anytype, errbuf: []const u8) CoroutineResult {
+ if (this.bltn().stderr.needsIO()) |safeguard| {
+ this.state.exec.output_waiting += 1;
+ this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
+ return .yield;
+ }
+ _ = this.bltn().writeNoIO(.stderr, errbuf);
+ return .cont;
+ }
+
+ pub fn onWriteErr(this: *Touch) void {
+ this.state.exec.output_done += 1;
+ }
+
+ pub fn writeOut(this: *Touch, childptr: anytype, output: *OutputSrc) CoroutineResult {
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.state.exec.output_waiting += 1;
+ const slice = output.slice();
+ log("THE SLICE: {d} {s}", .{ slice.len, slice });
+ this.bltn().stdout.enqueue(childptr, slice, safeguard);
+ return .yield;
+ }
+ _ = this.bltn().writeNoIO(.stdout, output.slice());
+ return .cont;
+ }
+
+ pub fn onWriteOut(this: *Touch) void {
+ this.state.exec.output_done += 1;
+ }
+
+ pub fn onDone(this: *Touch) void {
+ this.next();
+ }
+};
+
+pub const ShellTouchTask = struct {
+ touch: *Touch,
+
+ opts: Opts,
+ filepath: [:0]const u8,
+ cwd_path: [:0]const u8,
+
+ err: ?JSC.SystemError = null,
+ task: JSC.WorkPoolTask = .{ .callback = &runFromThreadPool },
+ event_loop: JSC.EventLoopHandle,
+ concurrent_task: JSC.EventLoopTask,
+
+ pub fn format(this: *const ShellTouchTask, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
+ _ = fmt; // autofix
+ _ = opts; // autofix
+ try writer.print("ShellTouchTask(0x{x}, filepath={s})", .{ @intFromPtr(this), this.filepath });
+ }
+
+ pub fn deinit(this: *ShellTouchTask) void {
+ if (this.err) |e| {
+ e.deref();
+ }
+ bun.default_allocator.destroy(this);
+ }
+
+ pub fn create(touch: *Touch, opts: Opts, filepath: [:0]const u8, cwd_path: [:0]const u8) *ShellTouchTask {
+ const task = bun.default_allocator.create(ShellTouchTask) catch bun.outOfMemory();
+ task.* = ShellTouchTask{
+ .touch = touch,
+ .opts = opts,
+ .cwd_path = cwd_path,
+ .filepath = filepath,
+ .event_loop = touch.bltn().eventLoop(),
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(touch.bltn().eventLoop()),
+ };
+ return task;
+ }
+
+ pub fn schedule(this: *@This()) void {
+ debug("{} schedule", .{this});
+ WorkPool.schedule(&this.task);
+ }
+
+ pub fn runFromMainThread(this: *@This()) void {
+ debug("{} runFromJS", .{this});
+ this.touch.onShellTouchTaskDone(this);
+ }
+
+ pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
+ this.runFromMainThread();
+ }
+
+ fn runFromThreadPool(task: *JSC.WorkPoolTask) void {
+ var this: *ShellTouchTask = @fieldParentPtr("task", task);
+ debug("{} runFromThreadPool", .{this});
+
+ // We have to give an absolute path
+ const filepath: [:0]const u8 = brk: {
+ if (ResolvePath.Platform.auto.isAbsolute(this.filepath)) break :brk this.filepath;
+ const parts: []const []const u8 = &.{
+ this.cwd_path[0..],
+ this.filepath[0..],
+ };
+ break :brk ResolvePath.joinZ(parts, .auto);
+ };
+
+ var node_fs = JSC.Node.NodeFS{};
+ const milliseconds: f64 = @floatFromInt(std.time.milliTimestamp());
+ const atime: JSC.Node.TimeLike = if (bun.Environment.isWindows) milliseconds / 1000.0 else JSC.Node.TimeLike{
+ .sec = @intFromFloat(@divFloor(milliseconds, std.time.ms_per_s)),
+ .nsec = @intFromFloat(@mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms),
+ };
+ const mtime = atime;
+ const args = JSC.Node.Arguments.Utimes{
+ .atime = atime,
+ .mtime = mtime,
+ .path = .{ .string = bun.PathString.init(filepath) },
+ };
+ if (node_fs.utimes(args, .sync).asErr()) |err| out: {
+ if (err.getErrno() == bun.C.E.NOENT) {
+ const perm = 0o664;
+ switch (Syscall.open(filepath, bun.O.CREAT | bun.O.WRONLY, perm)) {
+ .result => |fd| {
+ _ = bun.sys.close(fd);
+ break :out;
+ },
+ .err => |e| {
+ this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError();
+ break :out;
+ },
+ }
+ }
+ this.err = err.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError();
+ }
+
+ if (this.event_loop == .js) {
+ this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
+ } else {
+ this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
+ }
+ }
+};
+
+const Opts = struct {
+ /// -a
+ ///
+ /// change only the access time
+ access_time_only: bool = false,
+
+ /// -c, --no-create
+ ///
+ /// do not create any files
+ no_create: bool = false,
+
+ /// -d, --date=STRING
+ ///
+ /// parse STRING and use it instead of current time
+ date: ?[]const u8 = null,
+
+ /// -h, --no-dereference
+ ///
+ /// affect each symbolic link instead of any referenced file
+ /// (useful only on systems that can change the timestamps of a symlink)
+ no_dereference: bool = false,
+
+ /// -m
+ ///
+ /// change only the modification time
+ modification_time_only: bool = false,
+
+ /// -r, --reference=FILE
+ ///
+ /// use this file's times instead of current time
+ reference: ?[]const u8 = null,
+
+ /// -t STAMP
+ ///
+ /// use [[CC]YY]MMDDhhmm[.ss] instead of current time
+ timestamp: ?[]const u8 = null,
+
+ /// --time=WORD
+ ///
+ /// change the specified time:
+ /// WORD is access, atime, or use: equivalent to -a
+ /// WORD is modify or mtime: equivalent to -m
+ time: ?[]const u8 = null,
+
+ const Parse = FlagParser(*@This());
+
+ pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) {
+ return Parse.parseFlags(opts, args);
+ }
+
+ pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult {
+ _ = this;
+ if (bun.strings.eqlComptime(flag, "--no-create")) {
+ return .{
+ .unsupported = unsupportedFlag("--no-create"),
+ };
+ }
+
+ if (bun.strings.eqlComptime(flag, "--date")) {
+ return .{
+ .unsupported = unsupportedFlag("--date"),
+ };
+ }
+
+ if (bun.strings.eqlComptime(flag, "--reference")) {
+ return .{
+ .unsupported = unsupportedFlag("--reference=FILE"),
+ };
+ }
+
+ if (bun.strings.eqlComptime(flag, "--time")) {
+ return .{
+ .unsupported = unsupportedFlag("--reference=FILE"),
+ };
+ }
+
+ return null;
+ }
+
+ pub fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
+ _ = this;
+ switch (char) {
+ 'a' => {
+ return .{ .unsupported = unsupportedFlag("-a") };
+ },
+ 'c' => {
+ return .{ .unsupported = unsupportedFlag("-c") };
+ },
+ 'd' => {
+ return .{ .unsupported = unsupportedFlag("-d") };
+ },
+ 'h' => {
+ return .{ .unsupported = unsupportedFlag("-h") };
+ },
+ 'm' => {
+ return .{ .unsupported = unsupportedFlag("-m") };
+ },
+ 'r' => {
+ return .{ .unsupported = unsupportedFlag("-r") };
+ },
+ 't' => {
+ return .{ .unsupported = unsupportedFlag("-t") };
+ },
+ else => {
+ return .{ .illegal_option = smallflags[1 + i ..] };
+ },
+ }
+
+ return null;
+ }
+};
+
+pub inline fn bltn(this: *Touch) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("touch", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const debug = bun.Output.scoped(.ShellTouch, true);
+const Touch = @This();
+const log = debug;
+const std = @import("std");
+const bun = @import("root").bun;
+const shell = bun.shell;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const WorkPool = bun.JSC.WorkPool;
+const ResolvePath = bun.path;
+const Syscall = bun.sys;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ReadChunkAction = interpreter.ReadChunkAction;
+const FlagParser = interpreter.FlagParser;
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const OutputTask = interpreter.OutputTask;
+const CoroutineResult = interpreter.CoroutineResult;
+const OutputSrc = interpreter.OutputSrc;
diff --git a/src/shell/builtin/true.zig b/src/shell/builtin/true.zig
new file mode 100644
index 0000000000..da0781b6ac
--- /dev/null
+++ b/src/shell/builtin/true.zig
@@ -0,0 +1,26 @@
+pub fn start(this: *@This()) Maybe(void) {
+ this.bltn().done(0);
+ return Maybe(void).success;
+}
+
+pub fn onIOWriterChunk(_: *@This(), _: usize, _: ?JSC.SystemError) void {
+ // no IO is done
+}
+
+pub fn deinit(this: *@This()) void {
+ _ = this;
+}
+
+pub inline fn bltn(this: *@This()) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("true", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const bun = @import("root").bun;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
diff --git a/src/shell/builtin/which.zig b/src/shell/builtin/which.zig
new file mode 100644
index 0000000000..f48dc1673e
--- /dev/null
+++ b/src/shell/builtin/which.zig
@@ -0,0 +1,177 @@
+//! 1 arg => returns absolute path of the arg (not found becomes exit code 1)
+//!
+//! N args => returns absolute path of each separated by newline, if any path is not found, exit code becomes 1, but continues execution until all args are processed
+
+state: union(enum) {
+ idle,
+ one_arg,
+ multi_args: struct {
+ args_slice: []const [*:0]const u8,
+ arg_idx: usize,
+ had_not_found: bool = false,
+ state: union(enum) {
+ none,
+ waiting_write,
+ },
+ },
+ done,
+ err: JSC.SystemError,
+} = .idle,
+
+pub fn start(this: *Which) Maybe(void) {
+ const args = this.bltn().argsSlice();
+ if (args.len == 0) {
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ this.state = .one_arg;
+ this.bltn().stdout.enqueue(this, "\n", safeguard);
+ return Maybe(void).success;
+ }
+ _ = this.bltn().writeNoIO(.stdout, "\n");
+ this.bltn().done(1);
+ return Maybe(void).success;
+ }
+
+ if (this.bltn().stdout.needsIO() == null) {
+ const path_buf = bun.PathBufferPool.get();
+ defer bun.PathBufferPool.put(path_buf);
+ const PATH = this.bltn().parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice("");
+ var had_not_found = false;
+ for (args) |arg_raw| {
+ const arg = arg_raw[0..std.mem.len(arg_raw)];
+ const resolved = which(path_buf, PATH.slice(), this.bltn().parentCmd().base.shell.cwdZ(), arg) orelse {
+ had_not_found = true;
+ const buf = this.bltn().fmtErrorArena(.which, "{s} not found\n", .{arg});
+ _ = this.bltn().writeNoIO(.stdout, buf);
+ continue;
+ };
+
+ _ = this.bltn().writeNoIO(.stdout, resolved);
+ }
+ this.bltn().done(@intFromBool(had_not_found));
+ return Maybe(void).success;
+ }
+
+ this.state = .{
+ .multi_args = .{
+ .args_slice = args,
+ .arg_idx = 0,
+ .state = .none,
+ },
+ };
+ this.next();
+ return Maybe(void).success;
+}
+
+pub fn next(this: *Which) void {
+ var multiargs = &this.state.multi_args;
+ if (multiargs.arg_idx >= multiargs.args_slice.len) {
+ // Done
+ this.bltn().done(@intFromBool(multiargs.had_not_found));
+ return;
+ }
+
+ const arg_raw = multiargs.args_slice[multiargs.arg_idx];
+ const arg = arg_raw[0..std.mem.len(arg_raw)];
+
+ const path_buf = bun.PathBufferPool.get();
+ defer bun.PathBufferPool.put(path_buf);
+ const PATH = this.bltn().parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice("");
+
+ const resolved = which(path_buf, PATH.slice(), this.bltn().parentCmd().base.shell.cwdZ(), arg) orelse {
+ multiargs.had_not_found = true;
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ multiargs.state = .waiting_write;
+ this.bltn().stdout.enqueueFmtBltn(this, null, "{s} not found\n", .{arg}, safeguard);
+ // yield execution
+ return;
+ }
+
+ const buf = this.bltn().fmtErrorArena(null, "{s} not found\n", .{arg});
+ _ = this.bltn().writeNoIO(.stdout, buf);
+ this.argComplete();
+ return;
+ };
+
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ multiargs.state = .waiting_write;
+ this.bltn().stdout.enqueueFmtBltn(this, null, "{s}\n", .{resolved}, safeguard);
+ return;
+ }
+
+ const buf = this.bltn().fmtErrorArena(null, "{s}\n", .{resolved});
+ _ = this.bltn().writeNoIO(.stdout, buf);
+ this.argComplete();
+ return;
+}
+
+fn argComplete(this: *Which) void {
+ if (comptime bun.Environment.allow_assert) {
+ assert(this.state == .multi_args and this.state.multi_args.state == .waiting_write);
+ }
+
+ this.state.multi_args.arg_idx += 1;
+ this.state.multi_args.state = .none;
+ this.next();
+}
+
+pub fn onIOWriterChunk(this: *Which, _: usize, e: ?JSC.SystemError) void {
+ if (comptime bun.Environment.allow_assert) {
+ assert(this.state == .one_arg or
+ (this.state == .multi_args and this.state.multi_args.state == .waiting_write));
+ }
+
+ if (e != null) {
+ this.state = .{ .err = e.? };
+ this.bltn().done(e.?.getErrno());
+ return;
+ }
+
+ if (this.state == .one_arg) {
+ // Calling which with on arguments returns exit code 1
+ this.bltn().done(1);
+ return;
+ }
+
+ this.argComplete();
+}
+
+pub fn deinit(this: *Which) void {
+ log("({s}) deinit", .{@tagName(.which)});
+ _ = this;
+}
+
+pub inline fn bltn(this: *Which) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("which", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+// --
+const log = bun.Output.scoped(.which, true);
+const Which = @This();
+
+const std = @import("std");
+const bun = @import("root").bun;
+const shell = bun.shell;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const assert = bun.assert;
+
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ReadChunkAction = interpreter.ReadChunkAction;
+const FlagParser = interpreter.FlagParser;
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const EnvStr = interpreter.EnvStr;
+const which = bun.which;
diff --git a/src/shell/builtin/yes.zig b/src/shell/builtin/yes.zig
new file mode 100644
index 0000000000..2d01417501
--- /dev/null
+++ b/src/shell/builtin/yes.zig
@@ -0,0 +1,112 @@
+state: enum { idle, waiting_io, err, done } = .idle,
+expletive: []const u8 = "y",
+task: YesTask = undefined,
+
+pub fn start(this: *@This()) Maybe(void) {
+ const args = this.bltn().argsSlice();
+
+ if (args.len > 0) {
+ this.expletive = std.mem.sliceTo(args[0], 0);
+ }
+
+ if (this.bltn().stdout.needsIO()) |safeguard| {
+ const evtloop = this.bltn().eventLoop();
+ this.task = .{
+ .evtloop = evtloop,
+ .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop),
+ };
+ this.state = .waiting_io;
+ this.bltn().stdout.enqueue(this, this.expletive, safeguard);
+ this.bltn().stdout.enqueue(this, "\n", safeguard);
+ this.task.enqueue();
+ return Maybe(void).success;
+ }
+
+ var res: Maybe(usize) = undefined;
+ while (true) {
+ res = this.bltn().writeNoIO(.stdout, this.expletive);
+ if (res == .err) {
+ this.bltn().done(1);
+ return Maybe(void).success;
+ }
+ res = this.bltn().writeNoIO(.stdout, "\n");
+ if (res == .err) {
+ this.bltn().done(1);
+ return Maybe(void).success;
+ }
+ }
+ @compileError(unreachable);
+}
+
+pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void {
+ if (maybe_e) |e| {
+ defer e.deref();
+ this.state = .err;
+ this.bltn().done(1);
+ return;
+ }
+}
+
+pub inline fn bltn(this: *@This()) *Builtin {
+ const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("yes", this));
+ return @fieldParentPtr("impl", impl);
+}
+
+pub fn deinit(_: *@This()) void {}
+
+pub const YesTask = struct {
+ evtloop: JSC.EventLoopHandle,
+ concurrent_task: JSC.EventLoopTask,
+
+ pub fn enqueue(this: *@This()) void {
+ if (this.evtloop == .js) {
+ this.evtloop.js.tick();
+ this.evtloop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
+ } else {
+ this.evtloop.mini.loop.tick();
+ this.evtloop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
+ }
+ }
+
+ pub fn runFromMainThread(this: *@This()) void {
+ const yes: *Yes = @fieldParentPtr("task", this);
+
+ // Manually make safeguard since this task should not be created if output does not need IO
+ yes.bltn().stdout.enqueue(yes, yes.expletive, OutputNeedsIOSafeGuard{ .__i_know_what_i_am_doing_it_needs_io_yes = 0 });
+ yes.bltn().stdout.enqueue(yes, "\n", OutputNeedsIOSafeGuard{ .__i_know_what_i_am_doing_it_needs_io_yes = 0 });
+
+ this.enqueue();
+ }
+
+ pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
+ this.runFromMainThread();
+ }
+};
+
+// --
+const log = bun.Output.scoped(.Yes, true);
+const bun = @import("root").bun;
+const shell = bun.shell;
+const interpreter = @import("../interpreter.zig");
+const Interpreter = interpreter.Interpreter;
+const Builtin = Interpreter.Builtin;
+const Result = Interpreter.Builtin.Result;
+const ParseError = interpreter.ParseError;
+const ParseFlagResult = interpreter.ParseFlagResult;
+const ExitCode = shell.ExitCode;
+const IOReader = shell.IOReader;
+const IOWriter = shell.IOWriter;
+const IO = shell.IO;
+const IOVector = shell.IOVector;
+const IOVectorSlice = shell.IOVectorSlice;
+const IOVectorSliceMut = shell.IOVectorSliceMut;
+const Yes = @This();
+const ReadChunkAction = interpreter.ReadChunkAction;
+const JSC = bun.JSC;
+const Maybe = bun.sys.Maybe;
+const std = @import("std");
+const FlagParser = interpreter.FlagParser;
+
+const ShellSyscall = interpreter.ShellSyscall;
+const unsupportedFlag = interpreter.unsupportedFlag;
+const OutputNeedsIOSafeGuard = interpreter.OutputNeedsIOSafeGuard;
diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig
index 21e0fb1436..4a1d089f24 100644
--- a/src/shell/interpreter.zig
+++ b/src/shell/interpreter.zig
@@ -28,23 +28,23 @@ const JSC = bun.JSC;
const JSValue = bun.JSC.JSValue;
const JSPromise = bun.JSC.JSPromise;
const JSGlobalObject = bun.JSC.JSGlobalObject;
-const which = @import("../which.zig").which;
+const which = bun.which;
const Braces = @import("./braces.zig");
-const Syscall = @import("../sys.zig");
+const Syscall = bun.sys;
const Glob = @import("../glob.zig");
-const ResolvePath = @import("../resolver/resolve_path.zig");
-const DirIterator = @import("../bun.js/node/dir_iterator.zig");
-const TaggedPointerUnion = @import("../ptr.zig").TaggedPointerUnion;
-const TaggedPointer = @import("../ptr.zig").TaggedPointer;
-pub const WorkPoolTask = @import("../work_pool.zig").Task;
-pub const WorkPool = @import("../work_pool.zig").WorkPool;
+const ResolvePath = bun.path;
+const DirIterator = bun.DirIterator;
+const TaggedPointerUnion = bun.TaggedPointerUnion;
+const TaggedPointer = bun.TaggedPointer;
+pub const WorkPoolTask = JSC.WorkPoolTask;
+pub const WorkPool = JSC.WorkPool;
const windows = bun.windows;
const uv = windows.libuv;
const Maybe = JSC.Maybe;
const WTFStringImplStruct = @import("../string.zig").WTFStringImplStruct;
const Pipe = [2]bun.FileDescriptor;
-const shell = @import("./shell.zig");
+const shell = bun.shell;
const Token = shell.Token;
const ShellError = shell.ShellError;
const ast = shell.AST;
@@ -63,7 +63,7 @@ pub fn OOM(e: anyerror) noreturn {
bun.outOfMemory();
}
-const log = bun.Output.scoped(.SHELL, false);
+pub const log = bun.Output.scoped(.SHELL, false);
const assert = bun.assert;
@@ -318,262 +318,10 @@ pub const IO = struct {
}
};
-/// Environment strings need to be copied a lot
-/// So we make them reference counted
-///
-/// But sometimes we use strings that are statically allocated, or are allocated
-/// with a predetermined lifetime (e.g. strings in the AST). In that case we
-/// don't want to incur the cost of heap allocating them and refcounting them
-///
-/// So environment strings can be ref counted or borrowed slices
-pub const EnvStr = packed struct {
- ptr: u48,
- tag: Tag = .empty,
- len: usize = 0,
-
- const debug = bun.Output.scoped(.EnvStr, true);
-
- const Tag = enum(u16) {
- /// no value
- empty,
-
- /// Dealloced by reference counting
- refcounted,
-
- /// Memory is managed elsewhere so don't dealloc it
- slice,
- };
-
- inline fn initSlice(str: []const u8) EnvStr {
- if (str.len == 0)
- // Zero length strings may have invalid pointers, leading to a bad integer cast.
- return .{ .tag = .empty, .ptr = 0, .len = 0 };
-
- return .{
- .ptr = toPtr(str.ptr),
- .tag = .slice,
- .len = str.len,
- };
- }
-
- fn toPtr(ptr_val: *const anyopaque) u48 {
- const num: [8]u8 = @bitCast(@intFromPtr(ptr_val));
- return @bitCast(num[0..6].*);
- }
-
- fn initRefCounted(str: []const u8) EnvStr {
- if (str.len == 0)
- return .{ .tag = .empty, .ptr = 0, .len = 0 };
-
- return .{
- .ptr = toPtr(RefCountedStr.init(str)),
- .tag = .refcounted,
- };
- }
-
- pub fn slice(this: EnvStr) []const u8 {
- return switch (this.tag) {
- .empty => "",
- .slice => this.castSlice(),
- .refcounted => this.castRefCounted().byteSlice(),
- };
- }
-
- fn ref(this: EnvStr) void {
- if (this.asRefCounted()) |refc| {
- refc.ref();
- }
- }
-
- fn deref(this: EnvStr) void {
- if (this.asRefCounted()) |refc| {
- refc.deref();
- }
- }
-
- inline fn asRefCounted(this: EnvStr) ?*RefCountedStr {
- if (this.tag == .refcounted) return this.castRefCounted();
- return null;
- }
-
- inline fn castSlice(this: EnvStr) []const u8 {
- return @as([*]u8, @ptrFromInt(@as(usize, @intCast(this.ptr))))[0..this.len];
- }
-
- inline fn castRefCounted(this: EnvStr) *RefCountedStr {
- return @ptrFromInt(@as(usize, @intCast(this.ptr)));
- }
-};
-
-pub const RefCountedStr = struct {
- refcount: u32 = 1,
- len: u32 = 0,
- ptr: [*]const u8 = undefined,
-
- const debug = bun.Output.scoped(.RefCountedEnvStr, true);
-
- fn init(slice: []const u8) *RefCountedStr {
- debug("init: {s}", .{slice});
- const this = bun.default_allocator.create(RefCountedStr) catch bun.outOfMemory();
- this.* = .{
- .refcount = 1,
- .len = @intCast(slice.len),
- .ptr = slice.ptr,
- };
- return this;
- }
-
- fn byteSlice(this: *RefCountedStr) []const u8 {
- if (this.len == 0) return "";
- return this.ptr[0..this.len];
- }
-
- fn ref(this: *RefCountedStr) void {
- this.refcount += 1;
- }
-
- fn deref(this: *RefCountedStr) void {
- this.refcount -= 1;
- if (this.refcount == 0) {
- this.deinit();
- }
- }
-
- fn deinit(this: *RefCountedStr) void {
- debug("deinit: {s}", .{this.byteSlice()});
- this.freeStr();
- bun.default_allocator.destroy(this);
- }
-
- fn freeStr(this: *RefCountedStr) void {
- if (this.len == 0) return;
- bun.default_allocator.free(this.ptr[0..this.len]);
- }
-};
-
-/// TODO use this
-/// Either
-/// A: subshells (`$(...)` or `(...)`) or
-/// B: commands in a pipeline
-/// will need their own copy of the shell environment because they could modify it,
-/// and those changes shouldn't affect the surounding environment.
-///
-/// This results in a lot of copying, which is wasteful since most of the time
-/// A) or B) won't even mutate the environment anyway.
-///
-/// A way to reduce copying is to only do it when the env is mutated: copy-on-write.
-pub const CowEnvMap = bun.ptr.Cow(EnvMap, struct {
- pub fn copy(val: *const EnvMap) EnvMap {
- return val.clone();
- }
-
- pub fn deinit(val: *EnvMap) void {
- val.deinit();
- }
-});
-
-pub const EnvMap = struct {
- map: MapType,
-
- pub const Iterator = MapType.Iterator;
-
- const MapType = std.ArrayHashMap(EnvStr, EnvStr, struct {
- pub fn hash(self: @This(), s: EnvStr) u32 {
- _ = self;
- if (bun.Environment.isWindows) {
- return bun.CaseInsensitiveASCIIStringContext.hash(undefined, s.slice());
- }
- return std.array_hash_map.hashString(s.slice());
- }
- pub fn eql(self: @This(), a: EnvStr, b: EnvStr, b_index: usize) bool {
- _ = self;
- _ = b_index;
- if (bun.Environment.isWindows) {
- return bun.CaseInsensitiveASCIIStringContext.eql(undefined, a.slice(), b.slice(), undefined);
- }
- return std.array_hash_map.eqlString(a.slice(), b.slice());
- }
- }, true);
-
- fn init(alloc: Allocator) EnvMap {
- return .{ .map = MapType.init(alloc) };
- }
-
- fn initWithCapacity(alloc: Allocator, cap: usize) EnvMap {
- var map = MapType.init(alloc);
- map.ensureTotalCapacity(cap) catch bun.outOfMemory();
- return .{ .map = map };
- }
-
- fn deinit(this: *EnvMap) void {
- this.derefStrings();
- this.map.deinit();
- }
-
- fn insert(this: *EnvMap, key: EnvStr, val: EnvStr) void {
- const result = this.map.getOrPut(key) catch bun.outOfMemory();
- if (!result.found_existing) {
- key.ref();
- } else {
- result.value_ptr.deref();
- }
- val.ref();
- result.value_ptr.* = val;
- }
-
- fn iterator(this: *EnvMap) MapType.Iterator {
- return this.map.iterator();
- }
-
- fn clearRetainingCapacity(this: *EnvMap) void {
- this.derefStrings();
- this.map.clearRetainingCapacity();
- }
-
- fn ensureTotalCapacity(this: *EnvMap, new_capacity: usize) void {
- this.map.ensureTotalCapacity(new_capacity) catch bun.outOfMemory();
- }
-
- /// NOTE: Make sure you deref the string when done!
- fn get(this: *EnvMap, key: EnvStr) ?EnvStr {
- const val = this.map.get(key) orelse return null;
- val.ref();
- return val;
- }
-
- fn clone(this: *EnvMap) EnvMap {
- var new: EnvMap = .{
- .map = this.map.clone() catch bun.outOfMemory(),
- };
- new.refStrings();
- return new;
- }
-
- fn cloneWithAllocator(this: *EnvMap, allocator: Allocator) EnvMap {
- var new: EnvMap = .{
- .map = this.map.cloneWithAllocator(allocator) catch bun.outOfMemory(),
- };
- new.refStrings();
- return new;
- }
-
- fn refStrings(this: *EnvMap) void {
- var iter = this.map.iterator();
- while (iter.next()) |entry| {
- entry.key_ptr.ref();
- entry.value_ptr.ref();
- }
- }
-
- fn derefStrings(this: *EnvMap) void {
- var iter = this.map.iterator();
- while (iter.next()) |entry| {
- entry.key_ptr.deref();
- entry.value_ptr.deref();
- }
- }
-};
-
+pub const RefCountedStr = @import("./RefCountedStr.zig");
+pub const EnvStr = @import("./EnvStr.zig").EnvStr;
+pub const EnvMap = @import("./EnvMap.zig");
+pub const ParsedShellScript = @import("./ParsedShellScript.zig");
pub const ShellArgs = struct {
/// This is the arena used to allocate the input shell script's AST nodes,
/// tokens, and a string pool used to store all strings.
@@ -602,172 +350,6 @@ pub const ShellArgs = struct {
}
};
-pub const ParsedShellScript = struct {
- pub usingnamespace JSC.Codegen.JSParsedShellScript;
- args: ?*ShellArgs = null,
- /// allocated with arena in jsobjs
- jsobjs: std.ArrayList(JSValue),
- export_env: ?EnvMap = null,
- quiet: bool = false,
- cwd: ?bun.String = null,
- this_jsvalue: JSValue = .zero,
-
- fn take(
- this: *ParsedShellScript,
- globalObject: *JSC.JSGlobalObject,
- out_args: **ShellArgs,
- out_jsobjs: *std.ArrayList(JSValue),
- out_quiet: *bool,
- out_cwd: *?bun.String,
- out_export_env: *?EnvMap,
- ) void {
- _ = globalObject; // autofix
- out_args.* = this.args.?;
- out_jsobjs.* = this.jsobjs;
- out_quiet.* = this.quiet;
- out_cwd.* = this.cwd;
- out_export_env.* = this.export_env;
-
- this.args = null;
- this.jsobjs = std.ArrayList(JSValue).init(bun.default_allocator);
- this.cwd = null;
- this.export_env = null;
- }
-
- pub fn finalize(
- this: *ParsedShellScript,
- ) void {
- this.this_jsvalue = .zero;
- log("ParsedShellScript(0x{x}) finalize", .{@intFromPtr(this)});
- if (this.export_env) |*env| env.deinit();
- if (this.cwd) |*cwd| cwd.deref();
- for (this.jsobjs.items) |jsobj| {
- jsobj.unprotect();
- }
- if (this.args) |a| a.deinit();
- bun.destroy(this);
- }
-
- pub fn setCwd(this: *ParsedShellScript, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
- const arguments_ = callframe.arguments_old(2);
- var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice());
- const str_js = arguments.nextEat() orelse {
- return globalThis.throw("$`...`.cwd(): expected a string argument", .{});
- };
- const str = try bun.String.fromJS(str_js, globalThis);
- this.cwd = str;
- return .undefined;
- }
-
- pub fn setQuiet(this: *ParsedShellScript, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue {
- log("Interpreter(0x{x}) setQuiet()", .{@intFromPtr(this)});
- this.quiet = true;
- return .undefined;
- }
-
- pub fn setEnv(this: *ParsedShellScript, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
- const value1 = callframe.argument(0).getObject() orelse {
- return globalThis.throwInvalidArguments("env must be an object", .{});
- };
-
- var object_iter = try JSC.JSPropertyIterator(.{
- .skip_empty_name = false,
- .include_value = true,
- }).init(globalThis, value1);
- defer object_iter.deinit();
-
- var env: EnvMap = EnvMap.init(bun.default_allocator);
- env.ensureTotalCapacity(object_iter.len);
-
- // If the env object does not include a $PATH, it must disable path lookup for argv[0]
- // PATH = "";
-
- while (try object_iter.next()) |key| {
- const keyslice = key.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory();
- var value = object_iter.value;
- if (value == .undefined) continue;
-
- const value_str = try value.getZigString(globalThis);
- const slice = value_str.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory();
- const keyref = EnvStr.initRefCounted(keyslice);
- defer keyref.deref();
- const valueref = EnvStr.initRefCounted(slice);
- defer valueref.deref();
-
- env.insert(keyref, valueref);
- }
- if (this.export_env) |*previous| {
- previous.deinit();
- }
- this.export_env = env;
- return .undefined;
- }
-
- pub fn createParsedShellScript(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue {
- var shargs = ShellArgs.init();
-
- const arguments_ = callframe.arguments_old(2);
- const arguments = arguments_.slice();
- if (arguments.len < 2) {
- return globalThis.throwNotEnoughArguments("Bun.$", 2, arguments.len);
- }
- const string_args = arguments[0];
- const template_args_js = arguments[1];
- var template_args = template_args_js.arrayIterator(globalThis);
-
- var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, shargs.arena_allocator());
- var jsstrings = try std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4);
- defer {
- for (jsstrings.items[0..]) |bunstr| {
- bunstr.deref();
- }
- jsstrings.deinit();
- }
- var jsobjs = std.ArrayList(JSValue).init(shargs.arena_allocator());
- var script = std.ArrayList(u8).init(shargs.arena_allocator());
- try bun.shell.shellCmdFromJS(globalThis, string_args, &template_args, &jsobjs, &jsstrings, &script);
-
- var parser: ?bun.shell.Parser = null;
- var lex_result: ?shell.LexResult = null;
- const script_ast = Interpreter.parse(
- shargs.arena_allocator(),
- script.items[0..],
- jsobjs.items[0..],
- jsstrings.items[0..],
- &parser,
- &lex_result,
- ) catch |err| {
- if (err == shell.ParseError.Lex) {
- assert(lex_result != null);
- const str = lex_result.?.combineErrors(shargs.arena_allocator());
- return globalThis.throwPretty("{s}", .{str});
- }
-
- if (parser) |*p| {
- if (bun.Environment.allow_assert) {
- assert(p.errors.items.len > 0);
- }
- const errstr = p.combineErrors();
- return globalThis.throwPretty("{s}", .{errstr});
- }
-
- return globalThis.throwError(err, "failed to lex/parse shell");
- };
-
- shargs.script_ast = script_ast;
-
- const parsed_shell_script = bun.new(ParsedShellScript, .{
- .args = shargs,
- .jsobjs = jsobjs,
- });
- parsed_shell_script.this_jsvalue = JSC.Codegen.JSParsedShellScript.toJS(parsed_shell_script, globalThis);
- log("ParsedShellScript(0x{x}) create", .{@intFromPtr(parsed_shell_script)});
-
- bun.Analytics.Features.shell += 1;
- return parsed_shell_script.this_jsvalue;
- }
-};
-
/// This interpreter works by basically turning the AST into a state machine so
/// that execution can be suspended and resumed to support async.
pub const Interpreter = struct {
@@ -981,8 +563,7 @@ pub const Interpreter = struct {
return this.changeCwdImpl(interp, new_cwd_, false);
}
- pub fn changeCwdImpl(this: *ShellState, interp: *ThisInterpreter, new_cwd_: anytype, comptime in_init: bool) Maybe(void) {
- _ = interp; // autofix
+ pub fn changeCwdImpl(this: *ShellState, _: *ThisInterpreter, new_cwd_: anytype, comptime in_init: bool) Maybe(void) {
if (comptime @TypeOf(new_cwd_) != [:0]const u8 and @TypeOf(new_cwd_) != []const u8) {
@compileError("Bad type for new_cwd " ++ @typeName(@TypeOf(new_cwd_)));
}
@@ -1749,12 +1330,9 @@ pub const Interpreter = struct {
pub fn isRunning(
this: *ThisInterpreter,
- globalThis: *JSGlobalObject,
- callframe: *JSC.CallFrame,
+ _: *JSGlobalObject,
+ _: *JSC.CallFrame,
) bun.JSError!JSC.JSValue {
- _ = globalThis; // autofix
- _ = callframe; // autofix
-
return JSC.JSValue.jsBoolean(this.hasPendingActivity());
}
@@ -1877,7 +1455,7 @@ pub const Interpreter = struct {
Script,
});
- const Result = union(enum) {
+ pub const Result = union(enum) {
array_of_slice: *std.ArrayList([:0]const u8),
array_of_ptr: *std.ArrayList(?[*:0]const u8),
single: struct {
@@ -4344,7 +3922,6 @@ pub const Interpreter = struct {
exec: Exec = .none,
exit_code: ?ExitCode = null,
io: IO,
- freed: bool = false,
state: union(enum) {
idle,
@@ -5050,7 +4627,7 @@ pub const Interpreter = struct {
log("Spawn arena free", .{});
this.spawn_arena.deinit();
}
- this.freed = true;
+
this.io.deref();
this.base.interpreter.allocator.destroy(this);
}
@@ -5111,6013 +4688,7 @@ pub const Interpreter = struct {
}
};
- pub const Builtin = struct {
- kind: Kind,
- stdin: BuiltinIO.Input,
- stdout: BuiltinIO.Output,
- stderr: BuiltinIO.Output,
- exit_code: ?ExitCode = null,
-
- export_env: *EnvMap,
- cmd_local_env: *EnvMap,
-
- arena: *bun.ArenaAllocator,
- /// The following are allocated with the above arena
- args: *const std.ArrayList(?[*:0]const u8),
- args_slice: ?[]const [:0]const u8 = null,
- cwd: bun.FileDescriptor,
-
- impl: RealImpl,
-
- const RealImpl = union(Kind) {
- cat: Cat,
- touch: Touch,
- mkdir: Mkdir,
- @"export": Export,
- cd: Cd,
- echo: Echo,
- pwd: Pwd,
- which: Which,
- rm: Rm,
- mv: Mv,
- ls: Ls,
- exit: Exit,
- true: True,
- false: False,
- yes: Yes,
- seq: Seq,
- dirname: Dirname,
- basename: Basename,
- cp: Cp,
- };
-
- const Result = @import("../result.zig").Result;
-
- // Note: this enum uses @tagName, choose wisely!
- pub const Kind = enum {
- cat,
- touch,
- mkdir,
- @"export",
- cd,
- echo,
- pwd,
- which,
- rm,
- mv,
- ls,
- exit,
- true,
- false,
- yes,
- seq,
- dirname,
- basename,
- cp,
-
- pub const DISABLED_ON_POSIX: []const Kind = &.{ .cat, .cp };
-
- pub fn parentType(this: Kind) type {
- _ = this;
- }
-
- pub fn usageString(this: Kind) []const u8 {
- return switch (this) {
- .cat => "usage: cat [-belnstuv] [file ...]\n",
- .touch => "usage: touch [-A [-][[hh]mm]SS] [-achm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]]\n [-d YYYY-MM-DDThh:mm:SS[.frac][tz]] file ...\n",
- .mkdir => "usage: mkdir [-pv] [-m mode] directory_name ...\n",
- .@"export" => "",
- .cd => "",
- .echo => "",
- .pwd => "",
- .which => "",
- .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",
- .true => "",
- .false => "",
- .yes => "usage: yes [expletive]\n",
- .seq => "usage: seq [-w] [-f format] [-s string] [-t string] [first [incr]] last\n",
- .dirname => "usage: dirname string\n",
- .basename => "usage: basename string\n",
- .cp => "usage: cp [-R [-H | -L | -P]] [-fi | -n] [-aclpsvXx] source_file target_file\n cp [-R [-H | -L | -P]] [-fi | -n] [-aclpsvXx] source_file ... target_directory\n",
- };
- }
-
- fn forceEnableOnPosix() bool {
- return bun.getRuntimeFeatureFlag("BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS");
- }
-
- pub fn fromStr(str: []const u8) ?Builtin.Kind {
- const result = std.meta.stringToEnum(Builtin.Kind, str) orelse return null;
- if (bun.Environment.isWindows) return result;
- if (forceEnableOnPosix()) return result;
- inline for (Builtin.Kind.DISABLED_ON_POSIX) |disabled| {
- if (disabled == result) {
- log("{s} builtin disabled on posix for now", .{@tagName(disabled)});
- return null;
- }
- }
- return result;
- }
- };
-
- pub const BuiltinIO = struct {
- /// in the case of array buffer we simply need to write to the pointer
- /// in the case of blob, we write to the file descriptor
- pub const Output = union(enum) {
- fd: struct { writer: *IOWriter, captured: ?*bun.ByteList = null },
- /// array list not owned by this type
- buf: std.ArrayList(u8),
- arraybuf: ArrayBuf,
- blob: *Blob,
- ignore,
-
- const FdOutput = struct {
- writer: *IOWriter,
- captured: ?*bun.ByteList = null,
-
- // pub fn
- };
-
- pub fn ref(this: *Output) *Output {
- switch (this.*) {
- .fd => {
- this.fd.writer.ref();
- },
- .blob => this.blob.ref(),
- else => {},
- }
- return this;
- }
-
- pub fn deref(this: *Output) void {
- switch (this.*) {
- .fd => {
- this.fd.writer.deref();
- },
- .blob => this.blob.deref(),
- else => {},
- }
- }
-
- pub fn needsIO(this: *Output) ?OutputNeedsIOSafeGuard {
- return switch (this.*) {
- .fd => OutputNeedsIOSafeGuard{
- .__i_know_what_i_am_doing_it_needs_io_yes = 0,
- },
- else => null,
- };
- }
-
- /// You must check that `.needsIO() == true` before calling this!
- /// e.g.
- ///
- /// ```zig
- /// if (this.stderr.neesdIO()) |safeguard| {
- /// this.bltn.stderr.enqueueFmtBltn(this, .cd, fmt, args, safeguard);
- /// }
- /// ```
- pub fn enqueueFmtBltn(
- this: *@This(),
- ptr: anytype,
- comptime kind: ?Interpreter.Builtin.Kind,
- comptime fmt_: []const u8,
- args: anytype,
- _: OutputNeedsIOSafeGuard,
- ) void {
- this.fd.writer.enqueueFmtBltn(ptr, this.fd.captured, kind, fmt_, args);
- }
-
- pub fn enqueue(this: *@This(), ptr: anytype, buf: []const u8, _: OutputNeedsIOSafeGuard) void {
- this.fd.writer.enqueue(ptr, this.fd.captured, buf);
- }
- };
-
- pub const Input = union(enum) {
- fd: *IOReader,
- /// array list not ownedby this type
- buf: std.ArrayList(u8),
- arraybuf: ArrayBuf,
- blob: *Blob,
- ignore,
-
- pub fn ref(this: *Input) *Input {
- switch (this.*) {
- .fd => {
- this.fd.ref();
- },
- .blob => this.blob.ref(),
- else => {},
- }
- return this;
- }
-
- pub fn deref(this: *Input) void {
- switch (this.*) {
- .fd => {
- this.fd.deref();
- },
- .blob => this.blob.deref(),
- else => {},
- }
- }
-
- pub fn needsIO(this: *Input) bool {
- return switch (this.*) {
- .fd => true,
- else => false,
- };
- }
- };
-
- const ArrayBuf = struct {
- buf: JSC.ArrayBuffer.Strong,
- i: u32 = 0,
- };
-
- const Blob = struct {
- ref_count: usize = 1,
- blob: bun.JSC.WebCore.Blob,
- pub usingnamespace bun.NewRefCounted(Blob, _deinit, null);
-
- fn _deinit(this: *Blob) void {
- this.blob.deinit();
- bun.destroy(this);
- }
- };
- };
-
- pub fn argsSlice(this: *Builtin) []const [*:0]const u8 {
- const args_raw = this.args.items[1..];
- const args_len = std.mem.indexOfScalar(?[*:0]const u8, args_raw, null) orelse @panic("bad");
- if (args_len == 0)
- return &[_][*:0]const u8{};
-
- const args_ptr = args_raw.ptr;
- return @as([*][*:0]const u8, @ptrCast(args_ptr))[0..args_len];
- }
-
- pub inline fn callImpl(this: *Builtin, comptime Ret: type, comptime field: []const u8, args_: anytype) Ret {
- return switch (this.kind) {
- .cat => this.callImplWithType(Cat, Ret, "cat", field, args_),
- .touch => this.callImplWithType(Touch, Ret, "touch", field, args_),
- .mkdir => this.callImplWithType(Mkdir, Ret, "mkdir", field, args_),
- .@"export" => this.callImplWithType(Export, Ret, "export", field, args_),
- .echo => this.callImplWithType(Echo, Ret, "echo", field, args_),
- .cd => this.callImplWithType(Cd, Ret, "cd", field, args_),
- .which => this.callImplWithType(Which, Ret, "which", field, args_),
- .rm => this.callImplWithType(Rm, Ret, "rm", field, args_),
- .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_),
- .true => this.callImplWithType(True, Ret, "true", field, args_),
- .false => this.callImplWithType(False, Ret, "false", field, args_),
- .yes => this.callImplWithType(Yes, Ret, "yes", field, args_),
- .seq => this.callImplWithType(Seq, Ret, "seq", field, args_),
- .dirname => this.callImplWithType(Dirname, Ret, "dirname", field, args_),
- .basename => this.callImplWithType(Basename, Ret, "basename", field, args_),
- .cp => this.callImplWithType(Cp, Ret, "cp", field, args_),
- };
- }
-
- fn callImplWithType(this: *Builtin, comptime Impl: type, comptime Ret: type, comptime union_field: []const u8, comptime field: []const u8, args_: anytype) Ret {
- const self = &@field(this.impl, union_field);
- const args = brk: {
- var args: std.meta.ArgsTuple(@TypeOf(@field(Impl, field))) = undefined;
- args[0] = self;
-
- var i: usize = 1;
- inline for (args_) |a| {
- args[i] = a;
- i += 1;
- }
-
- break :brk args;
- };
- return @call(.auto, @field(Impl, field), args);
- }
-
- pub inline fn allocator(this: *Builtin) Allocator {
- return this.parentCmd().base.interpreter.allocator;
- }
-
- pub fn init(
- cmd: *Cmd,
- interpreter: *ThisInterpreter,
- kind: Kind,
- arena: *bun.ArenaAllocator,
- node: *const ast.Cmd,
- args: *const std.ArrayList(?[*:0]const u8),
- export_env: *EnvMap,
- cmd_local_env: *EnvMap,
- cwd: bun.FileDescriptor,
- io: *IO,
- comptime in_cmd_subst: bool,
- ) CoroutineResult {
- const stdin: BuiltinIO.Input = switch (io.stdin) {
- .fd => |fd| .{ .fd = fd.refSelf() },
- .ignore => .ignore,
- };
- const stdout: BuiltinIO.Output = switch (io.stdout) {
- .fd => |val| .{ .fd = .{ .writer = val.writer.refSelf(), .captured = val.captured } },
- .pipe => .{ .buf = std.ArrayList(u8).init(bun.default_allocator) },
- .ignore => .ignore,
- };
- const stderr: BuiltinIO.Output = switch (io.stderr) {
- .fd => |val| .{ .fd = .{ .writer = val.writer.refSelf(), .captured = val.captured } },
- .pipe => .{ .buf = std.ArrayList(u8).init(bun.default_allocator) },
- .ignore => .ignore,
- };
-
- cmd.exec = .{
- .bltn = Builtin{
- .kind = kind,
- .stdin = stdin,
- .stdout = stdout,
- .stderr = stderr,
- .exit_code = null,
- .arena = arena,
- .args = args,
- .export_env = export_env,
- .cmd_local_env = cmd_local_env,
- .cwd = cwd,
- .impl = undefined,
- },
- };
-
- switch (kind) {
- .rm => {
- cmd.exec.bltn.impl = .{
- .rm = Rm{
- .bltn = &cmd.exec.bltn,
- .opts = .{},
- },
- };
- },
- .echo => {
- cmd.exec.bltn.impl = .{
- .echo = Echo{
- .bltn = &cmd.exec.bltn,
- .output = std.ArrayList(u8).init(arena.allocator()),
- },
- };
- },
- inline else => |tag| {
- cmd.exec.bltn.impl = @unionInit(RealImpl, @tagName(tag), .{
- .bltn = &cmd.exec.bltn,
- });
- },
- }
-
- if (node.redirect_file) |file| brk: {
- if (comptime in_cmd_subst) {
- if (node.redirect.stdin) {
- stdin = .ignore;
- }
-
- if (node.redirect.stdout) {
- stdout = .ignore;
- }
-
- if (node.redirect.stderr) {
- stdout = .ignore;
- }
-
- break :brk;
- }
-
- switch (file) {
- .atom => {
- if (cmd.redirection_file.items.len == 0) {
- cmd.writeFailingError("bun: ambiguous redirect: at `{s}`\n", .{@tagName(kind)});
- return .yield;
- }
-
- // Regular files are not pollable on linux
- const is_pollable: bool = if (bun.Environment.isLinux) false else true;
-
- const path = cmd.redirection_file.items[0..cmd.redirection_file.items.len -| 1 :0];
- log("EXPANDED REDIRECT: {s}\n", .{cmd.redirection_file.items[0..]});
- const perm = 0o666;
- const is_nonblocking = false;
- const flags = node.redirect.toFlags();
- const redirfd = switch (ShellSyscall.openat(cmd.base.shell.cwd_fd, path, flags, perm)) {
- .err => |e| {
- cmd.writeFailingError("bun: {s}: {s}", .{ e.toShellSystemError().message, path });
- return .yield;
- },
- .result => |f| f,
- };
- if (node.redirect.stdin) {
- cmd.exec.bltn.stdin.deref();
- cmd.exec.bltn.stdin = .{ .fd = IOReader.init(redirfd, cmd.base.eventLoop()) };
- }
- if (node.redirect.stdout) {
- cmd.exec.bltn.stdout.deref();
- cmd.exec.bltn.stdout = .{ .fd = .{ .writer = IOWriter.init(redirfd, .{ .pollable = is_pollable, .nonblocking = is_nonblocking }, cmd.base.eventLoop()) } };
- }
- if (node.redirect.stderr) {
- cmd.exec.bltn.stderr.deref();
- cmd.exec.bltn.stderr = .{ .fd = .{ .writer = IOWriter.init(redirfd, .{ .pollable = is_pollable, .nonblocking = is_nonblocking }, cmd.base.eventLoop()) } };
- }
- },
- .jsbuf => |val| {
- const globalObject = interpreter.event_loop.js.global;
- if (interpreter.jsobjs[file.jsbuf.idx].asArrayBuffer(globalObject)) |buf| {
- const arraybuf: BuiltinIO.ArrayBuf = .{ .buf = JSC.ArrayBuffer.Strong{
- .array_buffer = buf,
- .held = JSC.Strong.create(buf.value, globalObject),
- }, .i = 0 };
-
- if (node.redirect.stdin) {
- cmd.exec.bltn.stdin.deref();
- cmd.exec.bltn.stdin = .{ .arraybuf = arraybuf };
- }
-
- if (node.redirect.stdout) {
- cmd.exec.bltn.stdout.deref();
- cmd.exec.bltn.stdout = .{ .arraybuf = arraybuf };
- }
-
- if (node.redirect.stderr) {
- cmd.exec.bltn.stderr.deref();
- cmd.exec.bltn.stderr = .{ .arraybuf = arraybuf };
- }
- } else if (interpreter.jsobjs[file.jsbuf.idx].as(JSC.WebCore.Body.Value)) |body| {
- if ((node.redirect.stdout or node.redirect.stderr) and !(body.* == .Blob and !body.Blob.needsToReadFile())) {
- // TODO: Locked->stream -> file -> blob conversion via .toBlobIfPossible() except we want to avoid modifying the Response/Request if unnecessary.
- cmd.base.interpreter.event_loop.js.global.throw("Cannot redirect stdout/stderr to an immutable blob. Expected a file", .{}) catch {};
- return .yield;
- }
-
- var original_blob = body.use();
- defer original_blob.deinit();
-
- const blob: *BuiltinIO.Blob = bun.new(BuiltinIO.Blob, .{
- .blob = original_blob.dupe(),
- });
-
- if (node.redirect.stdin) {
- cmd.exec.bltn.stdin.deref();
- cmd.exec.bltn.stdin = .{ .blob = blob };
- }
-
- if (node.redirect.stdout) {
- cmd.exec.bltn.stdout.deref();
- cmd.exec.bltn.stdout = .{ .blob = blob };
- }
-
- if (node.redirect.stderr) {
- cmd.exec.bltn.stderr.deref();
- cmd.exec.bltn.stderr = .{ .blob = blob };
- }
- } else if (interpreter.jsobjs[file.jsbuf.idx].as(JSC.WebCore.Blob)) |blob| {
- if ((node.redirect.stdout or node.redirect.stderr) and !blob.needsToReadFile()) {
- // TODO: Locked->stream -> file -> blob conversion via .toBlobIfPossible() except we want to avoid modifying the Response/Request if unnecessary.
- cmd.base.interpreter.event_loop.js.global.throw("Cannot redirect stdout/stderr to an immutable blob. Expected a file", .{}) catch {};
- return .yield;
- }
-
- const theblob: *BuiltinIO.Blob = bun.new(BuiltinIO.Blob, .{ .blob = blob.dupe() });
-
- if (node.redirect.stdin) {
- cmd.exec.bltn.stdin.deref();
- cmd.exec.bltn.stdin = .{ .blob = theblob };
- } else if (node.redirect.stdout) {
- cmd.exec.bltn.stdout.deref();
- cmd.exec.bltn.stdout = .{ .blob = theblob };
- } else if (node.redirect.stderr) {
- cmd.exec.bltn.stderr.deref();
- cmd.exec.bltn.stderr = .{ .blob = theblob };
- }
- } else {
- const jsval = cmd.base.interpreter.jsobjs[val.idx];
- cmd.base.interpreter.event_loop.js.global.throw("Unknown JS value used in shell: {}", .{jsval.fmtString(globalObject)}) catch {};
- return .yield;
- }
- },
- }
- } else if (node.redirect.duplicate_out) {
- if (node.redirect.stdout) {
- cmd.exec.bltn.stderr.deref();
- cmd.exec.bltn.stderr = cmd.exec.bltn.stdout.ref().*;
- }
-
- if (node.redirect.stderr) {
- cmd.exec.bltn.stdout.deref();
- cmd.exec.bltn.stdout = cmd.exec.bltn.stderr.ref().*;
- }
- }
-
- return .cont;
- }
-
- pub inline fn eventLoop(this: *const Builtin) JSC.EventLoopHandle {
- return this.parentCmd().base.eventLoop();
- }
-
- pub inline fn throw(this: *const Builtin, err: *const bun.shell.ShellErr) void {
- this.parentCmd().base.throw(err) catch {};
- }
-
- pub inline fn parentCmd(this: *const Builtin) *const Cmd {
- const union_ptr: *const Cmd.Exec = @fieldParentPtr("bltn", this);
- return @fieldParentPtr("exec", union_ptr);
- }
-
- pub inline fn parentCmdMut(this: *Builtin) *Cmd {
- const union_ptr: *Cmd.Exec = @fieldParentPtr("bltn", this);
- return @fieldParentPtr("exec", union_ptr);
- }
-
- pub fn done(this: *Builtin, exit_code: anytype) void {
- const code: ExitCode = switch (@TypeOf(exit_code)) {
- bun.C.E => @intFromEnum(exit_code),
- u1, u8, u16 => exit_code,
- comptime_int => exit_code,
- else => @compileError("Invalid type: " ++ @typeName(@TypeOf(exit_code))),
- };
- this.exit_code = code;
-
- var cmd = this.parentCmdMut();
- log("builtin done ({s}: exit={d}) cmd to free: ({x})", .{ @tagName(this.kind), code, @intFromPtr(cmd) });
- cmd.exit_code = this.exit_code.?;
-
- // Aggregate output data if shell state is piped and this cmd is piped
- if (cmd.io.stdout == .pipe and cmd.io.stdout == .pipe and this.stdout == .buf) {
- cmd.base.shell.buffered_stdout().append(bun.default_allocator, this.stdout.buf.items[0..]) catch bun.outOfMemory();
- }
- // Aggregate output data if shell state is piped and this cmd is piped
- if (cmd.io.stderr == .pipe and cmd.io.stderr == .pipe and this.stderr == .buf) {
- cmd.base.shell.buffered_stderr().append(bun.default_allocator, this.stderr.buf.items[0..]) catch bun.outOfMemory();
- }
-
- cmd.parent.childDone(cmd, this.exit_code.?);
- }
-
- pub fn start(this: *Builtin) Maybe(void) {
- switch (this.callImpl(Maybe(void), "start", .{})) {
- .err => |e| return Maybe(void).initErr(e),
- .result => {},
- }
-
- return Maybe(void).success;
- }
-
- pub fn deinit(this: *Builtin) void {
- this.callImpl(void, "deinit", .{});
-
- // No need to free it because it belongs to the parent cmd
- // _ = Syscall.close(this.cwd);
-
- this.stdout.deref();
- this.stderr.deref();
- this.stdin.deref();
-
- // Parent cmd frees this
- // this.arena.deinit();
- }
-
- /// If the stdout/stderr is supposed to be captured then get the bytelist associated with that
- pub fn stdBufferedBytelist(this: *Builtin, comptime io_kind: @Type(.enum_literal)) ?*bun.ByteList {
- if (comptime io_kind != .stdout and io_kind != .stderr) {
- @compileError("Bad IO" ++ @tagName(io_kind));
- }
-
- const io: *BuiltinIO = &@field(this, @tagName(io_kind));
- return switch (io.*) {
- .captured => if (comptime io_kind == .stdout) this.parentCmd().base.shell.buffered_stdout() else this.parentCmd().base.shell.buffered_stderr(),
- else => null,
- };
- }
-
- pub fn readStdinNoIO(this: *Builtin) []const u8 {
- return switch (this.stdin) {
- .arraybuf => |buf| buf.buf.slice(),
- .buf => |buf| buf.items[0..],
- .blob => |blob| blob.blob.sharedView(),
- else => "",
- };
- }
-
- /// **WARNING** You should make sure that stdout/stderr does not need IO (e.g. `.needsIO(.stderr)` is false before caling `.writeNoIO(.stderr, buf)`)
- pub fn writeNoIO(this: *Builtin, comptime io_kind: @Type(.enum_literal), buf: []const u8) Maybe(usize) {
- if (comptime io_kind != .stdout and io_kind != .stderr) {
- @compileError("Bad IO" ++ @tagName(io_kind));
- }
-
- if (buf.len == 0) return Maybe(usize).initResult(0);
-
- var io: *BuiltinIO.Output = &@field(this, @tagName(io_kind));
-
- switch (io.*) {
- .fd => @panic("writeNoIO(. " ++ @tagName(io_kind) ++ ", buf) can't write to a file descriptor, did you check that needsIO(." ++ @tagName(io_kind) ++ ") was false?"),
- .buf => {
- log("{s} write to buf len={d} str={s}{s}\n", .{ @tagName(this.kind), buf.len, buf[0..@min(buf.len, 16)], if (buf.len > 16) "..." else "" });
- io.buf.appendSlice(buf) catch bun.outOfMemory();
- return Maybe(usize).initResult(buf.len);
- },
- .arraybuf => {
- if (io.arraybuf.i >= io.arraybuf.buf.array_buffer.byte_len) {
- return Maybe(usize).initErr(Syscall.Error.fromCode(bun.C.E.NOSPC, .write));
- }
-
- const len = buf.len;
- if (io.arraybuf.i + len > io.arraybuf.buf.array_buffer.byte_len) {
- // std.ArrayList(comptime T: type)
- }
- const write_len = if (io.arraybuf.i + len > io.arraybuf.buf.array_buffer.byte_len)
- io.arraybuf.buf.array_buffer.byte_len - io.arraybuf.i
- else
- len;
-
- const slice = io.arraybuf.buf.slice()[io.arraybuf.i .. io.arraybuf.i + write_len];
- @memcpy(slice, buf[0..write_len]);
- io.arraybuf.i +|= @truncate(write_len);
- log("{s} write to arraybuf {d}\n", .{ @tagName(this.kind), write_len });
- return Maybe(usize).initResult(write_len);
- },
- .blob, .ignore => return Maybe(usize).initResult(buf.len),
- }
- }
-
- /// Error messages formatted to match bash
- fn taskErrorToString(this: *Builtin, comptime kind: Kind, err: anytype) []const u8 {
- switch (@TypeOf(err)) {
- Syscall.Error => {
- if (err.getErrorCodeTagName()) |entry| {
- _, const sys_errno = entry;
- if (bun.sys.coreutils_error_map.get(sys_errno)) |message| {
- if (err.path.len > 0) {
- return this.fmtErrorArena(kind, "{s}: {s}\n", .{ err.path, message });
- }
- return this.fmtErrorArena(kind, "{s}\n", .{message});
- }
- }
- return this.fmtErrorArena(kind, "unknown error {d}\n", .{err.errno});
- },
- JSC.SystemError => {
- if (err.path.length() == 0) return this.fmtErrorArena(kind, "{s}\n", .{err.message.byteSlice()});
- return this.fmtErrorArena(kind, "{s}: {s}\n", .{ err.message.byteSlice(), err.path });
- },
- bun.shell.ShellErr => return switch (err) {
- .sys => this.taskErrorToString(kind, err.sys),
- .custom => this.fmtErrorArena(kind, "{s}\n", .{err.custom}),
- .invalid_arguments => this.fmtErrorArena(kind, "{s}\n", .{err.invalid_arguments.val}),
- .todo => this.fmtErrorArena(kind, "{s}\n", .{err.todo}),
- },
- else => @compileError("Bad type: " ++ @typeName(err)),
- }
- }
-
- pub fn fmtErrorArena(this: *Builtin, comptime kind: ?Kind, comptime fmt_: []const u8, args: anytype) []u8 {
- const cmd_str = comptime if (kind) |k| @tagName(k) ++ ": " else "";
- const fmt = cmd_str ++ fmt_;
- return std.fmt.allocPrint(this.arena.allocator(), fmt, args) catch bun.outOfMemory();
- }
-
- pub const Cat = struct {
- const debug = bun.Output.scoped(.ShellCat, true);
-
- bltn: *Builtin,
- opts: Opts = .{},
- state: union(enum) {
- idle,
- exec_stdin: struct {
- in_done: bool = false,
- chunks_queued: usize = 0,
- chunks_done: usize = 0,
- errno: ExitCode = 0,
- },
- exec_filepath_args: struct {
- args: []const [*:0]const u8,
- idx: usize = 0,
- reader: ?*IOReader = null,
- chunks_queued: usize = 0,
- chunks_done: usize = 0,
- out_done: bool = false,
- in_done: bool = false,
-
- pub fn deinit(this: *@This()) void {
- if (this.reader) |r| r.deref();
- }
- },
- waiting_write_err,
- done,
- } = .idle,
-
- pub fn writeFailingError(this: *Cat, buf: []const u8, exit_code: ExitCode) Maybe(void) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .waiting_write_err;
- this.bltn.stderr.enqueue(this, buf, safeguard);
- return Maybe(void).success;
- }
-
- _ = this.bltn.writeNoIO(.stderr, buf);
-
- this.bltn.done(exit_code);
- return Maybe(void).success;
- }
-
- pub fn start(this: *Cat) Maybe(void) {
- const filepath_args = switch (this.opts.parse(this.bltn.argsSlice())) {
- .ok => |filepath_args| filepath_args,
- .err => |e| {
- const buf = switch (e) {
- .illegal_option => |opt_str| this.bltn.fmtErrorArena(.cat, "illegal option -- {s}\n", .{opt_str}),
- .show_usage => Builtin.Kind.cat.usageString(),
- .unsupported => |unsupported| this.bltn.fmtErrorArena(.cat, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
- };
-
- _ = this.writeFailingError(buf, 1);
- return Maybe(void).success;
- },
- };
-
- const should_read_from_stdin = filepath_args == null or filepath_args.?.len == 0;
-
- if (should_read_from_stdin) {
- this.state = .{
- .exec_stdin = .{},
- };
- } else {
- this.state = .{
- .exec_filepath_args = .{
- .args = filepath_args.?,
- },
- };
- }
-
- _ = this.next();
-
- return Maybe(void).success;
- }
-
- pub fn next(this: *Cat) void {
- switch (this.state) {
- .idle => @panic("Invalid state"),
- .exec_stdin => {
- if (!this.bltn.stdin.needsIO()) {
- this.state.exec_stdin.in_done = true;
- const buf = this.bltn.readStdinNoIO();
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.bltn.stdout.enqueue(this, buf, safeguard);
- } else {
- _ = this.bltn.writeNoIO(.stdout, buf);
- this.bltn.done(0);
- return;
- }
- return;
- }
- this.bltn.stdin.fd.addReader(this);
- this.bltn.stdin.fd.start();
- return;
- },
- .exec_filepath_args => {
- var exec = &this.state.exec_filepath_args;
- if (exec.idx >= exec.args.len) {
- exec.deinit();
- return this.bltn.done(0);
- }
-
- if (exec.reader) |r| r.deref();
-
- const arg = std.mem.span(exec.args[exec.idx]);
- exec.idx += 1;
- const dir = this.bltn.parentCmd().base.shell.cwd_fd;
- const fd = switch (ShellSyscall.openat(dir, arg, bun.O.RDONLY, 0)) {
- .result => |fd| fd,
- .err => |e| {
- const buf = this.bltn.taskErrorToString(.cat, e);
- _ = this.writeFailingError(buf, 1);
- exec.deinit();
- return;
- },
- };
-
- const reader = IOReader.init(fd, this.bltn.eventLoop());
- exec.chunks_done = 0;
- exec.chunks_queued = 0;
- exec.reader = reader;
- exec.reader.?.addReader(this);
- exec.reader.?.start();
- },
- .waiting_write_err => return,
- .done => this.bltn.done(0),
- }
- }
-
- pub fn onIOWriterChunk(this: *Cat, _: usize, err: ?JSC.SystemError) void {
- debug("onIOWriterChunk(0x{x}, {s}, had_err={any})", .{ @intFromPtr(this), @tagName(this.state), err != null });
- const errno: ExitCode = if (err) |e| brk: {
- defer e.deref();
- break :brk @as(ExitCode, @intCast(@intFromEnum(e.getErrno())));
- } else 0;
- // Writing to stdout errored, cancel everything and write error
- if (err) |e| {
- defer e.deref();
- switch (this.state) {
- .exec_stdin => {
- this.state.exec_stdin.errno = errno;
- // Cancel reader if needed
- if (!this.state.exec_stdin.in_done) {
- if (this.bltn.stdin.needsIO()) {
- this.bltn.stdin.fd.removeReader(this);
- }
- this.state.exec_stdin.in_done = true;
- }
- this.bltn.done(e.getErrno());
- },
- .exec_filepath_args => {
- var exec = &this.state.exec_filepath_args;
- if (exec.reader) |r| {
- r.removeReader(this);
- }
- exec.deinit();
- this.bltn.done(e.getErrno());
- },
- .waiting_write_err => this.bltn.done(e.getErrno()),
- else => @panic("Invalid state"),
- }
- return;
- }
-
- switch (this.state) {
- .exec_stdin => {
- this.state.exec_stdin.chunks_done += 1;
- if (this.state.exec_stdin.in_done and (this.state.exec_stdin.chunks_done >= this.state.exec_stdin.chunks_queued)) {
- this.bltn.done(0);
- return;
- }
- // Need to wait for more chunks to be written
- },
- .exec_filepath_args => {
- this.state.exec_filepath_args.chunks_done += 1;
- if (this.state.exec_filepath_args.chunks_done >= this.state.exec_filepath_args.chunks_queued) {
- this.state.exec_filepath_args.out_done = true;
- }
- if (this.state.exec_filepath_args.in_done and this.state.exec_filepath_args.out_done) {
- this.next();
- return;
- }
- // Wait for reader to be done
- return;
- },
- .waiting_write_err => this.bltn.done(1),
- else => @panic("Invalid state"),
- }
- }
-
- pub fn onIOReaderChunk(this: *Cat, chunk: []const u8) ReadChunkAction {
- debug("onIOReaderChunk(0x{x}, {s}, chunk_len={d})", .{ @intFromPtr(this), @tagName(this.state), chunk.len });
- switch (this.state) {
- .exec_stdin => {
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.state.exec_stdin.chunks_queued += 1;
- this.bltn.stdout.enqueue(this, chunk, safeguard);
- return .cont;
- }
- _ = this.bltn.writeNoIO(.stdout, chunk);
- },
- .exec_filepath_args => {
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.state.exec_filepath_args.chunks_queued += 1;
- this.bltn.stdout.enqueue(this, chunk, safeguard);
- return .cont;
- }
- _ = this.bltn.writeNoIO(.stdout, chunk);
- },
- else => @panic("Invalid state"),
- }
- return .cont;
- }
-
- pub fn onIOReaderDone(this: *Cat, err: ?JSC.SystemError) void {
- const errno: ExitCode = if (err) |e| brk: {
- defer e.deref();
- break :brk @as(ExitCode, @intCast(@intFromEnum(e.getErrno())));
- } else 0;
- debug("onIOReaderDone(0x{x}, {s}, errno={d})", .{ @intFromPtr(this), @tagName(this.state), errno });
-
- switch (this.state) {
- .exec_stdin => {
- this.state.exec_stdin.errno = errno;
- this.state.exec_stdin.in_done = true;
- if (errno != 0) {
- if ((this.state.exec_stdin.chunks_done >= this.state.exec_stdin.chunks_queued) or this.bltn.stdout.needsIO() == null) {
- this.bltn.done(errno);
- return;
- }
- this.bltn.stdout.fd.writer.cancelChunks(this);
- return;
- }
- if ((this.state.exec_stdin.chunks_done >= this.state.exec_stdin.chunks_queued) or this.bltn.stdout.needsIO() == null) {
- this.bltn.done(0);
- }
- },
- .exec_filepath_args => {
- this.state.exec_filepath_args.in_done = true;
- if (errno != 0) {
- if (this.state.exec_filepath_args.out_done or this.bltn.stdout.needsIO() == null) {
- this.state.exec_filepath_args.deinit();
- this.bltn.done(errno);
- return;
- }
- this.bltn.stdout.fd.writer.cancelChunks(this);
- return;
- }
- if (this.state.exec_filepath_args.out_done or (this.state.exec_filepath_args.chunks_done >= this.state.exec_filepath_args.chunks_queued) or this.bltn.stdout.needsIO() == null) {
- this.next();
- }
- },
- .done, .waiting_write_err, .idle => {},
- }
- }
-
- pub fn deinit(this: *Cat) void {
- _ = this; // autofix
- }
-
- const Opts = struct {
- /// -b
- ///
- /// Number the non-blank output lines, starting at 1.
- number_nonblank: bool = false,
-
- /// -e
- ///
- /// Display non-printing characters and display a dollar sign ($) at the end of each line.
- show_ends: bool = false,
-
- /// -n
- ///
- /// Number the output lines, starting at 1.
- number_all: bool = false,
-
- /// -s
- ///
- /// Squeeze multiple adjacent empty lines, causing the output to be single spaced.
- squeeze_blank: bool = false,
-
- /// -t
- ///
- /// Display non-printing characters and display tab characters as ^I at the end of each line.
- show_tabs: bool = false,
-
- /// -u
- ///
- /// Disable output buffering.
- disable_output_buffering: bool = false,
-
- /// -v
- ///
- /// Displays non-printing characters so they are visible.
- show_nonprinting: bool = false,
-
- const Parse = FlagParser(*@This());
-
- pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) {
- return Parse.parseFlags(opts, args);
- }
-
- pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult {
- _ = this; // autofix
- _ = flag;
- return null;
- }
-
- fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
- _ = this; // autofix
- switch (char) {
- 'b' => {
- return .{ .unsupported = unsupportedFlag("-b") };
- },
- 'e' => {
- return .{ .unsupported = unsupportedFlag("-e") };
- },
- 'n' => {
- return .{ .unsupported = unsupportedFlag("-n") };
- },
- 's' => {
- return .{ .unsupported = unsupportedFlag("-s") };
- },
- 't' => {
- return .{ .unsupported = unsupportedFlag("-t") };
- },
- 'u' => {
- return .{ .unsupported = unsupportedFlag("-u") };
- },
- 'v' => {
- return .{ .unsupported = unsupportedFlag("-v") };
- },
- else => {
- return .{ .illegal_option = smallflags[1 + i ..] };
- },
- }
-
- return null;
- }
- };
- };
-
- pub const Touch = struct {
- bltn: *Builtin,
- opts: Opts = .{},
- state: union(enum) {
- idle,
- exec: struct {
- started: bool = false,
- tasks_count: usize = 0,
- tasks_done: usize = 0,
- output_done: usize = 0,
- output_waiting: usize = 0,
- started_output_queue: bool = false,
- args: []const [*:0]const u8,
- err: ?JSC.SystemError = null,
- },
- waiting_write_err,
- done,
- } = .idle,
-
- pub fn format(this: *const Touch, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
- _ = fmt; // autofix
- _ = opts; // autofix
- try writer.print("Touch(0x{x}, state={s})", .{ @intFromPtr(this), @tagName(this.state) });
- }
-
- pub fn deinit(this: *Touch) void {
- log("{} deinit", .{this});
- }
-
- pub fn start(this: *Touch) Maybe(void) {
- const filepath_args = switch (this.opts.parse(this.bltn.argsSlice())) {
- .ok => |filepath_args| filepath_args,
- .err => |e| {
- const buf = switch (e) {
- .illegal_option => |opt_str| this.bltn.fmtErrorArena(.touch, "illegal option -- {s}\n", .{opt_str}),
- .show_usage => Builtin.Kind.touch.usageString(),
- .unsupported => |unsupported| this.bltn.fmtErrorArena(.touch, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
- };
-
- _ = this.writeFailingError(buf, 1);
- return Maybe(void).success;
- },
- } orelse {
- _ = this.writeFailingError(Builtin.Kind.touch.usageString(), 1);
- return Maybe(void).success;
- };
-
- this.state = .{
- .exec = .{
- .args = filepath_args,
- },
- };
-
- _ = this.next();
-
- return Maybe(void).success;
- }
-
- pub fn next(this: *Touch) void {
- switch (this.state) {
- .idle => @panic("Invalid state"),
- .exec => {
- var exec = &this.state.exec;
- if (exec.started) {
- if (this.state.exec.tasks_done >= this.state.exec.tasks_count and this.state.exec.output_done >= this.state.exec.output_waiting) {
- const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0;
- this.state = .done;
- this.bltn.done(exit_code);
- return;
- }
- return;
- }
-
- exec.started = true;
- exec.tasks_count = exec.args.len;
-
- for (exec.args) |dir_to_mk_| {
- const dir_to_mk = dir_to_mk_[0..std.mem.len(dir_to_mk_) :0];
- var task = ShellTouchTask.create(this, this.opts, dir_to_mk, this.bltn.parentCmd().base.shell.cwdZ());
- task.schedule();
- }
- },
- .waiting_write_err => return,
- .done => this.bltn.done(0),
- }
- }
-
- pub fn onIOWriterChunk(this: *Touch, _: usize, e: ?JSC.SystemError) void {
- if (this.state == .waiting_write_err) {
- return this.bltn.done(1);
- }
-
- if (e) |err| err.deref();
-
- this.next();
- }
-
- pub fn writeFailingError(this: *Touch, buf: []const u8, exit_code: ExitCode) Maybe(void) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .waiting_write_err;
- this.bltn.stderr.enqueue(this, buf, safeguard);
- return Maybe(void).success;
- }
-
- _ = this.bltn.writeNoIO(.stderr, buf);
-
- this.bltn.done(exit_code);
- return Maybe(void).success;
- }
-
- pub fn onShellTouchTaskDone(this: *Touch, task: *ShellTouchTask) void {
- log("{} onShellTouchTaskDone {} tasks_done={d} tasks_count={d}", .{ this, task, this.state.exec.tasks_done, this.state.exec.tasks_count });
-
- defer bun.default_allocator.destroy(task);
- this.state.exec.tasks_done += 1;
- const err = task.err;
-
- if (err) |e| {
- const output_task: *ShellTouchOutputTask = bun.new(ShellTouchOutputTask, .{
- .parent = this,
- .output = .{ .arrlist = .{} },
- .state = .waiting_write_err,
- });
- const error_string = this.bltn.taskErrorToString(.touch, e);
- this.state.exec.err = e;
- output_task.start(error_string);
- return;
- }
-
- this.next();
- }
-
- pub const ShellTouchOutputTask = OutputTask(Touch, .{
- .writeErr = ShellTouchOutputTaskVTable.writeErr,
- .onWriteErr = ShellTouchOutputTaskVTable.onWriteErr,
- .writeOut = ShellTouchOutputTaskVTable.writeOut,
- .onWriteOut = ShellTouchOutputTaskVTable.onWriteOut,
- .onDone = ShellTouchOutputTaskVTable.onDone,
- });
-
- const ShellTouchOutputTaskVTable = struct {
- pub fn writeErr(this: *Touch, childptr: anytype, errbuf: []const u8) CoroutineResult {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state.exec.output_waiting += 1;
- this.bltn.stderr.enqueue(childptr, errbuf, safeguard);
- return .yield;
- }
- _ = this.bltn.writeNoIO(.stderr, errbuf);
- return .cont;
- }
-
- pub fn onWriteErr(this: *Touch) void {
- this.state.exec.output_done += 1;
- }
-
- pub fn writeOut(this: *Touch, childptr: anytype, output: *OutputSrc) CoroutineResult {
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.state.exec.output_waiting += 1;
- const slice = output.slice();
- log("THE SLICE: {d} {s}", .{ slice.len, slice });
- this.bltn.stdout.enqueue(childptr, slice, safeguard);
- return .yield;
- }
- _ = this.bltn.writeNoIO(.stdout, output.slice());
- return .cont;
- }
-
- pub fn onWriteOut(this: *Touch) void {
- this.state.exec.output_done += 1;
- }
-
- pub fn onDone(this: *Touch) void {
- this.next();
- }
- };
-
- pub const ShellTouchTask = struct {
- touch: *Touch,
-
- opts: Opts,
- filepath: [:0]const u8,
- cwd_path: [:0]const u8,
-
- err: ?JSC.SystemError = null,
- task: JSC.WorkPoolTask = .{ .callback = &runFromThreadPool },
- event_loop: JSC.EventLoopHandle,
- concurrent_task: JSC.EventLoopTask,
-
- pub fn format(this: *const ShellTouchTask, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
- _ = fmt; // autofix
- _ = opts; // autofix
- try writer.print("ShellTouchTask(0x{x}, filepath={s})", .{ @intFromPtr(this), this.filepath });
- }
-
- const debug = bun.Output.scoped(.ShellTouchTask, true);
-
- pub fn deinit(this: *ShellTouchTask) void {
- if (this.err) |e| {
- e.deref();
- }
- bun.default_allocator.destroy(this);
- }
-
- pub fn create(touch: *Touch, opts: Opts, filepath: [:0]const u8, cwd_path: [:0]const u8) *ShellTouchTask {
- const task = bun.default_allocator.create(ShellTouchTask) catch bun.outOfMemory();
- task.* = ShellTouchTask{
- .touch = touch,
- .opts = opts,
- .cwd_path = cwd_path,
- .filepath = filepath,
- .event_loop = touch.bltn.eventLoop(),
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(touch.bltn.eventLoop()),
- };
- return task;
- }
-
- pub fn schedule(this: *@This()) void {
- debug("{} schedule", .{this});
- WorkPool.schedule(&this.task);
- }
-
- pub fn runFromMainThread(this: *@This()) void {
- debug("{} runFromJS", .{this});
- this.touch.onShellTouchTaskDone(this);
- }
-
- pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
- this.runFromMainThread();
- }
-
- fn runFromThreadPool(task: *JSC.WorkPoolTask) void {
- var this: *ShellTouchTask = @fieldParentPtr("task", task);
- debug("{} runFromThreadPool", .{this});
-
- // We have to give an absolute path
- const filepath: [:0]const u8 = brk: {
- if (ResolvePath.Platform.auto.isAbsolute(this.filepath)) break :brk this.filepath;
- const parts: []const []const u8 = &.{
- this.cwd_path[0..],
- this.filepath[0..],
- };
- break :brk ResolvePath.joinZ(parts, .auto);
- };
-
- var node_fs = JSC.Node.NodeFS{};
- const milliseconds: f64 = @floatFromInt(std.time.milliTimestamp());
- const atime: JSC.Node.TimeLike = if (bun.Environment.isWindows) milliseconds / 1000.0 else JSC.Node.TimeLike{
- .sec = @intFromFloat(@divFloor(milliseconds, std.time.ms_per_s)),
- .nsec = @intFromFloat(@mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms),
- };
- const mtime = atime;
- const args = JSC.Node.Arguments.Utimes{
- .atime = atime,
- .mtime = mtime,
- .path = .{ .string = bun.PathString.init(filepath) },
- };
- if (node_fs.utimes(args, .sync).asErr()) |err| out: {
- if (err.getErrno() == bun.C.E.NOENT) {
- const perm = 0o664;
- switch (Syscall.open(filepath, bun.O.CREAT | bun.O.WRONLY, perm)) {
- .result => |fd| {
- _ = bun.sys.close(fd);
- break :out;
- },
- .err => |e| {
- this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError();
- break :out;
- },
- }
- }
- this.err = err.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError();
- }
-
- if (this.event_loop == .js) {
- this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
- } else {
- this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
- }
- }
- };
-
- const Opts = struct {
- /// -a
- ///
- /// change only the access time
- access_time_only: bool = false,
-
- /// -c, --no-create
- ///
- /// do not create any files
- no_create: bool = false,
-
- /// -d, --date=STRING
- ///
- /// parse STRING and use it instead of current time
- date: ?[]const u8 = null,
-
- /// -h, --no-dereference
- ///
- /// affect each symbolic link instead of any referenced file
- /// (useful only on systems that can change the timestamps of a symlink)
- no_dereference: bool = false,
-
- /// -m
- ///
- /// change only the modification time
- modification_time_only: bool = false,
-
- /// -r, --reference=FILE
- ///
- /// use this file's times instead of current time
- reference: ?[]const u8 = null,
-
- /// -t STAMP
- ///
- /// use [[CC]YY]MMDDhhmm[.ss] instead of current time
- timestamp: ?[]const u8 = null,
-
- /// --time=WORD
- ///
- /// change the specified time:
- /// WORD is access, atime, or use: equivalent to -a
- /// WORD is modify or mtime: equivalent to -m
- time: ?[]const u8 = null,
-
- const Parse = FlagParser(*@This());
-
- pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) {
- return Parse.parseFlags(opts, args);
- }
-
- pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult {
- _ = this;
- if (bun.strings.eqlComptime(flag, "--no-create")) {
- return .{
- .unsupported = unsupportedFlag("--no-create"),
- };
- }
-
- if (bun.strings.eqlComptime(flag, "--date")) {
- return .{
- .unsupported = unsupportedFlag("--date"),
- };
- }
-
- if (bun.strings.eqlComptime(flag, "--reference")) {
- return .{
- .unsupported = unsupportedFlag("--reference=FILE"),
- };
- }
-
- if (bun.strings.eqlComptime(flag, "--time")) {
- return .{
- .unsupported = unsupportedFlag("--reference=FILE"),
- };
- }
-
- return null;
- }
-
- fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
- _ = this;
- switch (char) {
- 'a' => {
- return .{ .unsupported = unsupportedFlag("-a") };
- },
- 'c' => {
- return .{ .unsupported = unsupportedFlag("-c") };
- },
- 'd' => {
- return .{ .unsupported = unsupportedFlag("-d") };
- },
- 'h' => {
- return .{ .unsupported = unsupportedFlag("-h") };
- },
- 'm' => {
- return .{ .unsupported = unsupportedFlag("-m") };
- },
- 'r' => {
- return .{ .unsupported = unsupportedFlag("-r") };
- },
- 't' => {
- return .{ .unsupported = unsupportedFlag("-t") };
- },
- else => {
- return .{ .illegal_option = smallflags[1 + i ..] };
- },
- }
-
- return null;
- }
- };
- };
-
- pub const Mkdir = struct {
- bltn: *Builtin,
- opts: Opts = .{},
- state: union(enum) {
- idle,
- exec: struct {
- started: bool = false,
- tasks_count: usize = 0,
- tasks_done: usize = 0,
- output_waiting: u16 = 0,
- output_done: u16 = 0,
- args: []const [*:0]const u8,
- err: ?JSC.SystemError = null,
- },
- waiting_write_err,
- done,
- } = .idle,
-
- pub fn onIOWriterChunk(this: *Mkdir, _: usize, e: ?JSC.SystemError) void {
- if (e) |err| err.deref();
-
- switch (this.state) {
- .waiting_write_err => return this.bltn.done(1),
- .exec => {
- this.state.exec.output_done += 1;
- },
- .idle, .done => @panic("Invalid state"),
- }
-
- this.next();
- }
- pub fn writeFailingError(this: *Mkdir, buf: []const u8, exit_code: ExitCode) Maybe(void) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .waiting_write_err;
- this.bltn.stderr.enqueue(this, buf, safeguard);
- return Maybe(void).success;
- }
-
- _ = this.bltn.writeNoIO(.stderr, buf);
- // if (this.bltn.writeNoIO(.stderr, buf).asErr()) |e| {
- // return .{ .err = e };
- // }
-
- this.bltn.done(exit_code);
- return Maybe(void).success;
- }
-
- pub fn start(this: *Mkdir) Maybe(void) {
- const filepath_args = switch (this.opts.parse(this.bltn.argsSlice())) {
- .ok => |filepath_args| filepath_args,
- .err => |e| {
- const buf = switch (e) {
- .illegal_option => |opt_str| this.bltn.fmtErrorArena(.mkdir, "illegal option -- {s}\n", .{opt_str}),
- .show_usage => Builtin.Kind.mkdir.usageString(),
- .unsupported => |unsupported| this.bltn.fmtErrorArena(.mkdir, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
- };
-
- _ = this.writeFailingError(buf, 1);
- return Maybe(void).success;
- },
- } orelse {
- _ = this.writeFailingError(Builtin.Kind.mkdir.usageString(), 1);
- return Maybe(void).success;
- };
-
- this.state = .{
- .exec = .{
- .args = filepath_args,
- },
- };
-
- _ = this.next();
-
- return Maybe(void).success;
- }
-
- pub fn next(this: *Mkdir) void {
- switch (this.state) {
- .idle => @panic("Invalid state"),
- .exec => {
- var exec = &this.state.exec;
- if (exec.started) {
- if (this.state.exec.tasks_done >= this.state.exec.tasks_count and this.state.exec.output_done >= this.state.exec.output_waiting) {
- const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0;
- if (this.state.exec.err) |e| e.deref();
- this.state = .done;
- this.bltn.done(exit_code);
- return;
- }
- return;
- }
-
- exec.started = true;
- exec.tasks_count = exec.args.len;
-
- for (exec.args) |dir_to_mk_| {
- const dir_to_mk = dir_to_mk_[0..std.mem.len(dir_to_mk_) :0];
- var task = ShellMkdirTask.create(this, this.opts, dir_to_mk, this.bltn.parentCmd().base.shell.cwdZ());
- task.schedule();
- }
- },
- .waiting_write_err => return,
- .done => this.bltn.done(0),
- }
- }
-
- pub fn onShellMkdirTaskDone(this: *Mkdir, task: *ShellMkdirTask) void {
- defer task.deinit();
- this.state.exec.tasks_done += 1;
- var output = task.takeOutput();
- const err = task.err;
- const output_task: *ShellMkdirOutputTask = bun.new(ShellMkdirOutputTask, .{
- .parent = this,
- .output = .{ .arrlist = output.moveToUnmanaged() },
- .state = .waiting_write_err,
- });
-
- if (err) |e| {
- const error_string = this.bltn.taskErrorToString(.mkdir, e);
- this.state.exec.err = e;
- output_task.start(error_string);
- return;
- }
- output_task.start(null);
- }
-
- pub const ShellMkdirOutputTask = OutputTask(Mkdir, .{
- .writeErr = ShellMkdirOutputTaskVTable.writeErr,
- .onWriteErr = ShellMkdirOutputTaskVTable.onWriteErr,
- .writeOut = ShellMkdirOutputTaskVTable.writeOut,
- .onWriteOut = ShellMkdirOutputTaskVTable.onWriteOut,
- .onDone = ShellMkdirOutputTaskVTable.onDone,
- });
-
- const ShellMkdirOutputTaskVTable = struct {
- pub fn writeErr(this: *Mkdir, childptr: anytype, errbuf: []const u8) CoroutineResult {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state.exec.output_waiting += 1;
- this.bltn.stderr.enqueue(childptr, errbuf, safeguard);
- return .yield;
- }
- _ = this.bltn.writeNoIO(.stderr, errbuf);
- return .cont;
- }
-
- pub fn onWriteErr(this: *Mkdir) void {
- this.state.exec.output_done += 1;
- }
-
- pub fn writeOut(this: *Mkdir, childptr: anytype, output: *OutputSrc) CoroutineResult {
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.state.exec.output_waiting += 1;
- const slice = output.slice();
- log("THE SLICE: {d} {s}", .{ slice.len, slice });
- this.bltn.stdout.enqueue(childptr, slice, safeguard);
- return .yield;
- }
- _ = this.bltn.writeNoIO(.stdout, output.slice());
- return .cont;
- }
-
- pub fn onWriteOut(this: *Mkdir) void {
- this.state.exec.output_done += 1;
- }
-
- pub fn onDone(this: *Mkdir) void {
- this.next();
- }
- };
-
- pub fn deinit(this: *Mkdir) void {
- _ = this;
- }
-
- pub const ShellMkdirTask = struct {
- mkdir: *Mkdir,
-
- opts: Opts,
- filepath: [:0]const u8,
- cwd_path: [:0]const u8,
- created_directories: ArrayList(u8),
-
- err: ?JSC.SystemError = null,
- task: JSC.WorkPoolTask = .{ .callback = &runFromThreadPool },
- event_loop: JSC.EventLoopHandle,
- concurrent_task: JSC.EventLoopTask,
-
- const debug = bun.Output.scoped(.ShellMkdirTask, true);
-
- pub fn deinit(this: *ShellMkdirTask) void {
- this.created_directories.deinit();
- bun.default_allocator.destroy(this);
- }
-
- fn takeOutput(this: *ShellMkdirTask) ArrayList(u8) {
- const out = this.created_directories;
- this.created_directories = ArrayList(u8).init(bun.default_allocator);
- return out;
- }
-
- pub fn format(this: *const ShellMkdirTask, comptime fmt_: []const u8, options_: std.fmt.FormatOptions, writer: anytype) !void {
- _ = fmt_; // autofix
- _ = options_; // autofix
- try writer.print("ShellMkdirTask(0x{x}, filepath={s})", .{ @intFromPtr(this), this.filepath });
- }
-
- pub fn create(
- mkdir: *Mkdir,
- opts: Opts,
- filepath: [:0]const u8,
- cwd_path: [:0]const u8,
- ) *ShellMkdirTask {
- const task = bun.default_allocator.create(ShellMkdirTask) catch bun.outOfMemory();
- const evtloop = mkdir.bltn.parentCmd().base.eventLoop();
- task.* = ShellMkdirTask{
- .mkdir = mkdir,
- .opts = opts,
- .cwd_path = cwd_path,
- .filepath = filepath,
- .created_directories = ArrayList(u8).init(bun.default_allocator),
- .event_loop = evtloop,
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop),
- };
- return task;
- }
-
- pub fn schedule(this: *@This()) void {
- debug("{} schedule", .{this});
- WorkPool.schedule(&this.task);
- }
-
- pub fn runFromMainThread(this: *@This()) void {
- debug("{} runFromJS", .{this});
- this.mkdir.onShellMkdirTaskDone(this);
- }
-
- pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
- this.runFromMainThread();
- }
-
- fn runFromThreadPool(task: *JSC.WorkPoolTask) void {
- var this: *ShellMkdirTask = @fieldParentPtr("task", task);
- debug("{} runFromThreadPool", .{this});
-
- // We have to give an absolute path to our mkdir
- // implementation for it to work with cwd
- const filepath: [:0]const u8 = brk: {
- if (ResolvePath.Platform.auto.isAbsolute(this.filepath)) break :brk this.filepath;
- const parts: []const []const u8 = &.{
- this.cwd_path[0..],
- this.filepath[0..],
- };
- break :brk ResolvePath.joinZ(parts, .auto);
- };
-
- var node_fs = JSC.Node.NodeFS{};
- // Recursive
- if (this.opts.parents) {
- const args = JSC.Node.Arguments.Mkdir{
- .path = JSC.Node.PathLike{ .string = bun.PathString.init(filepath) },
- .recursive = true,
- .always_return_none = true,
- };
-
- var vtable = MkdirVerboseVTable{ .inner = this, .active = this.opts.verbose };
-
- switch (node_fs.mkdirRecursiveImpl(args, *MkdirVerboseVTable, &vtable)) {
- .result => {},
- .err => |e| {
- this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError();
- std.mem.doNotOptimizeAway(&node_fs);
- },
- }
- } else {
- const args = JSC.Node.Arguments.Mkdir{
- .path = JSC.Node.PathLike{ .string = bun.PathString.init(filepath) },
- .recursive = false,
- .always_return_none = true,
- };
- switch (node_fs.mkdirNonRecursive(args)) {
- .result => {
- if (this.opts.verbose) {
- this.created_directories.appendSlice(filepath[0..filepath.len]) catch bun.outOfMemory();
- this.created_directories.append('\n') catch bun.outOfMemory();
- }
- },
- .err => |e| {
- this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toShellSystemError();
- std.mem.doNotOptimizeAway(&node_fs);
- },
- }
- }
-
- if (this.event_loop == .js) {
- this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
- } else {
- this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
- }
- }
-
- const MkdirVerboseVTable = struct {
- inner: *ShellMkdirTask,
- active: bool,
-
- pub fn onCreateDir(vtable: *@This(), dirpath: bun.OSPathSliceZ) void {
- if (!vtable.active) return;
- if (bun.Environment.isWindows) {
- var buf: bun.PathBuffer = undefined;
- const str = bun.strings.fromWPath(&buf, dirpath[0..dirpath.len]);
- vtable.inner.created_directories.appendSlice(str) catch bun.outOfMemory();
- vtable.inner.created_directories.append('\n') catch bun.outOfMemory();
- } else {
- vtable.inner.created_directories.appendSlice(dirpath) catch bun.outOfMemory();
- vtable.inner.created_directories.append('\n') catch bun.outOfMemory();
- }
- return;
- }
- };
- };
-
- const Opts = struct {
- /// -m, --mode
- ///
- /// set file mode (as in chmod), not a=rwx - umask
- mode: ?u32 = null,
-
- /// -p, --parents
- ///
- /// no error if existing, make parent directories as needed,
- /// with their file modes unaffected by any -m option.
- parents: bool = false,
-
- /// -v, --verbose
- ///
- /// print a message for each created directory
- verbose: bool = false,
-
- const Parse = FlagParser(*@This());
-
- pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) {
- return Parse.parseFlags(opts, args);
- }
-
- pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult {
- if (bun.strings.eqlComptime(flag, "--mode")) {
- return .{ .unsupported = "--mode" };
- } else if (bun.strings.eqlComptime(flag, "--parents")) {
- this.parents = true;
- return .continue_parsing;
- } else if (bun.strings.eqlComptime(flag, "--vebose")) {
- this.verbose = true;
- return .continue_parsing;
- }
-
- return null;
- }
-
- fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
- switch (char) {
- 'm' => {
- return .{ .unsupported = "-m " };
- },
- 'p' => {
- this.parents = true;
- },
- 'v' => {
- this.verbose = true;
- },
- else => {
- return .{ .illegal_option = smallflags[1 + i ..] };
- },
- }
-
- return null;
- }
- };
- };
-
- pub const Export = struct {
- bltn: *Builtin,
- printing: bool = false,
-
- const Entry = struct {
- key: EnvStr,
- value: EnvStr,
-
- pub fn compare(context: void, this: @This(), other: @This()) bool {
- return bun.strings.cmpStringsAsc(context, this.key.slice(), other.key.slice());
- }
- };
-
- pub fn writeOutput(this: *Export, comptime io_kind: @Type(.enum_literal), comptime fmt: []const u8, args: anytype) Maybe(void) {
- if (this.bltn.stdout.needsIO()) |safeguard| {
- var output: *BuiltinIO.Output = &@field(this.bltn, @tagName(io_kind));
- this.printing = true;
- output.enqueueFmtBltn(this, .@"export", fmt, args, safeguard);
- return Maybe(void).success;
- }
-
- const buf = this.bltn.fmtErrorArena(.@"export", fmt, args);
- _ = this.bltn.writeNoIO(io_kind, buf);
- this.bltn.done(0);
- return Maybe(void).success;
- }
-
- pub fn onIOWriterChunk(this: *Export, _: usize, e: ?JSC.SystemError) void {
- if (comptime bun.Environment.allow_assert) {
- assert(this.printing);
- }
-
- const exit_code: ExitCode = if (e != null) brk: {
- defer e.?.deref();
- break :brk @intFromEnum(e.?.getErrno());
- } else 0;
-
- this.bltn.done(exit_code);
- }
-
- pub fn start(this: *Export) Maybe(void) {
- const args = this.bltn.argsSlice();
-
- // Calling `export` with no arguments prints all exported variables lexigraphically ordered
- if (args.len == 0) {
- var arena = this.bltn.arena;
-
- var keys = std.ArrayList(Entry).init(arena.allocator());
- var iter = this.bltn.export_env.iterator();
- while (iter.next()) |entry| {
- keys.append(.{
- .key = entry.key_ptr.*,
- .value = entry.value_ptr.*,
- }) catch bun.outOfMemory();
- }
-
- std.mem.sort(Entry, keys.items[0..], {}, Entry.compare);
-
- const len = brk: {
- var len: usize = 0;
- for (keys.items) |entry| {
- len += std.fmt.count("{s}={s}\n", .{ entry.key.slice(), entry.value.slice() });
- }
- break :brk len;
- };
- var buf = arena.allocator().alloc(u8, len) catch bun.outOfMemory();
- {
- var i: usize = 0;
- for (keys.items) |entry| {
- const written_slice = std.fmt.bufPrint(buf[i..], "{s}={s}\n", .{ entry.key.slice(), entry.value.slice() }) catch @panic("This should not happen");
- i += written_slice.len;
- }
- }
-
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.printing = true;
- this.bltn.stdout.enqueue(this, buf, safeguard);
-
- return Maybe(void).success;
- }
-
- _ = this.bltn.writeNoIO(.stdout, buf);
- this.bltn.done(0);
- return Maybe(void).success;
- }
-
- for (args) |arg_raw| {
- const arg_sentinel = arg_raw[0..std.mem.len(arg_raw) :0];
- const arg = arg_sentinel[0..arg_sentinel.len];
- if (arg.len == 0) continue;
-
- const eqsign_idx = std.mem.indexOfScalar(u8, arg, '=') orelse {
- if (!shell.isValidVarName(arg)) {
- const buf = this.bltn.fmtErrorArena(.@"export", "`{s}`: not a valid identifier", .{arg});
- return this.writeOutput(.stderr, "{s}\n", .{buf});
- }
- this.bltn.parentCmd().base.shell.assignVar(this.bltn.parentCmd().base.interpreter, EnvStr.initSlice(arg), EnvStr.initSlice(""), .exported);
- continue;
- };
-
- const label = arg[0..eqsign_idx];
- const value = arg_sentinel[eqsign_idx + 1 .. :0];
- this.bltn.parentCmd().base.shell.assignVar(this.bltn.parentCmd().base.interpreter, EnvStr.initSlice(label), EnvStr.initSlice(value), .exported);
- }
-
- this.bltn.done(0);
- return Maybe(void).success;
- }
-
- pub fn deinit(this: *Export) void {
- log("({s}) deinit", .{@tagName(.@"export")});
- _ = this;
- }
- };
-
- pub const Echo = struct {
- bltn: *Builtin,
-
- /// Should be allocated with the arena from Builtin
- output: std.ArrayList(u8),
-
- state: union(enum) {
- idle,
- waiting,
- done,
- } = .idle,
-
- pub fn start(this: *Echo) Maybe(void) {
- const args = this.bltn.argsSlice();
-
- var has_leading_newline: bool = false;
- const args_len = args.len;
- for (args, 0..) |arg, i| {
- const thearg = std.mem.span(arg);
- if (i < args_len - 1) {
- this.output.appendSlice(thearg) catch bun.outOfMemory();
- this.output.append(' ') catch bun.outOfMemory();
- } else {
- if (thearg.len > 0 and thearg[thearg.len - 1] == '\n') {
- has_leading_newline = true;
- }
- this.output.appendSlice(bun.strings.trimSubsequentLeadingChars(thearg, '\n')) catch bun.outOfMemory();
- }
- }
-
- if (!has_leading_newline) this.output.append('\n') catch bun.outOfMemory();
-
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.state = .waiting;
- this.bltn.stdout.enqueue(this, this.output.items[0..], safeguard);
- return Maybe(void).success;
- }
- _ = this.bltn.writeNoIO(.stdout, this.output.items[0..]);
- this.state = .done;
- this.bltn.done(0);
- return Maybe(void).success;
- }
-
- pub fn onIOWriterChunk(this: *Echo, _: usize, e: ?JSC.SystemError) void {
- if (comptime bun.Environment.allow_assert) {
- assert(this.state == .waiting);
- }
-
- if (e != null) {
- defer e.?.deref();
- this.bltn.done(e.?.getErrno());
- return;
- }
-
- this.state = .done;
- this.bltn.done(0);
- }
-
- pub fn deinit(this: *Echo) void {
- log("({s}) deinit", .{@tagName(.echo)});
- this.output.deinit();
- }
- };
-
- /// 1 arg => returns absolute path of the arg (not found becomes exit code 1)
- /// N args => returns absolute path of each separated by newline, if any path is not found, exit code becomes 1, but continues execution until all args are processed
- pub const Which = struct {
- bltn: *Builtin,
-
- state: union(enum) {
- idle,
- one_arg,
- multi_args: struct {
- args_slice: []const [*:0]const u8,
- arg_idx: usize,
- had_not_found: bool = false,
- state: union(enum) {
- none,
- waiting_write,
- },
- },
- done,
- err: JSC.SystemError,
- } = .idle,
-
- pub fn start(this: *Which) Maybe(void) {
- const args = this.bltn.argsSlice();
- if (args.len == 0) {
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.state = .one_arg;
- this.bltn.stdout.enqueue(this, "\n", safeguard);
- return Maybe(void).success;
- }
- _ = this.bltn.writeNoIO(.stdout, "\n");
- this.bltn.done(1);
- return Maybe(void).success;
- }
-
- if (this.bltn.stdout.needsIO() == null) {
- const path_buf = bun.PathBufferPool.get();
- defer bun.PathBufferPool.put(path_buf);
- const PATH = this.bltn.parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice("");
- var had_not_found = false;
- for (args) |arg_raw| {
- const arg = arg_raw[0..std.mem.len(arg_raw)];
- const resolved = which(path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse {
- had_not_found = true;
- const buf = this.bltn.fmtErrorArena(.which, "{s} not found\n", .{arg});
- _ = this.bltn.writeNoIO(.stdout, buf);
- continue;
- };
-
- _ = this.bltn.writeNoIO(.stdout, resolved);
- }
- this.bltn.done(@intFromBool(had_not_found));
- return Maybe(void).success;
- }
-
- this.state = .{
- .multi_args = .{
- .args_slice = args,
- .arg_idx = 0,
- .state = .none,
- },
- };
- this.next();
- return Maybe(void).success;
- }
-
- pub fn next(this: *Which) void {
- var multiargs = &this.state.multi_args;
- if (multiargs.arg_idx >= multiargs.args_slice.len) {
- // Done
- this.bltn.done(@intFromBool(multiargs.had_not_found));
- return;
- }
-
- const arg_raw = multiargs.args_slice[multiargs.arg_idx];
- const arg = arg_raw[0..std.mem.len(arg_raw)];
-
- const path_buf = bun.PathBufferPool.get();
- defer bun.PathBufferPool.put(path_buf);
- const PATH = this.bltn.parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice("");
-
- const resolved = which(path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse {
- multiargs.had_not_found = true;
- if (this.bltn.stdout.needsIO()) |safeguard| {
- multiargs.state = .waiting_write;
- this.bltn.stdout.enqueueFmtBltn(this, null, "{s} not found\n", .{arg}, safeguard);
- // yield execution
- return;
- }
-
- const buf = this.bltn.fmtErrorArena(null, "{s} not found\n", .{arg});
- _ = this.bltn.writeNoIO(.stdout, buf);
- this.argComplete();
- return;
- };
-
- if (this.bltn.stdout.needsIO()) |safeguard| {
- multiargs.state = .waiting_write;
- this.bltn.stdout.enqueueFmtBltn(this, null, "{s}\n", .{resolved}, safeguard);
- return;
- }
-
- const buf = this.bltn.fmtErrorArena(null, "{s}\n", .{resolved});
- _ = this.bltn.writeNoIO(.stdout, buf);
- this.argComplete();
- return;
- }
-
- fn argComplete(this: *Which) void {
- if (comptime bun.Environment.allow_assert) {
- assert(this.state == .multi_args and this.state.multi_args.state == .waiting_write);
- }
-
- this.state.multi_args.arg_idx += 1;
- this.state.multi_args.state = .none;
- this.next();
- }
-
- pub fn onIOWriterChunk(this: *Which, _: usize, e: ?JSC.SystemError) void {
- if (comptime bun.Environment.allow_assert) {
- assert(this.state == .one_arg or
- (this.state == .multi_args and this.state.multi_args.state == .waiting_write));
- }
-
- if (e != null) {
- this.state = .{ .err = e.? };
- this.bltn.done(e.?.getErrno());
- return;
- }
-
- if (this.state == .one_arg) {
- // Calling which with on arguments returns exit code 1
- this.bltn.done(1);
- return;
- }
-
- this.argComplete();
- }
-
- pub fn deinit(this: *Which) void {
- log("({s}) deinit", .{@tagName(.which)});
- _ = this;
- }
- };
-
- /// Some additional behaviour beyond basic `cd `:
- /// - `cd` by itself or `cd ~` will always put the user in their home directory.
- /// - `cd ~username` will put the user in the home directory of the specified user
- /// - `cd -` will put the user in the previous directory
- pub const Cd = struct {
- bltn: *Builtin,
- state: union(enum) {
- idle,
- waiting_write_stderr,
- done,
- err: Syscall.Error,
- } = .idle,
-
- fn writeStderrNonBlocking(this: *Cd, comptime fmt: []const u8, args: anytype) void {
- this.state = .waiting_write_stderr;
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.bltn.stderr.enqueueFmtBltn(this, .cd, fmt, args, safeguard);
- } else {
- const buf = this.bltn.fmtErrorArena(.cd, fmt, args);
- _ = this.bltn.writeNoIO(.stderr, buf);
- this.state = .done;
- this.bltn.done(1);
- }
- }
-
- pub fn start(this: *Cd) Maybe(void) {
- const args = this.bltn.argsSlice();
- if (args.len > 1) {
- this.writeStderrNonBlocking("too many arguments\n", .{});
- // yield execution
- return Maybe(void).success;
- }
-
- if (args.len == 1) {
- const first_arg = args[0][0..std.mem.len(args[0]) :0];
- switch (first_arg[0]) {
- '-' => {
- switch (this.bltn.parentCmd().base.shell.changePrevCwd(this.bltn.parentCmd().base.interpreter)) {
- .result => {},
- .err => |err| {
- return this.handleChangeCwdErr(err, this.bltn.parentCmd().base.shell.prevCwdZ());
- },
- }
- },
- '~' => {
- const homedir = this.bltn.parentCmd().base.shell.getHomedir();
- homedir.deref();
- switch (this.bltn.parentCmd().base.shell.changeCwd(this.bltn.parentCmd().base.interpreter, homedir.slice())) {
- .result => {},
- .err => |err| return this.handleChangeCwdErr(err, homedir.slice()),
- }
- },
- else => {
- switch (this.bltn.parentCmd().base.shell.changeCwd(this.bltn.parentCmd().base.interpreter, first_arg)) {
- .result => {},
- .err => |err| return this.handleChangeCwdErr(err, first_arg),
- }
- },
- }
- }
-
- this.bltn.done(0);
- return Maybe(void).success;
- }
-
- fn handleChangeCwdErr(this: *Cd, err: Syscall.Error, new_cwd_: []const u8) Maybe(void) {
- const errno: usize = @intCast(err.errno);
-
- switch (errno) {
- @as(usize, @intFromEnum(bun.C.E.NOTDIR)) => {
- if (this.bltn.stderr.needsIO() == null) {
- const buf = this.bltn.fmtErrorArena(.cd, "not a directory: {s}\n", .{new_cwd_});
- _ = this.bltn.writeNoIO(.stderr, buf);
- this.state = .done;
- this.bltn.done(1);
- // yield execution
- return Maybe(void).success;
- }
-
- this.writeStderrNonBlocking("not a directory: {s}\n", .{new_cwd_});
- return Maybe(void).success;
- },
- @as(usize, @intFromEnum(bun.C.E.NOENT)) => {
- if (this.bltn.stderr.needsIO() == null) {
- const buf = this.bltn.fmtErrorArena(.cd, "not a directory: {s}\n", .{new_cwd_});
- _ = this.bltn.writeNoIO(.stderr, buf);
- this.state = .done;
- this.bltn.done(1);
- // yield execution
- return Maybe(void).success;
- }
-
- this.writeStderrNonBlocking("not a directory: {s}\n", .{new_cwd_});
- return Maybe(void).success;
- },
- else => return Maybe(void).success,
- }
- }
-
- pub fn onIOWriterChunk(this: *Cd, _: usize, e: ?JSC.SystemError) void {
- if (comptime bun.Environment.allow_assert) {
- assert(this.state == .waiting_write_stderr);
- }
-
- if (e != null) {
- defer e.?.deref();
- this.bltn.done(e.?.getErrno());
- return;
- }
-
- this.state = .done;
- this.bltn.done(1);
- }
-
- pub fn deinit(this: *Cd) void {
- log("({s}) deinit", .{@tagName(.cd)});
- _ = this;
- }
- };
-
- pub const Pwd = struct {
- bltn: *Builtin,
- state: union(enum) {
- idle,
- waiting_io: struct {
- kind: enum { stdout, stderr },
- },
- err,
- done,
- } = .idle,
-
- pub fn start(this: *Pwd) Maybe(void) {
- const args = this.bltn.argsSlice();
- if (args.len > 0) {
- const msg = "pwd: too many arguments\n";
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .{ .waiting_io = .{ .kind = .stderr } };
- this.bltn.stderr.enqueue(this, msg, safeguard);
- return Maybe(void).success;
- }
-
- _ = this.bltn.writeNoIO(.stderr, msg);
- this.bltn.done(1);
- return Maybe(void).success;
- }
-
- const cwd_str = this.bltn.parentCmd().base.shell.cwd();
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.state = .{ .waiting_io = .{ .kind = .stdout } };
- this.bltn.stdout.enqueueFmtBltn(this, null, "{s}\n", .{cwd_str}, safeguard);
- return Maybe(void).success;
- }
- const buf = this.bltn.fmtErrorArena(null, "{s}\n", .{cwd_str});
-
- _ = this.bltn.writeNoIO(.stdout, buf);
-
- this.state = .done;
- this.bltn.done(0);
- return Maybe(void).success;
- }
-
- pub fn next(this: *Pwd) void {
- while (!(this.state == .err or this.state == .done)) {
- switch (this.state) {
- .waiting_io => return,
- .idle => @panic("Unexpected \"idle\" state in Pwd. This indicates a bug in Bun. Please file a GitHub issue."),
- .done, .err => unreachable,
- }
- }
-
- if (this.state == .done) {
- this.bltn.done(0);
- return;
- }
-
- if (this.state == .err) {
- this.bltn.done(1);
- return;
- }
- }
-
- pub fn onIOWriterChunk(this: *Pwd, _: usize, e: ?JSC.SystemError) void {
- if (comptime bun.Environment.allow_assert) {
- assert(this.state == .waiting_io);
- }
-
- if (e != null) {
- defer e.?.deref();
- this.state = .err;
- this.next();
- return;
- }
-
- this.state = switch (this.state.waiting_io.kind) {
- .stdout => .done,
- .stderr => .err,
- };
-
- this.next();
- }
-
- pub fn deinit(this: *Pwd) void {
- _ = this;
- }
- };
-
- pub const Ls = struct {
- bltn: *Builtin,
- opts: Opts = .{},
-
- state: union(enum) {
- idle,
- exec: struct {
- err: ?Syscall.Error = null,
- task_count: std.atomic.Value(usize),
- tasks_done: usize = 0,
- output_waiting: usize = 0,
- output_done: usize = 0,
- },
- waiting_write_err,
- done,
- } = .idle,
-
- pub fn start(this: *Ls) Maybe(void) {
- this.next();
- return Maybe(void).success;
- }
-
- pub fn writeFailingError(this: *Ls, buf: []const u8, exit_code: ExitCode) Maybe(void) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .waiting_write_err;
- this.bltn.stderr.enqueue(this, buf, safeguard);
- return Maybe(void).success;
- }
-
- _ = this.bltn.writeNoIO(.stderr, buf);
-
- this.bltn.done(exit_code);
- return Maybe(void).success;
- }
-
- fn next(this: *Ls) void {
- while (!(this.state == .done)) {
- switch (this.state) {
- .idle => {
- // Will be null if called with no args, in which case we just run once with "." directory
- const paths: ?[]const [*:0]const u8 = switch (this.parseOpts()) {
- .ok => |paths| paths,
- .err => |e| {
- const buf = switch (e) {
- .illegal_option => |opt_str| this.bltn.fmtErrorArena(.ls, "illegal option -- {s}\n", .{opt_str}),
- .show_usage => Builtin.Kind.ls.usageString(),
- };
-
- _ = this.writeFailingError(buf, 1);
- return;
- },
- };
-
- const task_count = if (paths) |p| p.len else 1;
-
- this.state = .{
- .exec = .{
- .task_count = std.atomic.Value(usize).init(task_count),
- },
- };
-
- const cwd = this.bltn.cwd;
- if (paths) |p| {
- for (p) |path_raw| {
- const path = path_raw[0..std.mem.len(path_raw) :0];
- var task = ShellLsTask.create(this, this.opts, &this.state.exec.task_count, cwd, path, this.bltn.eventLoop());
- task.schedule();
- }
- } else {
- var task = ShellLsTask.create(this, this.opts, &this.state.exec.task_count, cwd, ".", this.bltn.eventLoop());
- task.schedule();
- }
- },
- .exec => {
- // It's done
- log("Ls(0x{x}, state=exec) Check: tasks_done={d} task_count={d} output_done={d} output_waiting={d}", .{
- @intFromPtr(this),
- this.state.exec.tasks_done,
- this.state.exec.task_count.load(.monotonic),
- this.state.exec.output_done,
- this.state.exec.output_waiting,
- });
- if (this.state.exec.tasks_done >= this.state.exec.task_count.load(.monotonic) and this.state.exec.output_done >= this.state.exec.output_waiting) {
- const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0;
- this.state = .done;
- this.bltn.done(exit_code);
- return;
- }
- return;
- },
- .waiting_write_err => {
- return;
- },
- .done => unreachable,
- }
- }
-
- this.bltn.done(0);
- return;
- }
-
- pub fn deinit(this: *Ls) void {
- _ = this; // autofix
- }
-
- pub fn onIOWriterChunk(this: *Ls, _: usize, e: ?JSC.SystemError) void {
- if (e) |err| err.deref();
- if (this.state == .waiting_write_err) {
- return this.bltn.done(1);
- }
- this.state.exec.output_done += 1;
- this.next();
- }
-
- pub fn onShellLsTaskDone(this: *Ls, task: *ShellLsTask) void {
- defer task.deinit(true);
- this.state.exec.tasks_done += 1;
- var output = task.takeOutput();
- const err_ = task.err;
-
- // TODO: Reuse the *ShellLsTask allocation
- const output_task: *ShellLsOutputTask = bun.new(ShellLsOutputTask, .{
- .parent = this,
- .output = .{ .arrlist = output.moveToUnmanaged() },
- .state = .waiting_write_err,
- });
-
- if (err_) |err| {
- this.state.exec.err = err;
- const error_string = this.bltn.taskErrorToString(.ls, err);
- output_task.start(error_string);
- return;
- }
- output_task.start(null);
- }
-
- pub const ShellLsOutputTask = OutputTask(Ls, .{
- .writeErr = ShellLsOutputTaskVTable.writeErr,
- .onWriteErr = ShellLsOutputTaskVTable.onWriteErr,
- .writeOut = ShellLsOutputTaskVTable.writeOut,
- .onWriteOut = ShellLsOutputTaskVTable.onWriteOut,
- .onDone = ShellLsOutputTaskVTable.onDone,
- });
-
- const ShellLsOutputTaskVTable = struct {
- pub fn writeErr(this: *Ls, childptr: anytype, errbuf: []const u8) CoroutineResult {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state.exec.output_waiting += 1;
- this.bltn.stderr.enqueue(childptr, errbuf, safeguard);
- return .yield;
- }
- _ = this.bltn.writeNoIO(.stderr, errbuf);
- return .cont;
- }
-
- pub fn onWriteErr(this: *Ls) void {
- this.state.exec.output_done += 1;
- }
-
- pub fn writeOut(this: *Ls, childptr: anytype, output: *OutputSrc) CoroutineResult {
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.state.exec.output_waiting += 1;
- this.bltn.stdout.enqueue(childptr, output.slice(), safeguard);
- return .yield;
- }
- _ = this.bltn.writeNoIO(.stdout, output.slice());
- return .cont;
- }
-
- pub fn onWriteOut(this: *Ls) void {
- this.state.exec.output_done += 1;
- }
-
- pub fn onDone(this: *Ls) void {
- this.next();
- }
- };
-
- pub const ShellLsTask = struct {
- const debug = bun.Output.scoped(.ShellLsTask, true);
- ls: *Ls,
- opts: Opts,
-
- is_root: bool = true,
- task_count: *std.atomic.Value(usize),
-
- cwd: bun.FileDescriptor,
- /// Should be allocated with bun.default_allocator
- path: [:0]const u8 = &[0:0]u8{},
- /// Should use bun.default_allocator
- output: std.ArrayList(u8),
- is_absolute: bool = false,
- err: ?Syscall.Error = null,
- result_kind: enum { file, dir, idk } = .idk,
-
- event_loop: JSC.EventLoopHandle,
- concurrent_task: JSC.EventLoopTask,
- task: JSC.WorkPoolTask = .{
- .callback = workPoolCallback,
- },
-
- pub fn schedule(this: *@This()) void {
- JSC.WorkPool.schedule(&this.task);
- }
-
- pub fn create(ls: *Ls, opts: Opts, task_count: *std.atomic.Value(usize), cwd: bun.FileDescriptor, path: [:0]const u8, event_loop: JSC.EventLoopHandle) *@This() {
- const task = bun.default_allocator.create(@This()) catch bun.outOfMemory();
- task.* = @This(){
- .ls = ls,
- .opts = opts,
- .cwd = cwd,
- .path = bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory(),
- .output = std.ArrayList(u8).init(bun.default_allocator),
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(event_loop),
- .event_loop = event_loop,
- .task_count = task_count,
- };
- return task;
- }
-
- pub fn enqueue(this: *@This(), path: [:0]const u8) void {
- debug("enqueue: {s}", .{path});
- const new_path = this.join(
- bun.default_allocator,
- &[_][]const u8{
- this.path[0..this.path.len],
- path[0..path.len],
- },
- this.is_absolute,
- );
-
- var subtask = @This().create(this.ls, this.opts, this.task_count, this.cwd, new_path, this.event_loop);
- _ = this.task_count.fetchAdd(1, .monotonic);
- subtask.is_root = false;
- subtask.schedule();
- }
-
- inline fn join(this: *@This(), alloc: Allocator, subdir_parts: []const []const u8, is_absolute: bool) [:0]const u8 {
- _ = this; // autofix
- if (!is_absolute) {
- // If relative paths enabled, stdlib join is preferred over
- // ResolvePath.joinBuf because it doesn't try to normalize the path
- return std.fs.path.joinZ(alloc, subdir_parts) catch bun.outOfMemory();
- }
-
- const out = alloc.dupeZ(u8, bun.path.join(subdir_parts, .auto)) catch bun.outOfMemory();
-
- return out;
- }
-
- pub fn run(this: *@This()) void {
- const fd = switch (ShellSyscall.openat(this.cwd, this.path, bun.O.RDONLY | bun.O.DIRECTORY, 0)) {
- .err => |e| {
- switch (e.getErrno()) {
- bun.C.E.NOENT => {
- this.err = this.errorWithPath(e, this.path);
- },
- bun.C.E.NOTDIR => {
- this.result_kind = .file;
- this.addEntry(this.path);
- },
- else => {
- this.err = this.errorWithPath(e, this.path);
- },
- }
- return;
- },
- .result => |fd| fd,
- };
-
- defer {
- _ = Syscall.close(fd);
- debug("run done", .{});
- }
-
- if (!this.opts.list_directories) {
- if (!this.is_root) {
- const writer = this.output.writer();
- std.fmt.format(writer, "{s}:\n", .{this.path}) catch bun.outOfMemory();
- }
-
- var iterator = DirIterator.iterate(fd.asDir(), .u8);
- var entry = iterator.next();
-
- while (switch (entry) {
- .err => |e| {
- this.err = this.errorWithPath(e, this.path);
- return;
- },
- .result => |ent| ent,
- }) |current| : (entry = iterator.next()) {
- this.addEntry(current.name.sliceAssumeZ());
- if (current.kind == .directory and this.opts.recursive) {
- this.enqueue(current.name.sliceAssumeZ());
- }
- }
-
- return;
- }
-
- const writer = this.output.writer();
- std.fmt.format(writer, "{s}\n", .{this.path}) catch bun.outOfMemory();
- return;
- }
-
- fn shouldSkipEntry(this: *@This(), name: [:0]const u8) bool {
- if (this.opts.show_all) return false;
- if (this.opts.show_almost_all) {
- if (bun.strings.eqlComptime(name[0..1], ".") or bun.strings.eqlComptime(name[0..2], "..")) return true;
- }
- return false;
- }
-
- // TODO more complex output like multi-column
- fn addEntry(this: *@This(), name: [:0]const u8) void {
- const skip = this.shouldSkipEntry(name);
- debug("Entry: (skip={}) {s} :: {s}", .{ skip, this.path, name });
- if (skip) return;
- this.output.ensureUnusedCapacity(name.len + 1) catch bun.outOfMemory();
- this.output.appendSlice(name) catch bun.outOfMemory();
- this.output.append('\n') catch bun.outOfMemory();
- }
-
- fn errorWithPath(this: *@This(), err: Syscall.Error, path: [:0]const u8) Syscall.Error {
- _ = this;
- return err.withPath(bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory());
- }
-
- pub fn workPoolCallback(task: *JSC.WorkPoolTask) void {
- var this: *@This() = @fieldParentPtr("task", task);
- this.run();
- this.doneLogic();
- }
-
- fn doneLogic(this: *@This()) void {
- debug("Done", .{});
- if (this.event_loop == .js) {
- this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
- } else {
- this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
- }
- }
-
- pub fn takeOutput(this: *@This()) std.ArrayList(u8) {
- const ret = this.output;
- this.output = std.ArrayList(u8).init(bun.default_allocator);
- return ret;
- }
-
- pub fn runFromMainThread(this: *@This()) void {
- debug("runFromMainThread", .{});
- this.ls.onShellLsTaskDone(this);
- }
-
- pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
- this.runFromMainThread();
- }
-
- pub fn deinit(this: *@This(), comptime free_this: bool) void {
- debug("deinit {s}", .{if (free_this) "free_this=true" else "free_this=false"});
- bun.default_allocator.free(this.path);
- this.output.deinit();
- if (comptime free_this) bun.default_allocator.destroy(this);
- }
- };
-
- const Opts = struct {
- /// `-a`, `--all`
- /// Do not ignore entries starting with .
- show_all: bool = false,
-
- /// `-A`, `--almost-all`
- /// Do not list implied . and ..
- show_almost_all: bool = true,
-
- /// `--author`
- /// With -l, print the author of each file
- show_author: bool = false,
-
- /// `-b`, `--escape`
- /// Print C-style escapes for nongraphic characters
- escape: bool = false,
-
- /// `--block-size=SIZE`
- /// With -l, scale sizes by SIZE when printing them; e.g., '--block-size=M'
- block_size: ?usize = null,
-
- /// `-B`, `--ignore-backups`
- /// Do not list implied entries ending with ~
- ignore_backups: bool = false,
-
- /// `-c`
- /// Sort by, and show, ctime (time of last change of file status information); affects sorting and display based on options
- use_ctime: bool = false,
-
- /// `-C`
- /// List entries by columns
- list_by_columns: bool = false,
-
- /// `--color[=WHEN]`
- /// Color the output; WHEN can be 'always', 'auto', or 'never'
- color: ?[]const u8 = null,
-
- /// `-d`, `--directory`
- /// List directories themselves, not their contents
- list_directories: bool = false,
-
- /// `-D`, `--dired`
- /// Generate output designed for Emacs' dired mode
- dired_mode: bool = false,
-
- /// `-f`
- /// List all entries in directory order
- unsorted: bool = false,
-
- /// `-F`, `--classify[=WHEN]`
- /// Append indicator (one of */=>@|) to entries; WHEN can be 'always', 'auto', or 'never'
- classify: ?[]const u8 = null,
-
- /// `--file-type`
- /// Likewise, except do not append '*'
- file_type: bool = false,
-
- /// `--format=WORD`
- /// Specify format: 'across', 'commas', 'horizontal', 'long', 'single-column', 'verbose', 'vertical'
- format: ?[]const u8 = null,
-
- /// `--full-time`
- /// Like -l --time-style=full-iso
- full_time: bool = false,
-
- /// `-g`
- /// Like -l, but do not list owner
- no_owner: bool = false,
-
- /// `--group-directories-first`
- /// Group directories before files
- group_directories_first: bool = false,
-
- /// `-G`, `--no-group`
- /// In a long listing, don't print group names
- no_group: bool = false,
-
- /// `-h`, `--human-readable`
- /// With -l and -s, print sizes like 1K 234M 2G etc.
- human_readable: bool = false,
-
- /// `--si`
- /// Use powers of 1000 not 1024 for sizes
- si_units: bool = false,
-
- /// `-H`, `--dereference-command-line`
- /// Follow symbolic links listed on the command line
- dereference_cmd_symlinks: bool = false,
-
- /// `--dereference-command-line-symlink-to-dir`
- /// Follow each command line symbolic link that points to a directory
- dereference_cmd_dir_symlinks: bool = false,
-
- /// `--hide=PATTERN`
- /// Do not list entries matching shell PATTERN
- hide_pattern: ?[]const u8 = null,
-
- /// `--hyperlink[=WHEN]`
- /// Hyperlink file names; WHEN can be 'always', 'auto', or 'never'
- hyperlink: ?[]const u8 = null,
-
- /// `--indicator-style=WORD`
- /// Append indicator with style to entry names: 'none', 'slash', 'file-type', 'classify'
- indicator_style: ?[]const u8 = null,
-
- /// `-i`, `--inode`
- /// Print the index number of each file
- show_inode: bool = false,
-
- /// `-I`, `--ignore=PATTERN`
- /// Do not list entries matching shell PATTERN
- ignore_pattern: ?[]const u8 = null,
-
- /// `-k`, `--kibibytes`
- /// Default to 1024-byte blocks for file system usage
- kibibytes: bool = false,
-
- /// `-l`
- /// Use a long listing format
- long_listing: bool = false,
-
- /// `-L`, `--dereference`
- /// Show information for the file the symbolic link references
- dereference: bool = false,
-
- /// `-m`
- /// Fill width with a comma separated list of entries
- comma_separated: bool = false,
-
- /// `-n`, `--numeric-uid-gid`
- /// Like -l, but list numeric user and group IDs
- numeric_uid_gid: bool = false,
-
- /// `-N`, `--literal`
- /// Print entry names without quoting
- literal: bool = false,
-
- /// `-o`
- /// Like -l, but do not list group information
- no_group_info: bool = false,
-
- /// `-p`, `--indicator-style=slash`
- /// Append / indicator to directories
- slash_indicator: bool = false,
-
- /// `-q`, `--hide-control-chars`
- /// Print ? instead of nongraphic characters
- hide_control_chars: bool = false,
-
- /// `--show-control-chars`
- /// Show nongraphic characters as-is
- show_control_chars: bool = false,
-
- /// `-Q`, `--quote-name`
- /// Enclose entry names in double quotes
- quote_name: bool = false,
-
- /// `--quoting-style=WORD`
- /// Use quoting style for entry names
- quoting_style: ?[]const u8 = null,
-
- /// `-r`, `--reverse`
- /// Reverse order while sorting
- reverse_order: bool = false,
-
- /// `-R`, `--recursive`
- /// List subdirectories recursively
- recursive: bool = false,
-
- /// `-s`, `--size`
- /// Print the allocated size of each file, in blocks
- show_size: bool = false,
-
- /// `-S`
- /// Sort by file size, largest first
- sort_by_size: bool = false,
-
- /// `--sort=WORD`
- /// Sort by a specified attribute
- sort_method: ?[]const u8 = null,
-
- /// `--time=WORD`
- /// Select which timestamp to use for display or sorting
- time_method: ?[]const u8 = null,
-
- /// `--time-style=TIME_STYLE`
- /// Time/date format with -l
- time_style: ?[]const u8 = null,
-
- /// `-t`
- /// Sort by time, newest first
- sort_by_time: bool = false,
-
- /// `-T`, `--tabsize=COLS`
- /// Assume tab stops at each specified number of columns
- tabsize: ?usize = null,
-
- /// `-u`
- /// Sort by, and show, access time
- use_atime: bool = false,
-
- /// `-U`
- /// Do not sort; list entries in directory order
- no_sort: bool = false,
-
- /// `-v`
- /// Natural sort of (version) numbers within text
- natural_sort: bool = false,
-
- /// `-w`, `--width=COLS`
- /// Set output width to specified number of columns
- output_width: ?usize = null,
-
- /// `-x`
- /// List entries by lines instead of by columns
- list_by_lines: bool = false,
-
- /// `-X`
- /// Sort alphabetically by entry extension
- sort_by_extension: bool = false,
-
- /// `-Z`, `--context`
- /// Print any security context of each file
- show_context: bool = false,
-
- /// `--zero`
- /// End each output line with NUL, not newline
- end_with_nul: bool = false,
-
- /// `-1`
- /// List one file per line
- one_file_per_line: bool = false,
-
- /// `--help`
- /// Display help and exit
- show_help: bool = false,
-
- /// `--version`
- /// Output version information and exit
- show_version: bool = false,
-
- /// Custom parse error for invalid options
- const ParseError = union(enum) {
- illegal_option: []const u8,
- show_usage,
- };
- };
-
- pub fn parseOpts(this: *Ls) Result(?[]const [*:0]const u8, Opts.ParseError) {
- return this.parseFlags();
- }
-
- pub fn parseFlags(this: *Ls) Result(?[]const [*:0]const u8, Opts.ParseError) {
- const args = this.bltn.argsSlice();
- var idx: usize = 0;
- if (args.len == 0) {
- return .{ .ok = null };
- }
-
- while (idx < args.len) : (idx += 1) {
- const flag = args[idx];
- switch (this.parseFlag(flag[0..std.mem.len(flag)])) {
- .done => {
- const filepath_args = args[idx..];
- return .{ .ok = filepath_args };
- },
- .continue_parsing => {},
- .illegal_option => |opt_str| return .{ .err = .{ .illegal_option = opt_str } },
- }
- }
-
- return .{ .err = .show_usage };
- }
-
- pub fn parseFlag(this: *Ls, flag: []const u8) union(enum) { continue_parsing, done, illegal_option: []const u8 } {
- if (flag.len == 0) return .done;
- if (flag[0] != '-') return .done;
-
- // FIXME windows
- if (flag.len == 1) return .{ .illegal_option = "-" };
-
- const small_flags = flag[1..];
- for (small_flags) |char| {
- switch (char) {
- 'a' => {
- this.opts.show_all = true;
- },
- 'A' => {
- this.opts.show_almost_all = true;
- },
- 'b' => {
- this.opts.escape = true;
- },
- 'B' => {
- this.opts.ignore_backups = true;
- },
- 'c' => {
- this.opts.use_ctime = true;
- },
- 'C' => {
- this.opts.list_by_columns = true;
- },
- 'd' => {
- this.opts.list_directories = true;
- },
- 'D' => {
- this.opts.dired_mode = true;
- },
- 'f' => {
- this.opts.unsorted = true;
- },
- 'F' => {
- this.opts.classify = "always";
- },
- 'g' => {
- this.opts.no_owner = true;
- },
- 'G' => {
- this.opts.no_group = true;
- },
- 'h' => {
- this.opts.human_readable = true;
- },
- 'H' => {
- this.opts.dereference_cmd_symlinks = true;
- },
- 'i' => {
- this.opts.show_inode = true;
- },
- 'I' => {
- this.opts.ignore_pattern = ""; // This will require additional logic to handle patterns
- },
- 'k' => {
- this.opts.kibibytes = true;
- },
- 'l' => {
- this.opts.long_listing = true;
- },
- 'L' => {
- this.opts.dereference = true;
- },
- 'm' => {
- this.opts.comma_separated = true;
- },
- 'n' => {
- this.opts.numeric_uid_gid = true;
- },
- 'N' => {
- this.opts.literal = true;
- },
- 'o' => {
- this.opts.no_group_info = true;
- },
- 'p' => {
- this.opts.slash_indicator = true;
- },
- 'q' => {
- this.opts.hide_control_chars = true;
- },
- 'Q' => {
- this.opts.quote_name = true;
- },
- 'r' => {
- this.opts.reverse_order = true;
- },
- 'R' => {
- this.opts.recursive = true;
- },
- 's' => {
- this.opts.show_size = true;
- },
- 'S' => {
- this.opts.sort_by_size = true;
- },
- 't' => {
- this.opts.sort_by_time = true;
- },
- 'T' => {
- this.opts.tabsize = 8; // Default tab size, needs additional handling for custom sizes
- },
- 'u' => {
- this.opts.use_atime = true;
- },
- 'U' => {
- this.opts.no_sort = true;
- },
- 'v' => {
- this.opts.natural_sort = true;
- },
- 'w' => {
- this.opts.output_width = 0; // Default to no limit, needs additional handling for custom widths
- },
- 'x' => {
- this.opts.list_by_lines = true;
- },
- 'X' => {
- this.opts.sort_by_extension = true;
- },
- 'Z' => {
- this.opts.show_context = true;
- },
- '1' => {
- this.opts.one_file_per_line = true;
- },
- else => {
- return .{ .illegal_option = flag[1..2] };
- },
- }
- }
-
- return .continue_parsing;
- }
- };
-
- pub const Mv = struct {
- bltn: *Builtin,
- opts: Opts = .{},
- args: struct {
- sources: []const [*:0]const u8 = &[_][*:0]const u8{},
- target: [:0]const u8 = &[0:0]u8{},
- target_fd: ?bun.FileDescriptor = null,
- } = .{},
- state: union(enum) {
- idle,
- check_target: struct {
- task: ShellMvCheckTargetTask,
- state: union(enum) {
- running,
- done,
- },
- },
- executing: struct {
- task_count: usize,
- tasks_done: usize = 0,
- error_signal: std.atomic.Value(bool),
- tasks: []ShellMvBatchedTask,
- err: ?Syscall.Error = null,
- },
- done,
- waiting_write_err: struct {
- exit_code: ExitCode,
- },
- err,
- } = .idle,
-
- pub const ShellMvCheckTargetTask = struct {
- const debug = bun.Output.scoped(.MvCheckTargetTask, true);
- mv: *Mv,
-
- cwd: bun.FileDescriptor,
- target: [:0]const u8,
- result: ?Maybe(?bun.FileDescriptor) = null,
-
- task: ShellTask(@This(), runFromThreadPool, runFromMainThread, debug),
-
- pub fn runFromThreadPool(this: *@This()) void {
- const fd = switch (ShellSyscall.openat(this.cwd, this.target, bun.O.RDONLY | bun.O.DIRECTORY, 0)) {
- .err => |e| {
- switch (e.getErrno()) {
- bun.C.E.NOTDIR => {
- this.result = .{ .result = null };
- },
- else => {
- this.result = .{ .err = e };
- },
- }
- return;
- },
- .result => |fd| fd,
- };
- this.result = .{ .result = fd };
- }
-
- pub fn runFromMainThread(this: *@This()) void {
- this.mv.checkTargetTaskDone(this);
- }
-
- pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
- this.runFromMainThread();
- }
- };
-
- pub const ShellMvBatchedTask = struct {
- const BATCH_SIZE = 5;
- const debug = bun.Output.scoped(.MvBatchedTask, true);
-
- mv: *Mv,
- sources: []const [*:0]const u8,
- target: [:0]const u8,
- target_fd: ?bun.FileDescriptor,
- cwd: bun.FileDescriptor,
- error_signal: *std.atomic.Value(bool),
-
- err: ?Syscall.Error = null,
-
- task: ShellTask(@This(), runFromThreadPool, runFromMainThread, debug),
- event_loop: JSC.EventLoopHandle,
-
- pub fn runFromThreadPool(this: *@This()) void {
- // Moving multiple entries into a directory
- if (this.sources.len > 1) return this.moveMultipleIntoDir();
-
- const src = this.sources[0][0..std.mem.len(this.sources[0]) :0];
- // Moving entry into directory
- if (this.target_fd) |fd| {
- _ = fd;
-
- var buf: bun.PathBuffer = undefined;
- _ = this.moveInDir(src, &buf);
- return;
- }
-
- switch (Syscall.renameat(this.cwd, src, this.cwd, this.target)) {
- .err => |e| {
- if (e.getErrno() == .NOTDIR) {
- this.err = e.withPath(this.target);
- } else this.err = e;
- },
- else => {},
- }
- }
-
- pub fn moveInDir(this: *@This(), src: [:0]const u8, buf: *bun.PathBuffer) bool {
- const path_in_dir_ = bun.path.normalizeBuf(ResolvePath.basename(src), buf, .auto);
- if (path_in_dir_.len + 1 >= buf.len) {
- this.err = Syscall.Error.fromCode(bun.C.E.NAMETOOLONG, .rename);
- return false;
- }
- buf[path_in_dir_.len] = 0;
- const path_in_dir = buf[0..path_in_dir_.len :0];
-
- switch (Syscall.renameat(this.cwd, src, this.target_fd.?, path_in_dir)) {
- .err => |e| {
- const target_path = ResolvePath.joinZ(&[_][]const u8{
- this.target,
- ResolvePath.basename(src),
- }, .auto);
-
- this.err = e.withPath(bun.default_allocator.dupeZ(u8, target_path[0..]) catch bun.outOfMemory());
- return false;
- },
- else => {},
- }
-
- return true;
- }
-
- fn moveMultipleIntoDir(this: *@This()) void {
- var buf: bun.PathBuffer = undefined;
- var fixed_alloc = std.heap.FixedBufferAllocator.init(buf[0..bun.MAX_PATH_BYTES]);
-
- for (this.sources) |src_raw| {
- if (this.error_signal.load(.seq_cst)) return;
- defer fixed_alloc.reset();
-
- const src = src_raw[0..std.mem.len(src_raw) :0];
- if (!this.moveInDir(src, &buf)) {
- return;
- }
- }
- }
-
- /// From the man pages of `mv`:
- /// ```txt
- /// As the rename(2) call does not work across file systems, mv uses cp(1) and rm(1) to accomplish the move. The effect is equivalent to:
- /// rm -f destination_path && \
- /// cp -pRP source_file destination && \
- /// rm -rf source_file
- /// ```
- fn moveAcrossFilesystems(this: *@This(), src: [:0]const u8, dest: [:0]const u8) void {
- _ = this;
- _ = src;
- _ = dest;
-
- // TODO
- }
-
- pub fn runFromMainThread(this: *@This()) void {
- this.mv.batchedMoveTaskDone(this);
- }
-
- pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
- this.runFromMainThread();
- }
- };
-
- pub fn start(this: *Mv) Maybe(void) {
- return this.next();
- }
-
- pub fn writeFailingError(this: *Mv, buf: []const u8, exit_code: ExitCode) Maybe(void) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .{ .waiting_write_err = .{ .exit_code = exit_code } };
- this.bltn.stderr.enqueue(this, buf, safeguard);
- return Maybe(void).success;
- }
-
- _ = this.bltn.writeNoIO(.stderr, buf);
-
- this.bltn.done(exit_code);
- return Maybe(void).success;
- }
-
- pub fn next(this: *Mv) Maybe(void) {
- while (!(this.state == .done or this.state == .err)) {
- switch (this.state) {
- .idle => {
- if (this.parseOpts().asErr()) |e| {
- const buf = switch (e) {
- .illegal_option => |opt_str| this.bltn.fmtErrorArena(.mv, "illegal option -- {s}\n", .{opt_str}),
- .show_usage => Builtin.Kind.mv.usageString(),
- };
-
- return this.writeFailingError(buf, 1);
- }
- this.state = .{
- .check_target = .{
- .task = ShellMvCheckTargetTask{
- .mv = this,
- .cwd = this.bltn.parentCmd().base.shell.cwd_fd,
- .target = this.args.target,
- .task = .{
- .event_loop = this.bltn.parentCmd().base.eventLoop(),
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.bltn.parentCmd().base.eventLoop()),
- },
- },
- .state = .running,
- },
- };
- this.state.check_target.task.task.schedule();
- return Maybe(void).success;
- },
- .check_target => {
- if (this.state.check_target.state == .running) return Maybe(void).success;
- const check_target = &this.state.check_target;
-
- if (comptime bun.Environment.allow_assert) {
- assert(check_target.task.result != null);
- }
-
- const maybe_fd: ?bun.FileDescriptor = switch (check_target.task.result.?) {
- .err => |e| brk: {
- switch (e.getErrno()) {
- bun.C.E.NOENT => {
- // Means we are renaming entry, not moving to a directory
- if (this.args.sources.len == 1) break :brk null;
-
- const buf = this.bltn.fmtErrorArena(.mv, "{s}: No such file or directory\n", .{this.args.target});
- return this.writeFailingError(buf, 1);
- },
- else => {
- const sys_err = e.toShellSystemError();
- const buf = this.bltn.fmtErrorArena(.mv, "{s}: {s}\n", .{ sys_err.path.byteSlice(), sys_err.message.byteSlice() });
- return this.writeFailingError(buf, 1);
- },
- }
- },
- .result => |maybe_fd| maybe_fd,
- };
-
- // Trying to move multiple files into a file
- if (maybe_fd == null and this.args.sources.len > 1) {
- const buf = this.bltn.fmtErrorArena(.mv, "{s} is not a directory\n", .{this.args.target});
- return this.writeFailingError(buf, 1);
- }
-
- const count_per_task = ShellMvBatchedTask.BATCH_SIZE;
-
- const task_count = brk: {
- const sources_len: f64 = @floatFromInt(this.args.sources.len);
- const batch_size: f64 = @floatFromInt(count_per_task);
- const task_count: usize = @intFromFloat(@ceil(sources_len / batch_size));
- break :brk task_count;
- };
-
- this.args.target_fd = maybe_fd;
- const cwd_fd = this.bltn.parentCmd().base.shell.cwd_fd;
- const tasks = this.bltn.arena.allocator().alloc(ShellMvBatchedTask, task_count) catch bun.outOfMemory();
- // Initialize tasks
- {
- var i: usize = 0;
- while (i < tasks.len) : (i += 1) {
- const start_idx = i * count_per_task;
- const end_idx = @min(start_idx + count_per_task, this.args.sources.len);
- const sources = this.args.sources[start_idx..end_idx];
-
- tasks[i] = ShellMvBatchedTask{
- .mv = this,
- .cwd = cwd_fd,
- .target = this.args.target,
- .target_fd = this.args.target_fd,
- .sources = sources,
- // We set this later
- .error_signal = undefined,
- .task = .{
- .event_loop = this.bltn.parentCmd().base.eventLoop(),
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.bltn.parentCmd().base.eventLoop()),
- },
- .event_loop = this.bltn.parentCmd().base.eventLoop(),
- };
- }
- }
-
- this.state = .{
- .executing = .{
- .task_count = task_count,
- .error_signal = std.atomic.Value(bool).init(false),
- .tasks = tasks,
- },
- };
-
- for (this.state.executing.tasks) |*t| {
- t.error_signal = &this.state.executing.error_signal;
- t.task.schedule();
- }
-
- return Maybe(void).success;
- },
- // Shouldn't happen
- .executing => {},
- .waiting_write_err => {
- return Maybe(void).success;
- },
- .done, .err => unreachable,
- }
- }
-
- if (this.state == .done) {
- this.bltn.done(0);
- return Maybe(void).success;
- }
-
- this.bltn.done(1);
- return Maybe(void).success;
- }
-
- pub fn onIOWriterChunk(this: *Mv, _: usize, e: ?JSC.SystemError) void {
- defer if (e) |err| err.deref();
- switch (this.state) {
- .waiting_write_err => {
- if (e != null) {
- this.state = .err;
- _ = this.next();
- return;
- }
- this.bltn.done(this.state.waiting_write_err.exit_code);
- return;
- },
- else => @panic("Invalid state"),
- }
- }
-
- pub fn checkTargetTaskDone(this: *Mv, task: *ShellMvCheckTargetTask) void {
- _ = task;
-
- if (comptime bun.Environment.allow_assert) {
- assert(this.state == .check_target);
- assert(this.state.check_target.task.result != null);
- }
-
- this.state.check_target.state = .done;
- _ = this.next();
- return;
- }
-
- pub fn batchedMoveTaskDone(this: *Mv, task: *ShellMvBatchedTask) void {
- if (comptime bun.Environment.allow_assert) {
- assert(this.state == .executing);
- assert(this.state.executing.tasks_done < this.state.executing.task_count);
- }
-
- var exec = &this.state.executing;
-
- if (task.err) |err| {
- exec.error_signal.store(true, .seq_cst);
- if (exec.err == null) {
- exec.err = err;
- } else {
- bun.default_allocator.free(err.path);
- }
- }
-
- exec.tasks_done += 1;
- if (exec.tasks_done >= exec.task_count) {
- if (exec.err) |err| {
- const e = err.toShellSystemError();
- const buf = this.bltn.fmtErrorArena(.mv, "{}: {}\n", .{ e.path, e.message });
- _ = this.writeFailingError(buf, err.errno);
- return;
- }
- this.state = .done;
-
- _ = this.next();
- return;
- }
- }
-
- pub fn deinit(this: *Mv) void {
- if (this.args.target_fd != null and this.args.target_fd.? != bun.invalid_fd) {
- _ = Syscall.close(this.args.target_fd.?);
- }
- }
-
- const Opts = struct {
- /// `-f`
- ///
- /// Do not prompt for confirmation before overwriting the destination path. (The -f option overrides any previous -i or -n options.)
- force_overwrite: bool = true,
- /// `-h`
- ///
- /// If the target operand is a symbolic link to a directory, do not follow it. This causes the mv utility to rename the file source to the destination path target rather than moving source into the
- /// directory referenced by target.
- no_dereference: bool = false,
- /// `-i`
- ///
- /// Cause mv to write a prompt to standard error before moving a file that would overwrite an existing file. If the response from the standard input begins with the character ‘y’ or ‘Y’, the move is
- /// attempted. (The -i option overrides any previous -f or -n options.)
- interactive_mode: bool = false,
- /// `-n`
- ///
- /// Do not overwrite an existing file. (The -n option overrides any previous -f or -i options.)
- no_overwrite: bool = false,
- /// `-v`
- ///
- /// Cause mv to be verbose, showing files after they are moved.
- verbose_output: bool = false,
-
- const ParseError = union(enum) {
- illegal_option: []const u8,
- show_usage,
- };
- };
-
- pub fn parseOpts(this: *Mv) Result(void, Opts.ParseError) {
- const filepath_args = switch (this.parseFlags()) {
- .ok => |args| args,
- .err => |e| return .{ .err = e },
- };
-
- if (filepath_args.len < 2) {
- return .{ .err = .show_usage };
- }
-
- this.args.sources = filepath_args[0 .. filepath_args.len - 1];
- this.args.target = std.mem.span(filepath_args[filepath_args.len - 1]);
-
- return .ok;
- }
-
- pub fn parseFlags(this: *Mv) Result([]const [*:0]const u8, Opts.ParseError) {
- const args = this.bltn.argsSlice();
- var idx: usize = 0;
- if (args.len == 0) {
- return .{ .err = .show_usage };
- }
-
- while (idx < args.len) : (idx += 1) {
- const flag = args[idx];
- switch (this.parseFlag(flag[0..std.mem.len(flag)])) {
- .done => {
- const filepath_args = args[idx..];
- return .{ .ok = filepath_args };
- },
- .continue_parsing => {},
- .illegal_option => |opt_str| return .{ .err = .{ .illegal_option = opt_str } },
- }
- }
-
- return .{ .err = .show_usage };
- }
-
- pub fn parseFlag(this: *Mv, flag: []const u8) union(enum) { continue_parsing, done, illegal_option: []const u8 } {
- if (flag.len == 0) return .done;
- if (flag[0] != '-') return .done;
-
- const small_flags = flag[1..];
- for (small_flags) |char| {
- switch (char) {
- 'f' => {
- this.opts.force_overwrite = true;
- this.opts.interactive_mode = false;
- this.opts.no_overwrite = false;
- },
- 'h' => {
- this.opts.no_dereference = true;
- },
- 'i' => {
- this.opts.interactive_mode = true;
- this.opts.force_overwrite = false;
- this.opts.no_overwrite = false;
- },
- 'n' => {
- this.opts.no_overwrite = true;
- this.opts.force_overwrite = false;
- this.opts.interactive_mode = false;
- },
- 'v' => {
- this.opts.verbose_output = true;
- },
- else => {
- return .{ .illegal_option = "-" };
- },
- }
- }
-
- return .continue_parsing;
- }
- };
-
- pub const Rm = struct {
- bltn: *Builtin,
- opts: Opts,
- state: union(enum) {
- idle,
- parse_opts: struct {
- args_slice: []const [*:0]const u8,
- idx: u32 = 0,
- state: union(enum) {
- normal,
- wait_write_err,
- } = .normal,
- },
- exec: struct {
- // task: RmTask,
- filepath_args: []const [*:0]const u8,
- total_tasks: usize,
- err: ?Syscall.Error = null,
- lock: bun.Mutex = bun.Mutex{},
- error_signal: std.atomic.Value(bool) = .{ .raw = false },
- output_done: std.atomic.Value(usize) = .{ .raw = 0 },
- output_count: std.atomic.Value(usize) = .{ .raw = 0 },
- state: union(enum) {
- idle,
- waiting: struct {
- tasks_done: usize = 0,
- },
-
- pub fn tasksDone(this: *@This()) usize {
- return switch (this.*) {
- .idle => 0,
- .waiting => this.waiting.tasks_done,
- };
- }
- },
-
- fn incrementOutputCount(this: *@This(), comptime thevar: @Type(.enum_literal)) void {
- var atomicvar = &@field(this, @tagName(thevar));
- const result = atomicvar.fetchAdd(1, .seq_cst);
- log("[rm] {s}: {d} + 1", .{ @tagName(thevar), result });
- return;
- }
-
- fn getOutputCount(this: *@This(), comptime thevar: @Type(.enum_literal)) usize {
- var atomicvar = &@field(this, @tagName(thevar));
- return atomicvar.load(.seq_cst);
- }
- },
- done: struct { exit_code: ExitCode },
- err: ExitCode,
- } = .idle,
-
- pub const Opts = struct {
- /// `--no-preserve-root` / `--preserve-root`
- ///
- /// If set to false, then allow the recursive removal of the root directory.
- /// Safety feature to prevent accidental deletion of the root directory.
- preserve_root: bool = true,
-
- /// `-f`, `--force`
- ///
- /// Ignore nonexistent files and arguments, never prompt.
- force: bool = false,
-
- /// Configures how the user should be prompted on removal of files.
- prompt_behaviour: PromptBehaviour = .never,
-
- /// `-r`, `-R`, `--recursive`
- ///
- /// Remove directories and their contents recursively.
- recursive: bool = false,
-
- /// `-v`, `--verbose`
- ///
- /// Explain what is being done (prints which files/dirs are being deleted).
- verbose: bool = false,
-
- /// `-d`, `--dir`
- ///
- /// Remove empty directories. This option permits you to remove a directory
- /// without specifying `-r`/`-R`/`--recursive`, provided that the directory is
- /// empty.
- remove_empty_dirs: bool = false,
-
- const PromptBehaviour = union(enum) {
- /// `--interactive=never`
- ///
- /// Default
- never,
-
- /// `-I`, `--interactive=once`
- ///
- /// Once before removing more than three files, or when removing recursively.
- once: struct {
- removed_count: u32 = 0,
- },
-
- /// `-i`, `--interactive=always`
- ///
- /// Prompt before every removal.
- always,
- };
- };
-
- pub fn start(this: *Rm) Maybe(void) {
- return this.next();
- }
-
- pub noinline fn next(this: *Rm) Maybe(void) {
- while (this.state != .done and this.state != .err) {
- switch (this.state) {
- .idle => {
- this.state = .{
- .parse_opts = .{
- .args_slice = this.bltn.argsSlice(),
- },
- };
- continue;
- },
- .parse_opts => {
- var parse_opts = &this.state.parse_opts;
- switch (parse_opts.state) {
- .normal => {
- // This means there were no arguments or only
- // flag arguments meaning no positionals, in
- // either case we must print the usage error
- // string
- if (parse_opts.idx >= parse_opts.args_slice.len) {
- const error_string = Builtin.Kind.usageString(.rm);
- if (this.bltn.stderr.needsIO()) |safeguard| {
- parse_opts.state = .wait_write_err;
- this.bltn.stderr.enqueue(this, error_string, safeguard);
- return Maybe(void).success;
- }
-
- _ = this.bltn.writeNoIO(.stderr, error_string);
-
- this.bltn.done(1);
- return Maybe(void).success;
- }
-
- const idx = parse_opts.idx;
-
- const arg_raw = parse_opts.args_slice[idx];
- const arg = arg_raw[0..std.mem.len(arg_raw)];
-
- switch (parseFlag(&this.opts, this.bltn, arg)) {
- .continue_parsing => {
- parse_opts.idx += 1;
- continue;
- },
- .done => {
- if (this.opts.recursive) {
- this.opts.remove_empty_dirs = true;
- }
-
- if (this.opts.prompt_behaviour != .never) {
- const buf = "rm: \"-i\" is not supported yet";
- if (this.bltn.stderr.needsIO()) |safeguard| {
- parse_opts.state = .wait_write_err;
- this.bltn.stderr.enqueue(this, buf, safeguard);
- continue;
- }
-
- _ = this.bltn.writeNoIO(.stderr, buf);
-
- this.bltn.done(1);
- return Maybe(void).success;
- }
-
- const filepath_args_start = idx;
- const filepath_args = parse_opts.args_slice[filepath_args_start..];
-
- // Check that non of the paths will delete the root
- {
- var buf: bun.PathBuffer = undefined;
- const cwd = switch (Syscall.getcwd(&buf)) {
- .err => |err| {
- return .{ .err = err };
- },
- .result => |cwd| cwd,
- };
-
- for (filepath_args) |filepath| {
- const path = filepath[0..bun.len(filepath)];
- const resolved_path = if (ResolvePath.Platform.auto.isAbsolute(path)) path else bun.path.join(&[_][]const u8{ cwd, path }, .auto);
- const is_root = brk: {
- const normalized = bun.path.normalizeString(resolved_path, false, .auto);
- const dirname = ResolvePath.dirname(normalized, .auto);
- const is_root = std.mem.eql(u8, dirname, "");
- break :brk is_root;
- };
-
- if (is_root) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- parse_opts.state = .wait_write_err;
- this.bltn.stderr.enqueueFmtBltn(this, .rm, "\"{s}\" may not be removed\n", .{resolved_path}, safeguard);
- return Maybe(void).success;
- }
-
- const error_string = this.bltn.fmtErrorArena(.rm, "\"{s}\" may not be removed\n", .{resolved_path});
-
- _ = this.bltn.writeNoIO(.stderr, error_string);
-
- this.bltn.done(1);
- return Maybe(void).success;
- }
- }
- }
-
- const total_tasks = filepath_args.len;
- this.state = .{
- .exec = .{
- .filepath_args = filepath_args,
- .total_tasks = total_tasks,
- .state = .idle,
- .output_done = std.atomic.Value(usize).init(0),
- .output_count = std.atomic.Value(usize).init(0),
- },
- };
- // this.state.exec.task.schedule();
- // return Maybe(void).success;
- continue;
- },
- .illegal_option => {
- const error_string = "rm: illegal option -- -\n";
- if (this.bltn.stderr.needsIO()) |safeguard| {
- parse_opts.state = .wait_write_err;
- this.bltn.stderr.enqueue(this, error_string, safeguard);
- return Maybe(void).success;
- }
-
- _ = this.bltn.writeNoIO(.stderr, error_string);
-
- this.bltn.done(1);
- return Maybe(void).success;
- },
- .illegal_option_with_flag => {
- const flag = arg;
- if (this.bltn.stderr.needsIO()) |safeguard| {
- parse_opts.state = .wait_write_err;
- this.bltn.stderr.enqueueFmtBltn(this, .rm, "illegal option -- {s}\n", .{flag[1..]}, safeguard);
- return Maybe(void).success;
- }
- const error_string = this.bltn.fmtErrorArena(.rm, "illegal option -- {s}\n", .{flag[1..]});
-
- _ = this.bltn.writeNoIO(.stderr, error_string);
-
- this.bltn.done(1);
- return Maybe(void).success;
- },
- }
- },
- .wait_write_err => {
- @panic("Invalid");
- // // Errored
- // if (parse_opts.state.wait_write_err.err) |e| {
- // this.state = .{ .err = e };
- // continue;
- // }
-
- // // Done writing
- // if (this.state.parse_opts.state.wait_write_err.remain() == 0) {
- // this.state = .{ .done = .{ .exit_code = 0 } };
- // continue;
- // }
-
- // // yield execution to continue writing
- // return Maybe(void).success;
- },
- }
- },
- .exec => {
- const cwd = this.bltn.parentCmd().base.shell.cwd_fd;
- // Schedule task
- if (this.state.exec.state == .idle) {
- this.state.exec.state = .{ .waiting = .{} };
- for (this.state.exec.filepath_args) |root_raw| {
- const root = root_raw[0..std.mem.len(root_raw)];
- const root_path_string = bun.PathString.init(root[0..root.len]);
- const is_absolute = ResolvePath.Platform.auto.isAbsolute(root);
- var task = ShellRmTask.create(root_path_string, this, cwd, &this.state.exec.error_signal, is_absolute);
- task.schedule();
- // task.
- }
- }
-
- // do nothing
- return Maybe(void).success;
- },
- .done, .err => unreachable,
- }
- }
-
- if (this.state == .done) {
- this.bltn.done(0);
- return Maybe(void).success;
- }
-
- if (this.state == .err) {
- this.bltn.done(this.state.err);
- return Maybe(void).success;
- }
-
- return Maybe(void).success;
- }
-
- pub fn onIOWriterChunk(this: *Rm, _: usize, e: ?JSC.SystemError) void {
- log("Rm(0x{x}).onIOWriterChunk()", .{@intFromPtr(this)});
- if (comptime bun.Environment.allow_assert) {
- assert((this.state == .parse_opts and this.state.parse_opts.state == .wait_write_err) or
- (this.state == .exec and this.state.exec.state == .waiting and this.state.exec.output_count.load(.seq_cst) > 0));
- }
-
- if (this.state == .exec and this.state.exec.state == .waiting) {
- log("Rm(0x{x}) output done={d} output count={d}", .{ @intFromPtr(this), this.state.exec.getOutputCount(.output_done), this.state.exec.getOutputCount(.output_count) });
- this.state.exec.incrementOutputCount(.output_done);
- if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) {
- const code: ExitCode = if (this.state.exec.err != null) 1 else 0;
- this.bltn.done(code);
- return;
- }
- return;
- }
-
- if (e != null) {
- defer e.?.deref();
- this.state = .{ .err = @intFromEnum(e.?.getErrno()) };
- this.bltn.done(e.?.getErrno());
- return;
- }
-
- this.bltn.done(1);
- return;
- }
-
- // pub fn writeToStdoutFromAsyncTask(this: *Rm, comptime fmt: []const u8, args: anytype) Maybe(void) {
- // const buf = this.rm.bltn.fmtErrorArena(null, fmt, args);
- // if (!this.rm.bltn.stdout.needsIO()) {
- // this.state.exec.lock.lock();
- // defer this.state.exec.lock.unlock();
- // _ = this.rm.bltn.writeNoIO(.stdout, buf);
- // return Maybe(void).success;
- // }
-
- // var written: usize = 0;
- // while (written < buf.len) : (written += switch (Syscall.write(this.rm.bltn.stdout.fd, buf)) {
- // .err => |e| return Maybe(void).initErr(e),
- // .result => |n| n,
- // }) {}
-
- // return Maybe(void).success;
- // }
-
- pub fn deinit(this: *Rm) void {
- _ = this;
- }
-
- const ParseFlagsResult = enum {
- continue_parsing,
- done,
- illegal_option,
- illegal_option_with_flag,
- };
-
- fn parseFlag(this: *Opts, bltn: *Builtin, flag: []const u8) ParseFlagsResult {
- _ = bltn;
- if (flag.len == 0) return .done;
- if (flag[0] != '-') return .done;
- if (flag.len > 2 and flag[1] == '-') {
- if (bun.strings.eqlComptime(flag, "--preserve-root")) {
- this.preserve_root = true;
- return .continue_parsing;
- } else if (bun.strings.eqlComptime(flag, "--no-preserve-root")) {
- this.preserve_root = false;
- return .continue_parsing;
- } else if (bun.strings.eqlComptime(flag, "--recursive")) {
- this.recursive = true;
- return .continue_parsing;
- } else if (bun.strings.eqlComptime(flag, "--verbose")) {
- this.verbose = true;
- return .continue_parsing;
- } else if (bun.strings.eqlComptime(flag, "--dir")) {
- this.remove_empty_dirs = true;
- return .continue_parsing;
- } else if (bun.strings.eqlComptime(flag, "--interactive=never")) {
- this.prompt_behaviour = .never;
- return .continue_parsing;
- } else if (bun.strings.eqlComptime(flag, "--interactive=once")) {
- this.prompt_behaviour = .{ .once = .{} };
- return .continue_parsing;
- } else if (bun.strings.eqlComptime(flag, "--interactive=always")) {
- this.prompt_behaviour = .always;
- return .continue_parsing;
- }
-
- return .illegal_option;
- }
-
- const small_flags = flag[1..];
- for (small_flags) |char| {
- switch (char) {
- 'f' => {
- this.force = true;
- this.prompt_behaviour = .never;
- },
- 'r', 'R' => {
- this.recursive = true;
- },
- 'v' => {
- this.verbose = true;
- },
- 'd' => {
- this.remove_empty_dirs = true;
- },
- 'i' => {
- this.prompt_behaviour = .{ .once = .{} };
- },
- 'I' => {
- this.prompt_behaviour = .always;
- },
- else => {
- return .illegal_option_with_flag;
- },
- }
- }
-
- return .continue_parsing;
- }
-
- pub fn onShellRmTaskDone(this: *Rm, task: *ShellRmTask) void {
- var exec = &this.state.exec;
- const tasks_done = switch (exec.state) {
- .idle => @panic("Invalid state"),
- .waiting => brk: {
- exec.state.waiting.tasks_done += 1;
- const amt = exec.state.waiting.tasks_done;
- if (task.err) |err| {
- exec.err = err;
- const error_string = this.bltn.taskErrorToString(.rm, err);
- if (this.bltn.stderr.needsIO()) |safeguard| {
- log("Rm(0x{x}) task=0x{x} ERROR={s}", .{ @intFromPtr(this), @intFromPtr(task), error_string });
- exec.incrementOutputCount(.output_count);
- this.bltn.stderr.enqueue(this, error_string, safeguard);
- return;
- } else {
- _ = this.bltn.writeNoIO(.stderr, error_string);
- }
- }
- break :brk amt;
- },
- };
-
- log("ShellRmTask(0x{x}, task={s})", .{ @intFromPtr(task), task.root_path });
- // Wait until all tasks done and all output is written
- if (tasks_done >= this.state.exec.total_tasks and
- exec.getOutputCount(.output_done) >= exec.getOutputCount(.output_count))
- {
- this.state = .{ .done = .{ .exit_code = if (exec.err) |theerr| theerr.errno else 0 } };
- _ = this.next();
- return;
- }
- }
-
- fn writeVerbose(this: *Rm, verbose: *ShellRmTask.DirTask) void {
- if (this.bltn.stdout.needsIO()) |safeguard| {
- const buf = verbose.takeDeletedEntries();
- defer buf.deinit();
- this.bltn.stdout.enqueue(this, buf.items, safeguard);
- } else {
- _ = this.bltn.writeNoIO(.stdout, verbose.deleted_entries.items);
- _ = this.state.exec.incrementOutputCount(.output_done);
- if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) {
- this.bltn.done(if (this.state.exec.err != null) @as(ExitCode, 1) else @as(ExitCode, 0));
- return;
- }
- return;
- }
- }
-
- pub const ShellRmTask = struct {
- const debug = bun.Output.scoped(.AsyncRmTask, true);
-
- rm: *Rm,
- opts: Opts,
-
- cwd: bun.FileDescriptor,
- cwd_path: ?CwdPath = if (bun.Environment.isPosix) 0 else null,
-
- root_task: DirTask,
- root_path: bun.PathString = bun.PathString.empty,
- root_is_absolute: bool,
-
- error_signal: *std.atomic.Value(bool),
- err_mutex: bun.Mutex = .{},
- err: ?Syscall.Error = null,
-
- event_loop: JSC.EventLoopHandle,
- concurrent_task: JSC.EventLoopTask,
- task: JSC.WorkPoolTask = .{
- .callback = workPoolCallback,
- },
- join_style: JoinStyle,
-
- /// On Windows we allow posix path separators
- /// But this results in weird looking paths if we use our path.join function which uses the platform separator:
- /// `foo/bar + baz -> foo/bar\baz`
- ///
- /// So detect which path separator the user is using and prefer that.
- /// If both are used, pick the first one.
- const JoinStyle = union(enum) {
- posix,
- windows,
-
- pub fn fromPath(p: bun.PathString) JoinStyle {
- if (comptime bun.Environment.isPosix) return .posix;
- const backslash = std.mem.indexOfScalar(u8, p.slice(), '\\') orelse std.math.maxInt(usize);
- const forwardslash = std.mem.indexOfScalar(u8, p.slice(), '/') orelse std.math.maxInt(usize);
- if (forwardslash <= backslash)
- return .posix;
- return .windows;
- }
- };
-
- const CwdPath = if (bun.Environment.isWindows) [:0]const u8 else u0;
-
- const ParentRmTask = @This();
-
- pub const DirTask = struct {
- task_manager: *ParentRmTask,
- parent_task: ?*DirTask,
- path: [:0]const u8,
- is_absolute: bool = false,
- subtask_count: std.atomic.Value(usize),
- need_to_wait: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
- deleting_after_waiting_for_children: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
- kind_hint: EntryKindHint,
- task: JSC.WorkPoolTask = .{ .callback = runFromThreadPool },
- deleted_entries: std.ArrayList(u8),
- concurrent_task: JSC.EventLoopTask,
-
- const EntryKindHint = enum { idk, dir, file };
-
- pub fn takeDeletedEntries(this: *DirTask) std.ArrayList(u8) {
- debug("DirTask(0x{x} path={s}) takeDeletedEntries", .{ @intFromPtr(this), this.path });
- const ret = this.deleted_entries;
- this.deleted_entries = std.ArrayList(u8).init(ret.allocator);
- return ret;
- }
-
- pub fn runFromMainThread(this: *DirTask) void {
- debug("DirTask(0x{x}, path={s}) runFromMainThread", .{ @intFromPtr(this), this.path });
- this.task_manager.rm.writeVerbose(this);
- }
-
- pub fn runFromMainThreadMini(this: *DirTask, _: *void) void {
- this.runFromMainThread();
- }
-
- pub fn runFromThreadPool(task: *JSC.WorkPoolTask) void {
- var this: *DirTask = @fieldParentPtr("task", task);
- this.runFromThreadPoolImpl();
- }
-
- fn runFromThreadPoolImpl(this: *DirTask) void {
- defer {
- if (!this.deleting_after_waiting_for_children.load(.seq_cst)) {
- this.postRun();
- }
- }
-
- // Root, get cwd path on windows
- if (bun.Environment.isWindows) {
- if (this.parent_task == null) {
- var buf: bun.PathBuffer = undefined;
- const cwd_path = switch (Syscall.getFdPath(this.task_manager.cwd, &buf)) {
- .result => |p| bun.default_allocator.dupeZ(u8, p) catch bun.outOfMemory(),
- .err => |err| {
- debug("[runFromThreadPoolImpl:getcwd] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path });
- this.task_manager.err_mutex.lock();
- defer this.task_manager.err_mutex.unlock();
- if (this.task_manager.err == null) {
- this.task_manager.err = err;
- this.task_manager.error_signal.store(true, .seq_cst);
- }
- return;
- },
- };
- this.task_manager.cwd_path = cwd_path;
- }
- }
-
- debug("DirTask: {s}", .{this.path});
- this.is_absolute = ResolvePath.Platform.auto.isAbsolute(this.path[0..this.path.len]);
- switch (this.task_manager.removeEntry(this, this.is_absolute)) {
- .err => |err| {
- debug("[runFromThreadPoolImpl] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path });
- this.task_manager.err_mutex.lock();
- defer this.task_manager.err_mutex.unlock();
- if (this.task_manager.err == null) {
- this.task_manager.err = err;
- this.task_manager.error_signal.store(true, .seq_cst);
- } else {
- bun.default_allocator.free(err.path);
- }
- },
- .result => {},
- }
- }
-
- fn handleErr(this: *DirTask, err: Syscall.Error) void {
- debug("[handleErr] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path });
- this.task_manager.err_mutex.lock();
- defer this.task_manager.err_mutex.unlock();
- if (this.task_manager.err == null) {
- this.task_manager.err = err;
- this.task_manager.error_signal.store(true, .seq_cst);
- } else {
- bun.default_allocator.free(err.path);
- }
- }
-
- pub fn postRun(this: *DirTask) void {
- debug("DirTask(0x{x}, path={s}) postRun", .{ @intFromPtr(this), this.path });
- // // This is true if the directory has subdirectories
- // // that need to be deleted
- if (this.need_to_wait.load(.seq_cst)) return;
-
- // We have executed all the children of this task
- if (this.subtask_count.fetchSub(1, .seq_cst) == 1) {
- defer {
- if (this.task_manager.opts.verbose)
- this.queueForWrite()
- else
- this.deinit();
- }
-
- // If we have a parent and we are the last child, now we can delete the parent
- if (this.parent_task != null) {
- // It's possible that we queued this subdir task and it finished, while the parent
- // was still in the `removeEntryDir` function
- const tasks_left_before_decrement = this.parent_task.?.subtask_count.fetchSub(1, .seq_cst);
- const parent_still_in_remove_entry_dir = !this.parent_task.?.need_to_wait.load(.monotonic);
- if (!parent_still_in_remove_entry_dir and tasks_left_before_decrement == 2) {
- this.parent_task.?.deleteAfterWaitingForChildren();
- }
- return;
- }
-
- // Otherwise we are root task
- this.task_manager.finishConcurrently();
- }
-
- // Otherwise need to wait
- }
-
- pub fn deleteAfterWaitingForChildren(this: *DirTask) void {
- debug("DirTask(0x{x}, path={s}) deleteAfterWaitingForChildren", .{ @intFromPtr(this), this.path });
- // `runFromMainThreadImpl` has a `defer this.postRun()` so need to set this to true to skip that
- this.deleting_after_waiting_for_children.store(true, .seq_cst);
- this.need_to_wait.store(false, .seq_cst);
- var do_post_run = true;
- defer {
- if (do_post_run) this.postRun();
- }
- if (this.task_manager.error_signal.load(.seq_cst)) {
- return;
- }
-
- switch (this.task_manager.removeEntryDirAfterChildren(this)) {
- .err => |e| {
- debug("[deleteAfterWaitingForChildren] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(e.getErrno()), e.path });
- this.task_manager.err_mutex.lock();
- defer this.task_manager.err_mutex.unlock();
- if (this.task_manager.err == null) {
- this.task_manager.err = e;
- } else {
- bun.default_allocator.free(e.path);
- }
- },
- .result => |deleted| {
- if (!deleted) {
- do_post_run = false;
- }
- },
- }
- }
-
- pub fn queueForWrite(this: *DirTask) void {
- log("DirTask(0x{x}, path={s}) queueForWrite to_write={d}", .{ @intFromPtr(this), this.path, this.deleted_entries.items.len });
- if (this.deleted_entries.items.len == 0) return;
- if (this.task_manager.event_loop == .js) {
- this.task_manager.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
- } else {
- this.task_manager.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
- }
- }
-
- pub fn deinit(this: *DirTask) void {
- this.deleted_entries.deinit();
- // The root's path string is from Rm's argv so don't deallocate it
- // And the root task is actually a field on the struct of the AsyncRmTask so don't deallocate it either
- if (this.parent_task != null) {
- bun.default_allocator.free(this.path);
- bun.default_allocator.destroy(this);
- }
- }
- };
-
- pub fn create(root_path: bun.PathString, rm: *Rm, cwd: bun.FileDescriptor, error_signal: *std.atomic.Value(bool), is_absolute: bool) *ShellRmTask {
- const task = bun.default_allocator.create(ShellRmTask) catch bun.outOfMemory();
- task.* = ShellRmTask{
- .rm = rm,
- .opts = rm.opts,
- .cwd = cwd,
- .root_path = root_path,
- .root_task = DirTask{
- .task_manager = task,
- .parent_task = null,
- .path = root_path.sliceAssumeZ(),
- .subtask_count = std.atomic.Value(usize).init(1),
- .kind_hint = .idk,
- .deleted_entries = std.ArrayList(u8).init(bun.default_allocator),
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(rm.bltn.eventLoop()),
- },
- .event_loop = rm.bltn.parentCmd().base.eventLoop(),
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(rm.bltn.eventLoop()),
- .error_signal = error_signal,
- .root_is_absolute = is_absolute,
- .join_style = JoinStyle.fromPath(root_path),
- };
- return task;
- }
-
- pub fn schedule(this: *@This()) void {
- JSC.WorkPool.schedule(&this.task);
- }
-
- pub fn enqueue(this: *ShellRmTask, parent_dir: *DirTask, path: [:0]const u8, is_absolute: bool, kind_hint: DirTask.EntryKindHint) void {
- if (this.error_signal.load(.seq_cst)) {
- return;
- }
- const new_path = this.join(
- bun.default_allocator,
- &[_][]const u8{
- parent_dir.path[0..parent_dir.path.len],
- path[0..path.len],
- },
- is_absolute,
- );
- this.enqueueNoJoin(parent_dir, new_path, kind_hint);
- }
-
- pub fn enqueueNoJoin(this: *ShellRmTask, parent_task: *DirTask, path: [:0]const u8, kind_hint: DirTask.EntryKindHint) void {
- defer debug("enqueue: {s} {s}", .{ path, @tagName(kind_hint) });
-
- if (this.error_signal.load(.seq_cst)) {
- return;
- }
-
- var subtask = bun.default_allocator.create(DirTask) catch bun.outOfMemory();
- subtask.* = DirTask{
- .task_manager = this,
- .path = path,
- .parent_task = parent_task,
- .subtask_count = std.atomic.Value(usize).init(1),
- .kind_hint = kind_hint,
- .deleted_entries = std.ArrayList(u8).init(bun.default_allocator),
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.event_loop),
- };
-
- const count = parent_task.subtask_count.fetchAdd(1, .monotonic);
- if (comptime bun.Environment.allow_assert) {
- assert(count > 0);
- }
-
- JSC.WorkPool.schedule(&subtask.task);
- }
-
- pub fn getcwd(this: *ShellRmTask) bun.FileDescriptor {
- return this.cwd;
- }
-
- pub fn verboseDeleted(this: *@This(), dir_task: *DirTask, path: [:0]const u8) Maybe(void) {
- debug("deleted: {s}", .{path[0..path.len]});
- if (!this.opts.verbose) return Maybe(void).success;
- if (dir_task.deleted_entries.items.len == 0) {
- debug("DirTask(0x{x}, {s}) Incrementing output count (deleted={s})", .{ @intFromPtr(dir_task), dir_task.path, path });
- _ = this.rm.state.exec.incrementOutputCount(.output_count);
- }
- dir_task.deleted_entries.appendSlice(path[0..path.len]) catch bun.outOfMemory();
- dir_task.deleted_entries.append('\n') catch bun.outOfMemory();
- return Maybe(void).success;
- }
-
- pub fn finishConcurrently(this: *ShellRmTask) void {
- debug("finishConcurrently", .{});
- if (this.event_loop == .js) {
- this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
- } else {
- this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
- }
- }
-
- pub fn bufJoin(this: *ShellRmTask, buf: *bun.PathBuffer, parts: []const []const u8, syscall_tag: Syscall.Tag) Maybe([:0]const u8) {
- _ = syscall_tag; // autofix
-
- if (this.join_style == .posix) {
- return .{ .result = ResolvePath.joinZBuf(buf, parts, .posix) };
- } else return .{ .result = ResolvePath.joinZBuf(buf, parts, .windows) };
- }
-
- pub fn removeEntry(this: *ShellRmTask, dir_task: *DirTask, is_absolute: bool) Maybe(void) {
- var remove_child_vtable = RemoveFileVTable{
- .task = this,
- .child_of_dir = false,
- };
- var buf: bun.PathBuffer = undefined;
- switch (dir_task.kind_hint) {
- .idk, .file => return this.removeEntryFile(dir_task, dir_task.path, is_absolute, &buf, &remove_child_vtable),
- .dir => return this.removeEntryDir(dir_task, is_absolute, &buf),
- }
- }
-
- fn removeEntryDir(this: *ShellRmTask, dir_task: *DirTask, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
- const path = dir_task.path;
- const dirfd = this.cwd;
- debug("removeEntryDir({s})", .{path});
-
- // If `-d` is specified without `-r` then we can just use `rmdirat`
- if (this.opts.remove_empty_dirs and !this.opts.recursive) out_to_iter: {
- var delete_state = RemoveFileParent{
- .task = this,
- .treat_as_dir = true,
- .allow_enqueue = false,
- };
- while (delete_state.treat_as_dir) {
- switch (ShellSyscall.rmdirat(dirfd, path)) {
- .result => return Maybe(void).success,
- .err => |e| {
- switch (e.getErrno()) {
- bun.C.E.NOENT => {
- if (this.opts.force) return this.verboseDeleted(dir_task, path);
- return .{ .err = this.errorWithPath(e, path) };
- },
- bun.C.E.NOTDIR => {
- delete_state.treat_as_dir = false;
- if (this.removeEntryFile(dir_task, dir_task.path, is_absolute, buf, &delete_state).asErr()) |err| {
- return .{ .err = this.errorWithPath(err, path) };
- }
- if (!delete_state.treat_as_dir) return Maybe(void).success;
- if (delete_state.treat_as_dir) break :out_to_iter;
- },
- else => return .{ .err = this.errorWithPath(e, path) },
- }
- },
- }
- }
- }
-
- if (!this.opts.recursive) {
- return Maybe(void).initErr(Syscall.Error.fromCode(bun.C.E.ISDIR, .TODO).withPath(bun.default_allocator.dupeZ(u8, dir_task.path) catch bun.outOfMemory()));
- }
-
- const flags = bun.O.DIRECTORY | bun.O.RDONLY;
- const fd = switch (ShellSyscall.openat(dirfd, path, flags, 0)) {
- .result => |fd| fd,
- .err => |e| {
- switch (e.getErrno()) {
- bun.C.E.NOENT => {
- if (this.opts.force) return this.verboseDeleted(dir_task, path);
- return .{ .err = this.errorWithPath(e, path) };
- },
- bun.C.E.NOTDIR => {
- return this.removeEntryFile(dir_task, dir_task.path, is_absolute, buf, &DummyRemoveFile.dummy);
- },
- else => return .{ .err = this.errorWithPath(e, path) },
- }
- },
- };
-
- var close_fd = true;
- defer {
- // On posix we can close the file descriptor whenever, but on Windows
- // we need to close it BEFORE we delete
- if (close_fd) {
- _ = Syscall.close(fd);
- }
- }
-
- if (this.error_signal.load(.seq_cst)) {
- return Maybe(void).success;
- }
-
- var iterator = DirIterator.iterate(fd.asDir(), .u8);
- var entry = iterator.next();
-
- var remove_child_vtable = RemoveFileVTable{
- .task = this,
- .child_of_dir = true,
- };
-
- var i: usize = 0;
- while (switch (entry) {
- .err => |err| {
- return .{ .err = this.errorWithPath(err, path) };
- },
- .result => |ent| ent,
- }) |current| : (entry = iterator.next()) {
- debug("dir({s}) entry({s}, {s})", .{ path, current.name.slice(), @tagName(current.kind) });
- // TODO this seems bad maybe better to listen to kqueue/epoll event
- if (fastMod(i, 4) == 0 and this.error_signal.load(.seq_cst)) return Maybe(void).success;
-
- defer i += 1;
- switch (current.kind) {
- .directory => {
- this.enqueue(dir_task, current.name.sliceAssumeZ(), is_absolute, .dir);
- },
- else => {
- const name = current.name.sliceAssumeZ();
- const file_path = switch (this.bufJoin(
- buf,
- &[_][]const u8{
- path[0..path.len],
- name[0..name.len],
- },
- .unlink,
- )) {
- .err => |e| return .{ .err = e },
- .result => |p| p,
- };
-
- switch (this.removeEntryFile(dir_task, file_path, is_absolute, buf, &remove_child_vtable)) {
- .err => |e| return .{ .err = this.errorWithPath(e, current.name.sliceAssumeZ()) },
- .result => {},
- }
- },
- }
- }
-
- // Need to wait for children to finish
- if (dir_task.subtask_count.load(.seq_cst) > 1) {
- close_fd = true;
- dir_task.need_to_wait.store(true, .seq_cst);
- return Maybe(void).success;
- }
-
- if (this.error_signal.load(.seq_cst)) return Maybe(void).success;
-
- if (bun.Environment.isWindows) {
- close_fd = false;
- _ = Syscall.close(fd);
- }
-
- debug("[removeEntryDir] remove after children {s}", .{path});
- switch (ShellSyscall.unlinkatWithFlags(this.getcwd(), path, std.posix.AT.REMOVEDIR)) {
- .result => {
- switch (this.verboseDeleted(dir_task, path)) {
- .err => |e| return .{ .err = e },
- else => {},
- }
- return Maybe(void).success;
- },
- .err => |e| {
- switch (e.getErrno()) {
- bun.C.E.NOENT => {
- if (this.opts.force) {
- switch (this.verboseDeleted(dir_task, path)) {
- .err => |e2| return .{ .err = e2 },
- else => {},
- }
- return Maybe(void).success;
- }
-
- return .{ .err = this.errorWithPath(e, path) };
- },
- else => return .{ .err = e },
- }
- },
- }
- }
-
- const DummyRemoveFile = struct {
- var dummy: @This() = std.mem.zeroes(@This());
-
- pub fn onIsDir(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
- _ = this; // autofix
- _ = parent_dir_task; // autofix
- _ = path; // autofix
- _ = is_absolute; // autofix
- _ = buf; // autofix
-
- return Maybe(void).success;
- }
-
- pub fn onDirNotEmpty(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
- _ = this; // autofix
- _ = parent_dir_task; // autofix
- _ = path; // autofix
- _ = is_absolute; // autofix
- _ = buf; // autofix
-
- return Maybe(void).success;
- }
- };
-
- const RemoveFileVTable = struct {
- task: *ShellRmTask,
- child_of_dir: bool,
-
- pub fn onIsDir(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
- if (this.child_of_dir) {
- this.task.enqueueNoJoin(parent_dir_task, bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(), .dir);
- return Maybe(void).success;
- }
- return this.task.removeEntryDir(parent_dir_task, is_absolute, buf);
- }
-
- pub fn onDirNotEmpty(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
- if (this.child_of_dir) return .{ .result = this.task.enqueueNoJoin(parent_dir_task, bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(), .dir) };
- return this.task.removeEntryDir(parent_dir_task, is_absolute, buf);
- }
- };
-
- const RemoveFileParent = struct {
- task: *ShellRmTask,
- treat_as_dir: bool,
- allow_enqueue: bool = true,
- enqueued: bool = false,
-
- pub fn onIsDir(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
- _ = parent_dir_task; // autofix
- _ = path; // autofix
- _ = is_absolute; // autofix
- _ = buf; // autofix
-
- this.treat_as_dir = true;
- return Maybe(void).success;
- }
-
- pub fn onDirNotEmpty(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer) Maybe(void) {
- _ = is_absolute; // autofix
- _ = buf; // autofix
-
- this.treat_as_dir = true;
- if (this.allow_enqueue) {
- this.task.enqueueNoJoin(parent_dir_task, path, .dir);
- this.enqueued = true;
- }
- return Maybe(void).success;
- }
- };
-
- fn removeEntryDirAfterChildren(this: *ShellRmTask, dir_task: *DirTask) Maybe(bool) {
- debug("remove entry after children: {s}", .{dir_task.path});
- const dirfd = bun.toFD(this.cwd);
- var state = RemoveFileParent{
- .task = this,
- .treat_as_dir = true,
- };
- while (true) {
- if (state.treat_as_dir) {
- log("rmdirat({}, {s})", .{ dirfd, dir_task.path });
- switch (ShellSyscall.rmdirat(dirfd, dir_task.path)) {
- .result => {
- _ = this.verboseDeleted(dir_task, dir_task.path);
- return .{ .result = true };
- },
- .err => |e| {
- switch (e.getErrno()) {
- bun.C.E.NOENT => {
- if (this.opts.force) {
- _ = this.verboseDeleted(dir_task, dir_task.path);
- return .{ .result = true };
- }
- return .{ .err = this.errorWithPath(e, dir_task.path) };
- },
- bun.C.E.NOTDIR => {
- state.treat_as_dir = false;
- continue;
- },
- else => return .{ .err = this.errorWithPath(e, dir_task.path) },
- }
- },
- }
- } else {
- var buf: bun.PathBuffer = undefined;
- if (this.removeEntryFile(dir_task, dir_task.path, dir_task.is_absolute, &buf, &state).asErr()) |e| {
- return .{ .err = e };
- }
- if (state.enqueued) return .{ .result = false };
- if (state.treat_as_dir) continue;
- return .{ .result = true };
- }
- }
- }
-
- fn removeEntryFile(this: *ShellRmTask, parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *bun.PathBuffer, vtable: anytype) Maybe(void) {
- const VTable = std.meta.Child(@TypeOf(vtable));
- const Handler = struct {
- pub fn onIsDir(vtable_: anytype, parent_dir_task_: *DirTask, path_: [:0]const u8, is_absolute_: bool, buf_: *bun.PathBuffer) Maybe(void) {
- if (@hasDecl(VTable, "onIsDir")) {
- return VTable.onIsDir(vtable_, parent_dir_task_, path_, is_absolute_, buf_);
- }
- return Maybe(void).success;
- }
-
- pub fn onDirNotEmpty(vtable_: anytype, parent_dir_task_: *DirTask, path_: [:0]const u8, is_absolute_: bool, buf_: *bun.PathBuffer) Maybe(void) {
- if (@hasDecl(VTable, "onDirNotEmpty")) {
- return VTable.onDirNotEmpty(vtable_, parent_dir_task_, path_, is_absolute_, buf_);
- }
- return Maybe(void).success;
- }
- };
- const dirfd = bun.toFD(this.cwd);
- switch (ShellSyscall.unlinkatWithFlags(dirfd, path, 0)) {
- .result => return this.verboseDeleted(parent_dir_task, path),
- .err => |e| {
- debug("unlinkatWithFlags({s}) = {s}", .{ path, @tagName(e.getErrno()) });
- switch (e.getErrno()) {
- bun.C.E.NOENT => {
- if (this.opts.force)
- return this.verboseDeleted(parent_dir_task, path);
-
- return .{ .err = this.errorWithPath(e, path) };
- },
- bun.C.E.ISDIR => {
- return Handler.onIsDir(vtable, parent_dir_task, path, is_absolute, buf);
- },
- // This might happen if the file is actually a directory
- bun.C.E.PERM => {
- switch (builtin.os.tag) {
- // non-Linux POSIX systems and Windows return EPERM when trying to delete a directory, so
- // we need to handle that case specifically and translate the error
- .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos, .windows => {
- // If we are allowed to delete directories then we can call `unlink`.
- // If `path` points to a directory, then it is deleted (if empty) or we handle it as a directory
- // If it's actually a file, we get an error so we don't need to call `stat` to check that.
- if (this.opts.recursive or this.opts.remove_empty_dirs) {
- return switch (ShellSyscall.unlinkatWithFlags(this.getcwd(), path, std.posix.AT.REMOVEDIR)) {
- // it was empty, we saved a syscall
- .result => return this.verboseDeleted(parent_dir_task, path),
- .err => |e2| {
- return switch (e2.getErrno()) {
- // not empty, process directory as we would normally
- bun.C.E.NOTEMPTY => {
- // this.enqueueNoJoin(parent_dir_task, path, .dir);
- // return Maybe(void).success;
- return Handler.onDirNotEmpty(vtable, parent_dir_task, path, is_absolute, buf);
- },
- // actually a file, the error is a permissions error
- bun.C.E.NOTDIR => .{ .err = this.errorWithPath(e, path) },
- else => .{ .err = this.errorWithPath(e2, path) },
- };
- },
- };
- }
-
- // We don't know if it was an actual permissions error or it was a directory so we need to try to delete it as a directory
- return Handler.onIsDir(vtable, parent_dir_task, path, is_absolute, buf);
- },
- else => {},
- }
-
- return .{ .err = this.errorWithPath(e, path) };
- },
- else => return .{ .err = this.errorWithPath(e, path) },
- }
- },
- }
- }
-
- fn errorWithPath(this: *ShellRmTask, err: Syscall.Error, path: [:0]const u8) Syscall.Error {
- _ = this;
- return err.withPath(bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory());
- }
-
- inline fn join(this: *ShellRmTask, alloc: Allocator, subdir_parts: []const []const u8, is_absolute: bool) [:0]const u8 {
- _ = this;
- if (!is_absolute) {
- // If relative paths enabled, stdlib join is preferred over
- // ResolvePath.joinBuf because it doesn't try to normalize the path
- return std.fs.path.joinZ(alloc, subdir_parts) catch bun.outOfMemory();
- }
-
- const out = alloc.dupeZ(u8, bun.path.join(subdir_parts, .auto)) catch bun.outOfMemory();
-
- return out;
- }
-
- pub fn workPoolCallback(task: *JSC.WorkPoolTask) void {
- var this: *ShellRmTask = @alignCast(@fieldParentPtr("task", task));
- this.root_task.runFromThreadPoolImpl();
- }
-
- pub fn runFromMainThread(this: *ShellRmTask) void {
- this.rm.onShellRmTaskDone(this);
- }
-
- pub fn runFromMainThreadMini(this: *ShellRmTask, _: *void) void {
- this.rm.onShellRmTaskDone(this);
- }
-
- pub fn deinit(this: *ShellRmTask) void {
- bun.default_allocator.destroy(this);
- }
- };
- };
-
- 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\n")) % 256),
- error.InvalidCharacter => return this.fail("exit: numeric argument required\n"),
- };
- this.bltn.done(exit_code);
- return Maybe(void).success;
- },
- else => {
- return this.fail("exit: too many arguments\n");
- },
- }
- }
-
- fn fail(this: *Exit, msg: string) Maybe(void) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .waiting_io;
- this.bltn.stderr.enqueue(this, msg, safeguard);
- 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 => @panic("Unexpected \"idle\" state in Exit. This indicates a bug in Bun. Please file a GitHub issue."),
- .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) {
- 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;
- }
- };
-
- pub const True = struct {
- bltn: *Builtin,
-
- pub fn start(this: *@This()) Maybe(void) {
- this.bltn.done(0);
- return Maybe(void).success;
- }
-
- pub fn onIOWriterChunk(_: *@This(), _: usize, _: ?JSC.SystemError) void {
- // no IO is done
- }
-
- pub fn deinit(this: *@This()) void {
- _ = this;
- }
- };
-
- pub const False = struct {
- bltn: *Builtin,
-
- pub fn start(this: *@This()) Maybe(void) {
- this.bltn.done(1);
- return Maybe(void).success;
- }
-
- pub fn onIOWriterChunk(_: *@This(), _: usize, _: ?JSC.SystemError) void {
- // no IO is done
- }
-
- pub fn deinit(this: *@This()) void {
- _ = this;
- }
- };
-
- pub const Yes = struct {
- bltn: *Builtin,
- state: enum { idle, waiting_io, err, done } = .idle,
- expletive: string = "y",
- task: YesTask = undefined,
-
- pub fn start(this: *@This()) Maybe(void) {
- const args = this.bltn.argsSlice();
-
- if (args.len > 0) {
- this.expletive = std.mem.sliceTo(args[0], 0);
- }
-
- if (this.bltn.stdout.needsIO()) |safeguard| {
- const evtloop = this.bltn.eventLoop();
- this.task = .{
- .evtloop = evtloop,
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop),
- };
- this.state = .waiting_io;
- this.bltn.stdout.enqueue(this, this.expletive, safeguard);
- this.bltn.stdout.enqueue(this, "\n", safeguard);
- this.task.enqueue();
- return Maybe(void).success;
- }
-
- var res: Maybe(usize) = undefined;
- while (true) {
- res = this.bltn.writeNoIO(.stdout, this.expletive);
- if (res == .err) {
- this.bltn.done(1);
- return Maybe(void).success;
- }
- res = this.bltn.writeNoIO(.stdout, "\n");
- if (res == .err) {
- this.bltn.done(1);
- return Maybe(void).success;
- }
- }
- @compileError(unreachable);
- }
-
- pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void {
- if (maybe_e) |e| {
- defer e.deref();
- this.state = .err;
- this.bltn.done(1);
- return;
- }
- }
-
- pub fn deinit(this: *@This()) void {
- _ = this;
- }
-
- pub const YesTask = struct {
- evtloop: JSC.EventLoopHandle,
- concurrent_task: JSC.EventLoopTask,
-
- pub fn enqueue(this: *@This()) void {
- if (this.evtloop == .js) {
- this.evtloop.js.tick();
- this.evtloop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
- } else {
- this.evtloop.mini.loop.tick();
- this.evtloop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
- }
- }
-
- pub fn runFromMainThread(this: *@This()) void {
- const yes: *Yes = @fieldParentPtr("task", this);
-
- // Manually make safeguard since this task should not be created if output does not need IO
- yes.bltn.stdout.enqueue(yes, yes.expletive, OutputNeedsIOSafeGuard{ .__i_know_what_i_am_doing_it_needs_io_yes = 0 });
- yes.bltn.stdout.enqueue(yes, "\n", OutputNeedsIOSafeGuard{ .__i_know_what_i_am_doing_it_needs_io_yes = 0 });
-
- this.enqueue();
- }
-
- pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
- this.runFromMainThread();
- }
- };
- };
-
- pub const Seq = struct {
- bltn: *Builtin,
- state: enum { idle, waiting_io, err, done } = .idle,
- buf: std.ArrayListUnmanaged(u8) = .{},
- _start: f32 = 1,
- _end: f32 = 1,
- increment: f32 = 1,
- separator: string = "\n",
- terminator: string = "",
- fixed_width: bool = false,
-
- pub fn start(this: *@This()) Maybe(void) {
- const args = this.bltn.argsSlice();
- var iter = bun.SliceIterator([*:0]const u8).init(args);
-
- if (args.len == 0) {
- return this.fail(Builtin.Kind.usageString(.seq));
- }
- while (iter.next()) |item| {
- const arg = bun.sliceTo(item, 0);
-
- if (std.mem.eql(u8, arg, "-s") or std.mem.eql(u8, arg, "--separator")) {
- this.separator = bun.sliceTo(iter.next() orelse return this.fail("seq: option requires an argument -- s\n"), 0);
- continue;
- }
- if (std.mem.startsWith(u8, arg, "-s")) {
- this.separator = arg[2..];
- continue;
- }
-
- if (std.mem.eql(u8, arg, "-t") or std.mem.eql(u8, arg, "--terminator")) {
- this.terminator = bun.sliceTo(iter.next() orelse return this.fail("seq: option requires an argument -- t\n"), 0);
- continue;
- }
- if (std.mem.startsWith(u8, arg, "-t")) {
- this.terminator = arg[2..];
- continue;
- }
-
- if (std.mem.eql(u8, arg, "-w") or std.mem.eql(u8, arg, "--fixed-width")) {
- this.fixed_width = true;
- continue;
- }
-
- iter.index -= 1;
- break;
- }
-
- const maybe1 = iter.next().?;
- const int1 = std.fmt.parseFloat(f32, bun.sliceTo(maybe1, 0)) catch return this.fail("seq: invalid argument\n");
- this._end = int1;
- if (this._start > this._end) this.increment = -1;
-
- const maybe2 = iter.next();
- if (maybe2 == null) return this.do();
- const int2 = std.fmt.parseFloat(f32, bun.sliceTo(maybe2.?, 0)) catch return this.fail("seq: invalid argument\n");
- this._start = int1;
- this._end = int2;
- if (this._start < this._end) this.increment = 1;
- if (this._start > this._end) this.increment = -1;
-
- const maybe3 = iter.next();
- if (maybe3 == null) return this.do();
- const int3 = std.fmt.parseFloat(f32, bun.sliceTo(maybe3.?, 0)) catch return this.fail("seq: invalid argument\n");
- this._start = int1;
- this.increment = int2;
- this._end = int3;
-
- if (this.increment == 0) return this.fail("seq: zero increment\n");
- if (this._start > this._end and this.increment > 0) return this.fail("seq: needs negative decrement\n");
- if (this._start < this._end and this.increment < 0) return this.fail("seq: needs positive increment\n");
-
- return this.do();
- }
-
- fn fail(this: *@This(), msg: string) Maybe(void) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .err;
- this.bltn.stderr.enqueue(this, msg, safeguard);
- return Maybe(void).success;
- }
- _ = this.bltn.writeNoIO(.stderr, msg);
- this.bltn.done(1);
- return Maybe(void).success;
- }
-
- fn do(this: *@This()) Maybe(void) {
- var current = this._start;
- var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
- defer arena.deinit();
-
- while (if (this.increment > 0) current <= this._end else current >= this._end) : (current += this.increment) {
- const str = std.fmt.allocPrint(arena.allocator(), "{d}", .{current}) catch bun.outOfMemory();
- defer _ = arena.reset(.retain_capacity);
- _ = this.print(str);
- _ = this.print(this.separator);
- }
- _ = this.print(this.terminator);
-
- this.state = .done;
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.bltn.stdout.enqueue(this, this.buf.items, safeguard);
- } else {
- this.bltn.done(0);
- }
- return Maybe(void).success;
- }
-
- fn print(this: *@This(), msg: string) Maybe(void) {
- if (this.bltn.stdout.needsIO() != null) {
- this.buf.appendSlice(bun.default_allocator, msg) catch bun.outOfMemory();
- return Maybe(void).success;
- }
- const res = this.bltn.writeNoIO(.stdout, msg);
- if (res == .err) return Maybe(void).initErr(res.err);
- return Maybe(void).success;
- }
-
- pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void {
- if (maybe_e) |e| {
- defer e.deref();
- this.state = .err;
- this.bltn.done(1);
- return;
- }
- if (this.state == .done) {
- this.bltn.done(0);
- }
- if (this.state == .err) {
- this.bltn.done(1);
- }
- }
-
- pub fn deinit(this: *@This()) void {
- this.buf.deinit(bun.default_allocator);
- //seq
- }
- };
-
- pub const Dirname = struct {
- bltn: *Builtin,
- state: enum { idle, waiting_io, err, done } = .idle,
- buf: std.ArrayListUnmanaged(u8) = .{},
-
- pub fn start(this: *@This()) Maybe(void) {
- const args = this.bltn.argsSlice();
- var iter = bun.SliceIterator([*:0]const u8).init(args);
-
- if (args.len == 0) return this.fail(Builtin.Kind.usageString(.dirname));
-
- while (iter.next()) |item| {
- const arg = bun.sliceTo(item, 0);
- _ = this.print(bun.path.dirname(arg, .posix));
- _ = this.print("\n");
- }
-
- this.state = .done;
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.bltn.stdout.enqueue(this, this.buf.items, safeguard);
- } else {
- this.bltn.done(0);
- }
- return Maybe(void).success;
- }
-
- pub fn deinit(this: *@This()) void {
- this.buf.deinit(bun.default_allocator);
- //dirname
- }
-
- fn fail(this: *@This(), msg: string) Maybe(void) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .err;
- this.bltn.stderr.enqueue(this, msg, safeguard);
- return Maybe(void).success;
- }
- _ = this.bltn.writeNoIO(.stderr, msg);
- this.bltn.done(1);
- return Maybe(void).success;
- }
-
- fn print(this: *@This(), msg: string) Maybe(void) {
- if (this.bltn.stdout.needsIO() != null) {
- this.buf.appendSlice(bun.default_allocator, msg) catch bun.outOfMemory();
- return Maybe(void).success;
- }
- const res = this.bltn.writeNoIO(.stdout, msg);
- if (res == .err) return Maybe(void).initErr(res.err);
- return Maybe(void).success;
- }
-
- pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void {
- if (maybe_e) |e| {
- defer e.deref();
- this.state = .err;
- this.bltn.done(1);
- return;
- }
- if (this.state == .done) {
- this.bltn.done(0);
- }
- if (this.state == .err) {
- this.bltn.done(1);
- }
- }
- };
-
- pub const Basename = struct {
- bltn: *Builtin,
- state: enum { idle, waiting_io, err, done } = .idle,
- buf: std.ArrayListUnmanaged(u8) = .{},
-
- pub fn start(this: *@This()) Maybe(void) {
- const args = this.bltn.argsSlice();
- var iter = bun.SliceIterator([*:0]const u8).init(args);
-
- if (args.len == 0) return this.fail(Builtin.Kind.usageString(.basename));
-
- while (iter.next()) |item| {
- const arg = bun.sliceTo(item, 0);
- _ = this.print(bun.path.basename(arg));
- _ = this.print("\n");
- }
-
- this.state = .done;
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.bltn.stdout.enqueue(this, this.buf.items, safeguard);
- } else {
- this.bltn.done(0);
- }
- return Maybe(void).success;
- }
-
- pub fn deinit(this: *@This()) void {
- this.buf.deinit(bun.default_allocator);
- //basename
- }
-
- fn fail(this: *@This(), msg: string) Maybe(void) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .err;
- this.bltn.stderr.enqueue(this, msg, safeguard);
- return Maybe(void).success;
- }
- _ = this.bltn.writeNoIO(.stderr, msg);
- this.bltn.done(1);
- return Maybe(void).success;
- }
-
- fn print(this: *@This(), msg: string) Maybe(void) {
- if (this.bltn.stdout.needsIO() != null) {
- this.buf.appendSlice(bun.default_allocator, msg) catch bun.outOfMemory();
- return Maybe(void).success;
- }
- const res = this.bltn.writeNoIO(.stdout, msg);
- if (res == .err) return Maybe(void).initErr(res.err);
- return Maybe(void).success;
- }
-
- pub fn onIOWriterChunk(this: *@This(), _: usize, maybe_e: ?JSC.SystemError) void {
- if (maybe_e) |e| {
- defer e.deref();
- this.state = .err;
- this.bltn.done(1);
- return;
- }
- if (this.state == .done) {
- this.bltn.done(0);
- }
- if (this.state == .err) {
- this.bltn.done(1);
- }
- }
- };
-
- pub const Cp = struct {
- bltn: *Builtin,
- opts: Opts = .{},
- state: union(enum) {
- idle,
- exec: struct {
- target_path: [:0]const u8,
- paths_to_copy: []const [*:0]const u8,
- started: bool = false,
- /// this is thread safe as it is only incremented
- /// and decremented on the main thread by this struct
- tasks_count: u32 = 0,
- output_waiting: u32 = 0,
- output_done: u32 = 0,
- err: ?bun.shell.ShellErr = null,
-
- ebusy: if (bun.Environment.isWindows) EbusyState else struct {} = .{},
- },
- ebusy: struct {
- state: EbusyState,
- idx: usize = 0,
- main_exit_code: ExitCode = 0,
- },
- waiting_write_err,
- done,
- } = .idle,
-
- pub fn format(this: *const Cp, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
- try writer.print("Cp(0x{x})", .{@intFromPtr(this)});
- }
-
- /// On Windows it is possible to get an EBUSY error very simply
- /// by running the following command:
- ///
- /// `cp myfile.txt myfile.txt mydir/`
- ///
- /// Bearing in mind that the shell cp implementation creates a
- /// ShellCpTask for each source file, it's possible for one of the
- /// tasks to get EBUSY while trying to access the source file or the
- /// destination file.
- ///
- /// But it's fine to ignore the EBUSY error since at
- /// least one of them will succeed anyway.
- ///
- /// We handle this _after_ all the tasks have been
- /// executed, to avoid complicated synchronization on multiple
- /// threads, because the precise src or dest for each argument is
- /// not known until its corresponding ShellCpTask is executed by the
- /// threadpool.
- const EbusyState = struct {
- tasks: std.ArrayListUnmanaged(*ShellCpTask) = .{},
- absolute_targets: bun.StringArrayHashMapUnmanaged(void) = .{},
- absolute_srcs: bun.StringArrayHashMapUnmanaged(void) = .{},
-
- pub fn deinit(this: *EbusyState) void {
- // The tasks themselves are freed in `ignoreEbusyErrorIfPossible()`
- this.tasks.deinit(bun.default_allocator);
- for (this.absolute_targets.keys()) |tgt| {
- bun.default_allocator.free(tgt);
- }
- this.absolute_targets.deinit(bun.default_allocator);
- for (this.absolute_srcs.keys()) |tgt| {
- bun.default_allocator.free(tgt);
- }
- this.absolute_srcs.deinit(bun.default_allocator);
- }
- };
-
- pub fn start(this: *Cp) Maybe(void) {
- const maybe_filepath_args = switch (this.opts.parse(this.bltn.argsSlice())) {
- .ok => |args| args,
- .err => |e| {
- const buf = switch (e) {
- .illegal_option => |opt_str| this.bltn.fmtErrorArena(.cp, "illegal option -- {s}\n", .{opt_str}),
- .show_usage => Builtin.Kind.cp.usageString(),
- .unsupported => |unsupported| this.bltn.fmtErrorArena(.cp, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
- };
-
- _ = this.writeFailingError(buf, 1);
- return Maybe(void).success;
- },
- };
-
- if (maybe_filepath_args == null or maybe_filepath_args.?.len <= 1) {
- _ = this.writeFailingError(Builtin.Kind.cp.usageString(), 1);
- return Maybe(void).success;
- }
-
- const args = maybe_filepath_args orelse unreachable;
- const paths_to_copy = args[0 .. args.len - 1];
- const tgt_path = std.mem.span(args[args.len - 1]);
-
- this.state = .{ .exec = .{
- .target_path = tgt_path,
- .paths_to_copy = paths_to_copy,
- } };
-
- this.next();
-
- return Maybe(void).success;
- }
-
- pub fn ignoreEbusyErrorIfPossible(this: *Cp) void {
- if (!bun.Environment.isWindows) @compileError("dont call this plz");
-
- if (this.state.ebusy.idx < this.state.ebusy.state.tasks.items.len) {
- outer_loop: for (this.state.ebusy.state.tasks.items[this.state.ebusy.idx..], 0..) |task_, i| {
- const task: *ShellCpTask = task_;
- const failure_src = task.src_absolute.?;
- const failure_tgt = task.tgt_absolute.?;
- if (this.state.ebusy.state.absolute_targets.get(failure_tgt)) |_| {
- task.deinit();
- continue :outer_loop;
- }
- if (this.state.ebusy.state.absolute_srcs.get(failure_src)) |_| {
- task.deinit();
- continue :outer_loop;
- }
- this.state.ebusy.idx += i + 1;
- this.printShellCpTask(task);
- return;
- }
- }
-
- this.state.ebusy.state.deinit();
- const exit_code = this.state.ebusy.main_exit_code;
- this.state = .done;
- this.bltn.done(exit_code);
- }
-
- pub fn next(this: *Cp) void {
- while (this.state != .done) {
- switch (this.state) {
- .idle => @panic("Invalid state for \"Cp\": idle, this indicates a bug in Bun. Please file a GitHub issue"),
- .exec => {
- var exec = &this.state.exec;
- if (exec.started) {
- if (this.state.exec.tasks_count <= 0 and this.state.exec.output_done >= this.state.exec.output_waiting) {
- const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0;
- if (this.state.exec.err != null) {
- this.state.exec.err.?.deinit(bun.default_allocator);
- }
- if (comptime bun.Environment.isWindows) {
- if (exec.ebusy.tasks.items.len > 0) {
- this.state = .{ .ebusy = .{ .state = this.state.exec.ebusy, .main_exit_code = exit_code } };
- continue;
- }
- exec.ebusy.deinit();
- }
- this.state = .done;
- this.bltn.done(exit_code);
- return;
- }
- return;
- }
-
- exec.started = true;
- exec.tasks_count = @intCast(exec.paths_to_copy.len);
-
- const cwd_path = this.bltn.parentCmd().base.shell.cwdZ();
-
- // Launch a task for each argument
- for (exec.paths_to_copy) |path_raw| {
- const path = std.mem.span(path_raw);
- const cp_task = ShellCpTask.create(this, this.bltn.eventLoop(), this.opts, 1 + exec.paths_to_copy.len, path, exec.target_path, cwd_path);
- cp_task.schedule();
- }
- return;
- },
- .ebusy => {
- if (comptime bun.Environment.isWindows) {
- this.ignoreEbusyErrorIfPossible();
- return;
- } else @panic("Should only be called on Windows");
- },
- .waiting_write_err => return,
- .done => unreachable,
- }
- }
-
- this.bltn.done(0);
- }
-
- pub fn deinit(cp: *Cp) void {
- assert(cp.state == .done or cp.state == .waiting_write_err);
- }
-
- pub fn writeFailingError(this: *Cp, buf: []const u8, exit_code: ExitCode) Maybe(void) {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state = .waiting_write_err;
- this.bltn.stderr.enqueue(this, buf, safeguard);
- return Maybe(void).success;
- }
-
- _ = this.bltn.writeNoIO(.stderr, buf);
-
- this.bltn.done(exit_code);
- return Maybe(void).success;
- }
-
- pub fn onIOWriterChunk(this: *Cp, _: usize, e: ?JSC.SystemError) void {
- if (e) |err| err.deref();
- if (this.state == .waiting_write_err) {
- return this.bltn.done(1);
- }
- this.state.exec.output_done += 1;
- this.next();
- }
-
- pub fn onShellCpTaskDone(this: *Cp, task: *ShellCpTask) void {
- assert(this.state == .exec);
- log("task done: 0x{x} {d}", .{ @intFromPtr(task), this.state.exec.tasks_count });
- this.state.exec.tasks_count -= 1;
-
- const err_ = task.err;
-
- if (comptime bun.Environment.isWindows) {
- if (err_) |err| {
- if (err == .sys and
- err.sys.getErrno() == .BUSY and
- (task.tgt_absolute != null and
- err.sys.path.eqlUTF8(task.tgt_absolute.?)) or
- (task.src_absolute != null and
- err.sys.path.eqlUTF8(task.src_absolute.?)))
- {
- log("{} got ebusy {d} {d}", .{ this, this.state.exec.ebusy.tasks.items.len, this.state.exec.paths_to_copy.len });
- this.state.exec.ebusy.tasks.append(bun.default_allocator, task) catch bun.outOfMemory();
- this.next();
- return;
- }
- } else {
- const tgt_absolute = task.tgt_absolute;
- task.tgt_absolute = null;
- if (tgt_absolute) |tgt| this.state.exec.ebusy.absolute_targets.put(bun.default_allocator, tgt, {}) catch bun.outOfMemory();
- const src_absolute = task.src_absolute;
- task.src_absolute = null;
- if (src_absolute) |tgt| this.state.exec.ebusy.absolute_srcs.put(bun.default_allocator, tgt, {}) catch bun.outOfMemory();
- }
- }
-
- this.printShellCpTask(task);
- }
-
- pub fn printShellCpTask(this: *Cp, task: *ShellCpTask) void {
- // Deinitialize this task as we are starting a new one
- defer task.deinit();
-
- const err_ = task.err;
- var output = task.takeOutput();
-
- const output_task: *ShellCpOutputTask = bun.new(ShellCpOutputTask, .{
- .parent = this,
- .output = .{ .arrlist = output.moveToUnmanaged() },
- .state = .waiting_write_err,
- });
- if (err_) |err| {
- this.state.exec.err = err;
- const error_string = this.bltn.taskErrorToString(.cp, err);
- output_task.start(error_string);
- return;
- }
- output_task.start(null);
- }
-
- pub const ShellCpOutputTask = OutputTask(Cp, .{
- .writeErr = ShellCpOutputTaskVTable.writeErr,
- .onWriteErr = ShellCpOutputTaskVTable.onWriteErr,
- .writeOut = ShellCpOutputTaskVTable.writeOut,
- .onWriteOut = ShellCpOutputTaskVTable.onWriteOut,
- .onDone = ShellCpOutputTaskVTable.onDone,
- });
-
- const ShellCpOutputTaskVTable = struct {
- pub fn writeErr(this: *Cp, childptr: anytype, errbuf: []const u8) CoroutineResult {
- if (this.bltn.stderr.needsIO()) |safeguard| {
- this.state.exec.output_waiting += 1;
- this.bltn.stderr.enqueue(childptr, errbuf, safeguard);
- return .yield;
- }
- _ = this.bltn.writeNoIO(.stderr, errbuf);
- return .cont;
- }
-
- pub fn onWriteErr(this: *Cp) void {
- this.state.exec.output_done += 1;
- }
-
- pub fn writeOut(this: *Cp, childptr: anytype, output: *OutputSrc) CoroutineResult {
- if (this.bltn.stdout.needsIO()) |safeguard| {
- this.state.exec.output_waiting += 1;
- this.bltn.stdout.enqueue(childptr, output.slice(), safeguard);
- return .yield;
- }
- _ = this.bltn.writeNoIO(.stdout, output.slice());
- return .cont;
- }
-
- pub fn onWriteOut(this: *Cp) void {
- this.state.exec.output_done += 1;
- }
-
- pub fn onDone(this: *Cp) void {
- this.next();
- }
- };
-
- pub const ShellCpTask = struct {
- cp: *Cp,
-
- opts: Opts,
- operands: usize = 0,
- src: [:0]const u8,
- tgt: [:0]const u8,
- src_absolute: ?[:0]const u8 = null,
- tgt_absolute: ?[:0]const u8 = null,
- cwd_path: [:0]const u8,
- verbose_output_lock: bun.Mutex = .{},
- verbose_output: ArrayList(u8) = ArrayList(u8).init(bun.default_allocator),
-
- task: JSC.WorkPoolTask = .{ .callback = &runFromThreadPool },
- event_loop: JSC.EventLoopHandle,
- concurrent_task: JSC.EventLoopTask,
- err: ?bun.shell.ShellErr = null,
-
- const debug = bun.Output.scoped(.ShellCpTask, false);
-
- fn deinit(this: *ShellCpTask) void {
- debug("deinit", .{});
- this.verbose_output.deinit();
- if (this.err) |e| {
- e.deinit(bun.default_allocator);
- }
- if (this.src_absolute) |sc| {
- bun.default_allocator.free(sc);
- }
- if (this.tgt_absolute) |tc| {
- bun.default_allocator.free(tc);
- }
- bun.destroy(this);
- }
-
- pub fn schedule(this: *@This()) void {
- debug("schedule", .{});
- WorkPool.schedule(&this.task);
- }
-
- pub fn create(
- cp: *Cp,
- evtloop: JSC.EventLoopHandle,
- opts: Opts,
- operands: usize,
- src: [:0]const u8,
- tgt: [:0]const u8,
- cwd_path: [:0]const u8,
- ) *ShellCpTask {
- return bun.new(ShellCpTask, ShellCpTask{
- .cp = cp,
- .operands = operands,
- .opts = opts,
- .src = src,
- .tgt = tgt,
- .cwd_path = cwd_path,
- .event_loop = evtloop,
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop),
- });
- }
-
- fn takeOutput(this: *ShellCpTask) ArrayList(u8) {
- const out = this.verbose_output;
- this.verbose_output = ArrayList(u8).init(bun.default_allocator);
- return out;
- }
-
- pub fn ensureDest(nodefs: *JSC.Node.NodeFS, dest: bun.OSPathSliceZ) Maybe(void) {
- return switch (nodefs.mkdirRecursiveOSPath(dest, JSC.Node.Arguments.Mkdir.DefaultMode, false)) {
- .err => |err| Maybe(void){ .err = err },
- .result => Maybe(void).success,
- };
- }
-
- pub fn hasTrailingSep(path: [:0]const u8) bool {
- if (path.len == 0) return false;
- return ResolvePath.Platform.auto.isSeparator(path[path.len - 1]);
- }
-
- const Kind = enum {
- file,
- dir,
- };
-
- pub fn isDir(_: *ShellCpTask, path: [:0]const u8) Maybe(bool) {
- if (bun.Environment.isWindows) {
- const attributes = bun.sys.getFileAttributes(path[0..path.len]) orelse {
- const err: Syscall.Error = .{
- .errno = @intFromEnum(bun.C.SystemErrno.ENOENT),
- .syscall = .copyfile,
- .path = path,
- };
- return .{ .err = err };
- };
-
- return .{ .result = attributes.is_directory };
- }
- const stat = switch (Syscall.lstat(path)) {
- .result => |x| x,
- .err => |e| {
- return .{ .err = e };
- },
- };
- return .{ .result = bun.S.ISDIR(stat.mode) };
- }
-
- fn enqueueToEventLoop(this: *ShellCpTask) void {
- if (this.event_loop == .js) {
- this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
- } else {
- this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
- }
- }
-
- pub fn runFromMainThread(this: *ShellCpTask) void {
- debug("runFromMainThread", .{});
- this.cp.onShellCpTaskDone(this);
- }
-
- pub fn runFromMainThreadMini(this: *ShellCpTask, _: *void) void {
- this.runFromMainThread();
- }
-
- pub fn runFromThreadPool(task: *WorkPoolTask) void {
- debug("runFromThreadPool", .{});
- var this: *@This() = @fieldParentPtr("task", task);
- if (this.runFromThreadPoolImpl()) |e| {
- this.err = e;
- this.enqueueToEventLoop();
- return;
- }
- }
-
- fn runFromThreadPoolImpl(this: *ShellCpTask) ?bun.shell.ShellErr {
- var buf2: bun.PathBuffer = undefined;
- var buf3: bun.PathBuffer = undefined;
- // We have to give an absolute path to our cp
- // implementation for it to work with cwd
- const src: [:0]const u8 = brk: {
- if (ResolvePath.Platform.auto.isAbsolute(this.src)) break :brk this.src;
- const parts: []const []const u8 = &.{
- this.cwd_path[0..],
- this.src[0..],
- };
- break :brk ResolvePath.joinZ(parts, .auto);
- };
- var tgt: [:0]const u8 = brk: {
- if (ResolvePath.Platform.auto.isAbsolute(this.tgt)) break :brk this.tgt;
- const parts: []const []const u8 = &.{
- this.cwd_path[0..],
- this.tgt[0..],
- };
- break :brk ResolvePath.joinZBuf(buf2[0..bun.MAX_PATH_BYTES], parts, .auto);
- };
-
- // Cases:
- // SRC DEST
- // ----------------
- // file -> file
- // file -> folder
- // folder -> folder
- // ----------------
- // We need to check dest to see what it is
- // If it doesn't exist we need to create it
- const src_is_dir = switch (this.isDir(src)) {
- .result => |x| x,
- .err => |e| return bun.shell.ShellErr.newSys(e),
- };
-
- // Any source directory without -R is an error
- if (src_is_dir and !this.opts.recursive) {
- const errmsg = std.fmt.allocPrint(bun.default_allocator, "{s} is a directory (not copied)", .{this.src}) catch bun.outOfMemory();
- return .{ .custom = errmsg };
- }
-
- if (!src_is_dir and bun.strings.eql(src, tgt)) {
- const errmsg = std.fmt.allocPrint(bun.default_allocator, "{s} and {s} are identical (not copied)", .{ this.src, this.src }) catch bun.outOfMemory();
- return .{ .custom = errmsg };
- }
-
- const tgt_is_dir: bool, const tgt_exists: bool = switch (this.isDir(tgt)) {
- .result => |is_dir| .{ is_dir, true },
- .err => |e| brk: {
- if (e.getErrno() == bun.C.E.NOENT) {
- // If it has a trailing directory separator, its a directory
- const is_dir = hasTrailingSep(tgt);
- break :brk .{ is_dir, false };
- }
- return bun.shell.ShellErr.newSys(e);
- },
- };
-
- var copying_many = false;
-
- // Note:
- // The following logic is based on the POSIX spec:
- // https://man7.org/linux/man-pages/man1/cp.1p.html
-
- // Handle the "1st synopsis": source_file -> target_file
- if (!src_is_dir and !tgt_is_dir and this.operands == 2) {
- // Don't need to do anything here
- }
- // Handle the "2nd synopsis": -R source_files... -> target
- else if (this.opts.recursive) {
- if (tgt_exists) {
- const basename = ResolvePath.basename(src[0..src.len]);
- const parts: []const []const u8 = &.{
- tgt[0..tgt.len],
- basename,
- };
- tgt = ResolvePath.joinZBuf(buf3[0..bun.MAX_PATH_BYTES], parts, .auto);
- } else if (this.operands == 2) {
- // source_dir -> new_target_dir
- } else {
- const errmsg = std.fmt.allocPrint(bun.default_allocator, "directory {s} does not exist", .{this.tgt}) catch bun.outOfMemory();
- return .{ .custom = errmsg };
- }
- copying_many = true;
- }
- // Handle the "3rd synopsis": source_files... -> target
- else {
- if (src_is_dir) return .{ .custom = std.fmt.allocPrint(bun.default_allocator, "{s} is a directory (not copied)", .{this.src}) catch bun.outOfMemory() };
- if (!tgt_exists or !tgt_is_dir) return .{ .custom = std.fmt.allocPrint(bun.default_allocator, "{s} is not a directory", .{this.tgt}) catch bun.outOfMemory() };
- const basename = ResolvePath.basename(src[0..src.len]);
- const parts: []const []const u8 = &.{
- tgt[0..tgt.len],
- basename,
- };
- tgt = ResolvePath.joinZBuf(buf3[0..bun.MAX_PATH_BYTES], parts, .auto);
- copying_many = true;
- }
-
- this.src_absolute = bun.default_allocator.dupeZ(u8, src[0..src.len]) catch bun.outOfMemory();
- this.tgt_absolute = bun.default_allocator.dupeZ(u8, tgt[0..tgt.len]) catch bun.outOfMemory();
-
- const args = JSC.Node.Arguments.Cp{
- .src = JSC.Node.PathLike{ .string = bun.PathString.init(this.src_absolute.?) },
- .dest = JSC.Node.PathLike{ .string = bun.PathString.init(this.tgt_absolute.?) },
- .flags = .{
- .mode = @enumFromInt(0),
- .recursive = this.opts.recursive,
- .force = true,
- .errorOnExist = false,
- .deinit_paths = false,
- },
- };
-
- debug("Scheduling {s} -> {s}", .{ this.src_absolute.?, this.tgt_absolute.? });
- if (this.event_loop == .js) {
- const vm: *JSC.VirtualMachine = this.event_loop.js.getVmImpl();
- debug("Yoops", .{});
- _ = JSC.Node.ShellAsyncCpTask.createWithShellTask(
- vm.global,
- args,
- vm,
- bun.ArenaAllocator.init(bun.default_allocator),
- this,
- false,
- );
- } else {
- _ = JSC.Node.ShellAsyncCpTask.createMini(
- args,
- this.event_loop.mini,
- bun.ArenaAllocator.init(bun.default_allocator),
- this,
- );
- }
-
- return null;
- }
-
- fn onSubtaskFinish(this: *ShellCpTask, err: Maybe(void)) void {
- debug("onSubtaskFinish", .{});
- if (err.asErr()) |e| {
- this.err = bun.shell.ShellErr.newSys(e);
- }
- this.enqueueToEventLoop();
- }
-
- pub fn onCopyImpl(this: *ShellCpTask, src: [:0]const u8, dest: [:0]const u8) void {
- this.verbose_output_lock.lock();
- log("onCopy: {s} -> {s}\n", .{ src, dest });
- defer this.verbose_output_lock.unlock();
- var writer = this.verbose_output.writer();
- writer.print("{s} -> {s}\n", .{ src, dest }) catch bun.outOfMemory();
- }
-
- pub fn cpOnCopy(this: *ShellCpTask, src_: anytype, dest_: anytype) void {
- if (!this.opts.verbose) return;
- if (comptime bun.Environment.isPosix) return this.onCopyImpl(src_, dest_);
-
- var buf: bun.PathBuffer = undefined;
- var buf2: bun.PathBuffer = undefined;
- const src: [:0]const u8 = switch (@TypeOf(src_)) {
- [:0]const u8, [:0]u8 => src_,
- [:0]const u16, [:0]u16 => bun.strings.fromWPath(buf[0..], src_),
- else => @compileError("Invalid type: " ++ @typeName(@TypeOf(src_))),
- };
- const dest: [:0]const u8 = switch (@TypeOf(dest_)) {
- [:0]const u8, [:0]u8 => src_,
- [:0]const u16, [:0]u16 => bun.strings.fromWPath(buf2[0..], dest_),
- else => @compileError("Invalid type: " ++ @typeName(@TypeOf(dest_))),
- };
- this.onCopyImpl(src, dest);
- }
-
- pub fn cpOnFinish(this: *ShellCpTask, result: Maybe(void)) void {
- this.onSubtaskFinish(result);
- }
- };
-
- const Opts = packed struct {
- /// -f
- ///
- /// If the destination file cannot be opened, remove it and create a
- /// new file, without prompting for confirmation regardless of its
- /// permissions. (The -f option overrides any previous -n option.) The
- /// target file is not unlinked before the copy. Thus, any existing access
- /// rights will be retained.
- remove_and_create_new_file_if_not_found: bool = false,
-
- /// -H
- ///
- /// Take actions based on the type and contents of the file
- /// referenced by any symbolic link specified as a
- /// source_file operand.
- dereference_command_line_symlinks: bool = false,
-
- /// -i
- ///
- /// Write a prompt to standard error before copying to any
- /// existing non-directory destination file. If the
- /// response from the standard input is affirmative, the
- /// copy shall be attempted; otherwise, it shall not.
- interactive: bool = false,
-
- /// -L
- ///
- /// Take actions based on the type and contents of the file
- /// referenced by any symbolic link specified as a
- /// source_file operand or any symbolic links encountered
- /// during traversal of a file hierarchy.
- dereference_all_symlinks: bool = false,
-
- /// -P
- ///
- /// Take actions on any symbolic link specified as a
- /// source_file operand or any symbolic link encountered
- /// during traversal of a file hierarchy.
- preserve_symlinks: bool = false,
-
- /// -p
- ///
- /// Duplicate the following characteristics of each source
- /// file in the corresponding destination file:
- /// 1. The time of last data modification and time of last
- /// access.
- /// 2. The user ID and group ID.
- /// 3. The file permission bits and the S_ISUID and
- /// S_ISGID bits.
- preserve_file_attributes: bool = false,
-
- /// -R
- ///
- /// Copy file hierarchies.
- recursive: bool = false,
-
- /// -v
- ///
- /// Cause cp to be verbose, showing files as they are copied.
- verbose: bool = false,
-
- /// -n
- ///
- /// Do not overwrite an existing file. (The -n option overrides any previous -f or -i options.)
- overwrite_existing_file: bool = true,
-
- const Parse = FlagParser(*@This());
-
- pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) {
- return Parse.parseFlags(opts, args);
- }
-
- pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult {
- _ = this;
- _ = flag;
- return null;
- }
-
- fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
- switch (char) {
- 'f' => {
- return .{ .unsupported = unsupportedFlag("-f") };
- },
- 'H' => {
- return .{ .unsupported = unsupportedFlag("-H") };
- },
- 'i' => {
- return .{ .unsupported = unsupportedFlag("-i") };
- },
- 'L' => {
- return .{ .unsupported = unsupportedFlag("-L") };
- },
- 'P' => {
- return .{ .unsupported = unsupportedFlag("-P") };
- },
- 'p' => {
- return .{ .unsupported = unsupportedFlag("-P") };
- },
- 'R' => {
- this.recursive = true;
- return .continue_parsing;
- },
- 'v' => {
- this.verbose = true;
- return .continue_parsing;
- },
- 'n' => {
- this.overwrite_existing_file = true;
- this.remove_and_create_new_file_if_not_found = false;
- return .continue_parsing;
- },
- else => {
- return .{ .illegal_option = smallflags[i..] };
- },
- }
-
- return null;
- }
- };
- };
- };
+ pub const Builtin = @import("./Builtin.zig");
/// This type is reference counted, but deinitialization is queued onto the event loop
pub const IOReader = struct {
@@ -11323,35 +4894,6 @@ pub const Interpreter = struct {
pub const Readers = SmolList(ChildPtr, 4);
};
- pub const AsyncDeinitWriter = struct {
- ran: bool = false,
-
- pub fn enqueue(this: *@This()) void {
- if (this.ran) return;
- this.ran = true;
-
- var iowriter = this.writer();
-
- if (iowriter.evtloop == .js) {
- iowriter.evtloop.js.enqueueTaskConcurrent(iowriter.concurrent_task.js.from(this, .manual_deinit));
- } else {
- iowriter.evtloop.mini.enqueueTaskConcurrent(iowriter.concurrent_task.mini.from(this, "runFromMainThreadMini"));
- }
- }
-
- pub fn writer(this: *@This()) *IOWriter {
- return @alignCast(@fieldParentPtr("async_deinit", this));
- }
-
- pub fn runFromMainThread(this: *@This()) void {
- this.writer().__deinit();
- }
-
- pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
- this.runFromMainThread();
- }
- };
-
pub const AsyncDeinitReader = struct {
ran: bool = false,
@@ -11381,482 +4923,34 @@ pub const Interpreter = struct {
}
};
- pub const IOWriter = struct {
- writer: WriterImpl = if (bun.Environment.isWindows) .{} else .{
- .close_fd = false,
- },
- fd: bun.FileDescriptor,
- writers: Writers = .{ .inlined = .{} },
- buf: std.ArrayListUnmanaged(u8) = .{},
- /// quick hack to get windows working
- /// ideally this should be removed
- winbuf: if (bun.Environment.isWindows) std.ArrayListUnmanaged(u8) else u0 = if (bun.Environment.isWindows) .{} else 0,
- __idx: usize = 0,
- total_bytes_written: usize = 0,
- ref_count: u32 = 1,
- err: ?JSC.SystemError = null,
- evtloop: JSC.EventLoopHandle,
- concurrent_task: JSC.EventLoopTask,
- is_writing: if (bun.Environment.isWindows) bool else u0 = if (bun.Environment.isWindows) false else 0,
- async_deinit: AsyncDeinitWriter = .{},
- started: bool = false,
- flags: InitFlags = .{},
+ pub const IOWriter = @import("./IOWriter.zig");
- const debug = bun.Output.scoped(.IOWriter, true);
+ pub const AsyncDeinitWriter = struct {
+ ran: bool = false,
- const ChildPtr = IOWriterChildPtr;
+ pub fn enqueue(this: *@This()) void {
+ if (this.ran) return;
+ this.ran = true;
- /// ~128kb
- /// We shrunk the `buf` when we reach the last writer,
- /// but if this never happens, we shrink `buf` when it exceeds this threshold
- const SHRINK_THRESHOLD = 1024 * 128;
+ var iowriter = this.writer();
- pub const auto_poll = false;
-
- pub usingnamespace bun.NewRefCounted(@This(), asyncDeinit, "IOWriterRefCount");
- const This = @This();
- pub const WriterImpl = bun.io.BufferedWriter(
- This,
- onWrite,
- onError,
- onClose,
- getBuffer,
- null,
- );
- pub const Poll = WriterImpl;
-
- pub fn __onClose(_: *This) void {}
- pub fn __flush(_: *This) void {}
-
- pub fn refSelf(this: *This) *This {
- this.ref();
- return this;
- }
-
- pub const InitFlags = packed struct(u8) {
- pollable: bool = false,
- nonblocking: bool = false,
- is_socket: bool = false,
- __unused: u5 = 0,
- };
-
- pub fn init(fd: bun.FileDescriptor, flags: InitFlags, evtloop: JSC.EventLoopHandle) *This {
- const this = IOWriter.new(.{
- .fd = fd,
- .evtloop = evtloop,
- .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop),
- });
-
- this.writer.parent = this;
- this.flags = flags;
-
- debug("IOWriter(0x{x}, fd={}) init flags={any}", .{ @intFromPtr(this), fd, flags });
-
- return this;
- }
-
- pub fn __start(this: *This) Maybe(void) {
- debug("IOWriter(0x{x}, fd={}) __start()", .{ @intFromPtr(this), this.fd });
- if (this.writer.start(this.fd, this.flags.pollable).asErr()) |e_| {
- const e: bun.sys.Error = e_;
- if (bun.Environment.isPosix) {
- // We get this if we pass in a file descriptor that is not
- // pollable, for example a special character device like
- // /dev/null. If so, restart with polling disabled.
- //
- // It's also possible on Linux for EINVAL to be returned
- // when registering multiple writable/readable polls for the
- // same file descriptor. The shell code here makes sure to
- // _not_ run into that case, but it is possible.
- if (e.getErrno() == .INVAL) {
- debug("IOWriter(0x{x}, fd={}) got EINVAL", .{ @intFromPtr(this), this.fd });
- this.flags.pollable = false;
- this.flags.nonblocking = false;
- this.flags.is_socket = false;
- this.writer.handle = .{ .closed = {} };
- return __start(this);
- }
-
- if (bun.Environment.isLinux) {
- // On linux regular files are not pollable and return EPERM,
- // so restart if that's the case with polling disabled.
- if (e.getErrno() == .PERM) {
- this.flags.pollable = false;
- this.flags.nonblocking = false;
- this.flags.is_socket = false;
- this.writer.handle = .{ .closed = {} };
- return __start(this);
- }
- }
- }
-
- if (bun.Environment.isWindows) {
- // This might happen if the file descriptor points to NUL.
- // On Windows GetFileType(NUL) returns FILE_TYPE_CHAR, so
- // `this.writer.start()` will try to open it as a tty with
- // uv_tty_init, but this returns EBADF. As a workaround,
- // we'll try opening the file descriptor as a file.
- if (e.getErrno() == .BADF) {
- this.flags.pollable = false;
- this.flags.nonblocking = false;
- this.flags.is_socket = false;
- return this.writer.startWithFile(this.fd);
- }
- }
- return .{ .err = e };
- }
- if (comptime bun.Environment.isPosix) {
- if (this.flags.nonblocking) {
- this.writer.getPoll().?.flags.insert(.nonblocking);
- }
-
- if (this.flags.is_socket) {
- this.writer.getPoll().?.flags.insert(.socket);
- } else if (this.flags.pollable) {
- this.writer.getPoll().?.flags.insert(.fifo);
- }
- }
-
- return Maybe(void).success;
- }
-
- pub fn eventLoop(this: *This) JSC.EventLoopHandle {
- return this.evtloop;
- }
-
- /// Idempotent write call
- pub fn write(this: *This) void {
- if (!this.started) {
- log("IOWriter(0x{x}, fd={}) starting", .{ @intFromPtr(this), this.fd });
- if (this.__start().asErr()) |e| {
- this.onError(e);
- return;
- }
- this.started = true;
- if (comptime bun.Environment.isPosix) {
- if (this.writer.handle == .fd) {} else return;
- } else return;
- }
- if (bun.Environment.isWindows) {
- log("IOWriter(0x{x}, fd={}) write() is_writing={any}", .{ @intFromPtr(this), this.fd, this.is_writing });
- if (this.is_writing) return;
- this.is_writing = true;
- if (this.writer.startWithCurrentPipe().asErr()) |e| {
- this.onError(e);
- return;
- }
- return;
- }
-
- if (this.writer.handle == .poll) {
- if (!this.writer.handle.poll.isWatching()) {
- log("IOWriter(0x{x}, fd={}) calling this.writer.write()", .{ @intFromPtr(this), this.fd });
- this.writer.write();
- } else log("IOWriter(0x{x}, fd={}) poll already watching", .{ @intFromPtr(this), this.fd });
+ if (iowriter.evtloop == .js) {
+ iowriter.evtloop.js.enqueueTaskConcurrent(iowriter.concurrent_task.js.from(this, .manual_deinit));
} else {
- log("IOWriter(0x{x}, fd={}) no poll, calling write", .{ @intFromPtr(this), this.fd });
- this.writer.write();
+ iowriter.evtloop.mini.enqueueTaskConcurrent(iowriter.concurrent_task.mini.from(this, "runFromMainThreadMini"));
}
}
- /// Cancel the chunks enqueued by the given writer by
- /// marking them as dead
- pub fn cancelChunks(this: *This, ptr_: anytype) void {
- const ptr = switch (@TypeOf(ptr_)) {
- ChildPtr => ptr_,
- else => ChildPtr.init(ptr_),
- };
- if (this.writers.len() == 0) return;
- const idx = this.__idx;
- const slice: []Writer = this.writers.sliceMutable();
- if (idx >= slice.len) return;
- for (slice[idx..]) |*w| {
- if (w.ptr.ptr.repr._ptr == ptr.ptr.repr._ptr) {
- w.setDead();
- }
- }
+ pub fn writer(this: *@This()) *IOWriter {
+ return @alignCast(@fieldParentPtr("async_deinit", this));
}
- const Writer = struct {
- ptr: ChildPtr,
- len: usize,
- written: usize = 0,
- bytelist: ?*bun.ByteList = null,
-
- pub fn rawPtr(this: Writer) ?*anyopaque {
- return this.ptr.ptr.ptr();
- }
-
- pub fn isDead(this: Writer) bool {
- return this.ptr.ptr.isNull();
- }
-
- pub fn setDead(this: *Writer) void {
- this.ptr.ptr = ChildPtr.ChildPtrRaw.Null;
- }
- };
-
- pub const Writers = SmolList(Writer, 2);
-
- /// Skips over dead children and increments `total_bytes_written` by the
- /// amount they would have written so the buf is skipped as well
- pub fn skipDead(this: *This) void {
- const slice = this.writers.slice();
- for (slice[this.__idx..]) |*w| {
- if (w.isDead()) {
- this.__idx += 1;
- this.total_bytes_written += w.len - w.written;
- continue;
- }
- return;
- }
- return;
+ pub fn runFromMainThread(this: *@This()) void {
+ this.writer().__deinit();
}
- pub fn onWrite(this: *This, amount: usize, status: bun.io.WriteStatus) void {
- this.setWriting(false);
- debug("IOWriter(0x{x}, fd={}) onWrite({d}, {})", .{ @intFromPtr(this), this.fd, amount, status });
- if (this.__idx >= this.writers.len()) return;
- const child = this.writers.get(this.__idx);
- if (child.isDead()) {
- this.bump(child);
- } else {
- if (child.bytelist) |bl| {
- const written_slice = this.buf.items[this.total_bytes_written .. this.total_bytes_written + amount];
- bl.append(bun.default_allocator, written_slice) catch bun.outOfMemory();
- }
- this.total_bytes_written += amount;
- child.written += amount;
- if (status == .end_of_file) {
- const not_fully_written = !this.isLastIdx(this.__idx) or child.written < child.len;
- if (bun.Environment.allow_assert and not_fully_written) {
- bun.Output.debugWarn("IOWriter(0x{x}, fd={}) received done without fully writing data, check that onError is thrown", .{ @intFromPtr(this), this.fd });
- }
- return;
- }
-
- if (child.written >= child.len) {
- this.bump(child);
- }
- }
-
- const wrote_everything: bool = this.total_bytes_written >= this.buf.items.len;
-
- log("IOWriter(0x{x}, fd={}) wrote_everything={}, idx={d} writers={d} next_len={d}", .{ @intFromPtr(this), this.fd, wrote_everything, this.__idx, this.writers.len(), if (this.writers.len() >= 1) this.writers.get(0).len else 0 });
- if (!wrote_everything and this.__idx < this.writers.len()) {
- debug("IOWriter(0x{x}, fd={}) poll again", .{ @intFromPtr(this), this.fd });
- if (comptime bun.Environment.isWindows) {
- this.setWriting(true);
- this.writer.write();
- } else {
- if (this.writer.handle == .poll)
- this.writer.registerPoll()
- else
- this.writer.write();
- }
- }
- }
-
- pub fn onClose(this: *This) void {
- this.setWriting(false);
- }
-
- pub fn onError(this: *This, err__: bun.sys.Error) void {
- this.setWriting(false);
- const ee = err__.toShellSystemError();
- this.err = ee;
- log("IOWriter(0x{x}, fd={}) onError errno={s} errmsg={} errsyscall={}", .{ @intFromPtr(this), this.fd, @tagName(ee.getErrno()), ee.message, ee.syscall });
- var seen_alloc = std.heap.stackFallback(@sizeOf(usize) * 64, bun.default_allocator);
- var seen = std.ArrayList(usize).initCapacity(seen_alloc.get(), 64) catch bun.outOfMemory();
- defer seen.deinit();
- writer_loop: for (this.writers.slice()) |w| {
- if (w.isDead()) continue;
- const ptr = w.ptr.ptr.ptr();
- if (seen.items.len < 8) {
- for (seen.items[0..]) |item| {
- if (item == @intFromPtr(ptr)) {
- continue :writer_loop;
- }
- }
- } else if (std.mem.indexOfScalar(usize, seen.items[0..], @intFromPtr(ptr)) != null) {
- continue :writer_loop;
- }
-
- w.ptr.onWriteChunk(0, this.err);
- seen.append(@intFromPtr(ptr)) catch bun.outOfMemory();
- }
- }
-
- pub fn getBuffer(this: *This) []const u8 {
- const result = this.getBufferImpl();
- if (comptime bun.Environment.isWindows) {
- this.winbuf.clearRetainingCapacity();
- this.winbuf.appendSlice(bun.default_allocator, result) catch bun.outOfMemory();
- return this.winbuf.items;
- }
- log("IOWriter(0x{x}, fd={}) getBuffer = {d} bytes", .{ @intFromPtr(this), this.fd, result.len });
- return result;
- }
-
- fn getBufferImpl(this: *This) []const u8 {
- const writer = brk: {
- if (this.__idx >= this.writers.len()) {
- log("IOWriter(0x{x}, fd={}) getBufferImpl all writes done", .{ @intFromPtr(this), this.fd });
- return "";
- }
- log("IOWriter(0x{x}, fd={}) getBufferImpl idx={d} writer_len={d}", .{ @intFromPtr(this), this.fd, this.__idx, this.writers.len() });
- var writer = this.writers.get(this.__idx);
- if (!writer.isDead()) break :brk writer;
- log("IOWriter(0x{x}, fd={}) skipping dead", .{ @intFromPtr(this), this.fd });
- this.skipDead();
- if (this.__idx >= this.writers.len()) {
- log("IOWriter(0x{x}, fd={}) getBufferImpl all writes done", .{ @intFromPtr(this), this.fd });
- return "";
- }
- writer = this.writers.get(this.__idx);
- break :brk writer;
- };
- log("IOWriter(0x{x}, fd={}) getBufferImpl writer_len={} writer_written={}", .{ @intFromPtr(this), this.fd, writer.len, writer.written });
- const remaining = writer.len - writer.written;
- if (bun.Environment.allow_assert) {
- assert(!(writer.len == writer.written));
- }
- return this.buf.items[this.total_bytes_written .. this.total_bytes_written + remaining];
- }
-
- pub fn bump(this: *This, current_writer: *Writer) void {
- log("IOWriter(0x{x}, fd={}) bump(0x{x} {s})", .{ @intFromPtr(this), this.fd, @intFromPtr(current_writer), @tagName(current_writer.ptr.ptr.tag()) });
-
- const is_dead = current_writer.isDead();
- const written = current_writer.written;
- const child_ptr = current_writer.ptr;
-
- defer {
- if (!is_dead) child_ptr.onWriteChunk(written, null);
- }
-
- if (is_dead) {
- this.skipDead();
- } else {
- if (bun.Environment.allow_assert) {
- if (!is_dead) assert(current_writer.written == current_writer.len);
- }
- this.__idx += 1;
- }
-
- if (this.__idx >= this.writers.len()) {
- log("IOWriter(0x{x}, fd={}) all writers complete: truncating", .{ @intFromPtr(this), this.fd });
- this.buf.clearRetainingCapacity();
- this.__idx = 0;
- this.writers.clearRetainingCapacity();
- this.total_bytes_written = 0;
- return;
- }
-
- if (this.total_bytes_written >= SHRINK_THRESHOLD) {
- const slice = this.buf.items[this.total_bytes_written..];
- const remaining_len = slice.len;
- log("IOWriter(0x{x}, fd={}) exceeded shrink threshold: truncating (new_len={d}, writer_starting_idx={d})", .{ @intFromPtr(this), this.fd, remaining_len, this.__idx });
- if (slice.len == 0) {
- this.buf.clearRetainingCapacity();
- this.total_bytes_written = 0;
- } else {
- bun.copy(u8, this.buf.items[0..remaining_len], slice);
- this.buf.items.len = remaining_len;
- this.total_bytes_written = 0;
- }
- this.writers.truncate(this.__idx);
- this.__idx = 0;
- if (bun.Environment.allow_assert) {
- if (this.writers.len() > 0) {
- const first = this.writers.getConst(this.__idx);
- assert(this.buf.items.len >= first.len);
- }
- }
- }
- }
-
- pub fn enqueue(this: *This, ptr: anytype, bytelist: ?*bun.ByteList, buf: []const u8) void {
- const childptr = if (@TypeOf(ptr) == ChildPtr) ptr else ChildPtr.init(ptr);
- if (buf.len == 0) {
- log("IOWriter(0x{x}, fd={}) enqueue EMPTY", .{ @intFromPtr(this), this.fd });
- childptr.onWriteChunk(0, null);
- return;
- }
- const writer: Writer = .{
- .ptr = childptr,
- .len = buf.len,
- .bytelist = bytelist,
- };
- log("IOWriter(0x{x}, fd={}) enqueue(0x{x} {s}, buf_len={d}, buf={s}, writer_len={d})", .{ @intFromPtr(this), this.fd, @intFromPtr(writer.rawPtr()), @tagName(writer.ptr.ptr.tag()), buf.len, buf[0..@min(128, buf.len)], this.writers.len() + 1 });
- this.buf.appendSlice(bun.default_allocator, buf) catch bun.outOfMemory();
- this.writers.append(writer);
- this.write();
- }
-
- pub fn enqueueFmtBltn(
- this: *This,
- ptr: anytype,
- bytelist: ?*bun.ByteList,
- comptime kind: ?Interpreter.Builtin.Kind,
- comptime fmt_: []const u8,
- args: anytype,
- ) void {
- const cmd_str = comptime if (kind) |k| @tagName(k) ++ ": " else "";
- const fmt__ = cmd_str ++ fmt_;
- this.enqueueFmt(ptr, bytelist, fmt__, args);
- }
-
- pub fn enqueueFmt(
- this: *This,
- ptr: anytype,
- bytelist: ?*bun.ByteList,
- comptime fmt: []const u8,
- args: anytype,
- ) void {
- var buf_writer = this.buf.writer(bun.default_allocator);
- const start = this.buf.items.len;
- buf_writer.print(fmt, args) catch bun.outOfMemory();
- const end = this.buf.items.len;
- const writer: Writer = .{
- .ptr = if (@TypeOf(ptr) == ChildPtr) ptr else ChildPtr.init(ptr),
- .len = end - start,
- .bytelist = bytelist,
- };
- log("IOWriter(0x{x}, fd={}) enqueue(0x{x} {s}, {s})", .{ @intFromPtr(this), this.fd, @intFromPtr(writer.rawPtr()), @tagName(writer.ptr.ptr.tag()), this.buf.items[start..end] });
- this.writers.append(writer);
- this.write();
- }
-
- pub fn asyncDeinit(this: *@This()) void {
- debug("IOWriter(0x{x}, fd={}) asyncDeinit", .{ @intFromPtr(this), this.fd });
- this.async_deinit.enqueue();
- }
-
- pub fn __deinit(this: *This) void {
- debug("IOWriter(0x{x}, fd={}) deinit", .{ @intFromPtr(this), this.fd });
- if (bun.Environment.allow_assert) assert(this.ref_count == 0);
- this.buf.deinit(bun.default_allocator);
- if (comptime bun.Environment.isPosix) {
- if (this.writer.handle == .poll and this.writer.handle.poll.isRegistered()) {
- this.writer.handle.closeImpl(null, {}, false);
- }
- } else this.winbuf.deinit(bun.default_allocator);
- if (this.fd != bun.invalid_fd) _ = bun.sys.close(this.fd);
- this.writer.disableKeepingProcessAlive(this.evtloop);
- this.destroy();
- }
-
- pub fn isLastIdx(this: *This, idx: usize) bool {
- return idx == this.writers.len() -| 1;
- }
-
- /// Only does things on windows
- pub inline fn setWriting(this: *This, writing: bool) void {
- if (bun.Environment.isWindows) {
- log("IOWriter(0x{x}, fd={}) setWriting({any})", .{ @intFromPtr(this), this.fd, writing });
- this.is_writing = writing;
- }
+ pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
+ this.runFromMainThread();
}
};
};
@@ -12078,21 +5172,10 @@ pub fn ShellTask(
};
}
-// pub const Builtin =
-
inline fn errnocast(errno: anytype) u16 {
return @intCast(errno);
}
-inline fn fastMod(val: anytype, comptime rhs: comptime_int) @TypeOf(val) {
- const Value = @typeInfo(@TypeOf(val));
- if (Value != .int) @compileError("LHS of fastMod should be an int");
- if (Value.int.signedness != .unsigned) @compileError("LHS of fastMod should be unsigned");
- if (!comptime std.math.isPowerOfTwo(rhs)) @compileError("RHS of fastMod should be power of 2");
-
- return val & (rhs - 1);
-}
-
/// 'js' event loop will always return JSError
/// 'mini' event loop will always return noreturn and exit 1
fn throwShellErr(e: *const bun.shell.ShellErr, event_loop: JSC.EventLoopHandle) bun.JSError!noreturn {
@@ -12181,10 +5264,10 @@ pub const IOWriterChildPtr = struct {
/// - Any function that returns a file descriptor will return a uv file descriptor
/// - Sometimes windows doesn't have `*at()` functions like `rmdirat` so we have to join the directory path with the target path
/// - Converts Posix absolute paths to Windows absolute paths on Windows
-const ShellSyscall = struct {
+pub const ShellSyscall = struct {
pub const unlinkatWithFlags = Syscall.unlinkatWithFlags;
pub const rmdirat = Syscall.rmdirat;
- fn getPath(dirfd: anytype, to: [:0]const u8, buf: *bun.PathBuffer) Maybe([:0]const u8) {
+ pub fn getPath(dirfd: anytype, to: [:0]const u8, buf: *bun.PathBuffer) Maybe([:0]const u8) {
if (bun.Environment.isPosix) @compileError("Don't use this");
if (bun.strings.eqlComptime(to[0..to.len], "/dev/null")) {
return .{ .result = shell.WINDOWS_DEV_NULL };
@@ -12222,7 +5305,7 @@ const ShellSyscall = struct {
return .{ .result = joined };
}
- fn statat(dir: bun.FileDescriptor, path_: [:0]const u8) Maybe(bun.Stat) {
+ pub fn statat(dir: bun.FileDescriptor, path_: [:0]const u8) Maybe(bun.Stat) {
if (bun.Environment.isWindows) {
var buf: bun.PathBuffer = undefined;
const path = switch (getPath(dir, path_, &buf)) {
@@ -12239,7 +5322,7 @@ const ShellSyscall = struct {
return Syscall.fstatat(dir, path_);
}
- fn openat(dir: bun.FileDescriptor, path: [:0]const u8, flags: i32, perm: bun.Mode) Maybe(bun.FileDescriptor) {
+ pub fn openat(dir: bun.FileDescriptor, path: [:0]const u8, flags: i32, perm: bun.Mode) Maybe(bun.FileDescriptor) {
if (bun.Environment.isWindows) {
if (flags & bun.O.DIRECTORY != 0) {
if (ResolvePath.Platform.posix.isAbsolute(path[0..path.len])) {
@@ -12464,7 +5547,7 @@ pub fn FlagParser(comptime Opts: type) type {
return .{ .err = .show_usage };
}
- fn parseFlag(opts: Opts, flag: []const u8) ParseFlagResult {
+ pub fn parseFlag(opts: Opts, flag: []const u8) ParseFlagResult {
if (flag.len == 0) return .done;
if (flag[0] != '-') return .done;
diff --git a/src/shell/shell.zig b/src/shell/shell.zig
index 83edd3691b..bb4728adfd 100644
--- a/src/shell/shell.zig
+++ b/src/shell/shell.zig
@@ -308,8 +308,7 @@ pub const GlobalMini = struct {
};
}
- pub inline fn actuallyThrow(this: @This(), shellerr: ShellErr) void {
- _ = this; // autofix
+ pub inline fn actuallyThrow(_: @This(), shellerr: ShellErr) void {
shellerr.throwMini();
}
diff --git a/src/shell/subproc.zig b/src/shell/subproc.zig
index cec72799ae..31ba6b14c1 100644
--- a/src/shell/subproc.zig
+++ b/src/shell/subproc.zig
@@ -12,7 +12,7 @@ const Allocator = std.mem.Allocator;
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
-const Which = @import("../which.zig");
+const Which = bun.which;
const Async = bun.Async;
// const IPC = @import("../bun.js/ipc.zig");
const uws = bun.uws;