Repository navigation
Restructure result conventions (again) #70
Description
A new attempt to improve the results (#12 #22 #23 #38 #55) prompted by proposed changes to input configuration (#63), and with better understanding of some of original intent (#38 (comment)).
For context, the configuration syntax proposed in #63 is:
parseArgs({
args: process.argv.slice(2),
options: {
foo: {
short: 'f',
type: 'string', // defaults to 'boolean'
multiple: true,
// default: 'bar' - Future iteration
}
},
})
Result Properties
I propose result has properties for:
optionFound: object with properties andtruevalue for all parsed options found inargs.- e.g.
optionFound: { debug: true, name: true, mult: true }
- e.g.
values: object with properties and values for the parsed options- e.g.
values: { debug: true, name: 'John', mult: ['a', 'b'] }
- e.g.
positionals: array of positionals fromargs- e.g.
positionals: ['alpha']
- e.g.
This is a rename of flags to optionFound. The intent of flags name was the options without values, but the proposed meaning is all options found in args as per original proposal.
vs status quo: storing all the options found in optionFound matches the current implementation, but not the current README examples which are only showing true for boolean options. See also following section on values.
The optionFound field is initially redundant and can be calculated from values. However, it offers a semantic affordance now, and is not redundant if we add support for defaults (or setting known boolean flags to false, say) . Both Command and Yargs did not start out with a concept of whether the option was found in the input args, and that led to requests later as not possible to tell from a value whether was an explicit default (say for a string) or an implicit default (say for a boolean) or for from the parsed arguments.
Values
I propose values property holds for an option:
- string for option with a value in
args(normal case for option withtype:'string') truefor option used as boolean flag (normal case for option withtype:'boolean')- array if
multiples:true, withtrueand/or string elements
The change is storing true as the value for a found boolean option. I think this is the natural value, and is what other parsing libraries do such as Command and Yargs.
vs status quo: the current README examples are omitting boolean options entirely. The current implementation is storing undefined rather than true.
Activity
bcoe commentedon Feb 27, 2022
This design seems quite reasonable to me, with a nit that I don't love the variable name
optionFound. To match the other values returned, seems like a picking something plural would makes sense, some suggestions:optionsSet.optionsProvided.passedOptionsaaronccasanova commentedon Feb 27, 2022
+1 for
passedOptionsAgreed, this design seems reasonable to me as well. However, while were discussing the results object, I'd like to challenge the upcoming relevance of the name
values. If #63 lands andwithValuesis removed, isvaluesstill the best name to convey it is the parsed args based on the options config? I quite like the intuitiveness of the meow api where theflagsconfiguration is also the name of the parsedflagsresult.What are folks thoughts around this convention?
shadowspawn commentedon Feb 27, 2022
I like
passedOptionstoo on its own and not too bad beside options:A wild challenge would be to drop
passedOptionsfor now. A quick sanity check, is anyone actually wanting to usepassedOptionsin code or examples? I'm not looking for a compelling use case, I'm just checking if there is any interest in the extra result, or everyone just ignoring it because it does not affect how you envisage library being used!shadowspawn commentedon Feb 28, 2022
One complication with using
optionsas property in both directions is it requires extra code to make this work:(I don't see any issues against Meow though, so perhaps does not come up much in practice.)
ljharb commentedon Feb 28, 2022
I've definitely run into that kind of issue before, but i think "options" is a pretty confusing name to pass as a property here.
shadowspawn commentedon Mar 12, 2022
Nit. I have gone off
passedOptionsas a result property name since we pass "options" into the configuration. In fact, the unit tests usepassedOptionsas the variable name!parseargs/test/index.js
Line 31 in b412095
I wasn't enthusiastic about my original suggestion of
optionFoundeither. My current plan is I'll have a go at refactor withfoundOptionswhich at least has plural on end.aaronccasanova commentedon Mar 13, 2022
Can you point out the extra code? I'm struggling to see complications in the example.
passedOptionsstill makes sense and reads well to me with that framing. AuthorspassoptionsintoparseArgsand retrieved the (parsed)passedOptionson the other side.I'm not sure this should influence API naming conventions. We could simply rename the test vars from
passedOptionstooptionConfigsshadowspawn commentedon Mar 14, 2022
I wasn't very clear, or accurate... What I was thinking was if there was already a variable called
optionsin play then need to rename when destructuring, like from:to
shadowspawn commentedon Mar 14, 2022
foundOptionsare determined by the inputargs, and not the inputoptions.Although might be a good time to check expectations! In the future, if we added
defaultsto the inputoptions, I was assuming a default would not cause the option to appear infoundOptionsbut would cause the option to appear invalues.parsedOptions. How does that scan?ljharb commentedon Mar 14, 2022
If we ever add defaults, it absolutely must be possible to determine if a defaulted value was explicitly passed or not; to do this in yargs I have to default things to, eg,
Object('foo'), and then see if the value is the string foo or the cached Object foo, which is a horrific workaround."parsed" is better than "passed", because "passed" implies it's unchanged from what the user passed - but it is changed, because it's parsed.
shadowspawn commentedon Mar 14, 2022
For interest, Commander only added support for determining source of option value in v8.3.0 in late 2021 after about a decade without a mechanism. e.g.
'default'vs'CLI'bcoe commentedon Mar 16, 2022
@shadowspawn @ljharb what if rather than an object that contains all objects that were explicitly set, we have the opposite
defaultedwhich contains all the options that were set to their default value?I think this could serve a similar purpose we're looking to solve, but to me makes the return values less confusing. We could then perhaps run with:
I could also be pushed towards:
But I don't love the overloaded term
options.aaronccasanova commentedon Mar 16, 2022
How do we feel about just having one object for parsed options containing the value and usage metadata? I think all the discussion points in issue #43 and PR #63 for input options apply to the outputs e.g. More natural to have all the information in one place and lays the foundation for future iterations:
2 remaining items
aaronccasanova commentedon Mar 16, 2022
Here's a scrappy example of the
ParsedOptionstype with some examples of future iterations:
As an example of how this pattern can be leveraged, a
parsedOption.countfield could address @ljharb's callout in #80 (comment) aboutoptionConfig.type === 'boolean'andoptionConfig.multiple === truehaving a somewhat obscure result (e.g. an array oftruevalues[true, true, etc.]). Instead the result could simply be:shadowspawn commentedon Mar 17, 2022
Actually, I would rather delete it. (Which I'll suggest in next comment!) The sense of "defaulted" is referencing a feature we don't even have yet.
shadowspawn commentedon Mar 17, 2022
Short version: I propose we drop
optionFound. (The property previously known asargsandflags, and also suggested aspassedOptions,parsedOptions, and most recently inverteddefaulted...)Long version
I already speculated about dropping it earlier in this issue, and got no defenders (#70 (comment)). I think it is time to get serious about deleting it! I have been asking what it is for since Nov last year and got no compelling answers, and we are still trying to give it a sensible name, with no compelling use case. 😱
Why have the field at all?
Suggested: differentiate between the case where an option was passed no argument, vs., an option not being set at all (#13 (comment))
Counter: we can tell the difference without it. An option that appears in the input args will have an entry in
values, an option not set at all will not appear invalues.Suggested: simplify testing for existence of option in args (#24 (comment)).
e.g.
const fooExists = !! optionFound.fooCounter: with values as described here:
const fooExists = values.foo !== undefined(or!= nullif you prefer!)Slightly longer to be fair, but you are going to be working with
valuesanyway so not a new concept. Other packages use single property for the parsed options (#12 (comment)).Suggested: tell where value came from in future (e.g. if add defaults)
Counter: do what makes sense in future
shadowspawn commentedon Mar 17, 2022
My initial reaction is no, I think combining the values and usage metadata is overkill and complicating the primary target audience. i.e.
In the future, if there is demand for this as a base for building richer parsers and custom behaviour, I think there could be a metadata containing property. (Which could perhaps include the value as well so was self-contained.)
aaronccasanova commentedon Mar 17, 2022
This feels similar to the initial reactions when I proposed the restructured option configs API. I'm trying to imagine what
parseArgswill look like one year from now and make sure we're considering what API will support new features and enhancements. From what I can see, we are going down the same path as the original option configs API where information for a single option is scattered across the primary config object.Here is an example of what I'm trying to avoid after say one year of features and enhancements:
Please don't take my example features as literal suggestions, but rather to illustrate general enhancements to
parseArgs.As far as the proposal being complicated for the target audience, I'm not sure I follow. I believe my suggestion improves the robustness of the API and is actually more intuitive for the target audience.
ljharb commentedon Mar 17, 2022
Certainly if we have no "defaults" at all, then it's easy to see when an option isn't provided.
However, it seems pretty important for almost every CLI workflow i know to have defaults, and for some, it's critical to be able to know whether they're defaulted or not.
shadowspawn commentedon Mar 18, 2022
Fair concern. I don't see organic growth of user-level properties as a likely outcome. Other libraries have had a decade of features and enhancements, and have a single layer of object values available. See #12 (comment)
If we add more information for people building richer parsers, I do prefer the idea of putting that richer metadata into one property and not scattering across multiple top level properties.
Other metadata mentioned in #52 (comment) #52 (comment) #84
shadowspawn commentedon Mar 20, 2022
(More metadata in: #84)
I am thinking a property like
parseDetailswhich has data of use for people building on top of the library, but is usually ignored by direct users of the library. As with Aarons example, these properties are not suggestions just illustrations.e.g.
aaronccasanova commentedon Mar 28, 2022
100%, Totally valid argument. However, I still (personally) feel the combination of option value and metadata is the more intuitive/future-facing API. The closest example I can think of is RegExp match objects.
Notice: Each match object contains the full match, number and named capture group values, the start index, indices of capture groups, etc.
Now imagine if instead
String.prototype.matchAll()returned the matches in one object and all the metadata in a separate one. Wouldn't be as intuitive imo.I may be reaching a bit with the above demo, but am attempting to provide a real-world example of a parsed input that includes values and metadata in the same result to demonstrate the intuitiveness of the API.
bakkot commentedon Mar 28, 2022
@aaronccasanova:
I think the overwhelmingly common case is going to be just getting the value without any metadata, possibly with destructuring, as in
And that gets much more annoying if metadata is mixed with values. I think it's important to optimize for the common case.
Note that even in the
matchAllexample, there are separategroupsandindicesproperties, and thegroupsproperty is an object which directly associates values with named groups; the metadata for named groups, i.e. the indices, are in a separate.indices.groupsfield. We had the destructuring example in mind when designing named capture groups for regexes, IIRC.shadowspawn commentedon Mar 30, 2022
🔨 Moving towards MVP Milestone 1 (#87)
See #83 !
shadowspawn commentedon Apr 2, 2022
(Exploring interest, not pushing for a change!)
I do like
optionsas the result property withflagsgone:I am not keen on using same property on input and output despite the symmetry (#70 (comment)), and feel there is additional confusion on the input usage with common usage of "options" for an option bag.
Perhaps the input property could be renamed? Inspired in part by
strict:falseusage:aaronccasanova commentedon Apr 2, 2022
No objections from me. I was thinking the same thing after the initial concerns around using
optionsfor both the input and output. I'm a fan ofknownOptions,optionConfigs,optionsConfig, etc. Kind of likingoptionsConfig🤓 (naming convention reminds me of the package.jsonpublishConfigkey).shadowspawn commentedon Apr 13, 2022
Landed #83. Closing this, the culmination for five months of issues. 😅