diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index b8f5913827..5bf82aeecb 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -3296,7 +3296,7 @@ pub fn getShellConstructor( globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject, ) callconv(.C) JSC.JSValue { - return JSC.API.Shell.eval.Interpreter.getConstructor(globalThis); + return JSC.API.Shell.Interpreter.getConstructor(globalThis); } pub fn getGlobConstructor( diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index 5bc4d2aefb..0a7b52c153 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -26,7 +26,7 @@ pub const Classes = struct { pub const ExpectArrayContaining = JSC.Expect.ExpectArrayContaining; pub const FileSystemRouter = JSC.API.FileSystemRouter; pub const Glob = JSC.API.Glob; - pub const ShellInterpreter = JSC.API.Shell.eval.Interpreter; + pub const ShellInterpreter = JSC.API.Shell.Interpreter; pub const Bundler = JSC.API.JSBundler; pub const JSBundler = Bundler; pub const Transpiler = JSC.API.JSTranspiler; diff --git a/src/bun_js.zig b/src/bun_js.zig index db26dd0a7c..ec76be444d 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -136,7 +136,7 @@ pub const Run = struct { try bundle.runEnvLoader(false); const mini = JSC.MiniEventLoop.initGlobal(bundle.env); mini.top_level_dir = ctx.args.absolute_working_dir orelse ""; - return try bun.shell.Interpreter.initAndRunFromFile(mini, entry_path); + return bun.shell.Interpreter.initAndRunFromFile(ctx, mini, entry_path); } pub fn boot(ctx_: Command.Context, entry_path: string) !void { diff --git a/src/cli.zig b/src/cli.zig index 2548619b13..dac1d633a0 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1851,10 +1851,10 @@ pub const Command = struct { try HelpCommand.exec(allocator); }, .ExecCommand => { - const ctx = try Command.Context.create(allocator, log, .RunCommand); + var ctx = try Command.Context.create(allocator, log, .RunCommand); if (ctx.positionals.len > 1) { - try ExecCommand.exec(ctx); + try ExecCommand.exec(&ctx); } else Tag.printHelp(.ExecCommand, true); }, } diff --git a/src/cli/exec_command.zig b/src/cli/exec_command.zig index 055af45338..2ed2a11148 100644 --- a/src/cli/exec_command.zig +++ b/src/cli/exec_command.zig @@ -13,7 +13,7 @@ const open = @import("../open.zig"); const Command = bun.CLI.Command; pub const ExecCommand = struct { - pub fn exec(ctx: Command.Context) !void { + pub fn exec(ctx: *Command.Context) !void { const script = ctx.positionals[1]; // this is a hack: make dummy bundler so we can use its `.runEnvLoader()` function to populate environment variables probably should split out the functionality var bundle = try bun.Bundler.init( @@ -39,7 +39,7 @@ pub const ExecCommand = struct { }; const script_path = bun.path.join(parts, .auto); - const code = bun.shell.Interpreter.initAndRunFromSource(mini, script_path, script) catch |err| { + const code = bun.shell.Interpreter.initAndRunFromSource(ctx, mini, script_path, script) catch |err| { Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ script_path, @errorName(err) }); Global.exit(1); }; diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index cde73d15ce..6d0b302909 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -266,6 +266,7 @@ pub const RunCommand = struct { const log = Output.scoped(.RUN, false); fn runPackageScriptForeground( + ctx: *Command.Context, allocator: std.mem.Allocator, original_script: string, name: string, @@ -316,7 +317,7 @@ pub const RunCommand = struct { } const mini = bun.JSC.MiniEventLoop.initGlobal(env); - const code = bun.shell.Interpreter.initAndRunFromSource(mini, name, combined_script) catch |err| { + const code = bun.shell.Interpreter.initAndRunFromSource(ctx, mini, name, combined_script) catch |err| { if (!silent) { Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); } @@ -1450,6 +1451,7 @@ pub const RunCommand = struct { if (scripts.get(temp_script_buffer[1..])) |prescript| { if (!try runPackageScriptForeground( + &ctx, ctx.allocator, prescript, temp_script_buffer[1..], @@ -1464,6 +1466,7 @@ pub const RunCommand = struct { } if (!try runPackageScriptForeground( + &ctx, ctx.allocator, script_content, script_name_to_search, @@ -1478,6 +1481,7 @@ pub const RunCommand = struct { if (scripts.get(temp_script_buffer)) |postscript| { if (!try runPackageScriptForeground( + &ctx, ctx.allocator, postscript, temp_script_buffer, diff --git a/src/io/PipeWriter.zig b/src/io/PipeWriter.zig index c4c039c38f..ba5a502c3e 100644 --- a/src/io/PipeWriter.zig +++ b/src/io/PipeWriter.zig @@ -5,7 +5,7 @@ const JSC = bun.JSC; const uv = bun.windows.libuv; const Source = @import("./source.zig").Source; -const log = bun.Output.scoped(.PipeWriter, false); +const log = bun.Output.scoped(.PipeWriter, true); const FileType = @import("./pipes.zig").FileType; pub const WriteResult = union(enum) { diff --git a/src/io/source.zig b/src/io/source.zig index 7fd8c44ff7..41789428b3 100644 --- a/src/io/source.zig +++ b/src/io/source.zig @@ -2,7 +2,7 @@ const std = @import("std"); const bun = @import("root").bun; const uv = bun.windows.libuv; -const log = bun.Output.scoped(.PipeSource, false); +const log = bun.Output.scoped(.PipeSource, true); pub const Source = union(enum) { pipe: *Pipe, diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 08c647ee8b..4a7bb77f1a 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -34,8 +34,6 @@ const Syscall = @import("../sys.zig"); const Glob = @import("../glob.zig"); const ResolvePath = @import("../resolver/resolve_path.zig"); const DirIterator = @import("../bun.js/node/dir_iterator.zig"); -const CodepointIterator = @import("../string_immutable.zig").PackedCodepointIterator; -const isAllAscii = @import("../string_immutable.zig").isAllASCII; const TaggedPointerUnion = @import("../tagged_pointer.zig").TaggedPointerUnion; const TaggedPointer = @import("../tagged_pointer.zig").TaggedPointer; pub const WorkPoolTask = @import("../work_pool.zig").Task; @@ -43,6 +41,7 @@ pub const WorkPool = @import("../work_pool.zig").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"); @@ -65,7 +64,7 @@ pub fn OOM(e: anyerror) noreturn { @panic("Out of memory"); } -const log = bun.Output.scoped(.SHELL, false); +const log = bun.Output.scoped(.SHELL, true); pub fn assert(cond: bool, comptime msg: []const u8) void { if (bun.Environment.allow_assert) { @@ -158,7 +157,7 @@ const CowFd = struct { refcount: u32 = 1, being_used: bool = false, - const print = bun.Output.scoped(.CowFd, false); + const print = bun.Output.scoped(.CowFd, true); pub fn init(fd: bun.FileDescriptor) *CowFd { const this = bun.default_allocator.create(CowFd) catch bun.outOfMemory(); @@ -612,6 +611,7 @@ pub const EnvMap = struct { /// 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 { + command_ctx: *const bun.CLI.Command.Context, event_loop: JSC.EventLoopHandle, /// This is the arena used to allocate the input shell script's AST nodes, /// tokens, and a string pool used to store all strings. @@ -634,6 +634,7 @@ pub const Interpreter = struct { has_pending_activity: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), started: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + vm_args_utf8: std.ArrayList(JSC.ZigString.Slice), async_commands_executing: u32 = 0, flags: packed struct(u8) { @@ -1004,6 +1005,7 @@ pub const Interpreter = struct { script_heap.* = script_ast; const interpreter = switch (ThisInterpreter.init( + undefined, // command_ctx, unused when event_loop is .js .{ .js = globalThis.bunVM().event_loop }, allocator, &arena, @@ -1055,16 +1057,13 @@ pub const Interpreter = struct { /// If all initialization allocations succeed, the arena will be copied /// into the interpreter struct, so it is not a stale reference and safe to call `arena.deinit()` on error. pub fn init( + ctx: *const bun.CLI.Command.Context, event_loop: JSC.EventLoopHandle, allocator: Allocator, arena: *bun.ArenaAllocator, script: *ast.Script, jsobjs: []JSValue, ) shell.Result(*ThisInterpreter) { - var interpreter = allocator.create(ThisInterpreter) catch bun.outOfMemory(); - interpreter.event_loop = event_loop; - interpreter.allocator = allocator; - const export_env = brk: { // This will be set in the shell builtin to `process.env` if (event_loop == .js) break :brk EnvMap.init(allocator); @@ -1120,7 +1119,9 @@ pub const Interpreter = struct { const stdin_reader = IOReader.init(stdin_fd, event_loop); + const interpreter = allocator.create(ThisInterpreter) catch bun.outOfMemory(); interpreter.* = .{ + .command_ctx = ctx, .event_loop = event_loop, .script = script, @@ -1150,12 +1151,14 @@ pub const Interpreter = struct { .stdout = .pipe, .stderr = .pipe, }, + + .vm_args_utf8 = std.ArrayList(JSC.ZigString.Slice).init(bun.default_allocator), }; return .{ .result = interpreter }; } - pub fn initAndRunFromFile(mini: *JSC.MiniEventLoop, path: []const u8) !bun.shell.ExitCode { + pub fn initAndRunFromFile(ctx: *const bun.CLI.Command.Context, mini: *JSC.MiniEventLoop, path: []const u8) !bun.shell.ExitCode { var arena = bun.ArenaAllocator.init(bun.default_allocator); const src = src: { var file = try std.fs.cwd().openFile(path, .{}); @@ -1192,7 +1195,7 @@ pub const Interpreter = struct { }; const script_heap = try arena.allocator().create(ast.Script); script_heap.* = script; - var interp = switch (ThisInterpreter.init(.{ .mini = mini }, bun.default_allocator, &arena, script_heap, jsobjs)) { + var interp = switch (ThisInterpreter.init(ctx, .{ .mini = mini }, bun.default_allocator, &arena, script_heap, jsobjs)) { .err => |*e| { throwShellErr(e, .{ .mini = mini }); return 1; @@ -1229,7 +1232,7 @@ pub const Interpreter = struct { return code; } - pub fn initAndRunFromSource(mini: *JSC.MiniEventLoop, path_for_errors: []const u8, src: []const u8) !ExitCode { + pub fn initAndRunFromSource(ctx: *bun.CLI.Command.Context, mini: *JSC.MiniEventLoop, path_for_errors: []const u8, src: []const u8) !ExitCode { bun.Analytics.Features.standalone_shell += 1; var arena = bun.ArenaAllocator.init(bun.default_allocator); defer arena.deinit(); @@ -1255,7 +1258,7 @@ pub const Interpreter = struct { }; const script_heap = try arena.allocator().create(ast.Script); script_heap.* = script; - var interp: *ThisInterpreter = switch (ThisInterpreter.init(.{ .mini = mini }, bun.default_allocator, &arena, script_heap, jsobjs)) { + var interp: *ThisInterpreter = switch (ThisInterpreter.init(ctx, .{ .mini = mini }, bun.default_allocator, &arena, script_heap, jsobjs)) { .err => |*e| { throwShellErr(e, .{ .mini = mini }); return 1; @@ -1455,6 +1458,10 @@ pub const Interpreter = struct { this.resolve.deinit(); this.reject.deinit(); this.root_shell.deinitImpl(false, true); + for (this.vm_args_utf8.items[0..]) |str| { + str.deinit(); + } + this.vm_args_utf8.deinit(); this.allocator.destroy(this); } @@ -1611,6 +1618,16 @@ pub const Interpreter = struct { return &this.root_io; } + fn getVmArgsUtf8(this: *Interpreter, argv: []const *WTFStringImplStruct, idx: u8) []const u8 { + if (this.vm_args_utf8.items.len != argv.len) { + this.vm_args_utf8.ensureTotalCapacity(argv.len) catch bun.outOfMemory(); + for (argv) |arg| { + this.vm_args_utf8.append(arg.toUTF8(bun.default_allocator)) catch bun.outOfMemory(); + } + } + return this.vm_args_utf8.items[idx].slice(); + } + const AssignCtx = enum { cmd, shell, @@ -2132,6 +2149,9 @@ pub const Interpreter = struct { .Var => |label| { str_list.appendSlice(this.expandVar(label).slice()) catch bun.outOfMemory(); }, + .VarArgv => |int| { + str_list.appendSlice(this.expandVarArgv(int)) catch bun.outOfMemory(); + }, .asterisk => { str_list.append('*') catch bun.outOfMemory(); }, @@ -2184,6 +2204,38 @@ pub const Interpreter = struct { return value; } + fn expandVarArgv(this: *const Expansion, original_int: u8) []const u8 { + var int = original_int; + switch (this.base.interpreter.event_loop) { + .js => |js| { + if (int == 0) return bun.selfExePath() catch ""; + int -= 1; + + const vm = js.virtual_machine; + if (vm.main.len > 0) { + if (int == 0) return vm.main; + int -= 1; + } + + if (vm.worker) |worker| { + if (worker.argv) |argv| { + if (int >= argv.len) return ""; + return this.base.interpreter.getVmArgsUtf8(argv, int); + } + } + const argv = vm.argv; + if (int >= argv.len) return ""; + return argv[int]; + }, + .mini => { + const ctx = this.base.interpreter.command_ctx; + if (int >= 1 + ctx.passthrough.len) return ""; + if (int == 0) return ctx.positionals[ctx.positionals.len - 1 - int]; + return ctx.passthrough[int - 1]; + }, + } + } + fn currentWord(this: *Expansion) *const ast.SimpleAtom { return switch (this.node) { .simple => &this.node.simple, @@ -2214,6 +2266,7 @@ pub const Interpreter = struct { return switch (simple.*) { .Text => |txt| txt.len, .Var => |label| this.expandVar(label).len, + .VarArgv => |int| this.expandVarArgv(int).len, .brace_begin, .brace_end, .comma, .asterisk => 1, .double_asterisk => 2, .cmd_subst => |subst| { @@ -2242,7 +2295,7 @@ pub const Interpreter = struct { } pub const ShellGlobTask = struct { - const print = bun.Output.scoped(.ShellGlobTask, false); + const print = bun.Output.scoped(.ShellGlobTask, true); task: WorkPoolTask = .{ .callback = &runFromThreadPool }, @@ -5247,7 +5300,7 @@ pub const Interpreter = struct { } pub const Cat = struct { - const print = bun.Output.scoped(.ShellCat, false); + const print = bun.Output.scoped(.ShellCat, true); bltn: *Builtin, opts: Opts = .{}, @@ -5783,7 +5836,7 @@ pub const Interpreter = struct { try writer.print("ShellTouchTask(0x{x}, filepath={s})", .{ @intFromPtr(this), this.filepath }); } - const print = bun.Output.scoped(.ShellTouchTask, false); + const print = bun.Output.scoped(.ShellTouchTask, true); pub fn deinit(this: *ShellTouchTask) void { if (this.err) |e| { @@ -6167,7 +6220,7 @@ pub const Interpreter = struct { event_loop: JSC.EventLoopHandle, concurrent_task: JSC.EventLoopTask, - const print = bun.Output.scoped(.ShellMkdirTask, false); + const print = bun.Output.scoped(.ShellMkdirTask, true); fn takeOutput(this: *ShellMkdirTask) ArrayList(u8) { const out = this.created_directories; @@ -7042,7 +7095,7 @@ pub const Interpreter = struct { }; pub const ShellLsTask = struct { - const print = bun.Output.scoped(.ShellLsTask, false); + const print = bun.Output.scoped(.ShellLsTask, true); ls: *Ls, opts: Opts, @@ -7675,14 +7728,14 @@ pub const Interpreter = struct { } = .idle, pub const ShellMvCheckTargetTask = struct { - const print = bun.Output.scoped(.MvCheckTargetTask, false); + const print = bun.Output.scoped(.MvCheckTargetTask, true); mv: *Mv, cwd: bun.FileDescriptor, target: [:0]const u8, result: ?Maybe(?bun.FileDescriptor) = null, - task: shell.eval.ShellTask(@This(), runFromThreadPool, runFromMainThread, print), + task: ShellTask(@This(), runFromThreadPool, runFromMainThread, print), pub fn runFromThreadPool(this: *@This()) void { const fd = switch (ShellSyscall.openat(this.cwd, this.target, os.O.RDONLY | os.O.DIRECTORY, 0)) { @@ -7713,7 +7766,7 @@ pub const Interpreter = struct { pub const ShellMvBatchedTask = struct { const BATCH_SIZE = 5; - const print = bun.Output.scoped(.MvBatchedTask, false); + const print = bun.Output.scoped(.MvBatchedTask, true); mv: *Mv, sources: []const [*:0]const u8, @@ -7724,7 +7777,7 @@ pub const Interpreter = struct { err: ?Syscall.Error = null, - task: shell.eval.ShellTask(@This(), runFromThreadPool, runFromMainThread, print), + task: ShellTask(@This(), runFromThreadPool, runFromMainThread, print), event_loop: JSC.EventLoopHandle, pub fn runFromThreadPool(this: *@This()) void { @@ -8620,7 +8673,7 @@ pub const Interpreter = struct { } pub const ShellRmTask = struct { - const print = bun.Output.scoped(.AsyncRmTask, false); + const print = bun.Output.scoped(.AsyncRmTask, true); rm: *Rm, opts: Opts, @@ -9745,7 +9798,7 @@ pub const Interpreter = struct { pub const DEBUG_REFCOUNT_NAME: []const u8 = "IOWriterRefCount"; - const print = bun.Output.scoped(.IOWriter, false); + const print = bun.Output.scoped(.IOWriter, true); const ChildPtr = IOWriterChildPtr; diff --git a/src/shell/shell.zig b/src/shell/shell.zig index 1b8ef7b955..5a814dadb5 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -18,14 +18,14 @@ const CodepointIterator = @import("../string_immutable.zig").PackedCodepointIter const isAllAscii = @import("../string_immutable.zig").isAllASCII; const TaggedPointerUnion = @import("../tagged_pointer.zig").TaggedPointerUnion; -pub const eval = @import("./interpreter.zig"); pub const interpret = @import("./interpreter.zig"); pub const subproc = @import("./subproc.zig"); pub const EnvMap = interpret.EnvMap; pub const EnvStr = interpret.EnvStr; -pub const Interpreter = eval.Interpreter; +pub const Interpreter = interpret.Interpreter; pub const Subprocess = subproc.ShellSubprocess; +pub const ExitCode = interpret.ExitCode; pub const IOWriter = Interpreter.IOWriter; pub const IOReader = Interpreter.IOReader; // pub const IOWriter = interpret.IOWriter; @@ -108,23 +108,20 @@ pub const ShellErr = union(enum) { pub fn throwMini(this: @This()) void { defer this.deinit(bun.default_allocator); switch (this) { - .sys => { - const err = this.sys; - const str = std.fmt.allocPrint(bun.default_allocator, "bunsh: {s}: {}", .{ err.message, err.path }) catch bun.outOfMemory(); - bun.Output.prettyErrorln("error: Failed to due to error {s}", .{str}); + .sys => |err| { + bun.Output.prettyErrorln("error: Failed to due to error: bunsh: {s}: {}", .{ err.message, err.path }); bun.Global.exit(1); }, - .custom => { - bun.Output.prettyErrorln("error: Failed to due to error {s}", .{this.custom}); + .custom => |custom| { + bun.Output.prettyErrorln("error: Failed to due to error: {s}", .{custom}); bun.Global.exit(1); }, - .invalid_arguments => { - const str = std.fmt.allocPrint(bun.default_allocator, "bunsh: invalid arguments: {s}", .{this.invalid_arguments.val}) catch bun.outOfMemory(); - bun.Output.prettyErrorln("error: Failed to due to error {s}", .{str}); + .invalid_arguments => |invalid_arguments| { + bun.Output.prettyErrorln("error: Failed to due to error: bunsh: invalid arguments: {s}", .{invalid_arguments.val}); bun.Global.exit(1); }, - .todo => { - bun.Output.prettyErrorln("error: Failed to due to error TODO: {s}", .{this.todo}); + .todo => |todo| { + bun.Output.prettyErrorln("error: Failed to due to error: TODO: {s}", .{todo}); bun.Global.exit(1); }, } @@ -177,8 +174,8 @@ fn setEnv(name: [*:0]const u8, value: [*:0]const u8) void { /// [1] => write end pub const Pipe = [2]bun.FileDescriptor; -const log = bun.Output.scoped(.SHELL, false); -const logsys = bun.Output.scoped(.SYS, false); +const log = bun.Output.scoped(.SHELL, true); +const logsys = bun.Output.scoped(.SYS, true); pub const GlobalJS = struct { globalThis: *JSC.JSGlobalObject, @@ -822,7 +819,7 @@ pub const AST = struct { pub fn is_compound(self: *const Atom) bool { switch (self.*) { .compound => return true, - else => return false, + .simple => return false, } } @@ -847,6 +844,7 @@ pub const AST = struct { pub const SimpleAtom = union(enum) { Var: []const u8, + VarArgv: u8, Text: []const u8, asterisk, double_asterisk, @@ -860,15 +858,15 @@ pub const AST = struct { pub fn glob_hint(this: SimpleAtom) bool { return switch (this) { - .asterisk, .double_asterisk => true, - else => false, - }; - } - - pub fn mightNeedIO(this: SimpleAtom) bool { - return switch (this) { - .asterisk, .double_asterisk, .cmd_subst => true, - else => false, + .Var => false, + .VarArgv => false, + .Text => false, + .asterisk => true, + .double_asterisk => true, + .brace_begin => false, + .brace_end => false, + .comma => false, + .cmd_subst => false, }; } }; @@ -1608,11 +1606,34 @@ pub const Parser = struct { if (should_break) break; } }, + .VarArgv => |int| { + _ = self.expect(.VarArgv); + try atoms.append(.{ .VarArgv = int }); + if (next_delimits) { + _ = self.match(.Delimit); + if (should_break) break; + } + }, .OpenParen, .CloseParen => { try self.add_error("Unexpected token: `{s}`", .{if (peeked == .OpenParen) "(" else ")"}); return null; }, - else => return null, + .Pipe => return null, + .DoublePipe => return null, + .Ampersand => return null, + .DoubleAmpersand => return null, + .Redirect => return null, + .Dollar => return null, + .Eq => return null, + .Semicolon => return null, + .Newline => return null, + .CmdSubstQuoted => return null, + .CmdSubstEnd => return null, + .JSObjRef => return null, + .Delimit => return null, + .Eof => return null, + .DoubleBracketOpen => return null, + .DoubleBracketClose => return null, } } } @@ -1868,6 +1889,7 @@ pub const TokenTag = enum { OpenParen, CloseParen, Var, + VarArgv, Text, SingleQuotedText, DoubleQuotedText, @@ -1920,6 +1942,7 @@ pub const Token = union(TokenTag) { CloseParen, Var: TextRange, + VarArgv: u8, Text: TextRange, /// Quotation information is lost from the lexer -> parser stage and it is /// helpful to disambiguate from regular text and quoted text @@ -1936,9 +1959,22 @@ pub const Token = union(TokenTag) { pub const TextRange = struct { start: u32, end: u32, + + pub fn len(range: TextRange) u32 { + if (bun.Environment.allow_assert) std.debug.assert(range.start <= range.end); + return range.end - range.start; + } }; pub fn asHumanReadable(self: Token, strpool: []const u8) []const u8 { + const varargv_strings = blk: { + var res: [10][2]u8 = undefined; + for (&res, 0..) |*item, i| { + item[0] = '$'; + item[1] = @as(u8, @intCast(i)) + '0'; + } + break :blk res; + }; return switch (self) { .Pipe => "`|`", .DoublePipe => "`||`", @@ -1961,6 +1997,7 @@ pub const Token = union(TokenTag) { .OpenParen => "`(`", .CloseParen => "`)", .Var => strpool[self.Var.start..self.Var.end], + .VarArgv => &varargv_strings[self.VarArgv], .Text => strpool[self.Text.start..self.Text.end], .SingleQuotedText => strpool[self.SingleQuotedText.start..self.SingleQuotedText.end], .DoubleQuotedText => strpool[self.DoubleQuotedText.start..self.DoubleQuotedText.end], @@ -2328,12 +2365,23 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { // Handle variable try self.break_word(false); const var_tok = try self.eat_var(); - // empty var - if (var_tok.start == var_tok.end) { - try self.appendCharToStrPool('$'); - try self.break_word(false); - } else { - try self.tokens.append(.{ .Var = var_tok }); + + switch (var_tok.len()) { + 0 => { + try self.appendCharToStrPool('$'); + try self.break_word(false); + }, + 1 => blk: { + const c = self.strpool.items[var_tok.start]; + if (c >= '0' and c <= '9') { + try self.tokens.append(.{ .VarArgv = c - '0' }); + break :blk; + } + try self.tokens.append(.{ .Var = var_tok }); + }, + else => { + try self.tokens.append(.{ .Var = var_tok }); + }, } self.word_start = self.j; continue; @@ -2543,8 +2591,38 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { } } else if ((in_normal_space or in_redirect_operator) and self.tokens.items.len > 0 and switch (self.tokens.items[self.tokens.items.len - 1]) { - .Var, .Text, .SingleQuotedText, .DoubleQuotedText, .BraceBegin, .Comma, .BraceEnd, .CmdSubstEnd => true, - else => false, + .Var, + .VarArgv, + .Text, + .SingleQuotedText, + .DoubleQuotedText, + .BraceBegin, + .Comma, + .BraceEnd, + .CmdSubstEnd, + => true, + + .Pipe, + .DoublePipe, + .Ampersand, + .DoubleAmpersand, + .Redirect, + .Dollar, + .Asterisk, + .DoubleAsterisk, + .Eq, + .Semicolon, + .Newline, + .CmdSubstBegin, + .CmdSubstQuoted, + .OpenParen, + .CloseParen, + .JSObjRef, + .DoubleBracketOpen, + .DoubleBracketClose, + .Delimit, + .Eof, + => false, }) { try self.tokens.append(.Delimit); self.delimit_quote = false; @@ -2962,6 +3040,7 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { fn eat_var(self: *@This()) !Token.TextRange { const start = self.j; var i: usize = 0; + var is_int = false; // Eat until special character while (self.peek()) |result| { defer i += 1; @@ -2970,11 +3049,20 @@ pub fn NewLexer(comptime encoding: StringEncoding) type { if (i == 0) { switch (char) { - '=', '0'...'9' => return .{ .start = start, .end = self.j }, + '=' => return .{ .start = start, .end = self.j }, + '0'...'9' => { + is_int = true; + _ = self.eat().?; + try self.appendCharToStrPool(char); + continue; + }, 'a'...'z', 'A'...'Z', '_' => {}, else => return .{ .start = start, .end = self.j }, } } + if (is_int) { + return .{ .start = start, .end = self.j }; + } // if (char switch (char) { @@ -3459,6 +3547,7 @@ pub const Test = struct { CloseParen, Var: []const u8, + VarArgv: u8, Text: []const u8, SingleQuotedText: []const u8, DoubleQuotedText: []const u8, @@ -3473,6 +3562,7 @@ pub const Test = struct { pub fn from_real(the_token: Token, buf: []const u8) TestToken { switch (the_token) { .Var => |txt| return .{ .Var = buf[txt.start..txt.end] }, + .VarArgv => |int| return .{ .VarArgv = int }, .Text => |txt| return .{ .Text = buf[txt.start..txt.end] }, .SingleQuotedText => |txt| return .{ .SingleQuotedText = buf[txt.start..txt.end] }, .DoubleQuotedText => |txt| return .{ .DoubleQuotedText = buf[txt.start..txt.end] }, @@ -3937,7 +4027,6 @@ pub fn needsEscapeUtf8AsciiLatin1Slow(str: []const u8) bool { } return false; } -pub const ExitCode = eval.ExitCode; /// A list that can store its items inlined, and promote itself to a heap allocated bun.ByteList pub fn SmolList(comptime T: type, comptime INLINED_MAX: comptime_int) type { diff --git a/src/windows_c.zig b/src/windows_c.zig index f444a4ee3c..8584659612 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -1294,7 +1294,7 @@ pub fn renameAtW( return moveOpenedFileAt(src_fd, new_dir_fd, new_path_w, replace_if_exists); } -const log = bun.Output.scoped(.SYS, false); +const log = bun.Output.scoped(.SYS, true); /// With an open file source_fd, move it into the directory new_dir_fd with the name new_path_w. /// Does not close the file descriptor. diff --git a/test/js/bun/shell/env.positionals.test.ts b/test/js/bun/shell/env.positionals.test.ts new file mode 100644 index 0000000000..802900be1d --- /dev/null +++ b/test/js/bun/shell/env.positionals.test.ts @@ -0,0 +1,91 @@ +import { $, spawn } from "bun"; +import { describe, test, expect } from "bun:test"; +import { TestBuilder } from "./test_builder"; +import { bunEnv, bunExe } from "harness"; +import * as path from "node:path"; + +describe("$ argv", async () => { + for (let i = 0; i < process.argv.length; i++) { + const element = process.argv[i]; + TestBuilder.command`echo $${i}` + .exitCode(0) + .stdout(process.argv[i] + "\n") + .runAsTest(`$${i} should equal process.argv[${i}]`); + } +}); + +test("$ argv: standalone", async () => { + const script = path.join(import.meta.dir, "fixtures", "positionals.bun.sh"); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "run", script, "a", "b", "c"], + stdout: "pipe", + stdin: "ignore", + stderr: "pipe", + env: bunEnv, + }); + + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toBeEmpty(); + + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.split("\n")).toEqual([script, "a", "bb", ""]); +}); + +test("$ argv: standalone: not enough args", async () => { + const script = path.join(import.meta.dir, "fixtures", "positionals.bun.sh"); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "run", script], + stdout: "pipe", + stdin: "ignore", + stderr: "pipe", + env: bunEnv, + }); + + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toBeEmpty(); + + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.split("\n")).toEqual([script, "", "", ""]); +}); + +test("$ argv: standalone: only 10", async () => { + const script = path.join(import.meta.dir, "fixtures", "positionals2.bun.sh"); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "run", script, "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"], + stdout: "pipe", + stdin: "ignore", + stderr: "pipe", + env: bunEnv, + }); + + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toBeEmpty(); + + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.split("\n")).toEqual([script, "a", "bb", "c", "d", "e", "f", "g", "h", "i", "a0", ""]); +}); + +test("$ argv: standalone: non-ascii", async () => { + const script = path.join(import.meta.dir, "fixtures", "positionals2.bun.sh"); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "run", script, "キ", "テ", "ィ", "・", "ホ", "ワ", "イ", "ト"], + stdout: "pipe", + stdin: "ignore", + stderr: "pipe", + env: bunEnv, + }); + + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toBeEmpty(); + + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.split("\n")).toEqual([script, "キ", "テテ", "ィ", "・", "ホ", "ワ", "イ", "ト", "", "キ0", ""]); +}); diff --git a/test/js/bun/shell/fixtures/positionals.bun.sh b/test/js/bun/shell/fixtures/positionals.bun.sh new file mode 100644 index 0000000000..bc2ed98159 --- /dev/null +++ b/test/js/bun/shell/fixtures/positionals.bun.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +echo $0 +echo $1 +echo $2$2 diff --git a/test/js/bun/shell/fixtures/positionals2.bun.sh b/test/js/bun/shell/fixtures/positionals2.bun.sh new file mode 100644 index 0000000000..1fcf3c4de6 --- /dev/null +++ b/test/js/bun/shell/fixtures/positionals2.bun.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +echo $0 +echo $1 +echo $2$2 +echo $3 +echo $4 +echo $5 +echo $6 +echo $7 +echo $8 +echo $9 +echo $10