all repos — cmd @ 968a6b07a401a38f88958d5ba3e10d60abdc54ef

Unnamed repository; edit this file 'description' to name the repository.

group flags; default args; throw if extra pos args are provided
vi did:web:vt3e.cat
Sun, 10 May 2026 02:09:34 +0100
commit

968a6b07a401a38f88958d5ba3e10d60abdc54ef

parent

46e03878fdfb0dfc2fe0cf994e91d486e84715e3

2 files changed, 94 insertions(+), 38 deletions(-)

jump to
M src/index.tssrc/index.ts

@@ -4,13 +4,16 @@ export class Command<TContext extends Record<string, unknown> = {}> {

public name?: string; public description?: string; private flags: Record<string, FlagOptions> = {}; - private args: Record<string, ArgumentOptions<unknown>> = {}; + private args: Record<string, ArgumentOptions<unknown, unknown>> = {}; private positionals: Array<{ name: string; options: { description?: string; required?: boolean } | undefined; }> = []; private subcommands: Record<string, Command<Record<string, unknown>>> = {}; private actionFn?: (ctx: TContext) => void | Promise<void>; + + private shortToFlag: Record<string, string> = {}; + private shortToArg: Record<string, string> = {}; constructor(name?: string) { this.name = name;

@@ -31,46 +34,79 @@ name: TName,

options?: FlagOptions, ): Command<Prettify<TContext & { [K in TName]: boolean }>> { this.flags[name] = options || {}; + if (options?.short) this.shortToFlag[options.short] = name; return this as unknown as Command< Prettify<TContext & { [K in TName]: boolean }> >; } - addStringArgument<TName extends string, TReq extends boolean = false>( + addStringArgument< + TName extends string, + TReq extends boolean = false, + TDef extends string | undefined = undefined, + >( name: TName, - options?: Omit<ArgumentOptions<"string">, "type"> & { required?: TReq }, + options?: Omit<ArgumentOptions<"string", TDef>, "type"> & { + required?: TReq; + default?: TDef; + }, ): Command< Prettify< TContext & { - [K in TName]: TReq extends true ? string : string | undefined; + [K in TName]: TDef extends string + ? string + : TReq extends true + ? string + : string | undefined; } > > { this.args[name] = { ...options, type: "string" }; + if (options?.short) this.shortToArg[options.short] = name; return this as unknown as Command< Prettify< TContext & { - [K in TName]: TReq extends true ? string : string | undefined; + [K in TName]: TDef extends string + ? string + : TReq extends true + ? string + : string | undefined; } > >; } - addNumberArgument<TName extends string, TReq extends boolean = false>( + addNumberArgument< + TName extends string, + TReq extends boolean = false, + TDef extends number | undefined = undefined, + >( name: TName, - options?: Omit<ArgumentOptions<"number">, "type"> & { required?: TReq }, + options?: Omit<ArgumentOptions<"number", TDef>, "type"> & { + required?: TReq; + default?: TDef; + }, ): Command< Prettify< TContext & { - [K in TName]: TReq extends true ? number : number | undefined; + [K in TName]: TDef extends number + ? number + : TReq extends true + ? number + : number | undefined; } > > { this.args[name] = { ...options, type: "number" }; + if (options?.short) this.shortToArg[options.short] = name; return this as unknown as Command< Prettify< TContext & { - [K in TName]: TReq extends true ? number : number | undefined; + [K in TName]: TDef extends number + ? number + : TReq extends true + ? number + : number | undefined; } > >;

@@ -80,6 +116,7 @@ addChoiceArgument<

TName extends string, const TChoices extends readonly string[], TReq extends boolean = false, + TDef extends TChoices[number] | undefined = undefined, >( name: TName, options: {

@@ -87,23 +124,29 @@ choices: TChoices;

required?: TReq; description?: string; short?: string; + default?: TDef; }, ): Command< Prettify< TContext & { - [K in TName]: TReq extends true + [K in TName]: TDef extends TChoices[number] ? TChoices[number] - : TChoices[number] | undefined; + : TReq extends true + ? TChoices[number] + : TChoices[number] | undefined; } > > { this.args[name] = { ...options, type: "choice" }; + if (options?.short) this.shortToArg[options.short] = name; return this as unknown as Command< Prettify< TContext & { - [K in TName]: TReq extends true + [K in TName]: TDef extends TChoices[number] ? TChoices[number] - : TChoices[number] | undefined; + : TReq extends true + ? TChoices[number] + : TChoices[number] | undefined; } > >;

@@ -153,6 +196,11 @@ for (const [key, opts] of Object.entries(this.flags)) {

ctx[key] = opts.default ?? false; } + // init args to their defaults + for (const [key, opts] of Object.entries(this.args)) { + if (opts.default !== undefined) ctx[key] = opts.default; + } + let i = 0; while (i < argv.length) { const arg = argv[i] as string;

@@ -193,36 +241,43 @@ } else {

throw new Error(`Unknown option: --${name}`); } } - } - - if (isShort) { - const short = arg.slice(1); - const flagEntry = Object.entries(this.flags).find( - ([_, o]) => o.short === short, - ); - const argEntry = Object.entries(this.args).find( - ([_, o]) => o.short === short, - ); + } else if (isShort) { + const shorts = arg.slice(1).split(""); + for (let j = 0; j < shorts.length; j++) { + const short = shorts[j] as string; + const flagName = this.shortToFlag[short]; + const argName = this.shortToArg[short]; - if (flagEntry) { - ctx[flagEntry[0]] = true; - } else if (argEntry) { - const [name, opts] = argEntry; - let value: string | undefined; - if (i + 1 < argv.length && !(argv[i + 1] as string).startsWith("-")) { - value = argv[i + 1]; - i++; + if (flagName) { + ctx[flagName] = true; + } else if (argName) { + const opts = this.args[argName]; + let value: string | undefined; + if (j === shorts.length - 1) { + if ( + i + 1 < argv.length && + !(argv[i + 1] as string).startsWith("-") + ) { + value = argv[i + 1]; + i++; + } + } + ctx[argName] = this.parseValue(argName, value, opts!); + } else { + throw new Error(`Unknown short option: -${short}`); } - ctx[name] = this.parseValue(name, value, opts); - } else { - throw new Error(`Unknown short option: -${short}`); } + } else { + positionalsProvided.push(arg); } - if (!isLong && !isShort) positionalsProvided.push(arg); - i++; } + + if (positionalsProvided.length > this.positionals.length) + throw new Error( + `Unknown positional argument: ${positionalsProvided[this.positionals.length]}`, + ); for (let p = 0; p < this.positionals.length; p++) { const pos = this.positionals[p]!;

@@ -243,7 +298,7 @@

private parseValue( name: string, value: string | undefined, - opts: ArgumentOptions<unknown>, + opts: ArgumentOptions<unknown, unknown>, ) { if (value === undefined) throw new Error(`Option --${name} requires a value`);
M src/types.tssrc/types.ts

@@ -8,9 +8,10 @@ short?: string;

default?: boolean; }; -export type ArgumentOptions<TType> = { +export type ArgumentOptions<TType, TDef> = { description?: string; type: TType; required?: boolean; short?: string; + default?: TDef; };