From 5a2b953f5c2d9c3612ec63af0d730819aff68ea6 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 30 Aug 2020 19:55:01 +1200 Subject: [PATCH 1/4] Refactor and expand intro and parse ambiguity --- optional-options-docs.md | 172 ++++++++++++++++++++++++++++++++------- 1 file changed, 141 insertions(+), 31 deletions(-) diff --git a/optional-options-docs.md b/optional-options-docs.md index 4a6ca13ee..9944dc02a 100644 --- a/optional-options-docs.md +++ b/optional-options-docs.md @@ -1,60 +1,170 @@ -# Tricks and traps when using options with optional values +# More About Options -There are potential challenges using options with optional values. They seem quite attractive and the README used to use them more than options with require values but in practice, they are a bit tricky and aren't a free choice. +The README covers declaring and using options, and mostly parsing will work the way you and your users expect. This page covers some special cases +and subtle issues in depth. + +- [More About Options](#more-about-options) + - [Terminology](#terminology) + - [Options taking varying numbers of option-arguments](#options-taking-varying-numbers-of-option-arguments) + - [Parsing ambiguity](#parsing-ambiguity) + - [Alternative: Make `--` part of your syntax](#alternative-make----part-of-your-syntax) + - [Alternative: Put options last](#alternative-put-options-last) + - [Alternative: Use options instead of command-arguments](#alternative-use-options-instead-of-command-arguments) + - [Combining short flags with optional values](#combining-short-flags-with-optional-values) + - [Behaviour change from v5 to v6](#behaviour-change-from-v5-to-v6) ## Terminology -| Term(s) | Explanation | code example (if any) | -| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | -| option(s), flags, non-positional arguments | The term options consist of hyphen-minus characters
(that is ‘-’) followed by letters or digits.
options can take an argument or choose not to.
options that do not take an argument are term boolean flag(s) or boolean option(s) | `.option('-s, --small', 'small pizza size')` | -| option argument(s) | options are followed by an option argument.
If they are enclosed with square brackets `[]`, these option arguments are optional. | `.option('-o, --option [optionalValue]')` | -| operand(s), non-option argument(s) | arguments following the last options and option-arguments are named “operands” | `.arguments('[file]')` | +_Work in progress: this section may move to the main README, or a page of its own._ + +The command line arguments are made up of options, option-arguments, commands, and command-arguments. -## Parsing ambiguity +| Term | Explanation | +| --- | --- | +| option | an argument which begins with a dash and a character, or a double-dash and a word | +| option-argument| some options can take an argument | +| command | a program or command can have subcommands | +| command-argument | argument for the command (and not an option or option-argument) | -There is parsing ambiguity when using option as boolean flag and also having it accept operands (sometimes called a positional argument or command argument, referring to `Command.arguments()`) and subcommands. +For example: +```sh +my-utility command -o --option option-argument command-argument-1 command-argument-2 ``` + +In other references options are sometimes called flags, and command-arguments are sometimes called positional arguments. + +## Options taking varying numbers of option-arguments + +Certain options take a varying number of option-arguments: + +```js program - .arguments("[technique]") - .option("-i, --ingredient [ingredient]") - .action((args, cmdObj) => { - console.log(args); - console.log(cmdObj.opts()); + .option('-c, --compress [percentage]') // 0 or 1 + .option('--preprocess ') // 1 or more + .option('--test [name...]') // 0 or more +``` + +### Parsing ambiguity + +There is a potential downside to be aware of. If a command has both +command-arguments and options with varying option-arguments, this introduces a parsing ambiguity which may affect the user of your program. +Commander looks for option-arguments first, but the user may +intend the argument following the option as a command or command-argument. + +```js +program + .name('cook') + .arguments('[technique]') + .option('-i, --ingredient [ingredient]', 'add cheese or given ingredient') + .action((technique, options) => { + console.log(`technique: ${technique}`); + const ingredient = (options.ingredient === true) ? 'cheese' : options.ingredient; + console.log(`ingredient: ${ingredient}`); }); program.parse(); ``` -``` +```sh $ cook scrambled -scrambled -{ ingredient: undefined } +technique: scrambled +ingredient: undefined $ cook -i -undefined -{ ingredient: true } +technique: undefined +ingredient: cheese $ cook -i egg -undefined -{ ingredient: egg } +technique: undefined +ingredient: egg + +$ cook -i scrambled # oops +technique: undefined +ingredient: scrambled +``` + +The explicit way to resolve this is use `--` to indicate the end of the options and option-arguments: -$ cook -i scrambled -undefined -{ ingredient: scrambled } +```sh +$ node cook.js -i -- egg +technique: egg +ingredient: cheese ``` -For example, you may intend `scrambled` to be passed as a non-option argument. Instead, it will be read as the passed in value for ingredient. +If you want to avoid your users needing to learn when to use `--`, there are a few approaches you could take. + +### Alternative: Make `--` part of your syntax + +Rather than trying to teach your users what `--` does, you could just make it part of your syntax. + +```js +program.usage('[options] -- [technique]'); +``` -### Possible workarounds +```sh +$ cook --help +Usage: cook [options] -- [technique] -To reduce such ambiguity, you can do the following: +Options: + -i, --ingredient [ingredient] add cheese or given ingredient + -h, --help display help for command -1. always use `--` before operands -2. add your options after operands -3. convert operands into options! Options work pretty nicely together. +$ cook -- scrambled +technique: scrambled +ingredient: undefined + +$ cook -i -- scrambled +technique: scrambled +ingredient: cheese +``` + +### Alternative: Put options last + +Commander follows the GNU convention for parsing and allows options before or after the command-arguments, or intermingled. +So by putting the options last, the command-arguments do not get confused with the option-arguments. + +```js +program.usage('[technique] [options]'); +``` + +```sh +$ cook --help +Usage: cook [technique] [options] + +Options: + -i, --ingredient [ingredient] add cheese or given ingredient + -h, --help display help for command + +$ node cook.js scrambled -i +technique: scrambled +ingredient: cheese +``` + +### Alternative: Use options instead of command-arguments + +This is a bit more radical, but completely avoids the parsing ambiguity! + +```js +program + .name('cook') + .option('-t, --technique ', 'cooking technique') + .option('-i, --ingredient [ingredient]', 'add cheese or given ingredient') + .action((options) => { + console.log(`technique: ${options.technique}`); + const ingredient = (options.ingredient === true) ? 'cheese' : options.ingredient; + console.log(`ingredient: ${ingredient}`); + }); +``` + +```sh +$ cook -i -t scrambled +technique: scrambled +ingredient: cheese +``` -The POSIX convention is that options always come before operands. The GNU utility convention allows options to come before or after the operands. Commander follows the GNU convention and allows options before or after the operands. So by putting the options last, the option values do not get confused with the operands. +------ +_Work in progress: unreviewed from here down._ ## Combining short flags with optional values From 53cf35be1edf69df8f09f5ff36f0eaa16b48c85e Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 30 Aug 2020 20:18:07 +1200 Subject: [PATCH 2/4] Expand fragments a little, reword GNU reference --- optional-options-docs.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/optional-options-docs.md b/optional-options-docs.md index 9944dc02a..b407ecd9e 100644 --- a/optional-options-docs.md +++ b/optional-options-docs.md @@ -99,7 +99,9 @@ If you want to avoid your users needing to learn when to use `--`, there are a f Rather than trying to teach your users what `--` does, you could just make it part of your syntax. ```js +// ... program.usage('[options] -- [technique]'); +program.parse(); ``` ```sh @@ -121,11 +123,13 @@ ingredient: cheese ### Alternative: Put options last -Commander follows the GNU convention for parsing and allows options before or after the command-arguments, or intermingled. +Commander allows options before or after the command-arguments, or even intermixed (like GNU program argument conventions). So by putting the options last, the command-arguments do not get confused with the option-arguments. ```js +// ... program.usage('[technique] [options]'); +program.parse(); ``` ```sh @@ -155,6 +159,8 @@ program const ingredient = (options.ingredient === true) ? 'cheese' : options.ingredient; console.log(`ingredient: ${ingredient}`); }); + +program.parse(); ``` ```sh From 8113f41a910b3740f2fa2b92c60eae8167d45a0a Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 30 Aug 2020 20:25:01 +1200 Subject: [PATCH 3/4] Some tweaks, and add GNU link --- optional-options-docs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optional-options-docs.md b/optional-options-docs.md index b407ecd9e..eeb1bbb0a 100644 --- a/optional-options-docs.md +++ b/optional-options-docs.md @@ -21,7 +21,7 @@ The command line arguments are made up of options, option-arguments, commands, a | Term | Explanation | | --- | --- | -| option | an argument which begins with a dash and a character, or a double-dash and a word | +| option | an argument which begins with a dash and a character, or a double-dash and a string | | option-argument| some options can take an argument | | command | a program or command can have subcommands | | command-argument | argument for the command (and not an option or option-argument) | @@ -123,7 +123,7 @@ ingredient: cheese ### Alternative: Put options last -Commander allows options before or after the command-arguments, or even intermixed (like GNU program argument conventions). +Commander allows options before or after the command-arguments, or even intermixed (like [GNU program argument conventions](https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html)). So by putting the options last, the command-arguments do not get confused with the option-arguments. ```js From 3434f4d2773e51481c84925e776fe4678b58a4cd Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Sep 2020 20:54:36 +1200 Subject: [PATCH 4/4] Rework, and move file --- .../options-taking-varying-arguments.md | 163 +++++++----------- 1 file changed, 59 insertions(+), 104 deletions(-) rename optional-options-docs.md => docs/options-taking-varying-arguments.md (53%) diff --git a/optional-options-docs.md b/docs/options-taking-varying-arguments.md similarity index 53% rename from optional-options-docs.md rename to docs/options-taking-varying-arguments.md index eeb1bbb0a..566216f15 100644 --- a/optional-options-docs.md +++ b/docs/options-taking-varying-arguments.md @@ -1,42 +1,18 @@ -# More About Options +# Options taking varying numbers of option-arguments The README covers declaring and using options, and mostly parsing will work the way you and your users expect. This page covers some special cases and subtle issues in depth. -- [More About Options](#more-about-options) - - [Terminology](#terminology) - - [Options taking varying numbers of option-arguments](#options-taking-varying-numbers-of-option-arguments) - - [Parsing ambiguity](#parsing-ambiguity) +- [Options taking varying numbers of option-arguments](#options-taking-varying-numbers-of-option-arguments) + - [Parsing ambiguity](#parsing-ambiguity) - [Alternative: Make `--` part of your syntax](#alternative-make----part-of-your-syntax) - [Alternative: Put options last](#alternative-put-options-last) - [Alternative: Use options instead of command-arguments](#alternative-use-options-instead-of-command-arguments) - - [Combining short flags with optional values](#combining-short-flags-with-optional-values) - - [Behaviour change from v5 to v6](#behaviour-change-from-v5-to-v6) - -## Terminology - -_Work in progress: this section may move to the main README, or a page of its own._ - -The command line arguments are made up of options, option-arguments, commands, and command-arguments. - -| Term | Explanation | -| --- | --- | -| option | an argument which begins with a dash and a character, or a double-dash and a string | -| option-argument| some options can take an argument | -| command | a program or command can have subcommands | -| command-argument | argument for the command (and not an option or option-argument) | - -For example: - -```sh -my-utility command -o --option option-argument command-argument-1 command-argument-2 -``` - -In other references options are sometimes called flags, and command-arguments are sometimes called positional arguments. - -## Options taking varying numbers of option-arguments + - [Combining short options, and options taking arguments](#combining-short-options-and-options-taking-arguments) + - [Combining short options as if boolean](#combining-short-options-as-if-boolean) + - [Terminology](#terminology) -Certain options take a varying number of option-arguments: +Certain options take a varying number of arguments: ```js program @@ -45,7 +21,9 @@ program .option('--test [name...]') // 0 or more ``` -### Parsing ambiguity +This page uses examples with options taking 0 or 1 arguments, but the discussions also apply to variadic options taking more arguments. + +## Parsing ambiguity There is a potential downside to be aware of. If a command has both command-arguments and options with varying option-arguments, this introduces a parsing ambiguity which may affect the user of your program. @@ -79,7 +57,7 @@ $ cook -i egg technique: undefined ingredient: egg -$ cook -i scrambled # oops +$ cook -i scrambled # oops technique: undefined ingredient: scrambled ``` @@ -99,9 +77,7 @@ If you want to avoid your users needing to learn when to use `--`, there are a f Rather than trying to teach your users what `--` does, you could just make it part of your syntax. ```js -// ... program.usage('[options] -- [technique]'); -program.parse(); ``` ```sh @@ -123,13 +99,11 @@ ingredient: cheese ### Alternative: Put options last -Commander allows options before or after the command-arguments, or even intermixed (like [GNU program argument conventions](https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html)). +Commander follows the GNU convention for parsing and allows options before or after the command-arguments, or intermingled. So by putting the options last, the command-arguments do not get confused with the option-arguments. ```js -// ... program.usage('[technique] [options]'); -program.parse(); ``` ```sh @@ -159,8 +133,6 @@ program const ingredient = (options.ingredient === true) ? 'cheese' : options.ingredient; console.log(`ingredient: ${ingredient}`); }); - -program.parse(); ``` ```sh @@ -168,37 +140,41 @@ $ cook -i -t scrambled technique: scrambled ingredient: cheese ``` +## Combining short options, and options taking arguments ------- -_Work in progress: unreviewed from here down._ - -## Combining short flags with optional values +Multiple boolean short options can be combined after a single `-`, like `ls -al`. You can also include just +a single short option which might take a value, as any following characters will +be taken as the value. -optional options are option(s) which functions as a flag but may also take a value (declared using square brackets). +This means that by default you can not combine short options which may take an argument. -optional values (sometimes called option arguments) are values of these optional flag. - -``` +```js program - .option("-o, --others [count]", "others servings") - .option("-v, --vegan [count]", "vegan servings") - .option("-l, --halal [count]", "halal servings"); + .name('collect') + .option("-o, --other [count]", "other serving(s)") + .option("-v, --vegan [count]", "vegan serving(s)") + .option("-l, --halal [count]", "halal serving(s)"); program.parse(process.argv); -if (program.others) console.log(`others servings: ${program.others}`); -if (program.vegan) console.log(`vegan servings: ${program.vegan}`); -if (program.halal) console.log(`halal servings: ${program.halal}`); - +const opts = program.opts(); +if (opts.other) console.log(`other servings: ${opts.other}`); +if (opts.vegan) console.log(`vegan servings: ${opts.vegan}`); +if (opts.halal) console.log(`halal servings: ${opts.halal}`); ``` -In this example, you have to take note that optional options consume the value after the short flag. - -``` -$ collect -avl -any servings: vl +```sh +$ collect -o 3 +other servings: 3 +$ collect -o3 +other servings: 3 +$ collect -l -v +vegan servings: true +halal servings: true +$ collect -lv # oops +halal servings: v ``` -If you wish to use optional options as boolean options, you need to explicitly list them as individual options. +If you wish to use options taking varying arguments as boolean options, you need to specify them separately. ``` $ collect -a -v -l @@ -207,59 +183,38 @@ vegan servings: true halal servings: true ``` -Likewise for variadic options. While options can have a single optional value, variadic options can take in multiple optional values and have the same parsing complications. - -``` -program - .option("-c, --citizenship ", "countries you hold passport of") // 1 or more value(s) - .option("-i, --illness [illness...]", "known illness before travel") // 0 or more value(s) - .option("-s, --visa-approved [status]", "status of visa if not approved"); // 0 ir 1 value - -program.parse(); +### Combining short options as if boolean -console.log(`Citizen of: `, program.citizenship); -console.log(`Known illness(es): `, program.illness); -console.log(`visa approved: ${program.visaApproved}`); -``` +Before Commander v5, combining a short option and the value was not supported, and combined short flags were always expanded. +So `-avl` expanded to `-a -v -l`. -Optional options consume the value after the short flag and you will experience the following behaviour: +If you want backwards compatible behaviour, or prefer combining short options as booleans to combining short option and value, +you may change the behavior. -``` -$ node opt.js -si -Known illness(es): undefined -visa approved: i +To modify the parsing of options taking an optional value: -$ node opt.js -is -Known illness(es): [ 's' ] -visa approved: undefined +```js +.combineFlagAndOptionalValue(true) // `-v45` is treated like `--vegan=45`, this is the default behaviour +.combineFlagAndOptionalValue(false) // `-vl` is treated like `-v -l` ``` -If you wish to use variadic optional options as booleans, you will need to state them explicitly as follows: +## Terminology -``` -$ node opt.js -i -s -Known illness(es): true -visa approved: true -``` +_Work in progress: this section may move to the main README, or a page of its own._ -You should also be careful when you mix variadic optional options with variadic required options. A required option **always** consumes a value and so, you will not get any errors when the first value passed to it contains a '-' like so: +The command line arguments are made up of options, option-arguments, commands, and command-arguments. -``` -$ node opt.js -c -si -Citizen of: [ '-si' ] // Does not throw error -``` +| Term | Explanation | +| --- | --- | +| option | an argument which is a `-` followed by a character, or `--` followed by a word (or hyphenated words), like `-s` or `--short` | +| option-argument| some options can take an argument | +| command | a program or command can have subcommands | +| command-argument | argument for the command (and not an option or option-argument) | -``` -$ node opt.js -c -si -x -error: unknown option '-x' +For example: -$ node opt.js -c -si -i -Citizen of: [ '-si' ] -Known illness(es): true +```sh +my-utility command -o --option option-argument command-argument-1 command-argument-2 ``` -### Behaviour change from v5 to v6 - -Before Commander v5, `-ob` expanded to `-o -b`, which is different from the current behaviour in Commander v6 as explained above. - -This new behaviour may be an issue for people upgrading from older versions of Commander but we do have plans to prioritise combining flags over combining flag-and-value in the future. +In other references options are sometimes called flags, and command-arguments are sometimes called positional arguments or operands.