From ccfeb6dba13673dd5f8fd703c41329197bcf182a Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Wed, 18 Feb 2026 20:16:28 -0800 Subject: [PATCH] add subcommand support to argparse and migrate chad-native.ts --- lib/argparse.ts | 508 ++++++++++++++++++-- src/chad-native.ts | 149 ++---- tests/fixtures/argparse/double-dash.ts | 24 + tests/fixtures/argparse/equals-syntax.ts | 26 + tests/fixtures/argparse/subcommand-basic.ts | 37 ++ tests/network.test.ts | 2 +- tests/test-fixtures.ts | 21 + tsconfig.json | 2 +- 8 files changed, 617 insertions(+), 152 deletions(-) create mode 100644 tests/fixtures/argparse/double-dash.ts create mode 100644 tests/fixtures/argparse/equals-syntax.ts create mode 100644 tests/fixtures/argparse/subcommand-basic.ts diff --git a/lib/argparse.ts b/lib/argparse.ts index 175b0014..b5566392 100644 --- a/lib/argparse.ts +++ b/lib/argparse.ts @@ -14,6 +14,15 @@ export class ArgumentParser { argIsFlag: boolean[]; argDefaultValue: string[]; argIsPositional: boolean[]; + argSubcommands: string[]; + + // Subcommand metadata + subcmdNames: string[]; + subcmdDescriptions: string[]; + parsedSubcommand: string; + + // Rest args (after --) + restArgs: string[]; // Parallel arrays for parsed results parsedPositionals: string[]; @@ -32,6 +41,11 @@ export class ArgumentParser { this.argIsFlag = []; this.argDefaultValue = []; this.argIsPositional = []; + this.argSubcommands = []; + this.subcmdNames = []; + this.subcmdDescriptions = []; + this.parsedSubcommand = ""; + this.restArgs = []; this.parsedPositionals = []; this.parsedFlagNames = []; this.parsedFlagValues = []; @@ -39,6 +53,11 @@ export class ArgumentParser { this.parsedOptionValues = []; } + addSubcommand(name: string, desc: string): void { + this.subcmdNames.push(name); + this.subcmdDescriptions.push(desc); + } + // Add a boolean flag (e.g., -v, --verbose) addFlag(name: string, shortFlag: string, help: string): void { this.argNames.push(name); @@ -48,6 +67,18 @@ export class ArgumentParser { this.argIsFlag.push(true); this.argDefaultValue.push(""); this.argIsPositional.push(false); + this.argSubcommands.push(""); + } + + addScopedFlag(name: string, shortFlag: string, help: string, subcommands: string): void { + this.argNames.push(name); + this.argShortFlags.push(shortFlag); + this.argLongFlags.push(name); + this.argHelp.push(help); + this.argIsFlag.push(true); + this.argDefaultValue.push(""); + this.argIsPositional.push(false); + this.argSubcommands.push(subcommands); } // Add an option that takes a value (e.g., -o file.txt, --output file.txt) @@ -59,6 +90,18 @@ export class ArgumentParser { this.argIsFlag.push(false); this.argDefaultValue.push(defaultVal); this.argIsPositional.push(false); + this.argSubcommands.push(""); + } + + addScopedOption(name: string, shortFlag: string, help: string, defaultVal: string, subcommands: string): void { + this.argNames.push(name); + this.argShortFlags.push(shortFlag); + this.argLongFlags.push(name); + this.argHelp.push(help); + this.argIsFlag.push(false); + this.argDefaultValue.push(defaultVal); + this.argIsPositional.push(false); + this.argSubcommands.push(subcommands); } // Add a positional argument (e.g., filename) @@ -70,27 +113,123 @@ export class ArgumentParser { this.argIsFlag.push(false); this.argDefaultValue.push(""); this.argIsPositional.push(true); + this.argSubcommands.push(""); } - parse(argv: string[]): number { - // Clear previous results + isArgInScope(argIndex: number, subcommand: string): boolean { + if (this.argSubcommands[argIndex].length === 0) { + return true; + } + if (subcommand.length === 0) { + return false; + } + const scopes = this.argSubcommands[argIndex]; + let start = 0; + let pos = 0; + while (pos <= scopes.length) { + if (pos === scopes.length || scopes.charAt(pos) === ",") { + const part = scopes.substring(start, pos); + if (part.length > 0 && part === subcommand) { + return true; + } + start = pos + 1; + } + pos = pos + 1; + } + return false; + } + + findSubcommand(name: string): number { + let i = 0; + while (i < this.subcmdNames.length) { + if (this.subcmdNames[i].length > 0 && this.subcmdNames[i] === name) { + return i; + } + i = i + 1; + } + return -1; + } + + splitEqualsFlag(arg: string): string { + let pos = 0; + while (pos < arg.length) { + if (arg.charAt(pos) === "=") { + return arg.substring(0, pos); + } + pos = pos + 1; + } + return arg; + } + + splitEqualsValue(arg: string): string { + let pos = 0; + while (pos < arg.length) { + if (arg.charAt(pos) === "=") { + return arg.substring(pos + 1, arg.length); + } + pos = pos + 1; + } + return ""; + } + + hasEquals(arg: string): boolean { + let pos = 0; + while (pos < arg.length) { + if (arg.charAt(pos) === "=") { + return true; + } + pos = pos + 1; + } + return false; + } + + setFlagValue(argIndex: number): void { + const newFlagValues: boolean[] = []; + let flagIdx = 0; + while (flagIdx < this.parsedFlagNames.length) { + if (this.parsedFlagNames[flagIdx].length > 0 && this.argNames[argIndex].length > 0 && + this.parsedFlagNames[flagIdx] === this.argNames[argIndex]) { + newFlagValues.push(true); + } else { + newFlagValues.push(this.parsedFlagValues[flagIdx]); + } + flagIdx = flagIdx + 1; + } + this.parsedFlagValues = newFlagValues; + } + + setOptionValue(argIndex: number, value: string): void { + const newOptionValues: string[] = []; + let optIdx = 0; + while (optIdx < this.parsedOptionNames.length) { + if (this.parsedOptionNames[optIdx].length > 0 && this.argNames[argIndex].length > 0 && + this.parsedOptionNames[optIdx] === this.argNames[argIndex]) { + newOptionValues.push(value); + } else { + newOptionValues.push(this.parsedOptionValues[optIdx]); + } + optIdx = optIdx + 1; + } + this.parsedOptionValues = newOptionValues; + } + + initDefaults(): void { this.parsedPositionals = []; this.parsedFlagNames = []; this.parsedFlagValues = []; this.parsedOptionNames = []; this.parsedOptionValues = []; + this.parsedSubcommand = ""; + this.restArgs = []; - // Initialize defaults let i = 0; while (i < this.argNames.length) { - // Only process if argNames[i] is not empty/NULL if (this.argNames[i].length > 0) { if (this.argIsFlag[i]) { this.parsedFlagNames.push(this.argNames[i]); this.parsedFlagValues.push(false); } else if (!this.argIsPositional[i]) { this.parsedOptionNames.push(this.argNames[i]); - // Only push default value if it's not empty if (this.argDefaultValue[i].length > 0) { this.parsedOptionValues.push(this.argDefaultValue[i]); } else { @@ -100,69 +239,160 @@ export class ArgumentParser { } i = i + 1; } + } + + parseFlag(argv: string[], argIdx: number): number { + const raw = argv[argIdx]; + let flagPart = raw; + let valuePart = ""; + let gotEquals = false; - // Parse argv + if (this.hasEquals(raw)) { + flagPart = this.splitEqualsFlag(raw); + valuePart = this.splitEqualsValue(raw); + gotEquals = true; + } + + const argIndex = this.findArgument(flagPart); + + if (argIndex === -1) { + console.error("Unknown option: " + raw); + console.error("Try '" + this.programName + " --help' for more information"); + process.exit(1); + } + + if (this.argIsFlag[argIndex]) { + this.setFlagValue(argIndex); + return argIdx + 1; + } else { + if (gotEquals) { + this.setOptionValue(argIndex, valuePart); + return argIdx + 1; + } else { + const nextIdx = argIdx + 1; + if (nextIdx >= argv.length) { + console.error("Error: Option --" + this.argNames[argIndex] + " requires a value"); + process.exit(1); + } + this.setOptionValue(argIndex, argv[nextIdx]); + return nextIdx + 1; + } + } + } + + parse(argv: string[]): number { + this.initDefaults(); + + if (this.subcmdNames.length === 0) { + return this.parseSimple(argv); + } + return this.parseWithSubcommands(argv); + } + + parseSimple(argv: string[]): number { let argIdx = 0; while (argIdx < argv.length) { - // Check for help if (argv[argIdx].length > 0 && (argv[argIdx] === "-h" || argv[argIdx] === "--help")) { this.printHelp(); process.exit(0); } - // Check if it starts with dash (flag or option) + if (argv[argIdx].length > 0 && argv[argIdx] === "--") { + argIdx = argIdx + 1; + while (argIdx < argv.length) { + this.restArgs.push(argv[argIdx]); + argIdx = argIdx + 1; + } + return 0; + } + if (argv[argIdx].length > 0 && argv[argIdx].charAt(0) === "-") { - const argIndex = this.findArgument(argv[argIdx]); + argIdx = this.parseFlag(argv, argIdx); + } else { + if (argv[argIdx].length > 0) { + this.parsedPositionals.push(argv[argIdx]); + } + argIdx = argIdx + 1; + } + } + + return 0; + } - if (argIndex === -1) { - console.error("Unknown option: " + argv[argIdx]); + parseWithSubcommands(argv: string[]): number { + let argIdx = 0; + + while (argIdx < argv.length) { + const cur = argv[argIdx]; + + if (cur.length > 0 && (cur === "-h" || cur === "--help")) { + this.printHelp(); + process.exit(0); + } + + if (cur.length > 0 && cur.charAt(0) === "-") { + const flagPart = this.hasEquals(cur) ? this.splitEqualsFlag(cur) : cur; + const argIndex = this.findArgument(flagPart); + if (argIndex !== -1 && this.isArgInScope(argIndex, "")) { + argIdx = this.parseFlag(argv, argIdx); + } else if (argIndex !== -1) { + argIdx = this.parseFlag(argv, argIdx); + } else { + console.error("Unknown option: " + cur); console.error("Try '" + this.programName + " --help' for more information"); process.exit(1); } - - if (this.argIsFlag[argIndex]) { - // Set flag value by rebuilding array - const newFlagValues: boolean[] = []; - let flagIdx = 0; - while (flagIdx < this.parsedFlagNames.length) { - // Check length to avoid strcmp on NULL/empty strings - if (this.parsedFlagNames[flagIdx].length > 0 && this.argNames[argIndex].length > 0 && - this.parsedFlagNames[flagIdx] === this.argNames[argIndex]) { - newFlagValues.push(true); + } else { + if (cur.length > 0) { + const subcmdIdx = this.findSubcommand(cur); + if (subcmdIdx !== -1) { + this.parsedSubcommand = cur; + argIdx = argIdx + 1; + return this.parseAfterSubcommand(argv, argIdx); + } else { + const endsTs = cur.length >= 3 && cur.substr(cur.length - 3) === ".ts"; + const endsJs = cur.length >= 3 && cur.substr(cur.length - 3) === ".js"; + if (endsTs || endsJs) { + console.error(this.programName + ": error: missing command. did you mean " + this.programName + " build " + cur + "?"); } else { - newFlagValues.push(this.parsedFlagValues[flagIdx]); + console.error(this.programName + ": error: unknown command '" + cur + "'"); } - flagIdx = flagIdx + 1; - } - this.parsedFlagValues = newFlagValues; - argIdx = argIdx + 1; - } else { - // Get value - argIdx = argIdx + 1; - if (argIdx >= argv.length) { - console.error("Error: Option requires a value"); + console.error("Run " + this.programName + " --help for usage"); process.exit(1); } - // Set option value by rebuilding array - const newOptionValues: string[] = []; - let optIdx = 0; - while (optIdx < this.parsedOptionNames.length) { - // Check length to avoid strcmp on NULL/empty strings - if (this.parsedOptionNames[optIdx].length > 0 && this.argNames[argIndex].length > 0 && - this.parsedOptionNames[optIdx] === this.argNames[argIndex]) { - newOptionValues.push(argv[argIdx]); - } else { - newOptionValues.push(this.parsedOptionValues[optIdx]); - } - optIdx = optIdx + 1; - } - this.parsedOptionValues = newOptionValues; + } + argIdx = argIdx + 1; + } + } + + return 0; + } + + parseAfterSubcommand(argv: string[], startIdx: number): number { + let argIdx = startIdx; + + while (argIdx < argv.length) { + const cur = argv[argIdx]; + + if (cur.length > 0 && (cur === "-h" || cur === "--help")) { + this.printSubcommandHelp(this.parsedSubcommand); + process.exit(0); + } + + if (cur.length > 0 && cur === "--") { + argIdx = argIdx + 1; + while (argIdx < argv.length) { + this.restArgs.push(argv[argIdx]); argIdx = argIdx + 1; } + return 0; + } + + if (cur.length > 0 && cur.charAt(0) === "-") { + argIdx = this.parseFlag(argv, argIdx); } else { - // Positional argument - if (argv[argIdx].length > 0) { - this.parsedPositionals.push(argv[argIdx]); + if (cur.length > 0) { + this.parsedPositionals.push(cur); } argIdx = argIdx + 1; } @@ -195,6 +425,14 @@ export class ArgumentParser { } printHelp(): void { + if (this.subcmdNames.length > 0) { + this.printTopLevelHelp(); + } else { + this.printSimpleHelp(); + } + } + + printSimpleHelp(): void { console.log(this.programName); if (this.description.length > 0) { console.log(this.description); @@ -285,7 +523,179 @@ export class ArgumentParser { console.log(" Show this help message and exit"); } - // Public API - access parsed results without needing to pass args around + printTopLevelHelp(): void { + console.log(this.programName); + if (this.description.length > 0) { + console.log(this.description); + } + console.log(""); + console.log("Usage: " + this.programName + " [options]"); + console.log(""); + console.log("Commands:"); + let i = 0; + while (i < this.subcmdNames.length) { + let line = " " + this.subcmdNames[i]; + let padLen = 16 - this.subcmdNames[i].length; + if (padLen < 2) { + padLen = 2; + } + let pad = 0; + while (pad < padLen) { + line = line + " "; + pad = pad + 1; + } + line = line + this.subcmdDescriptions[i]; + console.log(line); + i = i + 1; + } + + let hasGlobalArgs = false; + i = 0; + while (i < this.argNames.length) { + if (!this.argIsPositional[i] && this.argSubcommands[i].length === 0) { + hasGlobalArgs = true; + } + i = i + 1; + } + + if (hasGlobalArgs) { + console.log(""); + console.log("Global options:"); + i = 0; + while (i < this.argNames.length) { + if (!this.argIsPositional[i] && this.argSubcommands[i].length === 0) { + this.printArgLine(i); + } + i = i + 1; + } + } + + console.log(""); + console.log(" -h, --help"); + console.log(" Show this help message and exit"); + console.log(""); + console.log("Run '" + this.programName + " --help' for more information on a command."); + } + + printSubcommandHelp(subcmd: string): void { + let subcmdDesc = ""; + let i = 0; + while (i < this.subcmdNames.length) { + if (this.subcmdNames[i] === subcmd) { + subcmdDesc = this.subcmdDescriptions[i]; + } + i = i + 1; + } + + console.log(this.programName + " " + subcmd); + if (subcmdDesc.length > 0) { + console.log(subcmdDesc); + } + console.log(""); + + let usage = "Usage: " + this.programName + " " + subcmd; + i = 0; + while (i < this.argNames.length) { + if (!this.argIsPositional[i] && this.isArgInScope(i, subcmd)) { + if (this.argShortFlags[i].length > 0) { + if (!this.argIsFlag[i]) { + usage = usage + " [-" + this.argShortFlags[i] + " <" + this.argNames[i] + ">]"; + } else { + usage = usage + " [-" + this.argShortFlags[i] + "]"; + } + } else { + if (!this.argIsFlag[i]) { + usage = usage + " [--" + this.argLongFlags[i] + " <" + this.argNames[i] + ">]"; + } else { + usage = usage + " [--" + this.argLongFlags[i] + "]"; + } + } + } + i = i + 1; + } + i = 0; + while (i < this.argNames.length) { + if (this.argIsPositional[i]) { + usage = usage + " <" + this.argNames[i] + ">"; + } + i = i + 1; + } + console.log(usage); + console.log(""); + + let hasOptions = false; + i = 0; + while (i < this.argNames.length) { + if (!this.argIsPositional[i] && this.isArgInScope(i, subcmd)) { + hasOptions = true; + } + i = i + 1; + } + + if (hasOptions) { + console.log("Options:"); + i = 0; + while (i < this.argNames.length) { + if (!this.argIsPositional[i] && this.isArgInScope(i, subcmd)) { + this.printArgLine(i); + } + i = i + 1; + } + } + + let hasPositionals = false; + i = 0; + while (i < this.argNames.length) { + if (this.argIsPositional[i]) { + hasPositionals = true; + } + i = i + 1; + } + + if (hasPositionals) { + console.log(""); + console.log("Arguments:"); + i = 0; + while (i < this.argNames.length) { + if (this.argIsPositional[i]) { + console.log(" " + this.argNames[i]); + console.log(" " + this.argHelp[i]); + } + i = i + 1; + } + } + + console.log(""); + console.log(" -h, --help"); + console.log(" Show this help message and exit"); + } + + printArgLine(i: number): void { + if (this.argShortFlags[i].length > 0) { + if (!this.argIsFlag[i] && this.argDefaultValue[i].length > 0) { + console.log(" -" + this.argShortFlags[i] + ", --" + this.argLongFlags[i] + " (default: " + this.argDefaultValue[i] + ")"); + } else { + console.log(" -" + this.argShortFlags[i] + ", --" + this.argLongFlags[i]); + } + } else { + if (!this.argIsFlag[i] && this.argDefaultValue[i].length > 0) { + console.log(" --" + this.argLongFlags[i] + " (default: " + this.argDefaultValue[i] + ")"); + } else { + console.log(" --" + this.argLongFlags[i]); + } + } + console.log(" " + this.argHelp[i]); + } + + // Public API + getSubcommand(): string { + return this.parsedSubcommand; + } + + getRestArgs(): string[] { + return this.restArgs; + } + getFlag(name: string): boolean { let i = 0; while (i < this.parsedFlagNames.length) { diff --git a/src/chad-native.ts b/src/chad-native.ts index 124fcbbb..c1d1d61f 100644 --- a/src/chad-native.ts +++ b/src/chad-native.ts @@ -1,5 +1,6 @@ import { compileNative, setSkipSemanticAnalysis, setEmitLLVMOnly, setTargetCpu } from './native-compiler-lib.js'; import { getDtsContent } from './codegen/stdlib/embedded-dts.js'; +import { ArgumentParser } from '../lib/argparse.js'; declare const fs: { existsSync(filename: string): boolean; @@ -23,54 +24,27 @@ declare const child_process: { const VERSION = '0.1.0'; -function printHelp(): void { - console.log('chad - compile TypeScript to native binaries via LLVM'); - console.log(''); - console.log('Usage: chad [options] '); - console.log(''); - console.log('Commands:'); - console.log(' build Compile to a native binary'); - console.log(' run Compile and run'); - console.log(' ir Emit LLVM IR only'); - console.log(' init Generate starter project (chadscript.d.ts, tsconfig.json, hello.ts)'); - console.log(' clean Remove the .build directory'); - console.log(''); - console.log('Options:'); - console.log(' -o Specify output file'); - console.log(' --skip-semantic-analysis Skip semantic analysis'); - console.log(' --target-cpu=CPU Set LLVM target CPU (default: native)'); - console.log(' -h, --help Show this help message'); - console.log(' --version Show version'); - console.log(''); - console.log('Examples:'); - console.log(' chad build hello.ts'); - console.log(' chad build hello.ts -o myapp'); - console.log(' chad run hello.ts'); - console.log(' chad ir hello.ts'); -} +const parser = new ArgumentParser('chad', 'compile TypeScript to native binaries via LLVM'); +parser.addSubcommand('build', 'Compile to a native binary'); +parser.addSubcommand('run', 'Compile and run'); +parser.addSubcommand('ir', 'Emit LLVM IR only'); +parser.addSubcommand('init', 'Generate starter project'); +parser.addSubcommand('clean', 'Remove the .build directory'); -function printVersion(): void { - console.log('chad ' + VERSION); -} +parser.addFlag('version', '', 'Show version'); +parser.addScopedOption('output', 'o', 'Specify output file', '', 'build,run,ir'); +parser.addScopedFlag('skip-semantic-analysis', '', 'Skip semantic analysis', 'build,run,ir'); +parser.addScopedOption('target-cpu', '', 'Set LLVM target CPU', 'native', 'build,run,ir'); +parser.addPositional('input', 'Input .ts or .js file'); -const args = process.argv; +parser.parse(process.argv); -if (args.length < 1) { - printHelp(); +if (parser.getFlag('version')) { + console.log('chad ' + VERSION); process.exit(0); } -const command = args[0]; - -if (command === '-h' || command === '--help') { - printHelp(); - process.exit(0); -} - -if (command === '--version') { - printVersion(); - process.exit(0); -} +const command = parser.getSubcommand(); if (command === 'init') { const dtsContent = getDtsContent(); @@ -107,83 +81,49 @@ if (command === 'clean') { process.exit(0); } -if (command !== 'build' && command !== 'run' && command !== 'ir' && command !== 'init') { - const endsWithTs = command.substr(command.length - 3) === '.ts'; - const endsWithJs = command.substr(command.length - 3) === '.js'; - if (endsWithTs || endsWithJs) { - console.log('chad: error: missing command. did you mean chad build ' + command + '?'); - } else { - console.log('chad: error: unknown command ' + command); - } - console.log('Run chad --help for usage'); - process.exit(1); +if (command.length === 0) { + parser.printHelp(); + process.exit(0); } -let inputFile: string | null = null; -let outputFile: string | null = null; -let argIdx = 1; -while (argIdx < args.length) { - const arg = args[argIdx]; - if (arg === '-h' || arg === '--help') { - printHelp(); - process.exit(0); - } else if (arg === '--version') { - printVersion(); - process.exit(0); - } else if (arg === '--skip-semantic-analysis') { - setSkipSemanticAnalysis(true); - argIdx = argIdx + 1; - } else if (arg.substr(0, 13) === '--target-cpu=') { - setTargetCpu(arg.substr(13)); - argIdx = argIdx + 1; - } else if (arg === '-o') { - argIdx = argIdx + 1; - if (argIdx < args.length) { - outputFile = args[argIdx]; - argIdx = argIdx + 1; - } - } else if (arg.substr(0, 1) === '-') { - console.log('chad: error: unknown option ' + arg); - console.log('Run chad --help for usage'); - process.exit(1); - } else if (inputFile === null) { - inputFile = arg; - argIdx = argIdx + 1; - } else { - argIdx = argIdx + 1; - } +if (parser.getFlag('skip-semantic-analysis')) { + setSkipSemanticAnalysis(true); +} + +const cpuOpt = parser.getOption('target-cpu'); +if (cpuOpt.length > 0) { + setTargetCpu(cpuOpt); } -if (inputFile === null) { +const inputFile = parser.getPositional(0); +if (inputFile.length === 0) { console.log('chad: error: no input files'); console.log('Usage: chad ' + command + ' [options] '); process.exit(1); throw new Error('unreachable'); } -let theInputFile: string = ''; -theInputFile = inputFile; - -if (!fs.existsSync(theInputFile)) { - console.log('chad: error: file not found: ' + theInputFile); +if (!fs.existsSync(inputFile)) { + console.log('chad: error: file not found: ' + inputFile); process.exit(1); throw new Error('unreachable'); } -let inputForOutput: string = theInputFile; +let inputForOutput: string = inputFile; if (inputForOutput.substr(0, 1) === '/') { inputForOutput = path.basename(inputForOutput); } -let theOutputFile: string = '.build/' + inputForOutput; -if (outputFile !== null) { - theOutputFile = outputFile; +let outputFile: string = '.build/' + inputForOutput; +const explicitOutput = parser.getOption('output'); +if (explicitOutput.length > 0) { + outputFile = explicitOutput; } else if (inputForOutput.substr(inputForOutput.length - 3) === '.ts') { - theOutputFile = '.build/' + inputForOutput.substr(0, inputForOutput.length - 3); + outputFile = '.build/' + inputForOutput.substr(0, inputForOutput.length - 3); } else if (inputForOutput.substr(inputForOutput.length - 3) === '.js') { - theOutputFile = '.build/' + inputForOutput.substr(0, inputForOutput.length - 3); + outputFile = '.build/' + inputForOutput.substr(0, inputForOutput.length - 3); } -const outputDir = path.dirname(theOutputFile); +const outputDir = path.dirname(outputFile); if (!fs.existsSync(outputDir)) { child_process.execSync('mkdir -p ' + outputDir); } @@ -192,13 +132,20 @@ if (command === 'ir') { setEmitLLVMOnly(true); } -compileNative(theInputFile, theOutputFile); +compileNative(inputFile, outputFile); if (command === 'run') { - const binPath = path.resolve(theOutputFile); + const binPath = path.resolve(outputFile); if (!fs.existsSync(binPath)) { console.log('chad: error: compilation produced no binary'); process.exit(1); } - child_process.execSync(binPath); + const rest = parser.getRestArgs(); + let runCmd = binPath; + let ri = 0; + while (ri < rest.length) { + runCmd = runCmd + ' ' + rest[ri]; + ri = ri + 1; + } + child_process.execSync(runCmd); } diff --git a/tests/fixtures/argparse/double-dash.ts b/tests/fixtures/argparse/double-dash.ts new file mode 100644 index 00000000..6669ace2 --- /dev/null +++ b/tests/fixtures/argparse/double-dash.ts @@ -0,0 +1,24 @@ +import { ArgumentParser } from '../../../lib/argparse.js'; + +const parser = new ArgumentParser('myapp', 'test double dash'); +parser.addSubcommand('run', 'Run the project'); +parser.addFlag('verbose', 'v', 'Enable verbose output'); +parser.addPositional('input', 'Input file'); + +parser.parse(process.argv); + +const cmd = parser.getSubcommand(); +console.log('cmd:' + cmd); + +const inp = parser.getPositional(0); +console.log('input:' + inp); + +const rest = parser.getRestArgs(); +let i = 0; +while (i < rest.length) { + console.log('rest:' + rest[i]); + i = i + 1; +} + +console.log('TEST_PASSED'); +process.exit(0); diff --git a/tests/fixtures/argparse/equals-syntax.ts b/tests/fixtures/argparse/equals-syntax.ts new file mode 100644 index 00000000..db7d62a9 --- /dev/null +++ b/tests/fixtures/argparse/equals-syntax.ts @@ -0,0 +1,26 @@ +import { ArgumentParser } from '../../../lib/argparse.js'; + +const parser = new ArgumentParser('myapp', 'test equals syntax'); +parser.addSubcommand('build', 'Build the project'); +parser.addOption('target-cpu', '', 'Set target CPU', 'native'); +parser.addOption('output', 'o', 'Output file', ''); +parser.addPositional('input', 'Input file'); + +parser.parse(process.argv); + +const cmd = parser.getSubcommand(); +console.log('cmd:' + cmd); + +const cpu = parser.getOption('target-cpu'); +console.log('cpu:' + cpu); + +const out = parser.getOption('output'); +if (out.length > 0) { + console.log('output:' + out); +} + +const inp = parser.getPositional(0); +console.log('input:' + inp); + +console.log('TEST_PASSED'); +process.exit(0); diff --git a/tests/fixtures/argparse/subcommand-basic.ts b/tests/fixtures/argparse/subcommand-basic.ts new file mode 100644 index 00000000..b182c0ea --- /dev/null +++ b/tests/fixtures/argparse/subcommand-basic.ts @@ -0,0 +1,37 @@ +import { ArgumentParser } from '../../../lib/argparse.js'; + +const parser = new ArgumentParser('myapp', 'test subcommands'); +parser.addSubcommand('build', 'Build the project'); +parser.addSubcommand('run', 'Run the project'); +parser.addSubcommand('clean', 'Clean build artifacts'); + +parser.addFlag('verbose', 'v', 'Enable verbose output'); +parser.addScopedOption('output', 'o', 'Output file', '', 'build,run'); +parser.addPositional('input', 'Input file'); + +parser.parse(process.argv); + +const cmd = parser.getSubcommand(); +if (cmd === 'build') { + const inp = parser.getPositional(0); + const out = parser.getOption('output'); + if (out.length > 0) { + console.log('build:' + inp + ':' + out); + } else { + console.log('build:' + inp); + } +} else if (cmd === 'run') { + const inp = parser.getPositional(0); + console.log('run:' + inp); +} else if (cmd === 'clean') { + console.log('clean'); +} else { + console.log('none'); +} + +if (parser.getFlag('verbose')) { + console.log('verbose'); +} + +console.log('TEST_PASSED'); +process.exit(0); diff --git a/tests/network.test.ts b/tests/network.test.ts index 0d4c85aa..e91595c8 100644 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -61,7 +61,7 @@ testSocket(); }); await new Promise((resolve) => { - server.listen(9876, '127.0.0.1', resolve); + server.listen(0, '127.0.0.1', resolve); }); try { diff --git a/tests/test-fixtures.ts b/tests/test-fixtures.ts index 89ae2330..654f073a 100644 --- a/tests/test-fixtures.ts +++ b/tests/test-fixtures.ts @@ -803,4 +803,25 @@ export const testCases: TestCase[] = [ expectTestPassed: true, description: 'Uint8Array constructor, index read/write, and .length' }, + { + name: 'argparse-subcommand-basic', + fixture: 'tests/fixtures/argparse/subcommand-basic.ts', + expectTestPassed: true, + description: 'ArgumentParser subcommand dispatch with flags and options', + args: ['build', 'hello.ts', '-o', 'out'] + }, + { + name: 'argparse-double-dash', + fixture: 'tests/fixtures/argparse/double-dash.ts', + expectTestPassed: true, + description: 'ArgumentParser -- separator puts remaining args into restArgs', + args: ['run', 'hello.ts', '--', 'arg1', 'arg2'] + }, + { + name: 'argparse-equals-syntax', + fixture: 'tests/fixtures/argparse/equals-syntax.ts', + expectTestPassed: true, + description: 'ArgumentParser --key=value option syntax', + args: ['--target-cpu=x86-64', 'build', 'hello.ts'] + }, ]; diff --git a/tsconfig.json b/tsconfig.json index 95e4c2ff..c1eb17a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,5 +16,5 @@ "moduleResolution": "node" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests", "src/chadc-native.ts"] + "exclude": ["node_modules", "dist", "tests", "src/chadc-native.ts", "src/chad-native.ts"] }