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;