Repository: yargs/yargs-parser
Pull request #19 (2016-04-03 10:04:32)

Fix issue with numeric character in group of options
When checking a group of options, if there is any number in the characters following the key being currently evaluated, then everything after the current key will be set as its value. This happens because the regex that checks for numbers is missing a `^` at the beginning, so strings like `'abc123'` are considered numbers.

An issue caused by this is that, for example, calling `-abc=123` results in `{"_": [], "a": "bc=123"}` instead of `{"_": [], "a": true, "b": true, "c": 123}`

coveralls: 
[![Coverage Status](https://coveralls.io/builds/5643628/badge)](https://coveralls.io/builds/5643628)

Coverage remained the same at 100.0% when pulling **d527a45370535ab6943ed64dd229fd4836cbf7f9 on elas7:fix-group** into **48b1e6a338f80417b3fd0fc3e39ae181332fdcb5 on yargs:master**.


Patch:
diff --git a/.npmignore b/.npmignore
deleted file mode 100644
index a399f75..0000000
--- a/.npmignore
+++ /dev/null
@@ -1,5 +0,0 @@
-.nyc_output
-node_modules
-.DS_Store
-yargs-logo.png
-./test/fixtures/package.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..0ba5b65
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,40 @@
+# Change Log
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+<a name="2.4.1"></a>
+## [2.4.1](https://github.com/yargs/yargs-parser/compare/v2.4.0...v2.4.1) (2016-07-16)
+
+
+### Bug Fixes
+
+* **count:** do not increment a default value ([#39](https://github.com/yargs/yargs-parser/issues/39)) ([b04a189](https://github.com/yargs/yargs-parser/commit/b04a189))
+
+
+
+<a name="2.4.0"></a>
+# [2.4.0](https://github.com/yargs/yargs-parser/compare/v2.3.0...v2.4.0) (2016-04-11)
+
+
+### Features
+
+* **environment:** Support nested options in environment variables ([#26](https://github.com/yargs/yargs-parser/issues/26)) thanks [@elas7](https://github.com/elas7) \o/ ([020778b](https://github.com/yargs/yargs-parser/commit/020778b))
+
+
+
+<a name="2.3.0"></a>
+# [2.3.0](https://github.com/yargs/yargs-parser/compare/v2.2.0...v2.3.0) (2016-04-09)
+
+
+### Bug Fixes
+
+* **boolean:** fix for boolean options with non boolean defaults (#20) ([2dbe86b](https://github.com/yargs/yargs-parser/commit/2dbe86b)), closes [(#20](https://github.com/(/issues/20)
+* **package:** remove tests from tarball ([0353c0d](https://github.com/yargs/yargs-parser/commit/0353c0d))
+* **parsing:** handle calling short option with an empty string as the next value. ([a867165](https://github.com/yargs/yargs-parser/commit/a867165))
+* boolean flag when next value contains the strings 'true' or 'false'. ([69941a6](https://github.com/yargs/yargs-parser/commit/69941a6))
+* update dependencies; add standard-version bin for next release (#24) ([822d9d5](https://github.com/yargs/yargs-parser/commit/822d9d5))
+
+### Features
+
+* **configuration:** Allow to pass configuration objects to yargs-parser ([0780900](https://github.com/yargs/yargs-parser/commit/0780900))
+* **normalize:** allow normalize to work with arrays ([e0eaa1a](https://github.com/yargs/yargs-parser/commit/e0eaa1a))
diff --git a/README.md b/README.md
index 4b76a99..95ef4b2 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@
 [![Coverage Status](https://coveralls.io/repos/yargs/yargs-parser/badge.svg?branch=)](https://coveralls.io/r/yargs/yargs-parser?branch=master)
 [![NPM version](https://img.shields.io/npm/v/yargs-parser.svg)](https://www.npmjs.com/package/yargs-parser)
 [![Windows Tests](https://img.shields.io/appveyor/ci/bcoe/yargs-parser/master.svg?label=Windows%20Tests)](https://ci.appveyor.com/project/bcoe/yargs-parser)
+[![Standard Version](https://img.shields.io/badge/release-standard%20version-brightgreen.svg)](https://github.com/conventional-changelog/standard-version)
+
 
 The mighty option parser used by [yargs](https://github.com/bcoe/yargs).
 
diff --git a/index.js b/index.js
index 834f8da..6e15d4e 100644
--- a/index.js
+++ b/index.js
@@ -19,6 +19,7 @@ function parse (args, opts) {
     'boolean-negation': true
   }, opts.configuration)
   var defaults = opts.default || {}
+  var configObjects = opts.configObjects || []
   var envPrefix = opts.envPrefix
   var newAliases = {}
   // allow a i18n handler to be passed in, default to a fake one (util.format).
@@ -212,8 +213,9 @@ function parse (args, opts) {
           continue
         }
 
+        // current letter is an alphabetic character and next value is a number
         if (/[A-Za-z]/.test(letters[j]) &&
-          /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
+          /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
           setArg(letters[j], next)
           broken = true
           break
@@ -240,12 +242,12 @@ function parse (args, opts) {
         } else {
           next = args[i + 1]
 
-          if (next && !/^(-|--)[^-]/.test(next) &&
+          if (next !== undefined && !/^(-|--)[^-]/.test(next) &&
             !checkAllAliases(key, flags.bools) &&
             !checkAllAliases(key, flags.counts)) {
             setArg(key, next)
             i++
-          } else if (next && /true|false/.test(next)) {
+          } else if (/^(true|false)$/.test(next)) {
             setArg(key, next)
             i++
           } else {
@@ -263,15 +265,18 @@ function parse (args, opts) {
   // order of precedence:
   // 1. command line arg
   // 2. value from config file
-  // 3. value from env var
-  // 4. configured default value
+  // 3. value from config objects
+  // 4. value from env var
+  // 5. configured default value
   applyEnvVars(argv, true) // special case: check env vars that point to config file
   setConfig(argv)
+  setConfigObjects()
   applyEnvVars(argv, false)
   applyDefaultsAndAliases(argv, flags.aliases, defaults)
 
+  // for any counts either not in args or without an explicit default, set to 0
   Object.keys(flags.counts).forEach(function (key) {
-    setArg(key, defaults[key])
+    if (!hasKey(argv, key.split('.'))) setArg(key, 0)
   })
 
   notFlags.forEach(function (key) {
@@ -331,50 +336,53 @@ function parse (args, opts) {
       if (!isUndefined(val) && !isNumber(val) && checkAllAliases(key, flags.numbers)) value = NaN
     }
 
-    if (checkAllAliases(key, flags.counts)) {
+    // increment a count given as arg (either no value or value parsed as boolean)
+    if (checkAllAliases(key, flags.counts) && (isUndefined(value) || typeof value === 'boolean')) {
       value = increment
     }
 
+    // Set normalized value when key is in 'normalize' and in 'arrays'
+    if (checkAllAliases(key, flags.normalize) && checkAllAliases(key, flags.arrays)) {
+      value = path.normalize(val)
+    }
+
     var splitKey = key.split('.')
     setKey(argv, splitKey, value)
 
-    // alias references an inner-value within
-    // a dot-notation object. see #279.
-    if (~key.indexOf('.') && flags.aliases[key]) {
+    // handle populating aliases of the full key
+    if (flags.aliases[key]) {
       flags.aliases[key].forEach(function (x) {
         x = x.split('.')
         setKey(argv, x, value)
       })
     }
 
-    ;(flags.aliases[splitKey[0]] || []).forEach(function (x) {
-      x = x.split('.')
+    // handle populating aliases of the first element of the dot-notation key
+    if (splitKey.length > 1 && configuration['dot-notation']) {
+      ;(flags.aliases[splitKey[0]] || []).forEach(function (x) {
+        x = x.split('.')
 
-      // handle populating dot notation for both
-      // the key and its aliases.
-      if (splitKey.length > 1) {
+        // expand alias with nested objects in key
         var a = [].concat(splitKey)
         a.shift() // nuke the old key.
         x = x.concat(a)
-      }
 
-      setKey(argv, x, value)
-    })
+        setKey(argv, x, value)
+      })
+    }
 
-    var keys = [key].concat(flags.aliases[key] || [])
-    for (var i = 0, l = keys.length; i < l; i++) {
-      if (flags.normalize[keys[i]]) {
-        keys.forEach(function (key) {
-          argv.__defineSetter__(key, function (v) {
-            val = path.normalize(v)
-          })
-
-          argv.__defineGetter__(key, function () {
-            return typeof val === 'string' ? path.normalize(val) : val
-          })
+    // Set normalize getter and setter when key is in 'normalize' but isn't an array
+    if (checkAllAliases(key, flags.normalize) && !checkAllAliases(key, flags.arrays)) {
+      var keys = [key].concat(flags.aliases[key] || [])
+      keys.forEach(function (key) {
+        argv.__defineSetter__(key, function (v) {
+          val = path.normalize(v)
         })
-        break
-      }
+
+        argv.__defineGetter__(key, function () {
+          return typeof val === 'string' ? path.normalize(val) : val
+        })
+      })
     }
   }
 
@@ -436,15 +444,30 @@ function parse (args, opts) {
     })
   }
 
+  // set all config objects passed in opts
+  function setConfigObjects () {
+    if (typeof configObjects === 'undefined') return
+    configObjects.forEach(function (configObject) {
+      setConfigObject(configObject)
+    })
+  }
+
   function applyEnvVars (argv, configOnly) {
     if (typeof envPrefix === 'undefined') return
 
     var prefix = typeof envPrefix === 'string' ? 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])
+        // get array of nested keys and convert them to camel case
+        var keys = envVar.split('__').map(function (key, i) {
+          if (i === 0) {
+            key = key.substring(prefix.length)
+          }
+          return camelCase(key)
+        })
+
+        if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && (!hasKey(argv, keys) || flags.defaulted[keys.join('.')])) {
+          setArg(keys.join('.'), process.env[envVar])
         }
       }
     })
@@ -494,7 +517,7 @@ function parse (args, opts) {
       o[key] = increment(o[key])
     } else if (o[key] === undefined && checkAllAliases(key, flags.arrays)) {
       o[key] = Array.isArray(value) ? value : [value]
-    } else if (o[key] === undefined || typeof o[key] === 'boolean') {
+    } else if (o[key] === undefined || checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
       o[key] = value
     } else if (Array.isArray(o[key])) {
       o[key].push(value)
@@ -645,8 +668,11 @@ function combineAliases (aliases) {
   return combined
 }
 
+// this function should only be called when a count is given as an arg
+// it is NOT called to set a default value
+// thus we can start the count at 1 instead of 0
 function increment (orig) {
-  return orig !== undefined ? orig + 1 : 0
+  return orig !== undefined ? orig + 1 : 1
 }
 
 function Parser (args, opts) {
diff --git a/package.json b/package.json
index c2429e4..c01eaa6 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,13 @@
 {
   "name": "yargs-parser",
-  "version": "2.2.0",
+  "version": "2.4.1",
   "description": "the mighty option parser used by yargs",
   "main": "index.js",
   "scripts": {
     "pretest": "standard",
     "test": "nyc mocha test/*.js",
-    "coverage": "nyc report --reporter=text-lcov | coveralls"
+    "coverage": "nyc report --reporter=text-lcov | coveralls",
+    "version": "standard-version"
   },
   "repository": {
     "url": "git@github.com:yargs/yargs-parser.git"
@@ -27,12 +28,17 @@
   "devDependencies": {
     "chai": "^3.5.0",
     "coveralls": "^2.11.8",
-    "mocha": "^2.4.5",
-    "nyc": "^6.1.1",
-    "standard": "^6.0.8"
+    "mocha": "^3.0.0",
+    "nyc": "^7.0.0",
+    "standard": "^7.1.0",
+    "standard-version": "^2.1.2"
   },
   "dependencies": {
-    "camelcase": "^2.1.1",
+    "camelcase": "^3.0.0",
     "lodash.assign": "^4.0.6"
-  }
+  },
+  "files": [
+    "lib",
+    "index.js"
+  ]
 }


Test patch:
diff --git a/test/yargs-parser.js b/test/yargs-parser.js
index ca1476c..6e400d3 100644
--- a/test/yargs-parser.js
+++ b/test/yargs-parser.js
@@ -40,6 +40,12 @@ describe('yargs-parser', function () {
     parse.should.have.property('_').with.length(0)
   })
 
+  it('should set the value of a single long option to the next supplied value, even if the value is empty', function () {
+    var parse = parser(['--pow', ''])
+    parse.should.have.property('pow', '')
+    parse.should.have.property('_').with.length(0)
+  })
+
   it('should set the value of a single long option if an = was used', function () {
     var parse = parser(['--pow=xixxle'])
     parse.should.have.property('pow', 'xixxle')
@@ -121,6 +127,12 @@ describe('yargs-parser', function () {
     argv._[0].should.be.a('number')
   })
 
+  it('should set the value of a single short option to the next supplied value, even if the value is empty', function () {
+    var parse = parser(['-p', ''])
+    parse.should.have.property('p', '')
+    parse.should.have.property('_').with.length(0)
+  })
+
   it('should not set the next value as the value of a short option if that option is explicitly defined as a boolean', function () {
     var parse = parser([ '-t', 'moo' ], {
       boolean: 't'
@@ -141,6 +153,15 @@ describe('yargs-parser', function () {
     parse.should.have.property('_').and.deep.equal(['moo'])
   })
 
+  it('should not set boolean options values if the next value only contains the words "true" or "false"', function () {
+    var parse = parser(['--verbose', 'aaatrueaaa', 'moo', '-t', 'aaafalseaaa'], {
+      boolean: ['t', 'verbose']
+    })
+    parse.should.have.property('verbose', true).and.be.a('boolean')
+    parse.should.have.property('t', true).and.be.a('boolean')
+    parse.should.have.property('_').and.deep.equal(['aaatrueaaa', 'moo', 'aaafalseaaa'])
+  })
+
   it('should allow defining options as boolean in groups', function () {
     var parse = parser([ '-x', '-z', 'one', 'two', 'three' ], {
       boolean: ['x', 'y', 'z']
@@ -236,6 +257,19 @@ describe('yargs-parser', function () {
       a.s = ['', 'path', 'to', 'new', 'dir', '..', '..', ''].join(path.sep)
       a.s.should.equal(['', 'path', 'to', ''].join(path.sep))
     })
+
+    it('should normalize when key is also an array', function () {
+      var a = parser([ '-s', ['', 'tmp', '..', ''].join(path.sep), ['', 'path', 'to', 'new', 'dir', '..', '..', ''].join(path.sep) ], {
+        alias: {
+          s: ['save']
+        },
+        normalize: 's',
+        array: 's'
+      })
+      var expected = [path.sep, ['', 'path', 'to', ''].join(path.sep)]
+      a.should.have.property('s').and.deep.equal(expected)
+      a.should.have.property('save').and.deep.equal(expected)
+    })
   })
 
   describe('alias', function () {
@@ -413,19 +447,6 @@ describe('yargs-parser', function () {
       argv.should.have.property('foo').and.deep.equal('banana')
     })
 
-    it('should load options and values from a file when config is used', function () {
-      var argv = parser([ '--settings', jsonPath, '--foo', 'bar' ], {
-        alias: {
-          z: 'zoom'
-        },
-        config: ['settings']
-      })
-
-      argv.should.have.property('herp', 'derp')
-      argv.should.have.property('zoom', 55)
-      argv.should.have.property('foo').and.deep.equal('bar')
-    })
-
     it("should allow config to be set as flag in 'option'", function () {
       var argv = parser([ '--settings', jsonPath, '--foo', 'bar' ], {
         alias: {
@@ -571,6 +592,102 @@ describe('yargs-parser', function () {
     })
   })
 
+  describe('config objects', function () {
+    it('should load options from config object', function () {
+      var argv = parser([ '--foo', 'bar' ], {
+        configObjects: [{
+          apple: 'apple',
+          banana: 42,
+          foo: 'baz'
+        }]
+      })
+
+      argv.should.have.property('apple', 'apple')
+      argv.should.have.property('banana', 42)
+      argv.should.have.property('foo', 'bar')
+    })
+
+    it('should use value from config object, if argv value is using default value', function () {
+      var argv = parser([], {
+        configObjects: [{
+          apple: 'apple',
+          banana: 42,
+          foo: 'baz'
+        }],
+        default: {
+          foo: 'banana'
+        }
+      })
+
+      argv.should.have.property('apple', 'apple')
+      argv.should.have.property('banana', 42)
+      argv.should.have.property('foo', 'baz')
+    })
+
+    it('should use value from config object to all aliases', function () {
+      var argv = parser([], {
+        configObjects: [{
+          apple: 'apple',
+          banana: 42,
+          foo: 'baz'
+        }],
+        alias: {
+          a: ['apple'],
+          banana: ['b']
+        }
+      })
+
+      argv.should.have.property('apple', 'apple')
+      argv.should.have.property('a', 'apple')
+      argv.should.have.property('banana', 42)
+      argv.should.have.property('b', 42)
+      argv.should.have.property('foo', 'baz')
+    })
+
+    it('should load nested options from config object', function () {
+      var argv = parser(['--nested.foo', 'bar'], {
+        configObjects: [{
+          a: 'a',
+          nested: {
+            foo: 'baz',
+            bar: 'bar'
+          },
+          b: 'b'
+        }]
+      })
+
+      argv.should.have.property('a', 'a')
+      argv.should.have.property('b', 'b')
+      argv.should.have.property('nested').and.deep.equal({
+        foo: 'bar',
+        bar: 'bar'
+      })
+    })
+
+    it('should use nested value from config object, if argv value is using default value', function () {
+      var argv = parser([], {
+        configObjects: [{
+          a: 'a',
+          nested: {
+            foo: 'baz',
+            bar: 'bar'
+          },
+          b: 'b'
+        }],
+        default: {
+          'nested.foo': 'banana'
+        }
+      })
+
+      argv.should.have.property('a', 'a')
+      argv.should.have.property('b', 'b')
+      argv.should.have.property('nested').and.deep.equal({
+        foo: 'baz',
+        bar: 'bar'
+      })
+    })
+  })
+
   describe('dot notation', function () {
     it('should allow object graph traversal via dot notation', function () {
       var argv = parser([
@@ -762,6 +879,29 @@ describe('yargs-parser', function () {
       argv.should.have.property('n', 123)
     })
 
+    it('should set n to the numeric value 123, with n at the end of a group', function () {
+      var argv = parser([ '-ab5n123' ])
+      argv.should.have.property('a', true)
+      argv.should.have.property('b', true)
+      argv.should.have.property('5', true)
+      argv.should.have.property('n', 123)
+      argv.should.have.property('_').with.length(0)
+    })
+
+    it('should set n to the numeric value 123, with = as separator', function () {
+      var argv = parser([ '-n=123' ])
+      argv.should.have.property('n', 123)
+    })
+
+    it('should set n to the numeric value 123, with n at the end of a group and = as separator', function () {
+      var argv = parser([ '-ab5n=123' ])
+      argv.should.have.property('a', true)
+      argv.should.have.property('b', true)
+      argv.should.have.property('5', true)
+      argv.should.have.property('n', 123)
+      argv.should.have.property('_').with.length(0)
+    })
+
     it('should set option "1" to true, option "2" to true, and option "3" to numeric value 456', function () {
       var argv = parser([ '-123', '456' ])
       argv.should.have.property('1', true)
@@ -866,7 +1006,7 @@ describe('yargs-parser', function () {
     })
 
     describe('for boolean options', function () {
-      [true, false, undefined].forEach(function (def) {
+      [true, false, undefined, null].forEach(function (def) {
         describe('with explicit ' + def + ' default', function () {
           var opts = {
             default: {
@@ -1146,6 +1286,60 @@ describe('yargs-parser', function () {
       parsed.verbose.should.equal(2)
       parsed.should.have.property('_').and.deep.equal(['moomoo'])
     })
+
+    it('should use a default value as is when no arg given', function () {
+      var parsed = parser([], {
+        count: 'v',
+        default: { v: 3 }
+      })
+      parsed.v.should.equal(3)
+
+      parsed = parser([], {
+        count: 'v',
+        default: { v: undefined }
+      })
+      expect(parsed.v).to.be.undefined
+
+      parsed = parser([], {
+        count: 'v',
+        default: { v: null }
+      })
+      expect(parsed.v).to.be.null
+
+      parsed = parser([], {
+        count: 'v',
+        default: { v: false }
+      })
+      parsed.v.should.equal(false)
+
+      parsed = parser([], {
+        count: 'v',
+        default: { v: 'hello' }
+      })
+      parsed.v.should.equal('hello')
+    })
+
+    it('should ignore a default value when arg given', function () {
+      var parsed = parser(['-vv', '-v', '-v'], {
+        count: 'v',
+        default: { v: 1 }
+      })
+      parsed.v.should.equal(4)
+    })
+
+    it('should increment regardless of arg value', function () {
+      var parsed = parser([
+        '-v',
+        '-v=true',
+        '-v', 'true',
+        '-v=false',
+        '-v', 'false',
+        '--no-v',
+        '-v=999',
+        '-v=foobar'
+      ], { count: 'v' })
+      parsed.v.should.equal(8)
+    })
   })
 
   describe('array', function () {
@@ -1511,6 +1705,39 @@ describe('yargs-parser', function () {
       result.should.have.property('truthy')
       result.z.should.equal(55)
     })
+
+    it('should apply all nested env vars', function () {
+      process.env.TEST_A = 'a'
+      process.env.TEST_NESTED_OPTION__FOO = 'baz'
+      process.env.TEST_NESTED_OPTION__BAR = 'bar'
+      var result = parser(['--nestedOption.foo', 'bar'], {
+        envPrefix: 'TEST'
+      })
+
+      result.should.have.property('a', 'a')
+      result.should.have.property('nestedOption').and.deep.equal({
+        foo: 'bar',
+        bar: 'bar'
+      })
+    })
+
+    it('should apply nested env var if argv value is using default value', function () {
+      process.env.TEST_A = 'a'
+      process.env.TEST_NESTED_OPTION__FOO = 'baz'
+      process.env.TEST_NESTED_OPTION__BAR = 'bar'
+      var result = parser([], {
+        envPrefix: 'TEST',
+        default: {
+          'nestedOption.foo': 'banana'
+        }
+      })
+
+      result.should.have.property('a', 'a')
+      result.should.have.property('nestedOption').and.deep.equal({
+        foo: 'baz',
+        bar: 'bar'
+      })
+    })
   })
 
   describe('configuration', function () {
@@ -1615,6 +1842,19 @@ describe('yargs-parser', function () {
 
         expect(parsed['alias.bar']).to.equal('abc')
       })
+
+      it('does not expand alias of first element of dot notation arguments', function () {
+        var parsed = parser(['--foo.bar', 'banana'], {
+          alias: {
+            'foo': ['f']
+          },
+          configuration: {
+            'dot-notation': false
+          }
+        })
+        expect(parsed['foo.bar']).to.equal('banana')
+        expect(parsed).not.to.include.keys('f.bar')
+      })
     })
 
     describe('parse numbers', function () {