-
Notifications
You must be signed in to change notification settings - Fork 13
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
Rewrite the CLI internals #265
Conversation
* master: Lazy load syntax definitions (#263)
* master: Rewrite the @siteimprove/alfa-math package (#268) Create security.md
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.
This looks like solid design choices.
The only real concern I have is about the impossibility to have --no-foo
version of boolean flags which, I'd say, is a fairly common construction in CLI.
Also, the PR description should be saved somewhere as internal doc 😀
* master: Support passing additional arguments to `Parser` Update README
I've added the ability to construct negatable flags through the |
* master: Avoid discarding rules when adding to `RuleTree` (#274)
* master: Rewrite the CLI internals (#265) Avoid discarding rules when adding to `RuleTree` (#274) Move presentational role conflict resolution down to Role.from (#273) Support passing additional arguments to `Parser` Update README Rewrite the @siteimprove/alfa-math package (#268) Create security.md Treat kind attribute as enumerated (#269) Account for presentational role conflict resolution in ARIA feature mappings (#264) Update lockfile Prevent infinite instantiation of `Interview` type (#266) Lazy load syntax definitions (#263) Add user agent styles for `<noscript>` element (#260) Fix some parse issues in `Media` `Preference.initial()` -> `Preference.unset()` Add scripting and user preferences as `Device` parameters (#259) Give build scripts a once-over
This pull request introduces new internals of the CLI package, removing the dependency on @oclif. The reason for doing this is twofold:
Initialisation times of the various @oclif packages weren't great. Doing a simple
$ alfa --help
took upwards of 2 seconds. With the changes introduced in this pull request, doing$ alfa --help
takes about 120 ms.There were some unfortunate "features" in @oclif that were causing annoyances. For starters, the way things were modelled in TypeScript made things like flags and positional arguments a little difficult to work with. Notably, positional arguments weren't typed at all. Several other annoyances were also encountered that I'll outline further below.
To this end, this pull request introduces 3 new building blocks for putting together command line applications.
Flag<T>
This type models a flag, or an option as they're perhaps more commonly known, with a value of type
T
. The term "flag" avoids collision with the existingOption<T>
type for modelling optional types. Also, since flags can be non-optional, it avoids the somewhat oxymoronic term "required option". Flags are constructed by supplying:A name, which is expected to be what's recognised when setting the flag on the command line.
A description, which is output as part of
--help
.A parser, which determines how the flag should be parsed given a list of command line arguments, typically called
argv
.Once constructed, the following methods are available for further modifying the behaviour and presentation of flags:
Flag<T>#type(type: string): Flag<T>
This method sets the "type" of the flag as shown as part of
--help
. For example,flag.type("string")
would result in a--help
output of--flag <string>
.Flag<T>#alias(alias: string): Flag<T>
This method defines an additional alias for the flag, which is another name that is expected to be recognised when setting the flag on the command line.
Flag<T>#default(value: T): Flag<T>
This method specifies the default value of the flag which will be used if the flag is not specified.
Flag<T>#optional(): Flag<Option<T>>
This method turns the flag into an optional flag.
Going from required to optional, rather than the other way around as seen in @oclif, makes the resulting types intuitive to understand and is more ergonomic than having a type like
Flag<T>
exposed asT | undefined
if it's optional or defaulting the type toFlag<Option<T>>
and trying to unwrap it toFlag<T>
it it's required. The latter is what I initially tried in order to keep things aligned with @oclif and it didn't turn out well.Flag<T>#repeatable(): Flag<Iterable<T>>
This method turns the flag into a repeatable flag. This means that the flag can be specified multiple times and the individual values joined into a list.
@oclif also supports something similar through the
multiple
option, but this has the downside of also allowing a list syntax for specifying values. This means that a flag can both be repeated, such as--flag foo --flag bar
, and also specified with a list of values, such as--flag foo bar
. The latter unfortunately risks positional arguments being parsed as part of a flag. For example, this manifested itself when one tried to filter the types of outcomes produced by$ alfa audit
:$ alfa audit --outcomes passed failed https://example.com
The above command would fail with an error stating that
https://example.com
isn't a valid outcome. The solution was to use the "end of options" indicator, which is by no means intuitive:$ alfa audit --outcomes passed failed -- https://example.com
Repeated flags don't suffer from this as the only thing they allow is repeating the same flag multiple times. Then, instead of overwriting the old flag once a new one is encountered, all flags are collected into a list. This means that the above has now turned into this:
$ alfa audit --outcome passed --outcome failed https://example.com
multiple
option and used pluralised names are now implemented as repeatable flags with singular names. This applies to the following flags:--headers
/--cookies
have been replaced by the repeatable--header
/--cookie
flags which now only accepts aheader:value
/cookie=value
pair. New non-repeatable--headers
/--cookies
flags that take a path to a JSON file with headers/cookies have been introduced.--outcomes
has been replaced by the repeatable--outcome
flag.Flag<T>#negatable(mapper: Mapper<T, T>): Flag<T>
This method turns the flag into a negatable flag. Negatable flags provide a special
--no-<flag>
variant that can be used to negate the flag as determined by themapper
argument.Flag<T>#choices<U extends T>(...choices: Array<U>): Flag<U>
This method specifies the allowed choices of value of the flag and is similar to calling
flag.filter(equals(...choices))
.Types
Out of the box support for the following types of flags is currently included:
Flag.string(): Flag<string>
, which constructs a flag that takes a string value. This flag therefore accepts any kind of value.Flag.number(): Flag<number>
, which constructs a flag that takes a number value for whichNumber.isFinite()
holds.Flag.integer(): Flag<number>
, which constructs a flag that takes an integer value for whichNumber.isInteger()
holds.Flag.boolean(): Flag<boolean>
, which constructs a flag that takes a boolean value,true
orfalse
. The value can also left out, which is the same as passingtrue
. Boolean flags are negatable by default with the negated variant flipping the truth value of the flag.Argument<T>
This type models a positional argument with a value of type
T
. Arguments are similar to flags but are recognised by position instead of by name.Command<F, A, S>
This type models a command with flags of type
F
, arguments of typeA
, and subcommands of typeS
. Commands are constructed by supplying:A name, which is expected to be what's recognised when the command is invoked as part of a command line application.
A version, which is output as part of
--help
.A description, which is output as part of
--help
.A record of flags, which are made available to the command runner.
Either:
A record of arguments, which are made available to the command runner.
A record of subcommands, which take the place of arguments and can be invoked using
$ <command> <subcommand>
.A parent command, if the command is used as a subcommand of another command.
A runner, which is a function that is invoked with the parsed flags and arguments specified previously. In the case of commands that contain subcommands, the runner is only invoked when no subcommand is specified.
One annoyance that this type solves compared to @oclif is the fact the keys in the records of flags and arguments in no way influence the names with which the flags and arguments are recognised on the command line and presented as part of
--help
, which is not the case in @oclif. This makes it possible to have repeatable flag such as--cookie
be exposed using a pluralised name such asflags.cookies
.