diff --git a/.prettierrc b/.prettierrc index 8b8417d..8304ba3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ { "semi": false, - "printWidth": 120, + "printWidth": 100, "singleQuote": false } diff --git a/src/command.ts b/src/command.ts index e8313d0..346614b 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,7 +1,8 @@ import { z } from "zod" import { ValidationError } from "./error" -import MassargOption, { MassargFlag, MassargNumber, OptionConfig, TypedOptionConfig } from "./option" -import { generateCommandsHelpTable, generateOptionsHelpTable, isZodError } from "./utils" +import Massarg from "./massarg" +import MassargOption, { TypedOptionConfig } from "./option" +import { generateCommandsHelpTable, generateOptionsHelpTable, isZodError, setOrPush } from "./utils" export const CommandConfig = (args: RunArgs) => z.object({ @@ -10,8 +11,10 @@ export const CommandConfig = (args: RunArgs) => aliases: z.string().array().optional(), run: z .function() - .args(args) - .returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<(args: z.infer) => Promise | void>, + .args(args, z.any()) + .returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType< + (args: z.infer, instance: MassargCommand>) => Promise | void + >, }) export type CommandConfig = z.infer>>> @@ -24,7 +27,10 @@ export default class MassargCommand { name: string description: string aliases: string[] - private _run?: (options: Args) => Promise | void + private _run?:

( + options: Args, + instance: Massarg

, + ) => Promise | void options: MassargOption[] = [] commands: MassargCommand[] = [] args: Partial = {} @@ -34,12 +40,14 @@ export default class MassargCommand { this.name = options.name this.description = options.description this.aliases = options.aliases ?? [] - this._run = options.run + this._run = options.run as typeof this._run } command(config: CommandConfig): MassargCommand command(config: MassargCommand): MassargCommand - command(config: CommandConfig | MassargCommand): MassargCommand { + command( + config: CommandConfig | MassargCommand, + ): MassargCommand { try { const command = config instanceof MassargCommand ? config : new MassargCommand(config) this.commands.push(command) @@ -47,7 +55,7 @@ export default class MassargCommand { } catch (e) { if (isZodError(e)) { throw new ValidationError({ - path: [config.name, ...e.issues[0].path.map((p) => p.toString())], + path: [this.name, config.name, ...e.issues[0].path.map((p) => p.toString())], code: e.issues[0].code, message: e.issues[0].message, }) @@ -60,13 +68,14 @@ export default class MassargCommand { option(config: TypedOptionConfig): MassargCommand option(config: TypedOptionConfig | MassargOption): MassargCommand { try { - const option = config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config) + const option = + config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config) this.options.push(option as MassargOption) return this } catch (e) { if (isZodError(e)) { throw new ValidationError({ - path: [config.name, ...e.issues[0].path.map((p) => p.toString())], + path: [this.name, config.name, ...e.issues[0].path.map((p) => p.toString())], code: e.issues[0].code, message: e.issues[0].message, }) @@ -75,65 +84,84 @@ export default class MassargCommand { } } - main(run: (options: Args) => Promise | void): MassargCommand { - this._run = run + main( + run: (options: Args, instance: MassargCommand) => Promise | void, + ): MassargCommand { + this._run = run as typeof this._run return this } - parse(argv: string[], args?: Partial): Promise | void { - console.log("parse:", this.name) - console.log(argv) + parse(argv: string[], args?: Partial, parent?: MassargCommand): Promise | void { this.args ??= {} this.args = { ...this.args, ...args } let _argv = [...argv] while (_argv.length) { const arg = _argv.shift()! - console.log("parsing:", arg, _argv) const found = this.options.some((o) => o._isOption(arg)) if (found) { - console.log("option:", arg, _argv) _argv = this.parseOption(arg, _argv) continue } const command = this.commands.find((c) => c.name === arg || c.aliases.includes(arg)) if (command) { - console.log("command:", arg, _argv) - return command.parse(_argv, this.args) + return command.parse(_argv, this.args, parent ?? this) } // TODO pass all un-handled args to an "args" option - console.log("Nothing to do", arg, _argv) } if (this._run) { - console.log("run:", this.args) - this._run({ ...args, ...this.args } as Args) + this._run({ ...args, ...this.args } as Args, parent ?? this) } } - private parseOption(arg: string, argv: string[]): string[] { + private parseOption(arg: string, argv: string[]) { const option = this.options.find((o) => o._match(arg)) - if (!option) { - // TODO create custom error object - throw new Error(`Unknown option ${arg}`) + throw new ValidationError({ + path: [arg], + code: "unknown_option", + message: "Unknown option", + }) } - const res = option.valueFromArgv([arg, ...argv]) - console.log("option class name", option.constructor.name) - if (option.isArray) { - this.args[res.key as keyof Args] ??= [] as Args[keyof Args] - const _a = this.args[res.key as keyof Args] as unknown[] - _a.push(res.value) as Args[keyof Args] - } else { - this.args[res.key as keyof Args] = res.value as Args[keyof Args] - } - console.log("option response:", { value: res.value, argv: res.argv }) + const res = option._parseDetails([arg, ...argv]) + this.args[res.key as keyof Args] = setOrPush( + res.value, + this.args[res.key as keyof Args], + option.isArray, + ) return res.argv } getArgs(argv: string[]): Args { - console.log("getArgs:", this.name) - console.log(argv) - return {} as Args + let args: Args = {} as Args + let _argv = [...argv] + while (_argv.length) { + const arg = _argv.shift()! + const found = this.options.some((o) => o._isOption(arg)) + if (found) { + const option = this.options.find((o) => o._match(arg)) + if (!option) { + throw new ValidationError({ + path: [arg], + code: "unknown_option", + message: "Unknown option", + }) + } + const res = option._parseDetails(argv) + args[res.key as keyof Args] = setOrPush( + res.value, + args[res.key as keyof Args], + option.isArray, + ) + continue + } + + const command = this.commands.find((c) => c.name === arg || c.aliases.includes(arg)) + if (command) { + break + } + } + return args } helpString(): string { @@ -151,6 +179,10 @@ export default class MassargCommand { .filter((s) => typeof s === "string") .join("\n") } + + printHelp(): void { + console.log(this.helpString()) + } } export { MassargCommand } diff --git a/src/error.ts b/src/error.ts index 3b24e0e..4ef089e 100644 --- a/src/error.ts +++ b/src/error.ts @@ -16,13 +16,28 @@ export class ParseError extends Error { path: string[] code: string message: string + received: unknown - constructor({ path, code, message }: { path: string[]; code: string; message: string }) { - const msg = `${path.join(".")}: ${message}` + constructor({ + path, + code, + message, + received, + }: { + path: string[] + code: string + message: string + received?: unknown + }) { + let msg = `${path.join(".")}: ${message}` + if (received) { + msg += ` (received: ${received})` + } super(msg) this.path = path this.code = code this.message = msg this.name = "ParseError" + this.received = received } } diff --git a/src/example.ts b/src/example.ts index c40e59b..bcce0ca 100644 --- a/src/example.ts +++ b/src/example.ts @@ -7,10 +7,11 @@ const args = massarg({ name: "my-cli", description: "This is an example CLI", }) - // .main((opts) => { - // console.log("Main command - printing all opts") - // console.log(opts) - // }) + .main((opts, parser) => { + console.log("Main command - printing all opts") + console.log(opts, "\n") + parser.printHelp() + }) .command( massarg<{ component: string }>({ name: "add", @@ -70,7 +71,7 @@ const args = massarg({ ) .option({ name: "bool", - description: "Example number option", + description: "Example boolean option", aliases: ["b"], type: "boolean", }) @@ -83,6 +84,6 @@ const args = massarg({ const opts = args.getArgs(process.argv.slice(2)) -console.log("Opts:", opts) +console.log("Opts:", opts, "\n") args.parse(process.argv.slice(2)) diff --git a/src/massarg.ts b/src/massarg.ts index 5893f5e..4ca7c05 100644 --- a/src/massarg.ts +++ b/src/massarg.ts @@ -19,7 +19,7 @@ export default class Massarg extends Massa export { Massarg } export function massarg( - options: MinimalCommandConfig, + options: MinimalCommandConfig ): MassargCommand { return new Massarg(options) } diff --git a/src/option.ts b/src/option.ts index 4708599..b41ec05 100644 --- a/src/option.ts +++ b/src/option.ts @@ -19,7 +19,9 @@ export const TypedOptionConfig = (type: T) => type: z.enum(["string", "number", "boolean"]).optional(), }), ) -export type TypedOptionConfig = z.infer>>> +export type TypedOptionConfig = z.infer< + ReturnType>> +> export const ArrayOptionConfig = (type: T) => TypedOptionConfig(z.array(type)).merge( @@ -27,8 +29,11 @@ export const ArrayOptionConfig = (type: T) => defaultValue: z.array(type).optional(), }), ) -export type ArrayOptionConfig = z.infer>>> +export type ArrayOptionConfig = z.infer< + ReturnType>> +> +// TODO turn to options const OPT_FULL_PREFIX = "--" const OPT_SHORT_PREFIX = "-" const NEGATE_FULL_PREFIX = "no-" @@ -64,11 +69,13 @@ export default class MassargOption { return new MassargOption(config as OptionConfig) } - valueFromArgv(argv: string[]): ArgvValue { + _parseDetails(argv: string[]): ArgvValue { // TODO: support --option=value argv.shift() + let input = "" try { - const value = this.parse(argv.shift()!) + input = argv.shift()! + const value = this.parse(input) return { key: this.name, value, argv } } catch (e) { if (isZodError(e)) { @@ -76,6 +83,7 @@ export default class MassargOption { path: [this.name, ...e.issues[0].path.map((p) => p.toString())], code: e.issues[0].code, message: e.issues[0].message, + received: JSON.stringify(input), }) } throw e @@ -109,7 +117,11 @@ export default class MassargOption { } _isOption(arg: string): boolean { - return arg.startsWith(OPT_FULL_PREFIX) || arg.startsWith(OPT_SHORT_PREFIX) || arg.startsWith(NEGATE_SHORT_PREFIX) + return ( + arg.startsWith(OPT_FULL_PREFIX) || + arg.startsWith(OPT_SHORT_PREFIX) || + arg.startsWith(NEGATE_SHORT_PREFIX) + ) } } @@ -121,14 +133,15 @@ export class MassargNumber extends MassargOption { }) } - valueFromArgv(argv: string[]): ArgvValue { + _parseDetails(argv: string[]): ArgvValue { try { - const { argv: _argv, value } = super.valueFromArgv(argv) + const { argv: _argv, value } = super._parseDetails(argv) if (isNaN(value)) { throw new ParseError({ path: [this.name], code: "invalid_type", message: "Expected a number", + received: JSON.stringify(argv[0]), }) } return { key: this.name, value, argv: _argv } @@ -138,6 +151,7 @@ export class MassargNumber extends MassargOption { path: [this.name, ...e.issues[0].path.map((p) => p.toString())], code: e.issues[0].code, message: e.issues[0].message, + received: JSON.stringify(argv[0]), }) } throw e @@ -153,7 +167,7 @@ export class MassargFlag extends MassargOption { }) } - valueFromArgv(argv: string[]): ArgvValue { + _parseDetails(argv: string[]): ArgvValue { try { const isNegation = argv[0]?.startsWith("^") argv.shift() diff --git a/src/utils.ts b/src/utils.ts index db72a65..c0d36b8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -50,7 +50,10 @@ export function generateHelpTable( return table.join("\n") } -export function generateOptionsHelpTable(options: MassargOption[], config?: GenerateTableOptions): string { +export function generateOptionsHelpTable( + options: MassargOption[], + config?: GenerateTableOptions, +): string { return generateHelpTable(options, { namePrefix: "--", aliasPrefix: "-", @@ -58,10 +61,24 @@ export function generateOptionsHelpTable(options: MassargOption[], conf }) } -export function generateCommandsHelpTable(commands: MassargCommand[], config?: GenerateTableOptions): string { +export function generateCommandsHelpTable( + commands: MassargCommand[], + config?: GenerateTableOptions, +): string { return generateHelpTable(commands, { namePrefix: "", aliasPrefix: "", ...config, }) } + +export function setOrPush( + newValue: unknown, + currentValue: T[] | T | undefined, + isArray: boolean, +): T { + if (isArray) { + return [...((currentValue as unknown[]) ?? []), newValue] as T + } + return newValue as T +}