diff --git a/docs/options-taking-varying-arguments.md b/docs/options-taking-varying-arguments.md new file mode 100644 index 000000000..566216f15 --- /dev/null +++ b/docs/options-taking-varying-arguments.md @@ -0,0 +1,220 @@ +# 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. + +- [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 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 arguments: + +```js +program + .option('-c, --compress [percentage]') // 0 or 1 + .option('--preprocess ') // 1 or more + .option('--test [name...]') // 0 or more +``` + +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. +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 +technique: scrambled +ingredient: undefined + +$ cook -i +technique: undefined +ingredient: cheese + +$ cook -i 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: + +```sh +$ node cook.js -i -- egg +technique: egg +ingredient: cheese +``` + +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]'); +``` + +```sh +$ cook --help +Usage: cook [options] -- [technique] + +Options: + -i, --ingredient [ingredient] add cheese or given ingredient + -h, --help display help for command + +$ 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 +``` +## Combining short options, and options taking arguments + +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. + +This means that by default you can not combine short options which may take an argument. + +```js +program + .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); + +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}`); +``` + +```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 options taking varying arguments as boolean options, you need to specify them separately. + +``` +$ collect -a -v -l +any servings: true +vegan servings: true +halal servings: true +``` + +### Combining short options as if boolean + +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`. + +If you want backwards compatible behaviour, or prefer combining short options as booleans to combining short option and value, +you may change the behavior. + +To modify the parsing of options taking an optional value: + +```js +.combineFlagAndOptionalValue(true) // `-v45` is treated like `--vegan=45`, this is the default behaviour +.combineFlagAndOptionalValue(false) // `-vl` is treated like `-v -l` +``` + +## 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 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) | + +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 or operands. diff --git a/optional-options-docs.md b/optional-options-docs.md deleted file mode 100644 index 4a6ca13ee..000000000 --- a/optional-options-docs.md +++ /dev/null @@ -1,149 +0,0 @@ -# Tricks and traps when using options with optional values - -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. - -## 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]')` | - -## Parsing ambiguity - -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. - -``` -program - .arguments("[technique]") - .option("-i, --ingredient [ingredient]") - .action((args, cmdObj) => { - console.log(args); - console.log(cmdObj.opts()); - }); - -program.parse(); -``` - -``` -$ cook scrambled -scrambled -{ ingredient: undefined } - -$ cook -i -undefined -{ ingredient: true } - -$ cook -i egg -undefined -{ ingredient: egg } - -$ cook -i scrambled -undefined -{ ingredient: scrambled } -``` - -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. - -### Possible workarounds - -To reduce such ambiguity, you can do the following: - -1. always use `--` before operands -2. add your options after operands -3. convert operands into options! Options work pretty nicely together. - -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. - -## Combining short flags with optional values - -optional options are option(s) which functions as a flag but may also take a value (declared using square brackets). - -optional values (sometimes called option arguments) are values of these optional flag. - -``` -program - .option("-o, --others [count]", "others servings") - .option("-v, --vegan [count]", "vegan servings") - .option("-l, --halal [count]", "halal servings"); -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}`); - -``` - -In this example, you have to take note that optional options consume the value after the short flag. - -``` -$ collect -avl -any servings: vl -``` - -If you wish to use optional options as boolean options, you need to explicitly list them as individual options. - -``` -$ collect -a -v -l -any servings: true -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(); - -console.log(`Citizen of: `, program.citizenship); -console.log(`Known illness(es): `, program.illness); -console.log(`visa approved: ${program.visaApproved}`); -``` - -Optional options consume the value after the short flag and you will experience the following behaviour: - -``` -$ node opt.js -si -Known illness(es): undefined -visa approved: i - -$ node opt.js -is -Known illness(es): [ 's' ] -visa approved: undefined -``` - -If you wish to use variadic optional options as booleans, you will need to state them explicitly as follows: - -``` -$ node opt.js -i -s -Known illness(es): true -visa approved: true -``` - -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: - -``` -$ node opt.js -c -si -Citizen of: [ '-si' ] // Does not throw error -``` - -``` -$ node opt.js -c -si -x -error: unknown option '-x' - -$ node opt.js -c -si -i -Citizen of: [ '-si' ] -Known illness(es): true -``` - -### 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.