Skip to content

Commit 7b93b1e

Browse files
committed
binary
1 parent 5a14c36 commit 7b93b1e

Some content is hidden

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

43 files changed

+3164
-979
lines changed

PROJECT.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,6 @@ xargs — build argument lists
195195
## Implementation phase 12
196196

197197
- Lazy load commands via dynamic import
198+
- Command should be eagerly registered
199+
- But their implementations should only be loaded when they are actually called
200+
- Make sure that files support Buffers and encoding

src/BashEnv.ts

Lines changed: 9 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,6 @@
1-
import { aliasCommand, unaliasCommand } from "./commands/alias/alias.js";
2-
import { awkCommand } from "./commands/awk/awk.js";
3-
import { basenameCommand } from "./commands/basename/basename.js";
4-
import { bashCommand, shCommand } from "./commands/bash/bash.js";
5-
import { catCommand } from "./commands/cat/cat.js";
6-
import { chmodCommand } from "./commands/chmod/chmod.js";
7-
import { clearCommand } from "./commands/clear/clear.js";
8-
import { cpCommand } from "./commands/cp/cp.js";
9-
import { cutCommand } from "./commands/cut/cut.js";
10-
import { dirnameCommand } from "./commands/dirname/dirname.js";
11-
import { duCommand } from "./commands/du/du.js";
12-
// Import commands
13-
import { echoCommand } from "./commands/echo/echo.js";
14-
import { envCommand, printenvCommand } from "./commands/env/env.js";
15-
import { findCommand } from "./commands/find/find.js";
16-
import { grepCommand } from "./commands/grep/grep.js";
17-
import { headCommand } from "./commands/head/head.js";
18-
import { historyCommand } from "./commands/history/history.js";
19-
import { lnCommand } from "./commands/ln/ln.js";
20-
import { lsCommand } from "./commands/ls/ls.js";
21-
import { mkdirCommand } from "./commands/mkdir/mkdir.js";
22-
import { mvCommand } from "./commands/mv/mv.js";
23-
import { printfCommand } from "./commands/printf/printf.js";
24-
import { pwdCommand } from "./commands/pwd/pwd.js";
25-
import { readlinkCommand } from "./commands/readlink/readlink.js";
26-
import { rmCommand } from "./commands/rm/rm.js";
27-
import { sedCommand } from "./commands/sed/sed.js";
28-
import { sortCommand } from "./commands/sort/sort.js";
29-
import { statCommand } from "./commands/stat/stat.js";
30-
import { tailCommand } from "./commands/tail/tail.js";
31-
import { teeCommand } from "./commands/tee/tee.js";
32-
import { touchCommand } from "./commands/touch/touch.js";
33-
import { trCommand } from "./commands/tr/tr.js";
34-
import { treeCommand } from "./commands/tree/tree.js";
35-
import { falseCommand, trueCommand } from "./commands/true/true.js";
36-
import { uniqCommand } from "./commands/uniq/uniq.js";
37-
import { wcCommand } from "./commands/wc/wc.js";
38-
import { xargsCommand } from "./commands/xargs/xargs.js";
1+
import { createLazyCommands } from "./commands/registry.js";
392
import { type IFileSystem, VirtualFs } from "./fs.js";
3+
import type { FileContent } from "./fs-interface.js";
404
import {
415
GlobExpander,
426
type Pipeline,
@@ -60,7 +24,7 @@ export interface BashEnvOptions {
6024
* Initial files to populate the virtual filesystem.
6125
* Only used when fs is not provided.
6226
*/
63-
files?: Record<string, string>;
27+
files?: Record<string, FileContent>;
6428
/**
6529
* Environment variables
6630
*/
@@ -90,7 +54,7 @@ export interface BashEnvOptions {
9054
}
9155

9256
export class BashEnv {
93-
private fs: IFileSystem;
57+
readonly fs: IFileSystem;
9458
private cwd: string;
9559
private env: Record<string, string>;
9660
private commands: CommandRegistry = new Map();
@@ -150,48 +114,11 @@ export class BashEnv {
150114
}
151115
}
152116

153-
// Register built-in commands
154-
this.registerCommand(echoCommand);
155-
this.registerCommand(catCommand);
156-
this.registerCommand(lsCommand);
157-
this.registerCommand(mkdirCommand);
158-
this.registerCommand(pwdCommand);
159-
this.registerCommand(touchCommand);
160-
this.registerCommand(rmCommand);
161-
this.registerCommand(cpCommand);
162-
this.registerCommand(mvCommand);
163-
this.registerCommand(headCommand);
164-
this.registerCommand(tailCommand);
165-
this.registerCommand(wcCommand);
166-
this.registerCommand(grepCommand);
167-
this.registerCommand(sortCommand);
168-
this.registerCommand(uniqCommand);
169-
this.registerCommand(findCommand);
170-
this.registerCommand(sedCommand);
171-
this.registerCommand(cutCommand);
172-
this.registerCommand(trCommand);
173-
this.registerCommand(trueCommand);
174-
this.registerCommand(falseCommand);
175-
this.registerCommand(basenameCommand);
176-
this.registerCommand(dirnameCommand);
177-
this.registerCommand(teeCommand);
178-
this.registerCommand(xargsCommand);
179-
this.registerCommand(envCommand);
180-
this.registerCommand(printenvCommand);
181-
this.registerCommand(treeCommand);
182-
this.registerCommand(statCommand);
183-
this.registerCommand(duCommand);
184-
this.registerCommand(awkCommand);
185-
this.registerCommand(chmodCommand);
186-
this.registerCommand(clearCommand);
187-
this.registerCommand(aliasCommand);
188-
this.registerCommand(unaliasCommand);
189-
this.registerCommand(historyCommand);
190-
this.registerCommand(lnCommand);
191-
this.registerCommand(readlinkCommand);
192-
this.registerCommand(printfCommand);
193-
this.registerCommand(bashCommand);
194-
this.registerCommand(shCommand);
117+
// Register built-in commands with lazy loading
118+
// Commands are registered eagerly but implementations load on first use
119+
for (const cmd of createLazyCommands()) {
120+
this.registerCommand(cmd);
121+
}
195122
}
196123

197124
registerCommand(command: Command): void {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, it } from "vitest";
2+
import { BashEnv } from "../../BashEnv.js";
3+
4+
describe("awk with binary content", () => {
5+
it("should process binary file with awk", async () => {
6+
const env = new BashEnv({
7+
files: {
8+
"/data.bin": new Uint8Array([
9+
0x31,
10+
0x20,
11+
0x32,
12+
0x0a, // 1 2\n
13+
0x33,
14+
0x20,
15+
0x34,
16+
0x0a, // 3 4\n
17+
]),
18+
},
19+
});
20+
21+
const result = await env.exec("awk '{print $1 + $2}' /data.bin");
22+
expect(result.stdout).toBe("3\n7\n");
23+
expect(result.exitCode).toBe(0);
24+
});
25+
});

src/commands/awk/awk.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Command, CommandContext, ExecResult } from "../../types.js";
22
import { hasHelpFlag, showHelp, unknownOption } from "../help.js";
3-
import type { AwkContext } from "./types.js";
4-
import { parseAwkProgram } from "./parser.js";
53
import { executeAwkAction, matchesPattern } from "./executor.js";
4+
import { parseAwkProgram } from "./parser.js";
5+
import type { AwkContext } from "./types.js";
66

77
const awkHelp = {
88
name: "awk",

src/commands/awk/executor.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import type { AwkContext } from "./types.js";
2-
import {
3-
evaluateExpression,
4-
evaluateCondition,
5-
evaluateConcatenation,
6-
} from "./expressions.js";
1+
import { evaluateCondition, evaluateExpression } from "./expressions.js";
72
import { findMatchingBrace } from "./parser.js";
3+
import type { AwkContext } from "./types.js";
84

95
export function executeAwkAction(action: string, ctx: AwkContext): string {
106
let output = "";
@@ -511,7 +507,7 @@ function evaluatePrintf(args: string, ctx: AwkContext): string {
511507
valueIdx++;
512508
i = j + 1;
513509
} else if (spec === "d" || spec === "i") {
514-
let val = values[valueIdx]
510+
const val = values[valueIdx]
515511
? Math.floor(Number(evaluateExpression(values[valueIdx], ctx)))
516512
: 0;
517513
let valStr = String(val);
@@ -522,7 +518,7 @@ function evaluatePrintf(args: string, ctx: AwkContext): string {
522518
valueIdx++;
523519
i = j + 1;
524520
} else if (spec === "f") {
525-
let val = values[valueIdx]
521+
const val = values[valueIdx]
526522
? Number(evaluateExpression(values[valueIdx], ctx))
527523
: 0;
528524
const prec = precision ? parseInt(precision, 10) : 6;

src/commands/awk/expressions.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import type { AwkContext } from "./types.js";
21
import {
3-
awkLength,
4-
awkSubstr,
2+
awkGsub,
53
awkIndex,
4+
awkLength,
65
awkSplit,
6+
awkSprintf,
77
awkSub,
8-
awkGsub,
8+
awkSubstr,
99
awkTolower,
1010
awkToupper,
11-
awkSprintf,
1211
} from "./functions.js";
12+
import type { AwkContext } from "./types.js";
1313

1414
export function evaluateExpression(
1515
expr: string,
@@ -292,7 +292,9 @@ export function evaluateCondition(condition: string, ctx: AwkContext): boolean {
292292
if (inMatch) {
293293
const key = String(evaluateExpression(inMatch[1].trim(), ctx));
294294
const arrayName = inMatch[2];
295-
return !!(ctx.arrays[arrayName] && ctx.arrays[arrayName][key] !== undefined);
295+
return !!(
296+
ctx.arrays[arrayName] && ctx.arrays[arrayName][key] !== undefined
297+
);
296298
}
297299

298300
// NR comparisons
@@ -308,8 +310,7 @@ export function evaluateCondition(condition: string, ctx: AwkContext): boolean {
308310
if (fieldRegex) {
309311
const fieldNum = parseInt(fieldRegex[1], 10);
310312
const pattern = fieldRegex[2];
311-
const fieldVal =
312-
fieldNum === 0 ? ctx.line : ctx.fields[fieldNum - 1] || "";
313+
const fieldVal = fieldNum === 0 ? ctx.line : ctx.fields[fieldNum - 1] || "";
313314
return new RegExp(pattern).test(fieldVal);
314315
}
315316

@@ -318,8 +319,7 @@ export function evaluateCondition(condition: string, ctx: AwkContext): boolean {
318319
if (fieldNotRegex) {
319320
const fieldNum = parseInt(fieldNotRegex[1], 10);
320321
const pattern = fieldNotRegex[2];
321-
const fieldVal =
322-
fieldNum === 0 ? ctx.line : ctx.fields[fieldNum - 1] || "";
322+
const fieldVal = fieldNum === 0 ? ctx.line : ctx.fields[fieldNum - 1] || "";
323323
return !new RegExp(pattern).test(fieldVal);
324324
}
325325

src/commands/awk/functions.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,7 @@ export function awkSprintf(
190190

191191
const spec = format[j];
192192
if (spec === "s" || spec === "d" || spec === "i" || spec === "f") {
193-
const val = values[valueIdx]
194-
? evaluateExpr(values[valueIdx], ctx)
195-
: "";
193+
const val = values[valueIdx] ? evaluateExpr(values[valueIdx], ctx) : "";
196194
result += String(val);
197195
valueIdx++;
198196
i = j + 1;

src/commands/awk/parser.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,7 @@ export function parseAwkProgram(program: string): ParsedProgram {
7575
}
7676
} else {
7777
// Condition only (no action) or just a print expression
78-
if (
79-
remaining.startsWith("print") ||
80-
remaining.startsWith("printf")
81-
) {
78+
if (remaining.startsWith("print") || remaining.startsWith("printf")) {
8279
result.main.push({ pattern: null, action: remaining });
8380
} else {
8481
// It's a condition without action - default to print
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from "vitest";
2+
import { BashEnv } from "../../BashEnv.js";
3+
4+
describe("cat with binary files", () => {
5+
it("should output binary content unchanged", async () => {
6+
const env = new BashEnv({
7+
files: {
8+
"/binary.bin": new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]), // "Hello"
9+
},
10+
});
11+
12+
const result = await env.exec("cat /binary.bin");
13+
expect(result.stdout).toBe("Hello");
14+
expect(result.exitCode).toBe(0);
15+
});
16+
17+
it("should handle null bytes in content", async () => {
18+
const env = new BashEnv({
19+
files: {
20+
"/binary.bin": new Uint8Array([0x41, 0x00, 0x42, 0x00, 0x43]), // A\0B\0C
21+
},
22+
});
23+
24+
const result = await env.exec("cat /binary.bin");
25+
expect(result.stdout).toBe("A\0B\0C");
26+
expect(result.exitCode).toBe(0);
27+
});
28+
29+
it("should concatenate multiple binary files", async () => {
30+
const env = new BashEnv({
31+
files: {
32+
"/a.bin": new Uint8Array([0x41, 0x42]), // "AB"
33+
"/b.bin": new Uint8Array([0x43, 0x44]), // "CD"
34+
},
35+
});
36+
37+
const result = await env.exec("cat /a.bin /b.bin");
38+
expect(result.stdout).toBe("ABCD");
39+
expect(result.exitCode).toBe(0);
40+
});
41+
42+
it("should number lines in binary file with -n", async () => {
43+
const env = new BashEnv({
44+
files: {
45+
"/binary.bin": new Uint8Array([0x41, 0x0a, 0x42, 0x0a]), // "A\nB\n"
46+
},
47+
});
48+
49+
const result = await env.exec("cat -n /binary.bin");
50+
expect(result.stdout).toBe(" 1\tA\n 2\tB\n");
51+
expect(result.exitCode).toBe(0);
52+
});
53+
});

src/commands/cp/cp.binary.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, it } from "vitest";
2+
import { BashEnv } from "../../BashEnv.js";
3+
4+
describe("cp with binary files", () => {
5+
it("should copy binary file preserving content", async () => {
6+
const data = new Uint8Array([0x00, 0xff, 0x00, 0xff, 0x7f]);
7+
const env = new BashEnv({
8+
files: { "/src.bin": data },
9+
});
10+
11+
await env.exec("cp /src.bin /dst.bin");
12+
13+
// Check the copied file's raw bytes via the fs directly
14+
// (cat returns string which can't faithfully represent 0xff bytes)
15+
const copiedContent = await env.fs.readFileBuffer("/dst.bin");
16+
expect(copiedContent.length).toBe(5);
17+
expect(copiedContent[0]).toBe(0x00);
18+
expect(copiedContent[1]).toBe(0xff);
19+
expect(copiedContent[2]).toBe(0x00);
20+
expect(copiedContent[3]).toBe(0xff);
21+
expect(copiedContent[4]).toBe(0x7f);
22+
});
23+
});

0 commit comments

Comments
 (0)