feat: support variadic args natively#1554
Conversation
Update help rendering to add '...' suffix per-arg based on the `multiple` property, instead of only using command-level `strict === false`. The existing strict===false behavior is preserved as a fallback for backward compatibility.
Rewrites _args() in parse.ts to support variadic args using a shift/pop algorithm: pre-variadic args consume from the front, post-variadic args consume from the back, and everything remaining goes to the variadic arg as an array. Also updates validate.ts to skip UnexpectedArgsError when a variadic arg is present.
Add TypeScript overloads to ArgDefinition so that Args.string({ multiple: true })
correctly types the return as Arg<T[]> instead of Arg<T>. This mirrors how
FlagDefinition handles multiple: true with its overloads.
Break apart _args() by extracting parseArgInput, applyDefault, and tryStdin helpers. Inline the variadic path instead of a separate method to avoid passing 7 parameters. Remove "original logic" comments.
Verify that variadic args show ... suffix in USAGE line: - last position, first with trailing required, middle position - optional variadic shows [FILES...] - non-variadic siblings don't get the suffix
The cacheArgs function wasn't including the `multiple` property when caching arg metadata, which caused help text to miss the `...` suffix for variadic args when loaded from the manifest cache.
Verify that the shift/pop parsing algorithm correctly handles flags placed between positional arguments in various configurations: leading required + variadic, variadic + trailing required, both, and boolean flags mixed in.
Radiergummi
left a comment
There was a problem hiding this comment.
oh, this is amazing. Thank you so much for taking care of this!
|
Thanks for submitting this. I'll get it reviewed and QA'd, and let you know if anything needs to change. |
| await parse(['a', 'b'], { | ||
| args: { | ||
| first: Args.string({multiple: true}), | ||
| second: Args.string({multiple: true}), |
There was a problem hiding this comment.
Q: Just to make sure that this is actually failing specifically because it's two variadics and not because it's a non-required arg following a variadic, should second be required: true?
| }) | ||
|
|
||
| describe('parsing', () => { | ||
| it('variadic arg as last arg', async () => { |
There was a problem hiding this comment.
Just to be as thorough as possible, should this perhaps be split into two different tests called variadic as only arg and variadic as last arg?
There was a problem hiding this comment.
Agreed - the test had a single variadic arg with no other args, so it was actually "only arg" not "last arg". I've included a few more test cases in 8378f8b
| it('variadic arg in the middle', async () => { | ||
| const out = await parse(['tar', 'a', 'b', 'c', './out.tar'], { | ||
| args: { | ||
| format: Args.string({required: true}), |
There was a problem hiding this comment.
Your description of the algorithm indicates that args after the variadic have to be required, but don't the ones before it have to be required as well, otherwise it's unclear which arg to assign parameters to?
There was a problem hiding this comment.
You're right - arguments before a variadic arg were read left to right. If one of those were optional, the parser had no clear way to tell whether the user skipped it or meant their value for the next arg. So the behavior was consistent, but confusing. The validation in only checked args after a variadic one, and should also check args before it. 454ae3d includes a fix for this and also provides clearer messages (as you requested below)
|
@rexxars , I've left some comments. If you could please address them, then we should be good to move on to the QA stage. |
| variadicArgFound = true | ||
| } else if (variadicArgFound && !arg.required) { | ||
| // All args after a variadic arg must be required | ||
| throw new InvalidArgsSpecError({ |
There was a problem hiding this comment.
This error does nothing to indicate that the specific problem is a non-required arg following a variadic arg; it just says "invalid argument spec" and then lists the arguments. Could we get a more informative message here?
There was a problem hiding this comment.
Yep. 454ae3d adds clearer messages indicating what specifically is invalid :)
|
@rexxars , thank you for addressing the feedback. I'll conduct another review and do the necessary QA, and we can hopefully get this merged in the near future. |
|
@rexxars , the code looks good, and I've done the following QA checks:
However, I've noticed one gap: A variadic argument's |
Pushed a commit for this - but needs some careful eyes, typescript defs are getting a little unwieldy 😅 |
Native variadic positional arguments
Adds
multiple: truesupport to positional arg definitions, giving commands a type-safe way to accept a variable number of positional arguments.Before this, the only way to accept extra positional args was
strict: false, which bypasses all arg parsing — no type coercion, no validation, no per-arg help text. You just get a raw string array inargvand sort it out yourself.Now you can do:
And get properly typed, validated, parsed values:
How it works
The variadic arg doesn't have to be last. A shift/pop algorithm (proposed by @Radiergummi in #1346) handles placement anywhere in the arg list:
Constraints enforced at definition time:
required: trueTests
optionsvalidation on variadic args...suffixExamples
cpVariable number of args + a trailing, required arg
sumResolved issues