mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## Summary
- Implements the `-l` (long listing) flag functionality for the shell
`ls` builtin
- The flag was being parsed but never used - output was identical to
short format
- Now displays proper long listing format: file type, permissions, hard
link count, UID, GID, size, modification time, and filename
## Test plan
- [x] Added regression test in `test/regression/issue/25831.test.ts`
- [x] Test passes with debug build: `bun bd test
test/regression/issue/25831.test.ts`
- [x] Test fails with system bun (confirming the bug exists):
`USE_SYSTEM_BUN=1 bun test test/regression/issue/25831.test.ts`
Example output with fix:
```
$ bun -e 'import { $ } from "bun"; console.log(await $`ls -l`.text())'
drwxr-xr-x 2 1000 1000 4096 Jan 12 15:30 subdir
-rw-r--r-- 1 1000 1000 11 Jan 12 15:30 file.txt
```
Fixes #25831
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
170 lines
4.6 KiB
TypeScript
170 lines
4.6 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, tempDir } from "harness";
|
|
|
|
test("ls -l shows long listing format", async () => {
|
|
// Create temp directory with test files
|
|
using dir = tempDir("ls-long-listing", {
|
|
"file.txt": "hello world",
|
|
"script.sh": "#!/bin/bash\necho hello",
|
|
subdir: {
|
|
"nested.txt": "nested content",
|
|
},
|
|
});
|
|
|
|
// Run ls -l in the temp directory
|
|
await using proc = Bun.spawn({
|
|
cmd: [
|
|
bunExe(),
|
|
"-e",
|
|
`
|
|
import { $ } from "bun";
|
|
$.cwd("${String(dir).replace(/\\/g, "\\\\")}");
|
|
const result = await $\`ls -l\`.text();
|
|
console.log(result);
|
|
`,
|
|
],
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
// Verify no errors on stderr
|
|
expect(stderr).toBe("");
|
|
|
|
// Should show permission string (starts with - or d, followed by rwx/sStT permissions)
|
|
// Format: -rw-r--r-- 1 uid gid size date name
|
|
expect(stdout).toMatch(/^[-dlbcps][-rwxsStT]{9}/m); // Permission string pattern
|
|
expect(stdout).toContain("file.txt");
|
|
expect(stdout).toContain("script.sh");
|
|
expect(stdout).toContain("subdir");
|
|
|
|
// Verify that it's actually showing long format (contains size and date info)
|
|
// Long format has at least permissions, link count, uid, gid, size, date, name
|
|
const lines = stdout
|
|
.trim()
|
|
.split("\n")
|
|
.filter(line => line.includes("file.txt"));
|
|
expect(lines.length).toBeGreaterThan(0);
|
|
|
|
// Each line should have multiple space-separated fields
|
|
const fileLine = lines[0];
|
|
const fields = fileLine.trim().split(/\s+/);
|
|
expect(fields.length).toBeGreaterThanOrEqual(7); // perms, nlink, uid, gid, size, date fields, name
|
|
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test("ls without -l shows short format", async () => {
|
|
using dir = tempDir("ls-short-listing", {
|
|
"file1.txt": "content1",
|
|
"file2.txt": "content2",
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [
|
|
bunExe(),
|
|
"-e",
|
|
`
|
|
import { $ } from "bun";
|
|
$.cwd("${String(dir).replace(/\\/g, "\\\\")}");
|
|
const result = await $\`ls\`.text();
|
|
console.log(result);
|
|
`,
|
|
],
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
// Verify no errors on stderr
|
|
expect(stderr).toBe("");
|
|
|
|
// Short format should just show filenames, not permission strings
|
|
expect(stdout).not.toMatch(/^[-dlbcps][-rwxsStT]{9}/m);
|
|
expect(stdout).toContain("file1.txt");
|
|
expect(stdout).toContain("file2.txt");
|
|
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test("ls -al shows hidden files in long format", async () => {
|
|
using dir = tempDir("ls-all-long", {
|
|
".hidden": "hidden content",
|
|
"visible.txt": "visible content",
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [
|
|
bunExe(),
|
|
"-e",
|
|
`
|
|
import { $ } from "bun";
|
|
$.cwd("${String(dir).replace(/\\/g, "\\\\")}");
|
|
const result = await $\`ls -al\`.text();
|
|
console.log(result);
|
|
`,
|
|
],
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
// Verify no errors on stderr
|
|
expect(stderr).toBe("");
|
|
|
|
// Should show hidden files
|
|
expect(stdout).toContain(".hidden");
|
|
expect(stdout).toContain("visible.txt");
|
|
// Should also show . and .. entries
|
|
expect(stdout).toMatch(/^d[-rwxsStT]{9}.*\s\.$/m); // . directory
|
|
expect(stdout).toMatch(/^d[-rwxsStT]{9}.*\s\.\.$/m); // .. directory
|
|
|
|
// Should be in long format
|
|
expect(stdout).toMatch(/^[-dlbcps][-rwxsStT]{9}/m);
|
|
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test("ls -l shows directory type indicator", async () => {
|
|
using dir = tempDir("ls-dir-type", {
|
|
"regular-file.txt": "content",
|
|
subdir: {
|
|
"nested.txt": "nested",
|
|
},
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [
|
|
bunExe(),
|
|
"-e",
|
|
`
|
|
import { $ } from "bun";
|
|
$.cwd("${String(dir).replace(/\\/g, "\\\\")}");
|
|
const result = await $\`ls -l\`.text();
|
|
console.log(result);
|
|
`,
|
|
],
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
// Verify no errors on stderr
|
|
expect(stderr).toBe("");
|
|
|
|
// Directory should start with 'd'
|
|
expect(stdout).toMatch(/^d[-rwxsStT]{9}.*subdir$/m);
|
|
// Regular file should start with '-'
|
|
expect(stdout).toMatch(/^-[-rwxsStT]{9}.*regular-file\.txt$/m);
|
|
|
|
expect(exitCode).toBe(0);
|
|
});
|