diff --git a/playground/hello.ts b/playground/hello.ts index d272761..be9920e 100644 --- a/playground/hello.ts +++ b/playground/hello.ts @@ -28,6 +28,11 @@ const command = defineCommand({ default: "awesome", required: false, }, + likes: { + type: "multiPositional", + description: "Most liked things", + default: ["orange", "strawberry"], + }, }, run({ args }) { console.log(args); @@ -36,6 +41,7 @@ const command = defineCommand({ args.adj || "", args.name, args.age ? `You are ${args.age} years old.` : "", + args.likes?.length ? `You like ${args.likes.join(", ")}.` : "", ] .filter(Boolean) .join(" "); diff --git a/src/args.ts b/src/args.ts index 6fe9499..d24f502 100644 --- a/src/args.ts +++ b/src/args.ts @@ -12,7 +12,7 @@ export function parseArgs( boolean: [] as string[], string: [] as string[], alias: {} as Record, - default: {} as Record, + default: {} as Record, } satisfies ParseOptions; const args = resolveArgs(argsDef); @@ -60,7 +60,19 @@ export function parseArgs( }); for (const [, arg] of args.entries()) { - if (arg.type === "positional") { + if (arg.type === "multiPositional") { + if (positionalArguments.length > 0) { + parsedArgsProxy[arg.name] = [...positionalArguments]; + positionalArguments.length = 0; + } else if (arg.default === undefined && arg.required !== false) { + throw new CLIError( + `Missing required multiPositional argument: ${arg.name.toUpperCase()}`, + "EARG", + ); + } else { + parsedArgsProxy[arg.name] = arg.default; + } + } else if (arg.type === "positional") { const nextPositionalArgument = positionalArguments.shift(); if (nextPositionalArgument !== undefined) { parsedArgsProxy[arg.name] = nextPositionalArgument; diff --git a/src/types.ts b/src/types.ts index cb334c0..8956d3f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,10 @@ // ----- Args ----- -export type ArgType = "boolean" | "string" | "enum" | "positional" | undefined; +export type ArgType = "boolean" | "string" | "enum" | "positional" | "multiPositional" | undefined; // Args: Definition -export type _ArgDef = { +export type _ArgDef = { type?: T; description?: string; valueHint?: string; @@ -20,8 +20,14 @@ export type BooleanArgDef = Omit<_ArgDef<"boolean", boolean>, "options"> & { export type StringArgDef = Omit<_ArgDef<"string", string>, "options">; export type EnumArgDef = _ArgDef<"enum", string>; export type PositionalArgDef = Omit<_ArgDef<"positional", string>, "alias" | "options">; +export type MultiPositionalArgDef = Omit<_ArgDef<"multiPositional", string[]>, "alias" | "options">; -export type ArgDef = BooleanArgDef | StringArgDef | PositionalArgDef | EnumArgDef; +export type ArgDef = + | BooleanArgDef + | StringArgDef + | PositionalArgDef + | MultiPositionalArgDef + | EnumArgDef; export type ArgsDef = Record; @@ -44,6 +50,12 @@ type ParsedPositionalArg = T extends { type: "positional" } ? ResolveParsedArgType : never; +type ParsedMultiPositionalArg = T extends { + type: "multiPositional"; +} + ? ResolveParsedArgType + : never; + type ParsedStringArg = T extends { type: "string" } ? ResolveParsedArgType : never; @@ -68,6 +80,7 @@ type RawArgs = { // prettier-ignore type ParsedArg = T["type"] extends "positional" ? ParsedPositionalArg : + T["type"] extends "multiPositional" ? ParsedMultiPositionalArg : T["type"] extends "boolean" ? ParsedBooleanArg : T["type"] extends "string" ? ParsedStringArg : T["type"] extends "enum" ? ParsedEnumArg : diff --git a/src/usage.ts b/src/usage.ts index dbb5dfb..7ca72a2 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -35,7 +35,18 @@ export async function renderUsage( const usageLine = []; for (const arg of cmdArgs) { - if (arg.type === "positional") { + if (arg.type === "multiPositional") { + const name = arg.name.toUpperCase(); + const isRequired = arg.required !== false && arg.default === undefined; + // (isRequired ? " (required)" : " (optional)" + const defaultHint = arg.default ? `=[${arg.default}]` : ""; + posLines.push([ + colors.cyan(name + defaultHint), + arg.description || "", + arg.valueHint ? `<${arg.valueHint}>` : "", + ]); + usageLine.push(isRequired ? `<...${name}>` : `[...${name}]`); + } else if (arg.type === "positional") { const name = arg.name.toUpperCase(); const isRequired = arg.required !== false && arg.default === undefined; posLines.push([colors.cyan(name + renderValueHint(arg)), renderDescription(arg, isRequired)]); diff --git a/test/args.test.ts b/test/args.test.ts index 1ace019..7392393 100644 --- a/test/args.test.ts +++ b/test/args.test.ts @@ -37,6 +37,28 @@ describe("args", () => { { _: [], command: "subCommand" }, ], [[], { command: { type: "positional", required: false } }, { _: [] }], + /** + * MultiPositional + */ + [ + ["subCommand1", "subCommand2"], + { command: { type: "multiPositional" } }, + { + _: ["subCommand1", "subCommand2"], + command: ["subCommand1", "subCommand2"], + }, + ], + [ + [], + { + command: { + type: "multiPositional", + default: ["subCommand1", "subCommand2"], + }, + }, + { _: [], command: ["subCommand1", "subCommand2"] }, + ], + [[], { command: { type: "multiPositional", required: false } }, { _: [] }], /** * Enum */ @@ -68,6 +90,13 @@ describe("args", () => { }, "Missing required positional argument: NAME", ], + [ + [], + { + name: { type: "multiPositional" }, + }, + "Missing required multiPositional argument: NAME", + ], [ ["--value", "three"], { value: { type: "enum", options: ["one", "two"] } }, diff --git a/test/usage.test.ts b/test/usage.test.ts index ae38570..2a4aacb 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -25,6 +25,11 @@ describe("usage", () => { name: "pos", description: "A pos", }, + multiPositional: { + type: "multiPositional", + name: "pos", + description: "Multi positional", + }, enum: { type: "enum", name: "enum", @@ -44,11 +49,12 @@ describe("usage", () => { expect(usage).toMatchInlineSnapshot(` "A command (Commander) - USAGE Commander [OPTIONS] --foo= + USAGE Commander [OPTIONS] --foo= <...MULTIPOSITIONAL> ARGUMENTS - POS A pos (Required) + POS A pos (Required) + MULTIPOSITIONAL Multi positional OPTIONS