Skip to content

Commit

Permalink
Merge pull request #308 from nexdrew/env-vars
Browse files Browse the repository at this point in the history
Apply env vars to argv via prefix-based API
  • Loading branch information
bcoe committed Dec 3, 2015
2 parents 975edcc + 81be322 commit 34bc8cc
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 2 deletions.
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,63 @@ Optionally `.describe()` can take an object that maps keys to descriptions.

Should yargs attempt to detect the os' locale? Defaults to `true`.

.env([prefix])
--------------

Tell yargs to parse environment variables matching the given prefix and apply
them to argv as though they were command line arguments.

If this method is called with no argument or with an empty string or with `true`,
then all env vars will be applied to argv.

Program arguments are defined in this order of precedence:

1. Command line args
2. Config file
3. Env var
4. Configured defaults

```js
var argv = require('yargs')
.env('MY_PROGRAM')
.option('f', {
alias: 'fruit-thing',
default: 'apple'
})
.argv
console.log(argv)
```

```
$ node fruity.js
{ _: [],
f: 'apple',
'fruit-thing': 'apple',
fruitThing: 'apple',
'$0': 'fruity.js' }
```

```
$ MY_PROGRAM_FRUIT_THING=banana node fruity.js
{ _: [],
fruitThing: 'banana',
f: 'banana',
'fruit-thing': 'banana',
'$0': 'fruity.js' }
```

```
$ MY_PROGRAM_FRUIT_THING=banana node fruity.js -f cat
{ _: [],
f: 'cat',
'fruit-thing': 'cat',
fruitThing: 'cat',
'$0': 'fruity.js' }
```

Env var parsing is disabled by default, but you can also explicitly disable it
by calling `.env(false)`, e.g. if you need to undo previous configuration.

.epilog(str)
------------
.epilogue(str)
Expand Down
11 changes: 10 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ function Argv (processArgs, cwd) {
requiresArg: [],
count: [],
normalize: [],
config: []
config: [],
envPrefix: undefined
}

usage = Usage(self, y18n) // handle usage output.
Expand Down Expand Up @@ -342,6 +343,14 @@ function Argv (processArgs, cwd) {
return groups
}

// as long as options.envPrefix is not undefined,
// parser will apply env vars matching prefix to argv
self.env = function (prefix) {
if (prefix === false) options.envPrefix = undefined
else options.envPrefix = prefix || ''
return self
}

self.wrap = function (cols) {
usage.wrap(cols)
return self
Expand Down
21 changes: 21 additions & 0 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,14 @@ module.exports = function (args, opts, y18n) {
}
}

// order of precedence:
// 1. command line arg
// 2. value from config file
// 3. value from env var
// 4. configured default value
applyEnvVars(opts, argv, true) // special case: check env vars that point to config file
setConfig(argv)
applyEnvVars(opts, argv, false)
applyDefaultsAndAliases(argv, aliases, defaults)

Object.keys(flags.counts).forEach(function (key) {
Expand Down Expand Up @@ -351,6 +358,20 @@ module.exports = function (args, opts, y18n) {
})
}

function applyEnvVars (opts, argv, configOnly) {
if (typeof opts.envPrefix === 'undefined') return

var prefix = typeof opts.envPrefix === 'string' ? opts.envPrefix : ''
Object.keys(process.env).forEach(function (envVar) {
if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) {
var key = camelCase(envVar.substring(prefix.length))
if (((configOnly && flags.configs[key]) || !configOnly) && (!(key in argv) || flags.defaulted[key])) {
setArg(key, process.env[envVar])
}
}
})
}

function applyDefaultsAndAliases (obj, aliases, defaults) {
Object.keys(defaults).forEach(function (key) {
if (!hasKey(obj, key.split('.'))) {
Expand Down
135 changes: 135 additions & 0 deletions test/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1173,4 +1173,139 @@ describe('parser tests', function () {
result.f.should.equal(true)
})
})

describe('env vars', function () {
it('should apply all env vars if prefix is empty', function () {
process.env.ONE_FISH = 'twofish'
process.env.RED_FISH = 'bluefish'
var result = yargs().env('').parse([])

result.oneFish.should.equal('twofish')
result.redFish.should.equal('bluefish')
})

it('should apply only env vars matching prefix if prefix is valid string', function () {
process.env.ONE_FISH = 'twofish'
process.env.RED_FISH = 'bluefish'
process.env.GREEN_EGGS = 'sam'
process.env.GREEN_HAM = 'iam'
var result = yargs().env('GREEN').parse([])

result.eggs.should.equal('sam')
result.ham.should.equal('iam')
expect(result.oneFish).to.be.undefined
expect(result.redFish).to.be.undefined
})

it('should set aliases for options defined by env var', function () {
process.env.AIRFORCE_ONE = 'two'
var result = yargs()
.env('AIRFORCE')
.option('1', {
alias: 'one'
})
.alias('1', 'uno')
.parse([])

result['1'].should.equal('two')
result.one.should.equal('two')
result.uno.should.equal('two')
})

it('should prefer command line value over env var', function () {
process.env.FOO_BAR = 'ignore'
var result = yargs().env().parse(['--foo-bar', 'baz'])

result.fooBar.should.equal('baz')
})

it('should respect type for args defined by env var', function () {
process.env.MY_TEST_STRING = '1'
process.env.MY_TEST_NUMBER = '2'
var result = yargs().string('string').env('MY_TEST_').parse([])

result.string.should.equal('1')
result.number.should.equal(2)
})

it('should ignore env vars if enabled and subsequently disabled', function () {
process.env.STATE = 'denial'
var result = yargs().env(true).env(false).parse([])

expect(result.state).to.be.undefined
})

it('should set option from aliased env var', function () {
process.env.SPACE_X = 'awesome'
var result = yargs()
.env('SPACE')
.alias('xactly', 'x')
.parse([])

result.xactly.should.equal('awesome')
})

it('should prefer env var value over configured default', function () {
process.env.FOO_BALL = 'wut'
process.env.FOO_BOOL = 'true'
var result = yargs()
.env('FOO')
.option('ball', {
type: 'string',
default: 'baz'
})
.option('bool', {
type: 'boolean',
default: false
})
.parse([])

result.ball.should.equal('wut')
result.bool.should.equal(true)
})

var jsonPath = path.resolve(__dirname, './fixtures/config.json')
it('should prefer config file value over env var', function () {
process.env.CFG_HERP = 'zerp'
var result = yargs()
.env('CFG')
.config('cfg')
.option('herp', {
type: 'string',
default: 'nerp'
})
.parse(['--cfg', jsonPath])

result.herp.should.equal('derp')
})

it('should support an env var value as config file option', function () {
process.env.TUX_CFG = jsonPath
var result = yargs()
.env('TUX')
.config('cfg')
.default('z', 44)
.parse([])

result.should.have.property('herp')
result.should.have.property('foo')
result.should.have.property('version')
result.should.have.property('truthy')
result.z.should.equal(55)
})

it('should prefer cli config file option over env var config file option', function () {
process.env.MUX_CFG = path.resolve(__dirname, '../package.json')
var result = yargs()
.env('MUX')
.config('cfg')
.parse(['--cfg', jsonPath])

result.should.have.property('herp')
result.should.have.property('foo')
result.should.have.property('version')
result.should.have.property('truthy')
result.z.should.equal(55)
})
})
})
31 changes: 30 additions & 1 deletion test/yargs.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ describe('yargs dsl tests', function () {
.implies('foo', 'snuh')
.strict()
.exitProcess(false) // defaults to true.
.env('YARGS')
.reset()

var emptyOptions = {
Expand All @@ -219,7 +220,8 @@ describe('yargs dsl tests', function () {
requiresArg: [],
count: [],
normalize: [],
config: []
config: [],
envPrefix: undefined
}

expect(y.getOptions()).to.deep.equal(emptyOptions)
Expand Down Expand Up @@ -489,4 +491,31 @@ describe('yargs dsl tests', function () {
})
})
})

describe('env', function () {
it('translates no arg as empty prefix (parser applies all env vars)', function () {
var options = yargs.env().getOptions()
options.envPrefix.should.equal('')
})

it('accepts true as a valid prefix (parser applies all env vars)', function () {
var options = yargs.env(true).getOptions()
options.envPrefix.should.equal(true)
})

it('accepts empty string as a valid prefix (parser applies all env vars)', function () {
var options = yargs.env('').getOptions()
options.envPrefix.should.equal('')
})

it('accepts a string prefix', function () {
var options = yargs.env('COOL').getOptions()
options.envPrefix.should.equal('COOL')
})

it('translates false as undefined prefix (disables parsing of env vars)', function () {
var options = yargs.env(false).getOptions()
expect(options.envPrefix).to.be.undefined
})
})
})

0 comments on commit 34bc8cc

Please sign in to comment.