From 0d67c35d2536dce4c6b9f7fd22c9ac51467e4ad8 Mon Sep 17 00:00:00 2001 From: Phillip Clark Date: Mon, 25 May 2020 10:31:36 -0600 Subject: [PATCH 1/2] Added support for option value from environment variables --- Readme.md | 149 ++++++++++++++++++++---------- examples/options-flag-from-env.js | 42 +++++++++ examples/options-from-env.js | 43 +++++++++ index.js | 30 ++++-- tests/options.flags.test.js | 45 ++++++++- tests/options.opts.test.js | 2 +- typings/index.d.ts | 19 +++- 7 files changed, 268 insertions(+), 62 deletions(-) create mode 100755 examples/options-flag-from-env.js create mode 100755 examples/options-from-env.js diff --git a/Readme.md b/Readme.md index fcb995ab3..e8b28f6a8 100644 --- a/Readme.md +++ b/Readme.md @@ -9,41 +9,42 @@ The complete solution for [node.js](http://nodejs.org) command-line interfaces, Read this in other languages: English | [简体中文](./Readme_zh-CN.md) -- [Commander.js](#commanderjs) - - [Installation](#installation) - - [Declaring _program_ variable](#declaring-program-variable) - - [Options](#options) - - [Common option types, boolean and value](#common-option-types-boolean-and-value) - - [Default option value](#default-option-value) - - [Other option types, negatable boolean and flag|value](#other-option-types-negatable-boolean-and-flagvalue) - - [Custom option processing](#custom-option-processing) - - [Required option](#required-option) - - [Version option](#version-option) - - [Commands](#commands) - - [Specify the argument syntax](#specify-the-argument-syntax) - - [Action handler (sub)commands](#action-handler-subcommands) - - [Stand-alone executable (sub)commands](#stand-alone-executable-subcommands) - - [Automated help](#automated-help) - - [Custom help](#custom-help) - - [.usage and .name](#usage-and-name) - - [.help(cb)](#helpcb) - - [.outputHelp(cb)](#outputhelpcb) - - [.helpInformation()](#helpinformation) - - [.helpOption(flags, description)](#helpoptionflags-description) - - [.addHelpCommand()](#addhelpcommand) - - [Custom event listeners](#custom-event-listeners) - - [Bits and pieces](#bits-and-pieces) - - [.parse() and .parseAsync()](#parse-and-parseasync) - - [Avoiding option name clashes](#avoiding-option-name-clashes) - - [TypeScript](#typescript) - - [createCommand()](#createcommand) - - [Node options such as `--harmony`](#node-options-such-as---harmony) - - [Debugging stand-alone executable subcommands](#debugging-stand-alone-executable-subcommands) - - [Override exit handling](#override-exit-handling) - - [Examples](#examples) - - [License](#license) - - [Support](#support) - - [Commander for enterprise](#commander-for-enterprise) +- [Commander.js](#commanderjs) + - [Installation](#installation) + - [Declaring _program_ variable](#declaring-program-variable) + - [Options](#options) + - [Common option types, boolean and value](#common-option-types-boolean-and-value) + - [Default option value](#default-option-value) + - [Environment variables and options](#environment-variables-and-options) + - [Other option types, negatable boolean and flag|value](#other-option-types-negatable-boolean-and-flagvalue) + - [Custom option processing](#custom-option-processing) + - [Required option](#required-option) + - [Version option](#version-option) + - [Commands](#commands) + - [Specify the argument syntax](#specify-the-argument-syntax) + - [Action handler (sub)commands](#action-handler-subcommands) + - [Stand-alone executable (sub)commands](#stand-alone-executable-subcommands) + - [Automated help](#automated-help) + - [Custom help](#custom-help) + - [.usage and .name](#usage-and-name) + - [.help(cb)](#helpcb) + - [.outputHelp(cb)](#outputhelpcb) + - [.helpInformation()](#helpinformation) + - [.helpOption(flags, description)](#helpoptionflags-description) + - [.addHelpCommand()](#addhelpcommand) + - [Custom event listeners](#custom-event-listeners) + - [Bits and pieces](#bits-and-pieces) + - [.parse() and .parseAsync()](#parse-and-parseasync) + - [Avoiding option name clashes](#avoiding-option-name-clashes) + - [TypeScript](#typescript) + - [createCommand()](#createcommand) + - [Node options such as `--harmony`](#node-options-such-as---harmony) + - [Debugging stand-alone executable subcommands](#debugging-stand-alone-executable-subcommands) + - [Override exit handling](#override-exit-handling) + - [Examples](#examples) + - [License](#license) + - [Support](#support) + - [Commander for enterprise](#commander-for-enterprise) ## Installation @@ -63,15 +64,15 @@ program.version('0.0.1'); For larger programs which may use commander in multiple ways, including unit testing, it is better to create a local Command object to use. - ```js - const { Command } = require('commander'); - const program = new Command(); - program.version('0.0.1'); - ``` +```js +const { Command } = require('commander'); +const program = new Command(); +program.version('0.0.1'); +``` ## Options -Options are defined with the `.option()` method, also serving as documentation for the options. Each option can have a short flag (single character) and a long name, separated by a comma or space or vertical bar ('|'). +Options are defined with the `.option()` method, also serving as documentation for the options. Each option can have a short flag (single character) and a long name, separated by a comma or space or vertical bar ('|'). It is also possible to specify that an option's value may [be passed in via environment variables](#environment-variables-and-options). The options can be accessed as properties on the Command object. Multi-word options such as "--template-engine" are camel-cased, becoming `program.templateEngine` etc. See also optional new behaviour to [avoid name clashes](#avoiding-option-name-clashes). @@ -144,6 +145,54 @@ $ pizza-options --cheese stilton cheese: stilton ``` +### Environment variables and options + +Options can derive their value from an environment variable if a third `env:*` flag is specified. Any value present in the corresponding environment variable will become the option's initial value, overriding a default value if one is specified. + +When supplying values via environment variables, any value passed on the command line takes precedence. + +```js +const { program } = require('commander'); +const program = new commander.Command(); + +program + // environment variables can be declared alone + .option('env:DEBUG_LEVEL', 'debug level from environment') + .option('-s, --small', 'small pizza size') + // environment variables are more interesting when they relate to a command line option + .option('-p, --pizza-type , env:FAVOURITE_PIZZA', 'flavour of pizza'); + +program.parse(process.argv); + +if (program.DEBUG_LEVEL) console.log(program.opts()); +console.log('pizza details:'); +if (program.small) console.log('- small pizza size'); +if (program.pizzaType) console.log(`- ${program.pizzaType}`); +``` + +```bash +$ DEBUG_LEVEL=verbose pizza-options +{ DEBUG_LEVEL: 'verbose', small: undefined, pizzaType: undefined } +pizza details: +$ pizza-options -p +error: option '-p, --pizza-type , env:FAVOURITE_PIZZA' argument missing +$ DEBUG_LEVEL=info pizza-options -s -p vegetarian +{ DEBUG_LEVEL=info, small: true, pizzaType: 'vegetarian' } +pizza details: +- small pizza size +- vegetarian +$ FAVOURITE_PIZZA=pepperoni pizza-options --small +pizza details: +- small pizza size +- pepperoni +$ FAVOURITE_PIZZA=pepperoni pizza-options -s -p cheese +pizza details: +- small pizza size +- cheese +``` + +Environment variables are set as initial values while options are being prepared, so if using another module that pre-prepares the `process.env`, such as [dotenv](https://www.npmjs.com/package/dotenv), make sure it has completed before specifying the program's options. + ### Other option types, negatable boolean and flag|value You can specify a boolean option long name with a leading `no-` to set the option value to false when used. @@ -571,9 +620,9 @@ The first argument to `.parse` is the array of strings to parse. You may omit th If the arguments follow different conventions than node you can pass a `from` option in the second parameter: -- 'node': default, `argv[0]` is the application and `argv[1]` is the script being run, with user parameters after that -- 'electron': `argv[1]` varies depending on whether the electron application is packaged -- 'user': all of the arguments from the user +- 'node': default, `argv[0]` is the application and `argv[1]` is the script being run, with user parameters after that +- 'electron': `argv[1]` varies depending on whether the electron application is packaged +- 'user': all of the arguments from the user For example: @@ -593,9 +642,9 @@ existing properties of Command. There are two new routines to change the behaviour, and the default behaviour may change in the future: -- `storeOptionsAsProperties`: whether to store option values as properties on command object, or store separately (specify false) and access using `.opts()` -- `passCommandToAction`: whether to pass command to action handler, -or just the options (specify false) +- `storeOptionsAsProperties`: whether to store option values as properties on command object, or store separately (specify false) and access using `.opts()` +- `passCommandToAction`: whether to pass command to action handler, + or just the options (specify false) ([example](./examples/storeOptionsAsProperties-action.js)) @@ -648,8 +697,8 @@ customise the new subcommand (examples using [subclass](./examples/custom-comman You can enable `--harmony` option in two ways: -- Use `#! /usr/bin/env node --harmony` in the subcommands scripts. (Note Windows does not support this pattern.) -- Use the `--harmony` option when call the command, like `node --harmony examples/pm publish`. The `--harmony` option will be preserved when spawning subcommand process. +- Use `#! /usr/bin/env node --harmony` in the subcommands scripts. (Note Windows does not support this pattern.) +- Use the `--harmony` option when call the command, like `node --harmony examples/pm publish`. The `--harmony` option will be preserved when spawning subcommand process. ### Debugging stand-alone executable subcommands @@ -668,7 +717,7 @@ this behaviour and optionally supply a callback. The default override throws a ` The override callback is passed a `CommanderError` with properties `exitCode` number, `code` string, and `message`. The default override behaviour is to throw the error, except for async handling of executable subcommand completion which carries on. The normal display of error messages or version or help is not affected by the override which is called after the display. -``` js +```js program.exitOverride(); try { diff --git a/examples/options-flag-from-env.js b/examples/options-flag-from-env.js new file mode 100755 index 000000000..cbaef6e44 --- /dev/null +++ b/examples/options-flag-from-env.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +// Commander treats toggles flags when they are specified multiple times, this +// includes when the value is specified via environment variable. +// +// When specifying a boolean flag from an environment variable, it is necessary +// to coerce the value since environment variables are strings. +// +// Example output pretending command called toggle (or try directly with `node options-flag-from-env.js`) +// +// $ toggle +// disabled +// $ toggle -e +// enabled +// $ toggle -ee +// disabled +// $ ENABLE=t toggle +// enabled +// $ ENABLE=t toggle -e +// disabled +// $ ENABLE=f toggle --enable +// enabled + +// const commander = require('commander'); // (normal include) +const commander = require('../'); // include commander in git clone of commander repo +const program = new commander.Command(); + +function envBoolCoercion(val, prior) { + if (val !== undefined) { + return [true, 1, 'true', 't', 'yes', 'y', 'on'].indexOf( + typeof val === 'string' ? val.toLowerCase() : val + ) >= 0; + } + return !prior; +} + +program + .option('-e, --enable, env:ENABLE', 'enables the feature', envBoolCoercion); + +program.parse(process.argv); + +console.log(program.enable ? 'enabled' : 'disabled'); diff --git a/examples/options-from-env.js b/examples/options-from-env.js new file mode 100755 index 000000000..8049ab7a6 --- /dev/null +++ b/examples/options-from-env.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +// This is used as an example in the README for: +// Environment variables and options +// +// Example output pretending command called pizza-options (or try directly with `node options-from-env.js`) +// +// $ DEBUG_LEVEL=verbose pizza-options +// { DEBUG_LEVEL: 'verbose', small: undefined, pizzaType: undefined } +// pizza details: +// $ pizza-options -p +// error: option '-p, --pizza-type , env:FAVOURITE_PIZZA' argument missing +// $ DEBUG_LEVEL=info pizza-options -s -p vegetarian +// { DEBUG_LEVEL=info, small: true, pizzaType: 'vegetarian' } +// pizza details: +// - small pizza size +// - vegetarian +// $ FAVOURITE_PIZZA=pepperoni pizza-options --small +// pizza details: +// - small pizza size +// - pepperoni +// $ FAVOURITE_PIZZA=pepperoni pizza-options -s -p cheese +// pizza details: +// - small pizza size +// - cheese + +// const commander = require('commander'); // (normal include) +const commander = require('../'); // include commander in git clone of commander repo +const program = new commander.Command(); + +program + // environment variables can be declared alone + .option('env:DEBUG_LEVEL', 'debug level from environment') + .option('-s, --small', 'small pizza size') + // environment variables are more interesting when they relate to a command line option + .option('-p, --pizza-type , env:FAVOURITE_PIZZA', 'flavour of pizza'); + +program.parse(process.argv); + +if (program.DEBUG_LEVEL) console.log(program.opts()); +console.log('pizza details:'); +if (program.small) console.log('- small pizza size'); +if (program.pizzaType) console.log(`- ${program.pizzaType}`); diff --git a/index.js b/index.js index 987b79f0d..8485c9d47 100644 --- a/index.js +++ b/index.js @@ -24,9 +24,19 @@ class Option { this.optional = flags.indexOf('[') >= 0; // A value is optional when the option is specified. this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line. this.negate = flags.indexOf('-no-') !== -1; - const flagParts = flags.split(/[ ,|]+/); - if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1])) this.short = flagParts.shift(); - this.long = flagParts.shift(); + for (const part of flags.split(/[ ,|]+/)) { + if (/^-\w+/.test(part)) { + this.short = part; + } else if (/^--[\w-]+/.test(part)) { + this.long = part; + } else if (/^env:[\w-]+/.test(part)) { + this.env = part.substr(4); + } + } + if (!this.long && this.short) { + // compatibility with prior + this.long = this.short; + } this.description = description || ''; this.defaultValue = undefined; } @@ -39,7 +49,9 @@ class Option { */ name() { - return this.long.replace(/^--/, ''); + return this.long ? this.long.replace(/^--/, '') + // : this.short ? this.short.replace(/^-/, '') + : this.env; }; /** @@ -470,6 +482,12 @@ class Command extends EventEmitter { } } + // if option.env is defined and value present in environment, use value + if (option.env && process.env[option.env]) { + const env = process.env[option.env]; + this._setOptionValue(name, fn ? fn(env, defaultValue) : env); + } + // register the option this.options.push(option); @@ -556,7 +574,7 @@ class Command extends EventEmitter { return this._optionEx({}, flags, description, fn, defaultValue); }; - /* + /** * Add a required option which must have a value after parsing. This usually means * the option must be specified on the command line. (Otherwise the same as .option().) * @@ -891,7 +909,7 @@ class Command extends EventEmitter { this._dispatchSubcommand(this._defaultCommandName, operands, unknown); } else { if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) { - // probaby missing subcommand and no handler, user needs help + // probably missing subcommand and no handler, user needs help this._helpAndError(); } diff --git a/tests/options.flags.test.js b/tests/options.flags.test.js index 9c692461b..fa90c1ad3 100644 --- a/tests/options.flags.test.js +++ b/tests/options.flags.test.js @@ -10,7 +10,7 @@ test('when only short flag defined and specified then value is true', () => { expect(program.P).toBe(true); }); -// Sanity check that pepper is not true normally, as otherwise all the following tests would pass for thr wrong reasons! +// Sanity check that pepper is not true normally, as otherwise all the following tests would pass for the wrong reasons! test('when only long flag defined and not specified then value is undefined', () => { const program = new commander.Command(); program @@ -74,3 +74,46 @@ test('when "short long" flags defined and long specified then value is true', () program.parse(['node', 'test', '--pepper']); expect(program.pepper).toBe(true); }); + +const envBoolCoercion = (val, prior) => { + if (val !== undefined) { + return [true, 1, 'true', 't', 'yes', 'y', 'on'].indexOf( + typeof val === 'string' ? val.toLowerCase() : val + ) >= 0; + } + return !prior; +}; + +test('when "short,env" flags defined and short specified then value is true', () => { + // it is questionable for a boolean flag to be defined by the env given + // commander's toggle behavior when flags are defined multiple times. + process.env.PEPPER = 'false'; + const program = new commander.Command(); + program + .option('-p,env:PEPPER', 'add pepper', envBoolCoercion); + program.parse(['node', 'test', '-p']); + process.env.PEPPER = undefined; + expect(program.P).toBe(true); +}); + +test('when "short,env" flags defined and env specified then value is true', () => { + // only works because flag isn't set on command line + process.env.PEPPER = 'true'; + const program = new commander.Command(); + program + .option('-p,env:PEPPER', 'add pepper', envBoolCoercion); + program.parse(['node', 'test']); + process.env.PEPPER = undefined; + expect(program.P).toBe(true); +}); + +test('when "short,long,env" flags defined and env specified then value is true', () => { +// only works because flag isn't set on command line + process.env.PEPPER = 'true'; + const program = new commander.Command(); + program + .option('-p,--pepper,env:PEPPER', 'add pepper', envBoolCoercion); + program.parse(['node', 'test']); + process.env.PEPPER = undefined; + expect(program.pepper).toBe(true); +}); diff --git a/tests/options.opts.test.js b/tests/options.opts.test.js index 337a909e2..214afa7ec 100644 --- a/tests/options.opts.test.js +++ b/tests/options.opts.test.js @@ -1,6 +1,6 @@ const commander = require('../'); -// Test the `.opts()` way of accesing option values. +// Test the `.opts()` way of accessing option values. // Basic coverage of the main option types (leaving out negatable flags and options with optional values). test('when .version used then version in opts', () => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 6f41bf3cf..c985c31db 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -19,6 +19,7 @@ declare namespace commander { bool: boolean; short?: string; long: string; + env?: string; description: string; } type OptionConstructor = new (flags: string, description?: string) => Option; @@ -75,7 +76,7 @@ declare namespace commander { * ```ts * program * .command('start ', 'start named service') - * .command('stop [service]', 'stop named serice, or all if no name supplied'); + * .command('stop [service]', 'stop named service, or all if no name supplied'); * ``` * * @param nameAndArgs - command name and arguments, args are `` or `[optional]` and last may also be `variadic...` @@ -133,14 +134,21 @@ declare namespace commander { * Define option with `flags`, `description` and optional * coercion `fn`. * - * The `flags` string should contain both the short and long flags, - * separated by comma, a pipe or space. The following are all valid - * all will output this way when `--help` is used. + * The `flags` string should contain both short and long flags, and + * may contain an environment variable flag, separated by comma, a pipe + * or space. The following are all valid all will output this way + * when `--help` is used. * * "-p, --pepper" * "-p|--pepper" * "-p --pepper" * + * With optional environment variables flag: + * + * "-p, --pepper, env:PEPPER" + * "-p|--pepper|env:PEPPER" + * "-p --pepper env:PEPPER" + * * @example * // simple boolean defaulting to false * program.option('-p, --pepper', 'add pepper'); @@ -166,6 +174,9 @@ declare namespace commander { * program.chdir * // => "/tmp" * + * // required argument, possibly supplied from the environment + * program.option('-C, --chdir , env:MY_WORK_DIR', 'change the working directory'); + * * // optional argument * program.option('-c, --cheese [type]', 'add cheese [marble]'); * From fe6b399939181b77026ac699d961a1497e5394f7 Mon Sep 17 00:00:00 2001 From: Phillip Clark Date: Mon, 25 May 2020 12:18:43 -0600 Subject: [PATCH 2/2] reverse auto-format --- Readme.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Readme.md b/Readme.md index c81380827..ab901509b 100644 --- a/Readme.md +++ b/Readme.md @@ -14,12 +14,12 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Declaring _program_ variable](#declaring-program-variable) - [Options](#options) - [Common option types, boolean and value](#common-option-types-boolean-and-value) - - [Default option value](#default-option-value) - - [Environment variables and options](#environment-variables-and-options) - - [Other option types, negatable boolean and flag|value](#other-option-types-negatable-boolean-and-flagvalue) - - [Custom option processing](#custom-option-processing) - - [Required option](#required-option) - - [Version option](#version-option) + - [Default option value](#default-option-value) + - [Environment variables and options](#environment-variables-and-options) + - [Other option types, negatable boolean and flag|value](#other-option-types-negatable-boolean-and-flagvalue) + - [Custom option processing](#custom-option-processing) + - [Required option](#required-option) + - [Version option](#version-option) - [Commands](#commands) - [Specify the argument syntax](#specify-the-argument-syntax) - [Action handler (sub)commands](#action-handler-subcommands) @@ -44,7 +44,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Examples](#examples) - [License](#license) - [Support](#support) - - [Commander for enterprise](#commander-for-enterprise) + - [Commander for enterprise](#commander-for-enterprise) ## Installation