-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add async custom processing support. Add chainArgParserCalls()
for configuration. Additionally await thenable implicit and default option values and thenable default argument values
#1915
Conversation
Co-authored-by: John Gee <[email protected]>
The behavior can be configured via chainArgParserCalls()
The behavior can be configured via await() Co-authored-by: John Gee <[email protected]>
Possible since undefined properties are not checked
lib/command.js
Outdated
const parsed = this.parseOptions(unknown); | ||
this._parseOptionsEnv(); // after cli, so parseArg not called on both cli and env | ||
this._parseOptionsImplied(); | ||
chain = this._chainOrCall(chain, () => this._awaitOptionResavePromises()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a comment.
This is a nice logical position for processing the options just after they are defined. It does unfortunately spread the chaining logic through the top of this routine. I went for positions close to the hooks since I knew there was already chaining support for hooks.
(And processing async parseArgs for arguments when is no action handler spreads the chaining logic down through routine.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does unfortunately spread the chaining logic through the top of this routine.
But on the other hand, it unifies the option value awaiting for parent and leaf commands, and _dispatchSubcommand()
can remain unchanged. Also, it is actually quite nice to have the chain
declaration at the very top and return chain
at the bottom, that kind of sets the logical frame for the function.
Co-authored-by: John Gee <[email protected]>
I am not sure what scenario you have in mind here. Is this when the user has written their own async handling which the built-in chaining would effectively bypass? |
I laughed when I saw this, I have used "settle" in a couple of dubious places. Still getting terminology straight. |
The way Commander usually does this is copy "common" settings to subcommands as they are added. The routine that does the work is (This might simplify some of your tri-state behaviours down to two.) |
I mean the user should not rely on the library for async handling and instead ideally always have their own when calling |
If we only have the There are following problems with only having those two modes:
|
This is in accordance with how other properties containing information about the last parse call such as args & processedArgs do not get unset. Consistency in how repeated parse calls are handled could be improved by resolving tj#1916.
This comment was marked as outdated.
This comment was marked as outdated.
Update based on the answer to #1916
Is adding subsubcommands only after the subcommand was added to its parent also the intended pattern? If so, that would ruin my reasoning in the last 2 comments, but then it definitely needs to be mentioned somewhere in the docs. Update: only relevant for explicitly constructed subcommands added with |
Yes. In particular if you have changed any inherited default configuration on the root command, then need to do in this order to get settings propagated. (With default settings, I expect can construct in any order.) |
parse()
. Chain parseArg()
calls and await thenable option and argument values when parseAsync()
is called. Add Option.chainArgParserCalls()
and Argument.chainArgParserCalls()
for configurationchainArgParserCalls()
for configuration. Additionally await implicit and default option values
TODO
|
chainArgParserCalls()
for configuration. Additionally await implicit and default option valueschainArgParserCalls()
for configuration. Additionally await thenable implicit and default option values and thenable default argument values
Now has to be merged together with #1917 for consistent async warnings in |
For interest, the rationale for that design decision was to allow an author's custom parser to be used with both options and command-arguments. Like say |
Motivation: tj#1955
This was an epic journey from a few extra lines of code to a deep exploration of async implementation! Special mention of lots of explanation and lots of unit tests. I learnt more about async while reviewing and trying out these ideas, especially the error handling. I do not want to proceed with this at this time. My reservations are:
I think simple author implemented async support for a limited scenario could make a nice example file. Like in #1900 (comment) |
This is a rework of #1902 not relying on the hook infrastructure and not introducing
postArguments
hooks, i.e. all simplifications from this comment have been implemented.Changes summary
ChangeLog
Added
Option.chainArgParserCalls()
andArgument.chainArgParserCalls()
to control whether to chainparseArg
calls.parse()
calls, including default and implied values and ones returned from async argParsers, with a suggestion to use.parseAsync()
insteadChanged
parseArg
calls by default when.parseAsync()
is called.parseAsync()
is calledConfiguration details
Option.chainArgParserCalls()
/Argument.chainArgParserCalls()
true
(the default), next call to the function provided via.argParser()
will be chained to its return value if it is thenable.New life cycle description
The processing starts with an array of args. Each command processes and removes the options it understands, and passes the remaining args to the next subcommand. The final command calls the action handler.
Starting with top-level command (program):
parseAsync()
preSubcommand
hooks1Once reach final (leaf) command:
parseAsync()
preAction
hooks1postAction
hooks1Some explanations
Why enable chaining and awaiting for
parseAsync()
by default?A good question, especially because this introduces breaking changes. The decision is motivated by the fact it is most logical for the following code snippets to produce the same results:
Without chaining, a promise resolving to
'[object Promise]2'
is logged from inside the action instead of12
when running the second snippet.Without awaiting, an unhandled promise rejection is reported instead of logging
caught exception
when running the second snippet.Why not enable chaining and awaiting for
parse()
?To make the changes as "non-breaking" as possible (although they will be breaking anyway).
There might be production code like the one in the original example in Consider supporting async option/argument parsers #1900 somewhere. By not enabling awaiting for
parse()
by default, we make sure this code still works after the upgrade.Chaining and awaiting in
parse()
introduces the risk of unhandled promise rejections, so they should not be enabled unless the user explicitly requests it.Why is
chainArgParserCalls()
necessary?true
mode is motivated here and in the Why enable chaining and awaiting forparseAsync()
by default? section.false
mode is motivated here.Why store and await overwritten option values?
An end user might repeat an option even if it is not supposed to be repeated, i.e. if the parser ignores the old value supplied to it in the second parameter. If
chainArgParserCalls(false)
had been called on the option, this can result in unhandled promise rejections if overwritten option values are not awaited.Why pass
handleError
to_parseArg()
and not just the error message for invalid argument errors?It seems to be more or less an arbitrary design decision to give invalid argument errors thrown from option-argument and command-argument parsers the same
.code
values ('commander.invalidArgument'
). They could just as well be different, or there could theoretically be other specially handled errors specific only to option-argument parsers or only to command-argument parsers, so it just does not seem right for_parseArg()
to take over the error handling.Why use the word "await" and not "settle"?
To settle a promise means to fulfill or to reject it. That happens internally in the promise callback, an observer can only await the promise.
Peer PRs
…solving similar problems
parse()
in addition to the warnings for async argParser calls introduced here)Warnings need to be consistent with…
.command()
#1938.parse()
#1940Requires changes when merged with…
.suppressWarnings()
for warnings in #1915 #1931 #1938 #1940 #1955See also
Footnotes
_output a warning suggesting use of
parseAsync()
when parsing withparse()
and a thenable is returned ↩ ↩2 ↩3 ↩4 ↩5 ↩6