Compare commits

...

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
590131f384 [autofix.ci] apply automated fixes 2025-09-09 12:53:55 +00:00
Claude Bot
84d566de4d Enable background commands (&) in shell
Background commands using the '&' operator are now fully functional.
The infrastructure was already in place from PR #9631 but was
intentionally disabled. This commit enables the feature by:

- Uncommenting and fixing the parser code in shell.zig
- Adding Subshell support to the Async state
- Adding proper error checking for '&' followed by '||'
- Updating tests to reflect the new functionality

Fixes the issue where users got "Background commands '&' are not
supported yet" error when trying to run commands in the background.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-09 12:51:08 +00:00
5 changed files with 176 additions and 30 deletions

View File

@@ -1022,38 +1022,39 @@ pub const Parser = struct {
{
const expr = try self.parse_expr();
if (self.match(.Ampersand)) {
try self.add_error("Background commands \"&\" are not supported yet.", .{});
return ParseError.Unsupported;
// Uncomment when we enable ampersand
// switch (expr) {
// .binary => {
// var newexpr = expr;
// const right_alloc = try self.allocate(AST.Expr, newexpr.binary.right);
// const right: AST.Expr = .{ .@"async" = right_alloc };
// newexpr.binary.right = right;
// try exprs.append(newexpr);
// },
// else => {
// const @"async" = .{ .@"async" = try self.allocate(AST.Expr, expr) };
// try exprs.append(@"async");
// },
// }
switch (expr) {
.binary => {
var newexpr = expr;
const right_alloc = try self.allocate(AST.Expr, newexpr.binary.right);
const right: AST.Expr = .{ .@"async" = right_alloc };
newexpr.binary.right = right;
try exprs.append(newexpr);
},
else => {
const @"async" = AST.Expr{ .@"async" = try self.allocate(AST.Expr, expr) };
try exprs.append(@"async");
},
}
// _ = self.match_any_comptime(&.{ .Semicolon, .Newline });
_ = self.match_any_comptime(&.{ .Semicolon, .Newline });
// // Scripts like: `echo foo & && echo hi` aren't allowed because
// // `&&` and `||` require the left-hand side's exit code to be
// // immediately observable, but the `&` makes it run in the
// // background.
// //
// // So we do a quick check for this kind of syntax here, and
// // provide a helpful error message to the user.
// if (self.peek() == .DoubleAmpersand) {
// try self.add_error("\"&\" is not allowed on the left-hand side of \"&&\"", .{});
// return ParseError.Unsupported;
// }
// Scripts like: `echo foo & && echo hi` aren't allowed because
// `&&` and `||` require the left-hand side's exit code to be
// immediately observable, but the `&` makes it run in the
// background.
//
// So we do a quick check for this kind of syntax here, and
// provide a helpful error message to the user.
if (self.peek() == .DoubleAmpersand) {
try self.add_error("\"&\" is not allowed on the left-hand side of \"&&\"", .{});
return ParseError.Unsupported;
}
if (self.peek() == .DoublePipe) {
try self.add_error("\"&\" is not allowed on the left-hand side of \"||\"", .{});
return ParseError.Unsupported;
}
// break;
break;
}
try exprs.append(expr);

View File

@@ -24,6 +24,7 @@ pub const ChildPtr = StatePtrUnion(.{
Cmd,
If,
CondExpr,
Subshell,
});
pub fn format(this: *const Async, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
@@ -99,6 +100,21 @@ pub fn next(this: *Async) Yield {
CondExpr.ParentPtr.init(this),
this.io.copy(),
)),
.subshell => switch (Subshell.initDupeShellState(
this.base.interpreter,
this.base.shell,
this.node.subshell,
Subshell.ParentPtr.init(this),
this.io.copy(),
)) {
.result => |subshell| break :brk ChildPtr.init(subshell),
.err => |e| {
this.base.throw(&bun.shell.ShellErr.newSys(e));
this.state = .{ .done = 1 };
this.enqueueSelf();
return .suspended;
},
},
else => {
@panic("Encountered an unexpected child of an async command, this indicates a bug in Bun. Please open a GitHub issue.");
},
@@ -175,6 +191,7 @@ const Pipeline = bun.shell.Interpreter.Pipeline;
const ShellExecEnv = Interpreter.ShellExecEnv;
const State = bun.shell.Interpreter.State;
const Stmt = bun.shell.Interpreter.Stmt;
const Subshell = bun.shell.Interpreter.Subshell;
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
const log = bun.shell.interpret.log;

View File

@@ -21,6 +21,7 @@ pub const ParentPtr = StatePtrUnion(.{
Pipeline,
Binary,
Stmt,
Async,
});
pub const ChildPtr = StatePtrUnion(.{
@@ -199,6 +200,7 @@ const Yield = bun.shell.Yield;
const ast = bun.shell.AST;
const Interpreter = bun.shell.Interpreter;
const Async = bun.shell.Interpreter.Async;
const Binary = bun.shell.Interpreter.Binary;
const Expansion = bun.shell.Interpreter.Expansion;
const IO = bun.shell.Interpreter.IO;

View File

@@ -0,0 +1,125 @@
import { $ } from "bun";
import { describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { createTestBuilder } from "./test_builder";
const TestBuilder = createTestBuilder(import.meta.path);
describe("Background Commands (&)", () => {
test("background command syntax is accepted", async () => {
// Just verify the command doesn't error out anymore
const result = await $`echo "test" &`.quiet();
expect(result.exitCode).toBe(0);
});
test("error: & followed by &&", async () => {
await TestBuilder.command`echo "test" & && echo "should fail"`
.error('"&" is not allowed on the left-hand side of "&&"')
.run();
});
test("error: & followed by ||", async () => {
await TestBuilder.command`echo "test" & || echo "should fail"`
.error('"&" is not allowed on the left-hand side of "||"')
.run();
});
test("background command with file output", async () => {
const dir = mkdtempSync(join(tmpdir(), "bg-test-"));
const file = join(dir, "output.txt");
try {
await $`echo "to file" > ${file} &`.quiet();
// Give background task time to complete
await Bun.sleep(100);
const content = await Bun.file(file).text();
expect(content).toBe("to file\n");
} finally {
rmSync(dir, { recursive: true });
}
});
test("multiple background commands with file output", async () => {
const dir = mkdtempSync(join(tmpdir(), "bg-multi-"));
const file = join(dir, "output.txt");
try {
await $`echo "1" >> ${file} & echo "2" >> ${file} & echo "3" >> ${file}`.quiet();
await Bun.sleep(100);
const content = await Bun.file(file).text();
expect(content).toContain("1");
expect(content).toContain("2");
expect(content).toContain("3");
} finally {
rmSync(dir, { recursive: true });
}
});
test("background subshell", async () => {
// Simple subshell without redirection
const result = await $`(echo "in subshell") &`.text();
expect(result).toBe("in subshell\n");
});
test("background pipeline", async () => {
const dir = mkdtempSync(join(tmpdir(), "bg-pipeline-"));
const file = join(dir, "output.txt");
try {
await $`echo "test" | cat > ${file} &`.quiet();
await Bun.sleep(100);
const content = await Bun.file(file).text();
expect(content).toBe("test\n");
} finally {
rmSync(dir, { recursive: true });
}
});
test("background if statement", async () => {
const dir = mkdtempSync(join(tmpdir(), "bg-if-"));
const file = join(dir, "output.txt");
try {
await $`if true; then echo "in if" > ${file}; fi &`.quiet();
await Bun.sleep(100);
const content = await Bun.file(file).text();
expect(content).toBe("in if\n");
} finally {
rmSync(dir, { recursive: true });
}
});
test("background command with && on right side", async () => {
const dir = mkdtempSync(join(tmpdir(), "bg-and-"));
const file = join(dir, "output.txt");
try {
await $`echo "left" > ${file} && echo "right" >> ${file} &`.quiet();
await Bun.sleep(100);
const content = await Bun.file(file).text();
expect(content).toBe("left\nright\n");
} finally {
rmSync(dir, { recursive: true });
}
});
test("original issue example works", async () => {
// Test the exact example from the GitHub issue
const emulatorName = "test-emulator";
const ANDROID_HOME = "/fake/android/home";
// This should not throw an error anymore
const result = await $`"$ANDROID_HOME/emulator/emulator" -avd "${emulatorName}" -netdelay none -netspeed full &`
.quiet()
.nothrow();
// Command will fail because the emulator doesn't exist, but it shouldn't complain about &
expect(result.stderr.toString()).not.toContain("Background commands");
});
});

View File

@@ -725,7 +725,8 @@ describe("lex shell", () => {
test("Unexpected EOF", async () => {
await TestBuilder.command`echo hi |`.error("Unexpected EOF").run();
await TestBuilder.command`echo hi &`.error('Background commands "&" are not supported yet.').run();
// Background commands are now supported, so this should succeed
await TestBuilder.command`echo hi &`.stdout("hi\n").run();
});
test("Unclosed subshell", async () => {