Skip to content
Merged
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
130 changes: 130 additions & 0 deletions packages/just-bash/src/commands/cat/cat.display-flags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { describe, expect, it } from "vitest";
import { Bash } from "../../Bash.js";

describe("cat -E / -T (show ends and tabs)", () => {
it("-E appends $ before each newline", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\nb\\n" | cat -E');
expect(r.stdout).toBe("a$\nb$\n");
expect(r.stderr).toBe("");
expect(r.exitCode).toBe(0);
});

it("-E does not append $ to a final unterminated line", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\nb" | cat -E');
expect(r.stdout).toBe("a$\nb");
expect(r.stderr).toBe("");
});

it("-T renders TAB as ^I", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\tb\\n" | cat -T');
expect(r.stdout).toBe("a^Ib\n");
expect(r.stderr).toBe("");
});

it("long options --show-ends and --show-tabs work", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\tb\\n" | cat --show-ends --show-tabs');
expect(r.stdout).toBe("a^Ib$\n");
expect(r.stderr).toBe("");
});
});

describe("cat flag aliases (-A/-e/-t)", () => {
it("-A is equivalent to -vET", async () => {
const env = new Bash();
const a = await env.exec('printf "a\\tb\\n" | cat -A');
const vet = await env.exec('printf "a\\tb\\n" | cat -vET');
expect(a.stdout).toBe("a^Ib$\n");
expect(a.stdout).toBe(vet.stdout);
expect(a.stderr).toBe("");
});

it("-e is equivalent to -vE (does not expand tabs)", async () => {
const env = new Bash();
const e = await env.exec('printf "a\\tb\\n" | cat -e');
const vE = await env.exec('printf "a\\tb\\n" | cat -vE');
expect(e.stdout).toBe("a\tb$\n");
expect(e.stdout).toBe(vE.stdout);
});

it("-t is equivalent to -vT (no $ at end)", async () => {
const env = new Bash();
const t = await env.exec('printf "a\\tb\\n" | cat -t');
const vT = await env.exec('printf "a\\tb\\n" | cat -vT');
expect(t.stdout).toBe("a^Ib\n");
expect(t.stdout).toBe(vT.stdout);
});

it("--show-all is equivalent to -vET", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\tb\\n" | cat --show-all');
expect(r.stdout).toBe("a^Ib$\n");
});
});

describe("cat -n / -b numbering", () => {
it("-n numbers every line including blank lines", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\n\\nb\\n" | cat -n');
expect(r.stdout).toBe(" 1\ta\n 2\t\n 3\tb\n");
expect(r.stderr).toBe("");
});

it("-b numbers only non-blank lines", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\n\\nb\\n" | cat -b');
expect(r.stdout).toBe(" 1\ta\n\n 2\tb\n");
expect(r.stderr).toBe("");
});

it("-b overrides -n when both are given", async () => {
const env = new Bash();
const bn = await env.exec('printf "a\\n\\nb\\n" | cat -bn');
const b = await env.exec('printf "a\\n\\nb\\n" | cat -b');
expect(bn.stdout).toBe(" 1\ta\n\n 2\tb\n");
expect(bn.stdout).toBe(b.stdout);
});

it("--number-nonblank long option works", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\n\\nb\\n" | cat --number-nonblank');
expect(r.stdout).toBe(" 1\ta\n\n 2\tb\n");
});

it("numbers a final unterminated line with no trailing newline", async () => {
const env = new Bash();
const r = await env.exec('printf "a" | cat -n');
expect(r.stdout).toBe(" 1\ta");
expect(r.stderr).toBe("");
});
});

describe("cat -s (squeeze blank lines)", () => {
it("collapses runs of adjacent blank lines to one", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\n\\n\\n\\nb\\n" | cat -s');
expect(r.stdout).toBe("a\n\nb\n");
expect(r.stderr).toBe("");
});

it("collapses leading blank lines", async () => {
const env = new Bash();
const r = await env.exec('printf "\\n\\n\\na\\n" | cat -s');
expect(r.stdout).toBe("\na\n");
});

it("squeeze happens before numbering", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\n\\n\\nb\\n" | cat -sn');
expect(r.stdout).toBe(" 1\ta\n 2\t\n 3\tb\n");
});

it("--squeeze-blank long option works", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\n\\n\\nb\\n" | cat --squeeze-blank');
expect(r.stdout).toBe("a\n\nb\n");
});
});
117 changes: 117 additions & 0 deletions packages/just-bash/src/commands/cat/cat.flags-misc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { describe, expect, it } from "vitest";
import { Bash } from "../../Bash.js";

describe("cat combined and long options", () => {
it("combined -An expands to number + show-all", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\tb\\n" | cat -An');
expect(r.stdout).toBe(" 1\ta^Ib$\n");
expect(r.stderr).toBe("");
expect(r.exitCode).toBe(0);
});

it("combined -bE numbers non-blank and shows ends", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\n\\nb\\n" | cat -bE');
expect(r.stdout).toBe(" 1\ta$\n$\n 2\tb$\n");
expect(r.stderr).toBe("");
});

it("--number long option works", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\nb\\n" | cat --number');
expect(r.stdout).toBe(" 1\ta\n 2\tb\n");
});

it("--show-nonprinting long option works", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\001b\\n" | cat --show-nonprinting');
expect(r.stdout).toBe("a^Ab\n");
});
});

describe("cat -u (ignored no-op)", () => {
it("-u alone does not change output", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\nb\\n" | cat -u');
expect(r.stdout).toBe("a\nb\n");
expect(r.stderr).toBe("");
expect(r.exitCode).toBe(0);
});

it("-u combined with -n still numbers", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\n" | cat -un');
expect(r.stdout).toBe(" 1\ta\n");
expect(r.stderr).toBe("");
});
});

describe("cat unknown flags", () => {
it("errors on an unknown short flag", async () => {
const env = new Bash();
const r = await env.exec("cat -Z");
expect(r.stdout).toBe("");
expect(r.stderr).toBe("cat: invalid option -- 'Z'\n");
expect(r.exitCode).toBe(1);
});

it("errors on an unknown long flag", async () => {
const env = new Bash();
const r = await env.exec("cat --bogus");
expect(r.stdout).toBe("");
expect(r.stderr).toBe("cat: unrecognized option '--bogus'\n");
expect(r.exitCode).toBe(1);
});
});

describe("cat multi-file numbering with display flags", () => {
it("continues line numbers across files with -n", async () => {
const env = new Bash({
files: { "/a.txt": "a1\na2\n", "/b.txt": "b1\nb2\n" },
});
const r = await env.exec("cat -n /a.txt /b.txt");
expect(r.stdout).toBe(" 1\ta1\n 2\ta2\n 3\tb1\n 4\tb2\n");
expect(r.stderr).toBe("");
});

it("squeezes blank lines across the file boundary", async () => {
const env = new Bash({
files: { "/a.txt": "a\n\n", "/b.txt": "\n\nb\n" },
});
const r = await env.exec("cat -s /a.txt /b.txt");
expect(r.stdout).toBe("a\n\nb\n");
expect(r.stderr).toBe("");
});

it("applies -E across a file that does not end in newline", async () => {
const env = new Bash({
files: { "/a.txt": "a", "/b.txt": "b\n" },
});
const r = await env.exec("cat -E /a.txt /b.txt");
expect(r.stdout).toBe("ab$\n");
expect(r.stderr).toBe("");
});
});

describe("cat --help lists display flags", () => {
it("mentions each supported flag", async () => {
const env = new Bash();
const r = await env.exec("cat --help");
expect(r.exitCode).toBe(0);
for (const opt of [
"--show-all",
"--number-nonblank",
"--show-ends",
"--number",
"--squeeze-blank",
"--show-tabs",
"--show-nonprinting",
"-e",
"-t",
"-u",
]) {
expect(r.stdout).toContain(opt);
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import { Bash } from "../../Bash.js";

/** Independent reference implementation of the GNU `cat -v` byte table. */
function vRef(byte: number, showTabs: boolean): string {
if (byte === 9) return showTabs ? "^I" : "\t";
if (byte >= 32) {
if (byte < 127) return String.fromCharCode(byte);
if (byte === 127) return "^?";
const c = byte - 128;
if (c >= 32) return c === 127 ? "M-^?" : `M-${String.fromCharCode(c)}`;
return `M-^${String.fromCharCode(c + 64)}`;
}
return `^${String.fromCharCode(byte + 64)}`;
}

describe("cat -v byte table (GNU semantics)", () => {
it("transforms every byte 0-255 (except LF) exactly", async () => {
const codes: number[] = [];
for (let b = 0; b <= 255; b++) {
if (b !== 10) codes.push(b);
}
codes.push(10); // trailing newline terminator
const env = new Bash({ files: { "/bytes.bin": new Uint8Array(codes) } });
const r = await env.exec("cat -v /bytes.bin");

let expected = "";
for (const b of codes) {
if (b === 10) expected += "\n";
else expected += vRef(b, false);
}
expect(r.stdout).toBe(expected);
expect(r.stderr).toBe("");
expect(r.exitCode).toBe(0);
});

it("matches known control-character representations", async () => {
const env = new Bash({
files: {
"/c.bin": new Uint8Array([0, 1, 7, 8, 27, 31, 32, 126, 127, 10]),
},
});
const r = await env.exec("cat -v /c.bin");
expect(r.stdout).toBe("^@^A^G^H^[^_ ~^?\n");
});

it("renders high bytes with M- notation", async () => {
const env = new Bash({
files: {
"/h.bin": new Uint8Array([128, 129, 155, 159, 160, 200, 254, 255, 10]),
},
});
const r = await env.exec("cat -v /h.bin");
expect(r.stdout).toBe("M-^@M-^AM-^[M-^_M- M-HM-~M-^?\n");
});

it("leaves TAB literal under -v but renders ^I under -vT", async () => {
const env = new Bash({
files: { "/t.bin": new Uint8Array([97, 9, 98, 10]) },
});
const v = await env.exec("cat -v /t.bin");
expect(v.stdout).toBe("a\tb\n");
const vt = await env.exec("cat -vT /t.bin");
expect(vt.stdout).toBe("a^Ib\n");
});

it("never transforms the newline terminator", async () => {
const env = new Bash();
const r = await env.exec('printf "a\\nb\\n" | cat -v');
expect(r.stdout).toBe("a\nb\n");
});

it("renders UTF-8 multibyte bytes with M- notation", async () => {
// "é" is UTF-8 0xC3 0xA9
const env = new Bash({
files: { "/u.bin": new Uint8Array([0xc3, 0xa9, 10]) },
});
const r = await env.exec("cat -v /u.bin");
expect(r.stdout).toBe("M-CM-)\n");
});
});
Loading
Loading