diff --git a/packages/just-bash/src/commands/cat/cat.display-flags.test.ts b/packages/just-bash/src/commands/cat/cat.display-flags.test.ts new file mode 100644 index 00000000..22798329 --- /dev/null +++ b/packages/just-bash/src/commands/cat/cat.display-flags.test.ts @@ -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"); + }); +}); diff --git a/packages/just-bash/src/commands/cat/cat.flags-misc.test.ts b/packages/just-bash/src/commands/cat/cat.flags-misc.test.ts new file mode 100644 index 00000000..0a3a9d11 --- /dev/null +++ b/packages/just-bash/src/commands/cat/cat.flags-misc.test.ts @@ -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); + } + }); +}); diff --git a/packages/just-bash/src/commands/cat/cat.show-nonprinting.test.ts b/packages/just-bash/src/commands/cat/cat.show-nonprinting.test.ts new file mode 100644 index 00000000..b42d5fa3 --- /dev/null +++ b/packages/just-bash/src/commands/cat/cat.show-nonprinting.test.ts @@ -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"); + }); +}); diff --git a/packages/just-bash/src/commands/cat/cat.ts b/packages/just-bash/src/commands/cat/cat.ts index 867c85bc..5a7d3b34 100644 --- a/packages/just-bash/src/commands/cat/cat.ts +++ b/packages/just-bash/src/commands/cat/cat.ts @@ -9,15 +9,50 @@ const catHelp = { summary: "concatenate files and print on the standard output", usage: "cat [OPTION]... [FILE]...", options: [ + "-A, --show-all equivalent to -vET", + "-b, --number-nonblank number nonempty output lines, overrides -n", + "-e equivalent to -vE", + "-E, --show-ends display $ at end of each line", "-n, --number number all output lines", + "-s, --squeeze-blank suppress repeated empty output lines", + "-t equivalent to -vT", + "-T, --show-tabs display TAB characters as ^I", + "-u (ignored)", + "-v, --show-nonprinting use ^ and M- notation, except for LFD and TAB", " --help display this help and exit", ], }; const argDefs = { number: { short: "n", long: "number", type: "boolean" as const }, + numberNonblank: { + short: "b", + long: "number-nonblank", + type: "boolean" as const, + }, + showEnds: { short: "E", long: "show-ends", type: "boolean" as const }, + showTabs: { short: "T", long: "show-tabs", type: "boolean" as const }, + showNonprinting: { + short: "v", + long: "show-nonprinting", + type: "boolean" as const, + }, + showAll: { short: "A", long: "show-all", type: "boolean" as const }, + squeeze: { short: "s", long: "squeeze-blank", type: "boolean" as const }, + vE: { short: "e", type: "boolean" as const }, + vT: { short: "t", type: "boolean" as const }, + ignored: { short: "u", type: "boolean" as const }, }; +interface CatOptions { + numberAll: boolean; + numberNonblank: boolean; + showEnds: boolean; + showTabs: boolean; + showNonprinting: boolean; + squeeze: boolean; +} + export const catCommand: Command = { name: "cat", @@ -28,8 +63,26 @@ export const catCommand: Command = { const parsed = parseArgs("cat", args, argDefs); if (!parsed.ok) return parsed.error; + const f = parsed.result.flags; + + // Alias expansion: -A == -vET, -e == -vE, -t == -vT. + const showEnds = f.showEnds || f.showAll || f.vE; + const showTabs = f.showTabs || f.showAll || f.vT; + const showNonprinting = f.showNonprinting || f.showAll || f.vE || f.vT; + const squeeze = f.squeeze; + // -b overrides -n. + const numberNonblank = f.numberNonblank; + const numberAll = f.number && !numberNonblank; + + const opts: CatOptions = { + numberAll, + numberNonblank, + showEnds, + showTabs, + showNonprinting, + squeeze, + }; - const showLineNumbers = parsed.result.flags.number; const files = parsed.result.positional; // Read files (allows "-" for stdin) @@ -39,21 +92,30 @@ export const catCommand: Command = { stopOnError: false, }); - let stdout = ""; - let lineNumber = 1; + let stdout: string; + const transform = + showEnds || + showTabs || + showNonprinting || + squeeze || + numberAll || + numberNonblank; - for (const { content } of readResult.files) { - // cat is byte-clean: emit raw bytes unchanged. The output boundary + if (!transform) { + // Byte-clean fast path: emit raw bytes unchanged. The output boundary // (Bash.exec) decodes UTF-8 sequences back to Unicode for terminals. - const bytes = latin1FromBytes(content); - if (showLineNumbers) { - // Real bash continues line numbers across files - const result = addLineNumbers(bytes, lineNumber); - stdout += result.content; - lineNumber = result.nextLineNumber; - } else { - stdout += bytes; + stdout = ""; + for (const { content } of readResult.files) { + stdout += latin1FromBytes(content); + } + } else { + // Numbering and squeeze state continue across all inputs, so process + // the concatenated byte stream as a single unit. + let stream = ""; + for (const { content } of readResult.files) { + stream += latin1FromBytes(content); } + stdout = formatCat(stream, opts); } // cat is byte-clean: it forwards every byte of stdin / file content @@ -70,24 +132,80 @@ export const catCommand: Command = { }, }; -function addLineNumbers( - content: string, - startLine: number, -): { content: string; nextLineNumber: number } { - const lines = content.split("\n"); - // Don't number the trailing empty line if file ends with newline - const hasTrailingNewline = content.endsWith("\n"); - const linesToNumber = hasTrailingNewline ? lines.slice(0, -1) : lines; - - const numbered = linesToNumber.map((line, i) => { - const num = String(startLine + i).padStart(6, " "); - return `${num}\t${line}`; - }); - - return { - content: numbered.join("\n") + (hasTrailingNewline ? "\n" : ""), - nextLineNumber: startLine + linesToNumber.length, - }; +/** + * GNU `cat -v` byte transformation (per byte 0-255). LF and TAB are handled + * by the caller and never passed here. + */ +function showNonprintingByte(byte: number): string { + if (byte >= 32) { + if (byte < 127) return String.fromCharCode(byte); + if (byte === 127) return "^?"; + // byte >= 128 + const c = byte - 128; + if (c >= 32) return c === 127 ? "M-^?" : `M-${String.fromCharCode(c)}`; + return `M-^${String.fromCharCode(c + 64)}`; + } + // byte < 32 (TAB and LF already handled by caller) + return `^${String.fromCharCode(byte + 64)}`; +} + +/** Apply -v / -T transforms to a single line's bytes (excludes the newline). */ +function transformLine(line: string, opts: CatOptions): string { + if (!opts.showNonprinting && !opts.showTabs) return line; + let out = ""; + for (let i = 0; i < line.length; i++) { + const b = line.charCodeAt(i); + if (b === 9) { + out += opts.showTabs ? "^I" : "\t"; + } else if (opts.showNonprinting) { + out += showNonprintingByte(b); + } else { + out += line[i]; + } + } + return out; +} + +/** + * Format the concatenated byte stream applying numbering, squeeze, and the + * -v/-T/-E transforms in GNU per-line order. + */ +function formatCat(stream: string, opts: CatOptions): string { + const parts = stream.split("\n"); + let out = ""; + let lineNo = 1; + let prevBlank = false; + + for (let i = 0; i < parts.length; i++) { + const isLast = i === parts.length - 1; + const terminated = !isLast; + const content = parts[i]; + + // A trailing empty segment means the stream ended with a newline (or was + // empty): there is no final partial line to emit. + if (isLast && content === "") break; + + const blank = content === ""; + + // Squeeze: collapse runs of adjacent blank lines into a single blank line. + if (opts.squeeze && blank && prevBlank) { + continue; + } + prevBlank = blank; + + let prefix = ""; + if (opts.numberAll || (opts.numberNonblank && !blank)) { + prefix = `${String(lineNo).padStart(6, " ")}\t`; + lineNo++; + } + + out += prefix + transformLine(content, opts); + if (terminated) { + out += opts.showEnds ? "$\n" : "\n"; + } + } + + return out; } import type { CommandFuzzInfo } from "../fuzz-flags-types.js"; diff --git a/packages/just-bash/src/comparison-tests/cat-show.comparison.test.ts b/packages/just-bash/src/comparison-tests/cat-show.comparison.test.ts new file mode 100644 index 00000000..48bcacde --- /dev/null +++ b/packages/just-bash/src/comparison-tests/cat-show.comparison.test.ts @@ -0,0 +1,163 @@ +import { afterEach, beforeEach, describe, it } from "vitest"; +import { + cleanupTestDir, + compareOutputs, + createTestDir, + setupFiles, +} from "./fixture-runner.js"; + +// GNU coreutils `cat` display flags (-A -b -e -E -s -t -T -v -u). +// macOS ships BSD `cat`, which lacks -A/-E/-T/long options and diverges on +// -v notation, so these fixtures are recorded against GNU cat and locked. +const BASIC = "line 1\nline 2\nline 3\n"; +const BLANKS = "a\n\n\n\nb\n\n\nc\n"; +const TABS = "col1\tcol2\tcol3\n"; +const CTRL = "bell\x07end\nesc\x1bhere\ndel\x7fmark\n"; +const MIXED = "tab\there\nctrl\x07bell\ndel\x7fdel\n"; +const NOTRAIL = "tab\there no newline"; + +describe("cat display flags - GNU Comparison", () => { + let testDir: string; + + beforeEach(async () => { + testDir = await createTestDir(); + }); + + afterEach(async () => { + await cleanupTestDir(testDir); + }); + + it("should match -A (show-all)", async () => { + const env = await setupFiles(testDir, { "mixed.txt": MIXED }); + await compareOutputs(env, testDir, "cat -A mixed.txt"); + }); + + it("should match -A on file without trailing newline", async () => { + const env = await setupFiles(testDir, { "notrail.txt": NOTRAIL }); + await compareOutputs(env, testDir, "cat -A notrail.txt"); + }); + + it("should match -b (number-nonblank)", async () => { + const env = await setupFiles(testDir, { "blanks.txt": BLANKS }); + await compareOutputs(env, testDir, "cat -b blanks.txt"); + }); + + it("should match -e (== -vE)", async () => { + const env = await setupFiles(testDir, { "mixed.txt": MIXED }); + await compareOutputs(env, testDir, "cat -e mixed.txt"); + }); + + it("should match -E (show-ends)", async () => { + const env = await setupFiles(testDir, { "basic.txt": BASIC }); + await compareOutputs(env, testDir, "cat -E basic.txt"); + }); + + it("should match -E on file without trailing newline", async () => { + const env = await setupFiles(testDir, { "notrail.txt": NOTRAIL }); + await compareOutputs(env, testDir, "cat -E notrail.txt"); + }); + + it("should match -s (squeeze-blank)", async () => { + const env = await setupFiles(testDir, { "blanks.txt": BLANKS }); + await compareOutputs(env, testDir, "cat -s blanks.txt"); + }); + + it("should match -t (== -vT)", async () => { + const env = await setupFiles(testDir, { "tabs.txt": TABS }); + await compareOutputs(env, testDir, "cat -t tabs.txt"); + }); + + it("should match -T (show-tabs)", async () => { + const env = await setupFiles(testDir, { "tabs.txt": TABS }); + await compareOutputs(env, testDir, "cat -T tabs.txt"); + }); + + it("should match -v (show-nonprinting)", async () => { + const env = await setupFiles(testDir, { "ctrl.txt": CTRL }); + await compareOutputs(env, testDir, "cat -v ctrl.txt"); + }); + + it("should match -u (ignored no-op)", async () => { + const env = await setupFiles(testDir, { "basic.txt": BASIC }); + await compareOutputs(env, testDir, "cat -u basic.txt"); + }); +}); + +describe("cat combined flags - GNU Comparison", () => { + let testDir: string; + + beforeEach(async () => { + testDir = await createTestDir(); + }); + + afterEach(async () => { + await cleanupTestDir(testDir); + }); + + it("should match -An (show-all + number)", async () => { + const env = await setupFiles(testDir, { "mixed.txt": MIXED }); + await compareOutputs(env, testDir, "cat -An mixed.txt"); + }); + + it("should match -bE (number-nonblank + show-ends)", async () => { + const env = await setupFiles(testDir, { "blanks.txt": BLANKS }); + await compareOutputs(env, testDir, "cat -bE blanks.txt"); + }); + + it("should match -vET (== -A)", async () => { + const env = await setupFiles(testDir, { "mixed.txt": MIXED }); + await compareOutputs(env, testDir, "cat -vET mixed.txt"); + }); + + it("should match -nb (-b overrides -n)", async () => { + const env = await setupFiles(testDir, { "blanks.txt": BLANKS }); + await compareOutputs(env, testDir, "cat -nb blanks.txt"); + }); + + it("should match -sn (squeeze + number)", async () => { + const env = await setupFiles(testDir, { "blanks.txt": BLANKS }); + await compareOutputs(env, testDir, "cat -sn blanks.txt"); + }); +}); + +describe("cat long options - GNU Comparison", () => { + let testDir: string; + + beforeEach(async () => { + testDir = await createTestDir(); + }); + + afterEach(async () => { + await cleanupTestDir(testDir); + }); + + it("should match --show-all", async () => { + const env = await setupFiles(testDir, { "mixed.txt": MIXED }); + await compareOutputs(env, testDir, "cat --show-all mixed.txt"); + }); + + it("should match --number-nonblank", async () => { + const env = await setupFiles(testDir, { "blanks.txt": BLANKS }); + await compareOutputs(env, testDir, "cat --number-nonblank blanks.txt"); + }); + + it("should match --show-ends", async () => { + const env = await setupFiles(testDir, { "basic.txt": BASIC }); + await compareOutputs(env, testDir, "cat --show-ends basic.txt"); + }); + + it("should match --squeeze-blank", async () => { + const env = await setupFiles(testDir, { "blanks.txt": BLANKS }); + await compareOutputs(env, testDir, "cat --squeeze-blank blanks.txt"); + }); + + it("should match --show-tabs", async () => { + const env = await setupFiles(testDir, { "tabs.txt": TABS }); + await compareOutputs(env, testDir, "cat --show-tabs tabs.txt"); + }); + + it("should match --show-nonprinting", async () => { + const env = await setupFiles(testDir, { "ctrl.txt": CTRL }); + await compareOutputs(env, testDir, "cat --show-nonprinting ctrl.txt"); + }); +}); diff --git a/packages/just-bash/src/comparison-tests/fixtures/cat-show.comparison.fixtures.json b/packages/just-bash/src/comparison-tests/fixtures/cat-show.comparison.fixtures.json new file mode 100644 index 00000000..47c7a606 --- /dev/null +++ b/packages/just-bash/src/comparison-tests/fixtures/cat-show.comparison.fixtures.json @@ -0,0 +1,222 @@ +{ + "044a8a4e0fa51fa2": { + "command": "cat --show-nonprinting ctrl.txt", + "files": { + "ctrl.txt": "bell\u0007end\nesc\u001bhere\ndelmark\n" + }, + "stdout": "bell^Gend\nesc^[here\ndel^?mark\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "20119c401f76835e": { + "command": "cat -nb blanks.txt", + "files": { + "blanks.txt": "a\n\n\n\nb\n\n\nc\n" + }, + "stdout": " 1\ta\n\n\n\n 2\tb\n\n\n 3\tc\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "4294948bf58cb6fd": { + "command": "cat --show-tabs tabs.txt", + "files": { + "tabs.txt": "col1\tcol2\tcol3\n" + }, + "stdout": "col1^Icol2^Icol3\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "452a30834ad08854": { + "command": "cat --squeeze-blank blanks.txt", + "files": { + "blanks.txt": "a\n\n\n\nb\n\n\nc\n" + }, + "stdout": "a\n\nb\n\nc\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "5850160a7e5e4076": { + "command": "cat -v ctrl.txt", + "files": { + "ctrl.txt": "bell\u0007end\nesc\u001bhere\ndelmark\n" + }, + "stdout": "bell^Gend\nesc^[here\ndel^?mark\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "599408168547599e": { + "command": "cat -bE blanks.txt", + "files": { + "blanks.txt": "a\n\n\n\nb\n\n\nc\n" + }, + "stdout": " 1\ta$\n$\n$\n$\n 2\tb$\n$\n$\n 3\tc$\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "6b86128309f82b35": { + "command": "cat -sn blanks.txt", + "files": { + "blanks.txt": "a\n\n\n\nb\n\n\nc\n" + }, + "stdout": " 1\ta\n 2\t\n 3\tb\n 4\t\n 5\tc\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "70dc12563ec9b4d7": { + "command": "cat -E notrail.txt", + "files": { + "notrail.txt": "tab\there no newline" + }, + "stdout": "tab\there no newline", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "7760243dfcbf947e": { + "command": "cat -vET mixed.txt", + "files": { + "mixed.txt": "tab\there\nctrl\u0007bell\ndeldel\n" + }, + "stdout": "tab^Ihere$\nctrl^Gbell$\ndel^?del$\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "9980f3f6aa971e21": { + "command": "cat -A notrail.txt", + "files": { + "notrail.txt": "tab\there no newline" + }, + "stdout": "tab^Ihere no newline", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "9a837367dd8691d7": { + "command": "cat -e mixed.txt", + "files": { + "mixed.txt": "tab\there\nctrl\u0007bell\ndeldel\n" + }, + "stdout": "tab\there$\nctrl^Gbell$\ndel^?del$\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "9e1e48c2e33d9997": { + "command": "cat -u basic.txt", + "files": { + "basic.txt": "line 1\nline 2\nline 3\n" + }, + "stdout": "line 1\nline 2\nline 3\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "ab7b66d85210c7d9": { + "command": "cat --number-nonblank blanks.txt", + "files": { + "blanks.txt": "a\n\n\n\nb\n\n\nc\n" + }, + "stdout": " 1\ta\n\n\n\n 2\tb\n\n\n 3\tc\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "ac561dbe85ab9a02": { + "command": "cat --show-all mixed.txt", + "files": { + "mixed.txt": "tab\there\nctrl\u0007bell\ndeldel\n" + }, + "stdout": "tab^Ihere$\nctrl^Gbell$\ndel^?del$\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "b392b9efa61475e0": { + "command": "cat -t tabs.txt", + "files": { + "tabs.txt": "col1\tcol2\tcol3\n" + }, + "stdout": "col1^Icol2^Icol3\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "bf25bc8aec889481": { + "command": "cat --show-ends basic.txt", + "files": { + "basic.txt": "line 1\nline 2\nline 3\n" + }, + "stdout": "line 1$\nline 2$\nline 3$\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "ca3ac60006423c04": { + "command": "cat -b blanks.txt", + "files": { + "blanks.txt": "a\n\n\n\nb\n\n\nc\n" + }, + "stdout": " 1\ta\n\n\n\n 2\tb\n\n\n 3\tc\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "d3d5075ff97a97ca": { + "command": "cat -An mixed.txt", + "files": { + "mixed.txt": "tab\there\nctrl\u0007bell\ndeldel\n" + }, + "stdout": " 1\ttab^Ihere$\n 2\tctrl^Gbell$\n 3\tdel^?del$\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "de9d74ef7f2470c4": { + "command": "cat -A mixed.txt", + "files": { + "mixed.txt": "tab\there\nctrl\u0007bell\ndeldel\n" + }, + "stdout": "tab^Ihere$\nctrl^Gbell$\ndel^?del$\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "e453713c81330d88": { + "command": "cat -T tabs.txt", + "files": { + "tabs.txt": "col1\tcol2\tcol3\n" + }, + "stdout": "col1^Icol2^Icol3\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "fea0ba381c025a27": { + "command": "cat -s blanks.txt", + "files": { + "blanks.txt": "a\n\n\n\nb\n\n\nc\n" + }, + "stdout": "a\n\nb\n\nc\n", + "stderr": "", + "exitCode": 0, + "locked": true + }, + "feaa71664184f56c": { + "command": "cat -E basic.txt", + "files": { + "basic.txt": "line 1\nline 2\nline 3\n" + }, + "stdout": "line 1$\nline 2$\nline 3$\n", + "stderr": "", + "exitCode": 0, + "locked": true + } +}