Skip to content

Commit a849ea2

Browse files
authored
Unskip 31 (#24)
* Unskip many tests * Unskip more * fs * skip * LINENO
1 parent 1e2e8df commit a849ea2

File tree

80 files changed

+787
-324
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+787
-324
lines changed

src/Bash.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
isLazyCommand,
2121
} from "./custom-commands.js";
2222
import { InMemoryFs } from "./fs/in-memory-fs/in-memory-fs.js";
23+
import { initFilesystem } from "./fs/init.js";
2324
import type { IFileSystem, InitialFiles } from "./fs/interface.js";
2425
import {
2526
ArithmeticError,
@@ -205,6 +206,9 @@ export class Bash {
205206
commandCount: 0,
206207
lastExitCode: 0,
207208
lastArg: "", // $_ is initially empty (or could be shell name)
209+
startTime: Date.now(),
210+
lastBackgroundPid: 0,
211+
currentLine: 1, // $LINENO starts at 1
208212
options: {
209213
errexit: false,
210214
pipefail: false,
@@ -216,21 +220,9 @@ export class Bash {
216220
loopDepth: 0,
217221
};
218222

219-
// Create essential directories for InMemoryFs
220-
if (fs instanceof InMemoryFs) {
221-
try {
222-
// Always create /bin for PATH-based command resolution
223-
fs.mkdirSync("/bin", { recursive: true });
224-
fs.mkdirSync("/usr/bin", { recursive: true });
225-
// Create additional directories only for default layout
226-
if (this.useDefaultLayout) {
227-
fs.mkdirSync("/home/user", { recursive: true });
228-
fs.mkdirSync("/tmp", { recursive: true });
229-
}
230-
} catch {
231-
// Ignore errors - directories may already exist
232-
}
233-
}
223+
// Initialize filesystem with standard directories and device files
224+
// Only applies to InMemoryFs - other filesystems use real directories
225+
initFilesystem(fs, this.useDefaultLayout);
234226

235227
if (cwd !== "/" && fs instanceof InMemoryFs) {
236228
try {
@@ -265,10 +257,14 @@ export class Bash {
265257

266258
registerCommand(command: Command): void {
267259
this.commands.set(command.name, command);
268-
// Always create command stubs in /bin for PATH-based resolution
269-
if (this.fs instanceof InMemoryFs) {
260+
// Create command stubs in /bin for PATH-based resolution
261+
// Works for both InMemoryFs and OverlayFs (both have writeFileSync)
262+
const fs = this.fs as {
263+
writeFileSync?: (path: string, content: string) => void;
264+
};
265+
if (typeof fs.writeFileSync === "function") {
270266
try {
271-
this.fs.writeFileSync(
267+
fs.writeFileSync(
272268
`/bin/${command.name}`,
273269
`#!/bin/bash\n# Built-in command: ${command.name}\n`,
274270
);

src/ast/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
/** Base interface for all AST nodes */
1919
export interface ASTNode {
2020
type: string;
21+
/** Source line number (1-based) for $LINENO tracking. May be 0 or undefined for synthesized nodes. */
22+
line?: number;
2123
}
2224

2325
/** Position information for error reporting */

src/commands/ls/ls.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ describe("ls", () => {
2121
cwd: "/",
2222
});
2323
const result = await env.exec("ls");
24-
// /bin and /usr always exist for PATH-based command resolution
25-
expect(result.stdout).toBe("bin\nfile.txt\nusr\n");
24+
// /bin, /usr, /dev, /proc always exist for PATH-based command resolution and system compatibility
25+
expect(result.stdout).toBe("bin\ndev\nfile.txt\nproc\nusr\n");
2626
expect(result.stderr).toBe("");
2727
});
2828

src/commands/mkdir/mkdir.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ describe("mkdir", () => {
99
expect(result.stderr).toBe("");
1010
expect(result.exitCode).toBe(0);
1111
const ls = await env.exec("ls /");
12-
// /bin and /usr always exist for PATH-based command resolution
13-
expect(ls.stdout).toBe("bin\nnewdir\nusr\n");
12+
// /bin, /usr, /dev, /proc always exist
13+
expect(ls.stdout).toBe("bin\ndev\nnewdir\nproc\nusr\n");
1414
});
1515

1616
it("should create multiple directories", async () => {
1717
const env = new Bash({ cwd: "/" });
1818
await env.exec("mkdir /dir1 /dir2 /dir3");
1919
const ls = await env.exec("ls /");
20-
// /bin and /usr always exist for PATH-based command resolution
21-
expect(ls.stdout).toBe("bin\ndir1\ndir2\ndir3\nusr\n");
20+
// /bin, /usr, /dev, /proc always exist
21+
expect(ls.stdout).toBe("bin\ndev\ndir1\ndir2\ndir3\nproc\nusr\n");
2222
});
2323

2424
it("should create nested directories with -p", async () => {

src/commands/rm/rm.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ describe("rm", () => {
2424
});
2525
await env.exec("rm /a.txt /b.txt /c.txt");
2626
const ls = await env.exec("ls /");
27-
// /bin and /usr always exist for PATH-based command resolution
28-
expect(ls.stdout).toBe("bin\nusr\n");
27+
// /bin, /usr, /dev, /proc always exist
28+
expect(ls.stdout).toBe("bin\ndev\nproc\nusr\n");
2929
});
3030

3131
it("should error on missing file", async () => {

src/fs/init.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Filesystem Initialization
3+
*
4+
* Sets up the default filesystem structure for the bash environment
5+
* including /dev, /proc, and common directories.
6+
*/
7+
8+
import { formatProcStatus, KERNEL_VERSION } from "../shell-metadata.js";
9+
import type { IFileSystem } from "./interface.js";
10+
11+
/**
12+
* Interface for filesystems that support sync initialization
13+
* (both InMemoryFs and OverlayFs implement these)
14+
*/
15+
interface SyncInitFs {
16+
mkdirSync(path: string, options?: { recursive?: boolean }): void;
17+
writeFileSync(path: string, content: string | Uint8Array): void;
18+
}
19+
20+
/**
21+
* Check if filesystem supports sync initialization
22+
*/
23+
function isSyncInitFs(fs: IFileSystem): fs is IFileSystem & SyncInitFs {
24+
const maybeFs = fs as unknown as Partial<SyncInitFs>;
25+
return (
26+
typeof maybeFs.mkdirSync === "function" &&
27+
typeof maybeFs.writeFileSync === "function"
28+
);
29+
}
30+
31+
/**
32+
* Initialize common directories like /home/user and /tmp
33+
*/
34+
function initCommonDirectories(
35+
fs: SyncInitFs,
36+
useDefaultLayout: boolean,
37+
): void {
38+
// Always create /bin for PATH-based command resolution
39+
fs.mkdirSync("/bin", { recursive: true });
40+
fs.mkdirSync("/usr/bin", { recursive: true });
41+
42+
// Create additional directories only for default layout
43+
if (useDefaultLayout) {
44+
fs.mkdirSync("/home/user", { recursive: true });
45+
fs.mkdirSync("/tmp", { recursive: true });
46+
}
47+
}
48+
49+
/**
50+
* Initialize /dev with common device files
51+
*/
52+
function initDevFiles(fs: SyncInitFs): void {
53+
fs.mkdirSync("/dev", { recursive: true });
54+
fs.writeFileSync("/dev/null", "");
55+
fs.writeFileSync("/dev/zero", new Uint8Array(0));
56+
fs.writeFileSync("/dev/stdin", "");
57+
fs.writeFileSync("/dev/stdout", "");
58+
fs.writeFileSync("/dev/stderr", "");
59+
}
60+
61+
/**
62+
* Initialize /proc with simulated process information
63+
*/
64+
function initProcFiles(fs: SyncInitFs): void {
65+
fs.mkdirSync("/proc/self/fd", { recursive: true });
66+
67+
// Kernel version (from shared metadata)
68+
fs.writeFileSync("/proc/version", `${KERNEL_VERSION}\n`);
69+
70+
// Process info (from shared metadata)
71+
fs.writeFileSync("/proc/self/exe", "/bin/bash");
72+
fs.writeFileSync("/proc/self/cmdline", "bash\0");
73+
fs.writeFileSync("/proc/self/comm", "bash\n");
74+
fs.writeFileSync("/proc/self/status", formatProcStatus());
75+
76+
// File descriptors
77+
fs.writeFileSync("/proc/self/fd/0", "/dev/stdin");
78+
fs.writeFileSync("/proc/self/fd/1", "/dev/stdout");
79+
fs.writeFileSync("/proc/self/fd/2", "/dev/stderr");
80+
}
81+
82+
/**
83+
* Initialize the filesystem with standard directories and files
84+
* Works with both InMemoryFs and OverlayFs (both write to memory)
85+
*/
86+
export function initFilesystem(
87+
fs: IFileSystem,
88+
useDefaultLayout: boolean,
89+
): void {
90+
// Initialize for filesystems that support sync methods (InMemoryFs and OverlayFs)
91+
if (isSyncInitFs(fs)) {
92+
initCommonDirectories(fs, useDefaultLayout);
93+
initDevFiles(fs);
94+
initProcFiles(fs);
95+
}
96+
}

src/fs/overlay-fs/overlay-fs.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,52 @@ export class OverlayFs implements IFileSystem {
145145
return this.mountPoint;
146146
}
147147

148+
/**
149+
* Create a virtual directory in memory (sync, for initialization)
150+
*/
151+
mkdirSync(path: string, _options?: MkdirOptions): void {
152+
const normalized = this.normalizePath(path);
153+
const parts = normalized.split("/").filter(Boolean);
154+
let current = "";
155+
for (const part of parts) {
156+
current += `/${part}`;
157+
if (!this.memory.has(current)) {
158+
this.memory.set(current, {
159+
type: "directory",
160+
mode: 0o755,
161+
mtime: new Date(),
162+
});
163+
}
164+
}
165+
}
166+
167+
/**
168+
* Create a virtual file in memory (sync, for initialization)
169+
*/
170+
writeFileSync(path: string, content: string | Uint8Array): void {
171+
const normalized = this.normalizePath(path);
172+
// Ensure parent directories exist
173+
const parent = this.getDirname(normalized);
174+
if (parent !== "/") {
175+
this.mkdirSync(parent);
176+
}
177+
const buffer =
178+
content instanceof Uint8Array
179+
? content
180+
: new TextEncoder().encode(content);
181+
this.memory.set(normalized, {
182+
type: "file",
183+
content: buffer,
184+
mode: 0o644,
185+
mtime: new Date(),
186+
});
187+
}
188+
189+
private getDirname(path: string): string {
190+
const lastSlash = path.lastIndexOf("/");
191+
return lastSlash === 0 ? "/" : path.slice(0, lastSlash);
192+
}
193+
148194
/**
149195
* Normalize a virtual path (resolve . and .., ensure starts with /)
150196
*/

src/interpreter/expansion.ts

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -635,33 +635,104 @@ export async function expandWordWithGlob(
635635
}
636636
}
637637

638-
// Handle unquoted $@ and $* specially - they should expand to individual args
639-
// without IFS-based word splitting
640-
if (!hasQuoted && hasArrayVar) {
641-
// Check if this is purely $@ or $* (not part of a larger word)
642-
if (wordParts.length === 1 && wordParts[0].type === "ParameterExpansion") {
643-
const param = wordParts[0].parameter;
644-
if (param === "@" || param === "*") {
645-
// Get individual positional parameters
646-
const numParams = Number.parseInt(ctx.state.env["#"] || "0", 10);
647-
if (numParams === 0) {
648-
return { values: [], quoted: false };
649-
}
650-
const params: string[] = [];
651-
for (let i = 1; i <= numParams; i++) {
652-
params.push(ctx.state.env[String(i)] || "");
638+
// Handle "$@" and "$*" with adjacent text inside double quotes, e.g., "-$@-"
639+
// "$@": Each positional parameter becomes a separate word, with prefix joined to first
640+
// and suffix joined to last. If no params, produces nothing (or just prefix+suffix if present)
641+
// "$*": All params joined with IFS as ONE word. If no params, produces one empty word.
642+
if (wordParts.length === 1 && wordParts[0].type === "DoubleQuoted") {
643+
const dqPart = wordParts[0];
644+
// Find if there's a $@ or $* inside
645+
let atIndex = -1;
646+
let isStar = false;
647+
for (let i = 0; i < dqPart.parts.length; i++) {
648+
const p = dqPart.parts[i];
649+
if (
650+
p.type === "ParameterExpansion" &&
651+
(p.parameter === "@" || p.parameter === "*")
652+
) {
653+
atIndex = i;
654+
isStar = p.parameter === "*";
655+
break;
656+
}
657+
}
658+
659+
if (atIndex !== -1) {
660+
// Check if this is a simple $@ or $* without operations like ${*-default}
661+
const paramPart = dqPart.parts[atIndex];
662+
if (paramPart.type === "ParameterExpansion" && paramPart.operation) {
663+
// Has an operation - let normal expansion handle it
664+
atIndex = -1;
665+
}
666+
}
667+
668+
if (atIndex !== -1) {
669+
// Get positional parameters
670+
const numParams = Number.parseInt(ctx.state.env["#"] || "0", 10);
671+
672+
// Expand prefix (parts before $@/$*)
673+
let prefix = "";
674+
for (let i = 0; i < atIndex; i++) {
675+
prefix += await expandPart(ctx, dqPart.parts[i]);
676+
}
677+
678+
// Expand suffix (parts after $@/$*)
679+
let suffix = "";
680+
for (let i = atIndex + 1; i < dqPart.parts.length; i++) {
681+
suffix += await expandPart(ctx, dqPart.parts[i]);
682+
}
683+
684+
if (numParams === 0) {
685+
if (isStar) {
686+
// "$*" with no params -> one empty word (prefix + suffix)
687+
return { values: [prefix + suffix], quoted: true };
653688
}
654-
return { values: params, quoted: false };
689+
// "$@" with no params -> no words (unless there's prefix/suffix)
690+
const combined = prefix + suffix;
691+
return { values: combined ? [combined] : [], quoted: true };
692+
}
693+
694+
// Get individual positional parameters
695+
const params: string[] = [];
696+
for (let i = 1; i <= numParams; i++) {
697+
params.push(ctx.state.env[String(i)] || "");
655698
}
699+
700+
if (isStar) {
701+
// "$*" - join all params with IFS into one word
702+
const ifsSep = getIfsSeparator(ctx.state.env);
703+
return {
704+
values: [prefix + params.join(ifsSep) + suffix],
705+
quoted: true,
706+
};
707+
}
708+
709+
// "$@" - each param is a separate word
710+
// Join prefix with first, suffix with last
711+
if (params.length === 1) {
712+
return { values: [prefix + params[0] + suffix], quoted: true };
713+
}
714+
715+
const result = [
716+
prefix + params[0],
717+
...params.slice(1, -1),
718+
params[params.length - 1] + suffix,
719+
];
720+
return { values: result, quoted: true };
656721
}
657722
}
658723

724+
// Note: Unquoted $@ and $* are handled by normal expansion + word splitting.
725+
// They expand to positional parameters joined by space, then split on IFS.
726+
// The special handling above is only for quoted "$@" and "$*" inside double quotes.
727+
659728
// No brace expansion or single value - use original logic
660729
// Word splitting based on IFS
661730
// If IFS is set to empty string, no word splitting occurs
662731
// Word splitting applies to results of parameter expansion, command substitution, and arithmetic expansion
732+
// Note: hasQuoted being true does NOT prevent word splitting - unquoted expansions like $a in $a"$b"
733+
// should still be split. The smartWordSplit function handles this by treating quoted parts as
734+
// non-splittable segments that join with adjacent fields.
663735
if (
664-
!hasQuoted &&
665736
(hasCommandSub || hasArrayVar || hasParamExpansion) &&
666737
!isIfsEmpty(ctx.state.env)
667738
) {

0 commit comments

Comments
 (0)