Skip to content
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

Added support for option value from environment variables #1266

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 57 additions & 8 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
- [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)
Expand Down Expand Up @@ -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).

Expand Down Expand Up @@ -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 <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 <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.
Expand Down Expand Up @@ -595,7 +644,7 @@ There are two new routines to change the behaviour, and the default behaviour ma

- `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)
or just the options (specify false)

([example](./examples/storeOptionsAsProperties-action.js))

Expand Down Expand Up @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions examples/options-flag-from-env.js
Original file line number Diff line number Diff line change
@@ -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');
43 changes: 43 additions & 0 deletions examples/options-from-env.js
Original file line number Diff line number Diff line change
@@ -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 <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 <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}`);
30 changes: 24 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,19 @@ class Option {
this.optional = flags.includes('['); // 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.includes('-no-');
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;
}
Expand All @@ -39,7 +49,9 @@ class Option {
*/

name() {
return this.long.replace(/^--/, '');
return this.long ? this.long.replace(/^--/, '')
// : this.short ? this.short.replace(/^-/, '')
: this.env;
};

/**
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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().)
*
Expand Down Expand Up @@ -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();
}

Expand Down
45 changes: 44 additions & 1 deletion tests/options.flags.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
});
2 changes: 1 addition & 1 deletion tests/options.opts.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
19 changes: 15 additions & 4 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,7 +76,7 @@ declare namespace commander {
* ```ts
* program
* .command('start <service>', '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 `<required>` or `[optional]` and last may also be `variadic...`
Expand Down Expand Up @@ -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');
Expand All @@ -166,6 +174,9 @@ declare namespace commander {
* program.chdir
* // => "/tmp"
*
* // required argument, possibly supplied from the environment
* program.option('-C, --chdir <path>, env:MY_WORK_DIR', 'change the working directory');
*
* // optional argument
* program.option('-c, --cheese [type]', 'add cheese [marble]');
*
Expand Down