Skip to content
2 changes: 2 additions & 0 deletions append_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/workspace/bun
/workspace/bun
Comment on lines +1 to +2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for references to append_output.txt and /workspace/bun path

echo "=== Searching for references to 'append_output' ==="
rg -n 'append_output' --type ts --type js --type rust || echo "No matches found"

echo -e "\n=== Searching for '/workspace/bun' path references ==="
rg -n '/workspace/bun' --type ts --type js --type rust || echo "No matches found"

echo -e "\n=== Checking .gitignore for txt files and append_output ==="
if [ -f .gitignore ]; then
  echo "Current .gitignore rules related to txt:"
  grep -E '\.txt$|append_output' .gitignore || echo "No matching .gitignore rules found for .txt or append_output"
else
  echo ".gitignore file not found"
fi

echo -e "\n=== File status check ==="
if [ -f append_output.txt ]; then
  echo "File exists at repository root"
  wc -l append_output.txt
  cat append_output.txt
else
  echo "File does not exist"
fi

echo -e "\n=== Git status of append_output.txt ==="
git status append_output.txt 2>/dev/null || echo "Not tracked by git or git not available"

Repository: oven-sh/bun

Length of output: 776


Move append_output.txt to the test directory or add it to .gitignore.

This file is referenced in test/js/bun/shell/file-io.test.ts:252 as an output target for shell redirect testing (&>> append_output.txt). However, it's currently at the repository root and tracked by git. Test artifacts should either:

  • Be placed in test/js/bun/shell/ per repository conventions, or
  • Added to .gitignore (like other test artifacts such as failing-tests.txt)

The current content (/workspace/bun repeated twice) is leftover test output from previous runs, not intentional file content. Ensure the file is properly cleaned up after tests execute or excluded from version control.

🤖 Prompt for AI Agents
In append_output.txt around lines 1 to 2: this file is a leftover test artifact
(contains repeated "/workspace/bun") but is tracked at repo root; either move it
into the test directory (test/js/bun/shell/) so test output targets write there,
or add append_output.txt to .gitignore like other test artifacts; additionally
update the test (test/js/bun/shell/file-io.test.ts line ~252) to clean up the
file after the test (or write to a temp path inside the test dir) so it is not
committed in future runs.

1 change: 1 addition & 0 deletions pwd_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/workspace/bun

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for pwd_output.txt and append_output.txt
find . -name "pwd_output.txt" -o -name "append_output.txt" 2>/dev/null | head -20

Repository: oven-sh/bun

Length of output: 91


🏁 Script executed:

# Also search for file-io.test.ts
find . -name "file-io.test.ts" 2>/dev/null

Repository: oven-sh/bun

Length of output: 90


🏁 Script executed:

# Search for references to these files in the codebase
rg "pwd_output|append_output" --type ts --type js -l 2>/dev/null

Repository: oven-sh/bun

Length of output: 88


🏁 Script executed:

# Examine the file-io.test.ts to understand how these files are used
cat -n test/js/bun/shell/file-io.test.ts

Repository: oven-sh/bun

Length of output: 12375


🏁 Script executed:

# Check the content of both files
echo "=== pwd_output.txt ===" && cat pwd_output.txt && echo -e "\n=== append_output.txt ===" && cat append_output.txt

Repository: oven-sh/bun

Length of output: 134


🏁 Script executed:

# Check if these files are mentioned in git status or gitignore
git ls-files | grep -E "pwd_output|append_output"

Repository: oven-sh/bun

Length of output: 87


Remove test artifacts from version control and add to .gitignore.

Both pwd_output.txt and append_output.txt are generated test output files created by the pwd &> pwd_output.txt and pwd &>> append_output.txt commands in test/js/bun/shell/file-io.test.ts (lines 245, 252). These should not be committed to the repository. Either:

  1. Add both files to .gitignore, or
  2. Update the tests to use tempDir() like other test cases in the same file to ensure outputs are written to isolated temporary directories instead of the repo root.
🤖 Prompt for AI Agents
In pwd_output.txt (line 1) and related test artifacts append_output.txt, remove
these generated test output files from version control and either add both
filenames to .gitignore at the repo root or modify the tests in
test/js/bun/shell/file-io.test.ts (around lines ~245–252) to write outputs into
a temporary directory (use tempDir() like other tests) so outputs are isolated;
commit the .gitignore change or the test update and delete the tracked
pwd_output.txt and append_output.txt from the repo (git rm --cached) before
committing.

431 changes: 260 additions & 171 deletions src/shell/Builtin.zig

Large diffs are not rendered by default.

176 changes: 162 additions & 14 deletions src/shell/shell.zig
Original file line number Diff line number Diff line change
Expand Up @@ -805,8 +805,7 @@ pub const AST = struct {
pub const Cmd = struct {
assigns: []Assign,
name_and_args: []Atom,
redirect: RedirectFlags = .{},
redirect_file: ?Redirect = null,
redirects: Redirects = .{},

pub fn memoryCost(this: *const @This()) usize {
var cost: usize = @sizeOf(Cmd);
Expand All @@ -816,12 +815,56 @@ pub const AST = struct {
for (this.name_and_args) |*atom| {
cost += atom.memoryCost();
}
cost += this.redirects.memoryCost();
return cost;
}
};

if (this.redirect_file) |*redirect_file| {
cost += redirect_file.memoryCost();
}
/// Redirect destinations for stdin, stdout, and stderr
pub const Redirects = struct {
stdin: RedirectTarget = .none,
stdout: RedirectTarget = .none,
stderr: RedirectTarget = .none,

/// Type for specifying which IO stream
pub const IoKind = enum { stdin, stdout, stderr };

pub fn memoryCost(this: *const @This()) usize {
var cost: usize = @sizeOf(Redirects);
cost += this.stdin.memoryCost();
cost += this.stdout.memoryCost();
cost += this.stderr.memoryCost();
return cost;
}

/// Check if a given io stream is redirected elsewhere
pub fn redirectsElsewhere(this: *const Redirects, comptime io_kind: IoKind) bool {
return @field(this, @tagName(io_kind)) != .none;
}
};

/// Where a stream is redirected to
pub const RedirectTarget = union(enum) {
/// No redirect - use the default for this stream
none: void,
/// Redirect to a file path (from shell text)
atom: struct {
atom: Atom,
append: bool = false,
},
/// Redirect to a JS object (ArrayBuffer, Blob, etc.)
jsbuf: JSBuf,
/// Duplicate to another fd (e.g., 2>&1 means stderr duplicates to stdout's destination)
dup: enum { stdin, stdout, stderr },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove ?RedirectKind and use none: void instead so we avoid the struct size increase unnecessarily

pub fn memoryCost(this: *const @This()) usize {
return switch (this.*) {
.none => 0,
.atom => |a| @sizeOf(RedirectTarget) + a.atom.memoryCost(),
.jsbuf => @sizeOf(RedirectTarget),
.dup => @sizeOf(RedirectTarget),
};
}
};

/// Bit flags for redirects:
Expand All @@ -833,8 +876,6 @@ pub const AST = struct {
/// - `1>>` = Redirect.Append | Redirect.Stdout
/// - `2>>` = Redirect.Append | Redirect.Stderr
/// - `&>>` = Redirect.Append | Redirect.Stdout | Redirect.Stderr
///
/// Multiple redirects and redirecting stdin is not supported yet.
pub const RedirectFlags = packed struct(u8) {
stdin: bool = false,
stdout: bool = false,
Expand All @@ -849,7 +890,9 @@ pub const AST = struct {
return @as(u8, @bitCast(this)) == 0;
}

pub fn redirectsElsewhere(this: RedirectFlags, io_kind: enum { stdin, stdout, stderr }) bool {
pub const IoKind = enum { stdin, stdout, stderr };

pub fn redirectsElsewhere(this: RedirectFlags, io_kind: IoKind) bool {
return switch (io_kind) {
.stdin => this.stdin,
.stdout => if (this.duplicate_out) !this.stdout else this.stdout,
Expand Down Expand Up @@ -1636,16 +1679,76 @@ pub const Parser = struct {
while (try self.parse_atom()) |arg| {
try name_and_args.append(arg);
}
const parsed_redirect = try self.parse_redirect();
const redirects = try self.parse_redirects();

return .{ .cmd = .{
.assigns = assigns.items[0..],
.name_and_args = name_and_args.items[0..],
.redirect_file = parsed_redirect.redirect,
.redirect = parsed_redirect.flags,
.redirects = redirects,
} };
}

/// Parse zero or more redirections and build a Redirects struct.
/// Each redirect updates the appropriate field (stdin, stdout, stderr).
/// For duplicate redirects like 2>&1, we set the target to .dup.
fn parse_redirects(self: *Parser) !AST.Redirects {
var redirects: AST.Redirects = .{};

while (self.match(.Redirect)) {
const flags = self.prev().Redirect;

// Empty flags means a no-op redirect (e.g., >&1 which redirects stdout to stdout)
if (flags.isEmpty()) {
continue;
}

// Handle fd duplication (2>&1, 1>&2, >&0, >&2)
if (flags.duplicate_out) {
// For "2>&1": flags.stdout=true means stderr goes to stdout
// For "1>&2": flags.stderr=true means stdout goes to stderr
// For ">&0": flags.stdin=true means stdout goes to stdin
// For ">&2": flags.stderr=true means stdout goes to stderr
if (flags.stdin) {
// >&0 - redirect stdout to stdin
redirects.stdout = .{ .dup = .stdin };
} else if (flags.stdout) {
redirects.stderr = .{ .dup = .stdout };
} else if (flags.stderr) {
redirects.stdout = .{ .dup = .stderr };
}
continue;
}

// Parse the redirect target (file path or JS object)
const target: AST.RedirectTarget = target: {
if (self.match(.JSObjRef)) {
const obj_ref = self.prev().JSObjRef;
break :target .{ .jsbuf = AST.JSBuf.new(obj_ref) };
}

const atom = try self.parse_atom() orelse {
try self.add_error("Redirection with no file", .{});
return ParseError.Expected;
};
break :target .{ .atom = .{ .atom = atom, .append = flags.append } };
};

// Set the appropriate redirect field based on which fd is being redirected
if (flags.stdin) {
redirects.stdin = target;
}
if (flags.stdout) {
redirects.stdout = target;
}
if (flags.stderr) {
redirects.stderr = target;
}
}

return redirects;
}

/// Parse a single redirect (used for subshells which only support one redirect)
fn parse_redirect(self: *Parser) !ParsedRedirect {
const has_redirect = self.match(.Redirect);
const redirect = if (has_redirect) self.prev().Redirect else AST.RedirectFlags{};
Expand All @@ -1656,16 +1759,15 @@ pub const Parser = struct {
break :redirect_file .{ .jsbuf = AST.JSBuf.new(obj_ref) };
}

const redirect_file = try self.parse_atom() orelse {
const redirect_target = try self.parse_atom() orelse {
if (redirect.duplicate_out) break :redirect_file null;
try self.add_error("Redirection with no file", .{});
return ParseError.Expected;
};
break :redirect_file .{ .atom = redirect_file };
break :redirect_file .{ .atom = redirect_target };
}
break :redirect_file null;
};
// TODO check for multiple redirects and error
return .{ .flags = redirect, .redirect = redirect_file };
}

Expand Down Expand Up @@ -2964,6 +3066,52 @@ pub fn NewLexer(comptime encoding: StringEncoding) type {
};
}

// Check for >&0, >&1, or >&2 (fd duplication without explicit source fd)
// >&0 means redirect stdout to stdin (unusual but valid in POSIX)
// >&1 means stdout goes to stdout (no-op)
// >&2 means stdout goes to stderr
if (dir == .out) {
if (self.peek()) |peeked| {
if (!peeked.escaped and peeked.char == '&') {
_ = self.eat();
if (self.peek()) |peeked2| {
switch (peeked2.char) {
'0' => {
// >&0 means redirect stdout to stdin (unusual but valid)
_ = self.eat();
return .{
.stdin = true,
.duplicate_out = true,
};
},
'1' => {
// >&1 means stdout to stdout (no-op)
// Just consume the token and return a plain > redirect
// with no effect - the parser will not set any redirect
_ = self.eat();
// Return empty flags - this is effectively a no-op
return .{};
},
'2' => {
// >&2 means stdout goes to stderr
_ = self.eat();
return .{
.stdout = false, // We're redirecting stdout
.stderr = true, // to stderr
.duplicate_out = true,
};
},
else => {
// Not a valid fd dup, backtrack the '&'
// Actually we can't easily backtrack, so this case will
// be handled as an error later. For now just return > redirect.
},
}
}
}
}
}

return switch (dir) {
.out => AST.RedirectFlags.@">"(),
.in => AST.RedirectFlags.@"<"(),
Expand Down
Loading
Loading