mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
feat(repl): add shell command support and fix async event loop handling
- Add $`command` syntax for running shell commands in REPL - Transform $`...` to await Bun.$`...` for shell execution - Fix event loop handling for async operations by calling autoTick() - Now properly awaits Bun.sleep(), setTimeout(), and Bun.$ operations - Add tests for shell commands and Bun.sleep Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1296,11 +1296,70 @@ pub const Repl = struct {
|
||||
}
|
||||
|
||||
fn runShellCommand(self: *Self, code: []const u8) !void {
|
||||
_ = code;
|
||||
_ = self;
|
||||
// Execute shell command using Bun's shell
|
||||
// TODO: Integrate with shell interpreter
|
||||
Output.pretty("<yellow>Shell mode not yet implemented<r>\n", .{});
|
||||
// Transform $`command` into await Bun.$`command` and execute it
|
||||
// The shell template tag returns a ShellPromise which gets awaited
|
||||
|
||||
const start_time = std.time.nanoTimestamp();
|
||||
|
||||
// Parse the shell command from the input
|
||||
// Formats: $`command` or $ `command`
|
||||
var cmd_start: usize = 0;
|
||||
if (strings.startsWith(code, "$`")) {
|
||||
cmd_start = 1; // Skip the $, keep the backtick
|
||||
} else if (strings.startsWith(code, "$ `")) {
|
||||
cmd_start = 2; // Skip "$ ", keep the backtick
|
||||
} else {
|
||||
Output.pretty("<red>Invalid shell command syntax. Use $`command`<r>\n", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the JavaScript code to execute the shell command
|
||||
// Using Bun.$ template literal with await for automatic promise handling
|
||||
const shell_code = code[cmd_start..];
|
||||
|
||||
// Wrap in await Bun.$ - use the same transformation as normal code
|
||||
const js_code = try std.fmt.allocPrint(self.allocator, "await Bun.${s}", .{shell_code});
|
||||
defer self.allocator.free(js_code);
|
||||
|
||||
// Transform code using the transpiler with replMode (for top-level await support)
|
||||
const transformed = self.transformCode(js_code) catch |err| {
|
||||
Output.pretty("<red>Shell command transformation failed: {s}<r>\n", .{@errorName(err)});
|
||||
return;
|
||||
};
|
||||
defer self.allocator.free(transformed);
|
||||
|
||||
if (transformed.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the transformed code
|
||||
const wrapper_result = self.executeCode(transformed) catch |err| {
|
||||
Output.pretty("<red>Shell command failed: {s}<r>\n", .{@errorName(err)});
|
||||
return;
|
||||
};
|
||||
|
||||
// Handle async result (the shell command returns a promise)
|
||||
if (wrapper_result.asPromise()) |_| {
|
||||
self.awaitAndPrintResult(wrapper_result, start_time);
|
||||
} else {
|
||||
const end_time = std.time.nanoTimestamp();
|
||||
const elapsed_ms = @as(f64, @floatFromInt(end_time - start_time)) / 1_000_000.0;
|
||||
|
||||
// Extract the actual result value from the wrapper object
|
||||
const result = if (wrapper_result.isObject()) blk: {
|
||||
const val = wrapper_result.get(self.global, "value") catch break :blk wrapper_result;
|
||||
break :blk val orelse wrapper_result;
|
||||
} else wrapper_result;
|
||||
|
||||
// Print result
|
||||
if (!result.isUndefined()) {
|
||||
self.printResult(result);
|
||||
}
|
||||
|
||||
if (self.show_timing) {
|
||||
Output.pretty("<d>({d:.2}ms)<r>\n", .{elapsed_ms});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -304,4 +304,59 @@ describe("bun repl", () => {
|
||||
expect(stdout).toContain("3");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("shell command syntax works", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "repl"],
|
||||
env: bunEnv,
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
proc.stdin.write("$`echo hello from shell`\n");
|
||||
proc.stdin.end();
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout).toContain("hello from shell");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("Bun.$ template literal works", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "repl"],
|
||||
env: bunEnv,
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
proc.stdin.write("await Bun.$`echo test output`\n");
|
||||
proc.stdin.end();
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout).toContain("test output");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("Bun.sleep works", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "repl"],
|
||||
env: bunEnv,
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
proc.stdin.write("await Bun.sleep(10)\n");
|
||||
proc.stdin.write("'slept'\n");
|
||||
proc.stdin.end();
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout).toContain("slept");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user