diff --git a/tools/roms/canonicalize.test.ts b/tools/roms/canonicalize.test.ts index 7f9e851af..fbf424bd7 100644 --- a/tools/roms/canonicalize.test.ts +++ b/tools/roms/canonicalize.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { existsSync, mkdtempSync, writeFileSync, readFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, writeFileSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { @@ -191,23 +191,34 @@ describe("matchAndReport", () => { describe("main", () => { test("reports missing datfile value clearly", () => { const originalStderrWrite = process.stderr.write; - const originalExit = process.exit; let stderr = ""; process.stderr.write = ((chunk: string | Uint8Array) => { stderr += String(chunk); return true; }) as typeof process.stderr.write; - process.exit = ((code?: number) => { - throw new Error(`exit:${code}`); - }) as typeof process.exit; try { - expect(() => main(["--datfile", "--dir", "/tmp"])).toThrow("exit:64"); + const code = main(["--datfile"]); + expect(code).toBe(64); expect(stderr).toContain("missing value for --datfile"); } finally { process.stderr.write = originalStderrWrite; - process.exit = originalExit; + } + }); + + test("accepts hyphen-prefixed datfile and directory values", () => { + const tmp = mkdtempSync(join(tmpdir(), "rom-cli-hyphen-")); + const originalCwd = process.cwd(); + writeFileSync(join(tmp, "-set.dat"), FIXTURE_DATFILE); + mkdirSync(join(tmp, "-roms")); + + try { + process.chdir(tmp); + const code = main(["--datfile", "-set.dat", "--dir", "-roms"]); + expect(code).toBe(0); + } finally { + process.chdir(originalCwd); } }); }); diff --git a/tools/roms/canonicalize.ts b/tools/roms/canonicalize.ts index 01e920ac7..fe622bac4 100644 --- a/tools/roms/canonicalize.ts +++ b/tools/roms/canonicalize.ts @@ -253,6 +253,15 @@ interface Args { readonly apply: boolean; } +class ArgError extends Error { + readonly exitCode: number; + + constructor(message: string, exitCode: number) { + super(message); + this.exitCode = exitCode; + } +} + function parseArgs(argv: readonly string[]): Args { let datfile: string | undefined; let dir: string | undefined; @@ -260,9 +269,8 @@ function parseArgs(argv: readonly string[]): Args { function readOptionValue(index: number, flag: string): string { const value = argv[index + 1]; - if (value === undefined || value.startsWith("-")) { - process.stderr.write(`missing value for ${flag}\n`); - process.exit(64); + if (value === undefined) { + throw new ArgError(`missing value for ${flag}`, 64); } return value; } @@ -284,26 +292,32 @@ function parseArgs(argv: readonly string[]): Args { " --dir Directory containing ROM files to match.\n" + " --apply Actually rename files (default: dry-run report).\n", ); - process.exit(0); + throw new ArgError("", 0); } else { - process.stderr.write(`unknown arg: ${arg}\n`); - process.exit(64); + throw new ArgError(`unknown arg: ${arg}`, 64); } } if (!datfile) { - process.stderr.write("--datfile is required\n"); - process.exit(64); + throw new ArgError("--datfile is required", 64); } if (!dir) { - process.stderr.write("--dir is required\n"); - process.exit(64); + throw new ArgError("--dir is required", 64); } return { datfile, dir, apply }; } export function main(argv: readonly string[]): number { - const args = parseArgs(argv); + let args: Args; + try { + args = parseArgs(argv); + } catch (e) { + if (e instanceof ArgError) { + if (e.message) process.stderr.write(`${e.message}\n`); + return e.exitCode; + } + throw e; + } if (!existsSync(args.datfile)) { process.stderr.write(`datfile not found: ${args.datfile}\n`);