Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions src/runtime/shell/builtin/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::shell::ExitCode;
use crate::shell::builtin::{Builtin, IoKind, Kind};
use crate::shell::interpreter::{
EventLoopHandle, Interpreter, NodeId, OutputSrc, OutputTask, OutputTaskVTable, ShellTask,
shell_openat,
shell_openat, shell_statat,
};
use crate::shell::io_writer::{ChildPtr, WriterTag};
use crate::shell::yield_::Yield;
Expand Down Expand Up @@ -459,14 +459,19 @@ impl ShellLsTask {
let fd = match shell_openat(this.cwd, this.path.as_zstr(), O::RDONLY | O::DIRECTORY, 0) {
Err(e) => {
match e.get_errno() {
E::ENOENT => {
this.err = Some(this.error_with_path(&e));
}
E::ENOTDIR => {
// Clone the path to dodge the &mut/& borrow overlap.
let p = ZBox::from_bytes(this.path.as_bytes());
this.add_entry(p.as_bytes(), this.cwd);
}
// ENOTDIR is also what the open reports when a parent
// component of the path is a file (`ls file/x`). Only an
// operand that exists is listed as a file.
E::ENOTDIR => match shell_statat(this.cwd, this.path.as_zstr()) {
Ok(_) => {
// Clone the path to dodge the &mut/& borrow overlap.
let p = ZBox::from_bytes(this.path.as_bytes());
this.add_entry(p.as_bytes(), this.cwd);
}
Err(stat_err) => {
this.err = Some(this.error_with_path(&stat_err));
}
},
_ => {
this.err = Some(this.error_with_path(&e));
}
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/shell/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2227,7 +2227,7 @@ fn shell_get_path<'a>(
/// Windows: rewrite the path via `shell_get_path` then `bun_sys::stat`, tagging
/// the error with the *original* `path_` (not the rewritten one). POSIX: plain
/// `bun_sys::fstatat(dir, path_)`.
// consumed by states/CondExpr (`[[ -e/-f/-d ... ]]`)
// consumed by states/CondExpr (`[[ -e/-f/-d ... ]]`) and the `ls` builtin
pub(crate) fn shell_statat(dir: Fd, path_: &bun_core::ZStr) -> bun_sys::Result<bun_sys::Stat> {
#[cfg(windows)]
{
Expand Down
28 changes: 28 additions & 0 deletions test/js/bun/shell/commands/ls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,34 @@ describe.concurrent("bunshell ls", () => {
.run();
});

// The O_DIRECTORY open also fails with ENOTDIR when a parent component of
// the operand is a file. Windows can report that case as ENOENT instead.
const notADirectory = (s: string) =>
isPosix
? expect(s).toBe("ls: file/x: Not a directory\n")
: expect(s).toMatch(/^ls: file\/x: (Not a directory|No such file or directory)\n$/);

test("operand whose parent is a regular file", async () => {
await TestBuilder.command`ls file/x`.file("file", "").exitCode(1).stderr(notADirectory).run();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
});

test("operand whose parent is a regular file with -l", async () => {
await TestBuilder.command`ls -l file/x`.file("file", "").exitCode(1).stderr(notADirectory).run();
});

test("operand whose parent is a regular file with -d", async () => {
await TestBuilder.command`ls -d file/x`.file("file", "").exitCode(1).stderr(notADirectory).run();
});

test("file operand next to an operand whose parent is a regular file", async () => {
await TestBuilder.command`ls file file/x`
.file("file", "")
.exitCode(1)
.stdout("file\n")
.stderr(notADirectory)
.run();
});

test("invalid flag", async () => {
await TestBuilder.command`ls -z`
.exitCode(1)
Expand Down
Loading