Repository: yargs/yargs-parser
Pull request #181 (2019-06-07 18:59:19)

feature: add configuration option "collect-unknown-options"
Fixes yargs/yargs#1243

All new code is covered in tests. It shouldn't make any difference, but I used the Wallaby.js VSCode extension for test running and coverage, instead of the npm scripts.

I tried to match the existing code style as well as possible, but please feel free to fix any issues I created.

bcoe: @henderea thanks for the patch. Mind running `./node_modules/.bin/standard --fix`. 

@juergba bother you for a code review?
juergba: @bcoe I will need some days for my code review.

On my first glance the documentation is missing.

> _: an array representing the positional arguments.

I'm not sure wether it is a good idea to use property `_` for this purpose. Positional arguments and unkown options are completely different objects.
henderea: @juergba I can work on documentation, but the whole point of this feature is to treat unknown options like regular arguments.  If it doesn't do that, then the whole change is pointless for my needs.

Perhaps the name should be updated?  The idea of this mode is to treat unknown options like regular arguments, so I'm pretty sure `_` is the right place for that.  I'm not always the best at naming in code 😅

If you look at the issue I filed that this PR fixes, yargs/yargs#1243, you'll see that my intention is to allow unknown options to appear in a varargs of a command, so unless `_` won't do that, I'm pretty sure I'm putting it in the right place.
henderea: Node 6 support fixed (didn't realize `Object.values` was an issue, probably should have looked at the build failure 😅).  I've also changed it to `parse-unknown-options: false`, and removed the `maybeCoerceNumber` from my new code.  It seems the existing number coercion for unhandled args in the final else is what caused the numeric arguments in my tests, so my tests were actually not changed by the removal of coercion from my new code.

I'll plan to expand tests over the weekend if I get to it.
bcoe: @henderea @juergba perhaps we can take a step back here, why do you want this behavior?

Tools like [nyc](https://github.com/istanbuljs/nyc) and [mocha](https://github.com/mochajs/mocha) have similar needs, in which they want to be able to run a command where some options are intended for the application, and some are intended for the program being instrumented, e.g.,

```
nyc --option 1 foo --option 3 --option 4
```

We've introduced the configuration [halt-on-non-option(https://github.com/yargs/yargs-parser#halt-at-non-option) to support this use-case, which I think is pretty close to what you're trying to do?
bcoe: @henderea just bumping up this conversation,  was wondering if `--halt-on-non-option` could potentially give you close to the behavior that you're looking for, for the application you're building.

The behavior of stopping parsing once you hit the first non `--option` argument is fairly common for tools that instrument a file (wondering if this is close to your use-case).
henderea: @bcoe: sorry, haven't gotten around to expanding the test suite yet

As for `--halt-on-non-option`, my use case is actually related to passing a full command string (including flags) as a parameter to a script.  Basically, i want to be able to have `./script.js command ls -lh ~` (with command defined as `'command <exe> [args...]'`) be able to capture `-lh ~` in the `args`.

Does `--halt-on-non-option` allow flags to be captured in a varargs when appearing after a non-option?  if so, then that might do what I need.  But if the `'command <exe> [args...]'` command definition won't allow me to capture options untouched in the `args` when `<exe>` is a non-option, then it won't work for my needs.
henderea: Also, related to `--halt-on-non-option`, can that be applied only for certain commands?  I would still like other commands to be able to end with a flag, placed after regular arguments.

The tool I want this for is something I'm converting from ruby, and the ruby library I use for command and option handling by default behaves like my `parse-unknown-options: false` code, treating unknown options as regular positional arguments.

I'll try to get some additional tests written, but I think my needs will only be fully handled by something similar to this pull request.
henderea: For example, I want to have the following definitions:

`'list <pattern>'` with boolean option `-l`
and
`'run <exe> [args...]'` with no options

I want to be able to run
`./script.js list -l pat`
and
`./script.js list pat -l`
with both setting
```
{
  pattern: 'pat',
  l: true
}
```
but have
`./script.js run ls -l .`
set
```
{
  exe: 'ls',
  args: ['-l', '.']
}
```

As a note, the `list` command lists something from a specific JSON file, and `run` runs an arbitrary shell command.  There is nothing in common between `list` and `run ls`.  Also note that this is a simplified set of commands with different names from my actual intended usage, but they _are_ representative of my desired usage and behavior.
henderea: @bcoe: If possible, I would like for this feature or something else that will treat unknown options as regular arguments to be added to yargs.  It looks like the `halt-on-non-option` flag would have to apply to _all_ commands, but I want to parse options both before _and_ after positional arguments on commands that have options.  If I don't want to parse any options on a given command, I will just not define any.

When I write a script, I will define EVERY option that I want to handle, and anything I _don't_ define shouldn't be treated as an option.  The ruby library I use for argument parsing (thor) does this _by default_, and I personally feel that it is rather odd to set options that haven't been defined.  The way I see it, my script should only support options that have been defined, and should fail on undefined options, _but only if they don't fit into an argument or varargs_.
bcoe: @henderea :wave: thanks for filling me in regarding your use case, I'll try to get back to you with a response soonish :smile: when I can give this my full attention.
juergba: > I agree with @juergba, I would introduce a new key on argv called unknownOptions, which for your use case could just be combined with _ in your application.

Meanwhile I have changed my mind on this:
- `halt-at-non-option` also uses the `_` property, so unparsed arguments are already treated as positional arguments.
- let's take follwing example: "--ls aa bb cc". `ls` is an unknown option, so we don't know anything about its type and how many of the following arguments should have to be consumed.
  - should the values `aa`, `bb`, `cc` be handled as positional arguments ==>  `arg._` ?
  - should we handle them as input to the unknown option `ls` ==> `argv.unknownOptions` ?

We could end up with spreading our unknown arguments over to different properties.

> ☝️ hasUnknownOption would just need to drop handle:
-xyz: ensuring that x, y, and z are all configured.
--foo=bar: extracting just the foo and checking that it's configured.
--foo: extracting just the foo and making sure it's configured.

This list is not complete, the extraction of the plain key out of the raw argument is more complex than this. E.g. boolean negation, dot anotation are missing. We would have to copy the extraction logic into `hasUnknownOption`, I'm not convinced of this. 




bcoe: @juergba I'd like to avoid adding conditional logic throughout the parser, to handle the `--unknown-options` case, I'd also like to avoid logic that requires that a key be rebuilt with less than perfect information, e.g, the case of:

```js
_.push(key)
_.push('=')
```

we could just continue to enumerate some of these other cases:

```
--no-[key]
--[key].foo
```

☝️ it's a finite list.
henderea: Well, I don't necessarily have an issue with simplifying the logic if it still does the same thing, but putting the unknown options into a separate property and merging them could alter the order of arguments, which would not be desirable.

If there is a way to rewrite the logic to get the same effect with less-invasive changes, I'm all for that.  I just want to be sure it still works the same way, or at least that I can get the same result through some method.  Altering the order of arguments because they were split between 2 different properties is not desireable.
bcoe: @henderea I'm open to putting the options in `_` :+1: but I like the idea of encapsulating the logic for skipping a key in the first check of our `if`/`else` block, this way we don't end up breaking the feature over and over again as we make slight tweaks to different option types (I've seen this happen for other constructs like negation and dot notation).
bcoe: @henderea just following up 👍 to summarize:

1. I'm comfortable with the idea of us putting the unknown options in `_`.
2. I'd like the check for an unknown option to ideally be its own logic before the rest of option parsing, so that we don't have to perform the check in a variety of places (even though it creates some extra parsing logic in the case of `collect-unknown-options`, I like that we don't need to add additional checks to ever other type of argument).
3. let's use `collect-unknown-options` as the config setting.
henderea: @bcoe Sure, I can make the changes.  The thing is, I'm not 100% sure of the form those changes would take.  How would I detect that a key should be skipped before parsing the actual key?  If you could give me a bit more direction on that, I could make the changes.
bcoe: @henderea I can try to pseudo code something, but my thinking was you only have a few forms of keys:

```
--foo
-foo
--foo=99
-foo=99
```

☝️ so have a little helper function that runs before the rest of our checks and extracts the `foo`, if we handle `foo` in our parser, move to the next steps, otherwise put the whole key `--foo`, or `--foo=99`, etc., into unknown arguments.
henderea: @bcoe I've made the requested changes.  I ended up creating a function that takes `arg` and some regex patterns, and checks if parsing `arg` with any of the patterns results in a key (in match group 1) that has been configured.  The name I gave it is `checkFlags`, but you may want to give it a better name.  Code naming isn't a strong point of mine 😅 
henderea: I realized after my first implementation of the updated code that it wasn't checking short flags individually, so I updated my code to handle that, and also added in the check for negative numbers that I saw in the short flags handling, along with renaming `checkFlags` to `hasFlagsMatching` so it matches more closely with my `hasAnyFlag` and new `hasAllShortFlags` functions.
bcoe: @henderea thanks for taking this on :+1: will provide review soon.
henderea: It looks like the windows builds have some issues on travis:

```
$ nvs add 10
Downloading bootstrap node from https://nodejs.org/dist/v10.12.0/node-v10.12.0-win-x64.7z
######################################################################## 100.0%
The command "nvs add 10" failed and exited with 1 during .
```

I think I saw this same type of error for an `nvs add 8` command on a windows node 8 build for this PR before.

On that note, is there a good way to re-trigger the build?
henderea: @bcoe if the code is good, is that it for my part of the PR?  Or do I need to do some sort of changelog entry or other non-code stuff like I did when I contributed to RVM once?
bcoe: @henderea all the CHANGELOG updates, etc., are automatic :+1:

We should however update the README.md with the new feature. Haven't forgotten about you, have just been overcommitted on a few other projects. Your work is appreciated.
henderea: @bcoe I guess I was assuming the README.md changes were something you would handle and that you were just mentioning it, but did you actually mean for _me_ to handle it?  I can do it if needed, I just want to make sure you aren't waiting on me to do something I wasn't aware was on my plate 😅 
bcoe: @henderea mind adding a new section to the README describing the new configuration setting, I can provide review on the wording afterwards :+1: 

If you don't have the time, I can try to write something up when I land this.
henderea: @bcoe: updated the readme.  here's what i put (based off of "strip dashed" and put right after it, at the end of the section):

### collect unknown options

* default: `false`
* key: `collect-unknown-options`

Should unknown options be collected into `_`?  An unknown option is one that is not configured in `opts`.

_If disabled_

```sh
node example.js --unknown-option --known-option 2
{ _: [], unknownOption: true, knownOption: 2 }
```

_If enabled_

```sh
node example.js --unknown-option --known-option 2
{ _: ['--unknown-option'], knownOption: 2 }
```
kellyselden: This feature worked as needed for my use case. Given code like:

```
  let parsed = parse(process.argv.slice(3), {
    string: ['b'],
    configuration: {
      'collect-unknown-options': true
    }
  });
```

(slice(3) since it is a command) yields

```
Object {_: Array(2), b: "foo"}
    _:Array(2) ["--unused-option1=3", "--unused-option2=5"]
    b:"foo"
    __proto__:Object {constructor: , __defineGetter__: , __defineSetter__: , …}
```

Patch:
diff --git a/README.md b/README.md
index 95d70b9..f754df0 100644
--- a/README.md
+++ b/README.md
@@ -386,6 +386,28 @@ node example.js --test-field 1
 { _: [], testField: 1 }
 ```
 
+### collect unknown options
+
+* default: `false`
+* key: `collect-unknown-options`
+
+Should unknown options be collected into `_`?  An unknown option is one that is not
+configured in `opts`.
+
+_If disabled_
+
+```sh
+node example.js --unknown-option --known-option 2
+{ _: [], unknownOption: true, knownOption: 2 }
+```
+
+_If enabled_
+
+```sh
+node example.js --unknown-option --known-option 2
+{ _: ['--unknown-option'], knownOption: 2 }
+```
+
 ## Special Thanks
 
 The yargs project evolves from optimist and minimist. It owes its
diff --git a/index.js b/index.js
index cf7872c..08f9852 100644
--- a/index.js
+++ b/index.js
@@ -26,7 +26,8 @@ function parse (args, opts) {
     'set-placeholder-key': false,
     'halt-at-non-option': false,
     'strip-aliased': false,
-    'strip-dashed': false
+    'strip-dashed': false,
+    'collect-unknown-options': false
   }, opts.configuration)
   var defaults = opts.default || {}
   var configObjects = opts.configObjects || []
@@ -142,8 +143,10 @@ function parse (args, opts) {
     var next
     var value
 
+    if (configuration['collect-unknown-options'] && isUnknownOption(arg)) {
+      argv._.push(arg)
     // -- separated by =
-    if (arg.match(/^--.+=/) || (
+    } else if (arg.match(/^--.+=/) || (
       !configuration['short-option-groups'] && arg.match(/^-.+=/)
     )) {
       // Using [\s\S] instead of . because js doesn't support the
@@ -757,6 +760,74 @@ function parse (args, opts) {
     return isSet
   }
 
+  function hasAnyFlag (key) {
+    var isSet = false
+    // XXX Switch to [].concat(...Object.values(flags)) once node.js 6 is dropped
+    var toCheck = [].concat(...Object.keys(flags).map(k => flags[k]))
+
+    toCheck.forEach(function (flag) {
+      if (flag[key]) isSet = flag[key]
+    })
+
+    return isSet
+  }
+
+  function hasFlagsMatching (arg, ...patterns) {
+    var hasFlag = false
+    var toCheck = [].concat(...patterns)
+    toCheck.forEach(function (pattern) {
+      var match = arg.match(pattern)
+      if (match && hasAnyFlag(match[1])) {
+        hasFlag = true
+      }
+    })
+    return hasFlag
+  }
+
+  // based on a simplified version of the short flag group parsing logic
+  function hasAllShortFlags (arg) {
+    // if this is a negative number, or doesn't start with a single hyphen, it's not a short flag group
+    if (arg.match(negative) || !arg.match(/^-[^-]+/)) { return false }
+    var hasAllFlags = true
+    var letters = arg.slice(1).split('')
+    var next
+    for (var j = 0; j < letters.length; j++) {
+      next = arg.slice(j + 2)
+
+      if (!hasAnyFlag(letters[j])) {
+        hasAllFlags = false
+        break
+      }
+
+      if ((letters[j + 1] && letters[j + 1] === '=') ||
+        next === '-' ||
+        (/[A-Za-z]/.test(letters[j]) && /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) ||
+        (letters[j + 1] && letters[j + 1].match(/\W/))) {
+        break
+      }
+    }
+    return hasAllFlags
+  }
+
+  function isUnknownOption (arg) {
+    // ignore negative numbers
+    if (arg.match(negative)) { return false }
+    // if this is a short option group and all of them are configured, it isn't unknown
+    if (hasAllShortFlags(arg)) { return false }
+    // e.g. '--count=2'
+    const flagWithEquals = /^-+([^=]+?)=[\s\S]*$/
+    // e.g. '-a' or '--arg'
+    const normalFlag = /^-+([^=]+?)$/
+    // e.g. '-a-'
+    const flagEndingInHyphen = /^-+([^=]+?)-$/
+    // e.g. '-abc123'
+    const flagEndingInDigits = /^-+([^=]+?)\d+$/
+    // e.g. '-a/usr/local'
+    const flagEndingInNonWordCharacters = /^-+([^=]+?)\W+.*$/
+    // check the different types of flag styles, including negatedBoolean, a pattern defined near the start of the parse method
+    return !hasFlagsMatching(arg, flagWithEquals, negatedBoolean, normalFlag, flagEndingInHyphen, flagEndingInDigits, flagEndingInNonWordCharacters)
+  }
+
   // make a best effor to pick a default value
   // for an option based on name and type.
   function defaultValue (key) {


Test patch:
diff --git a/test/yargs-parser.js b/test/yargs-parser.js
index cb1342c..27f8074 100644
--- a/test/yargs-parser.js
+++ b/test/yargs-parser.js
@@ -9,7 +9,7 @@ var path = require('path')
 
 describe('yargs-parser', function () {
   it('should parse a "short boolean"', function () {
-    var parse = parser([ '-b' ])
+    var parse = parser(['-b'])
     parse.should.not.have.property('--')
     parse.should.have.property('b').to.be.ok.and.be.a('boolean')
     parse.should.have.property('_').with.length(0)
@@ -166,7 +166,7 @@ describe('yargs-parser', function () {
   })
 
   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' ], {
+    var parse = parser(['-t', 'moo'], {
       boolean: 't'
     })
     parse.should.have.property('t', true).and.be.a('boolean')
@@ -208,7 +208,7 @@ describe('yargs-parser', function () {
   })
 
   it('should allow defining options as boolean in groups', function () {
-    var parse = parser([ '-x', '-z', 'one', 'two', 'three' ], {
+    var parse = parser(['-x', '-z', 'one', 'two', 'three'], {
       boolean: ['x', 'y', 'z']
     })
     parse.should.have.property('x', true).and.be.a('boolean')
@@ -243,25 +243,25 @@ describe('yargs-parser', function () {
   })
 
   it('should not convert numbers to type number if explicitly defined as strings', function () {
-    var s = parser([ '-s', '0001234' ], {
+    var s = parser(['-s', '0001234'], {
       string: 's'
     }).s
     s.should.be.a('string').and.equal('0001234')
-    var x = parser([ '-x', '56' ], {
+    var x = parser(['-x', '56'], {
       string: ['x']
     }).x
     x.should.be.a('string').and.equal('56')
   })
 
   it('should default numbers to undefined', function () {
-    var n = parser([ '-n' ], {
+    var n = parser(['-n'], {
       number: ['n']
     }).n
     expect(n).to.equal(undefined)
   })
 
   it('should default number to NaN if value is not a valid number', function () {
-    var n = parser([ '-n', 'string' ], {
+    var n = parser(['-n', 'string'], {
       number: ['n']
     }).n
     expect(n).to.deep.equal(NaN)
@@ -269,24 +269,24 @@ describe('yargs-parser', function () {
 
   // Fixes: https://github.com/bcoe/yargs/issues/68
   it('should parse flag arguments with no right-hand value as strings, if defined as strings', function () {
-    var s = parser([ '-s' ], {
+    var s = parser(['-s'], {
       string: ['s']
     }).s
     s.should.be.a('string').and.equal('')
 
-    s = parser([ '-sf' ], {
+    s = parser(['-sf'], {
       string: ['s']
     }).s
     s.should.be.a('string').and.equal('')
 
-    s = parser([ '--string' ], {
+    s = parser(['--string'], {
       string: ['string']
     }).string
     s.should.be.a('string').and.equal('')
   })
 
   it('should leave all non-hyphenated values as strings if _ is defined as a string', function () {
-    var s = parser([ '  ', '  ' ], {
+    var s = parser(['  ', '  '], {
       string: ['_']
     })._
     s.should.have.length(2)
@@ -296,7 +296,7 @@ describe('yargs-parser', function () {
 
   describe('normalize', function () {
     it('should normalize redundant paths', function () {
-      var a = parser([ '-s', ['', 'tmp', '..', ''].join(path.sep) ], {
+      var a = parser(['-s', ['', 'tmp', '..', ''].join(path.sep)], {
         alias: {
           s: ['save']
         },
@@ -316,7 +316,7 @@ describe('yargs-parser', function () {
     })
 
     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) ], {
+      var a = parser(['-s', ['', 'tmp', '..', ''].join(path.sep), ['', 'path', 'to', 'new', 'dir', '..', '..', ''].join(path.sep)], {
         alias: {
           s: ['save']
         },
@@ -331,7 +331,7 @@ describe('yargs-parser', function () {
 
   describe('alias', function () {
     it('should set alias value to the same value as the full option', function () {
-      var argv = parser([ '-f', '11', '--zoom', '55' ], {
+      var argv = parser(['-f', '11', '--zoom', '55'], {
         alias: {
           z: ['zoom']
         }
@@ -342,7 +342,7 @@ describe('yargs-parser', function () {
     })
 
     it('should allow multiple aliases to be specified', function () {
-      var argv = parser([ '-f', '11', '--zoom', '55' ], {
+      var argv = parser(['-f', '11', '--zoom', '55'], {
         alias: {
           z: ['zm', 'zoom']
         }
@@ -385,7 +385,7 @@ describe('yargs-parser', function () {
     })
 
     it('should allow transitive aliases to be specified', function () {
-      var argv = parser([ '-f', '11', '--zoom', '55' ], {
+      var argv = parser(['-f', '11', '--zoom', '55'], {
         alias: {
           z: 'zm',
           zm: 'zoom'
@@ -431,7 +431,7 @@ describe('yargs-parser', function () {
 
     // See: https://github.com/chevex/yargs/issues/12
     it('should load options and values from default config if specified', function () {
-      var argv = parser([ '--foo', 'bar' ], {
+      var argv = parser(['--foo', 'bar'], {
         alias: {
           z: 'zoom'
         },
@@ -520,7 +520,7 @@ describe('yargs-parser', function () {
     })
 
     it("should allow config to be set as flag in 'option'", function () {
-      var argv = parser([ '--settings', jsonPath, '--foo', 'bar' ], {
+      var argv = parser(['--settings', jsonPath, '--foo', 'bar'], {
         alias: {
           z: 'zoom'
         },
@@ -534,7 +534,7 @@ describe('yargs-parser', function () {
 
     it('should load options and values from a JS file when config has .js extention', function () {
       var jsPath = path.resolve(__dirname, './fixtures/settings.js')
-      var argv = parser([ '--settings', jsPath, '--foo', 'bar' ], {
+      var argv = parser(['--settings', jsPath, '--foo', 'bar'], {
         config: ['settings']
       })
 
@@ -599,7 +599,7 @@ describe('yargs-parser', function () {
 
     it('allows a custom parsing function to be provided', function () {
       var jsPath = path.resolve(__dirname, './fixtures/config.txt')
-      var argv = parser([ '--settings', jsPath, '--foo', 'bar' ], {
+      var argv = parser(['--settings', jsPath, '--foo', 'bar'], {
         config: {
           settings: function (configPath) {
             // as an example, parse an environment
@@ -624,7 +624,7 @@ describe('yargs-parser', function () {
 
     it('allows a custom parsing function to be provided as an alias', function () {
       var jsPath = path.resolve(__dirname, './fixtures/config.json')
-      var argv = parser([ '--settings', jsPath, '--foo', 'bar' ], {
+      var argv = parser(['--settings', jsPath, '--foo', 'bar'], {
         config: {
           s: function (configPath) {
             return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
@@ -666,7 +666,7 @@ describe('yargs-parser', function () {
 
   describe('config objects', function () {
     it('should load options from config object', function () {
-      var argv = parser([ '--foo', 'bar' ], {
+      var argv = parser(['--foo', 'bar'], {
         configObjects: [{
           apple: 'apple',
           banana: 42,
@@ -855,7 +855,7 @@ describe('yargs-parser', function () {
         }
       })
 
-      ;('foo.bar' in argv).should.equal(false)
+        ; ('foo.bar' in argv).should.equal(false)
     })
 
     it('should respect .string() for dot notation arguments', function () {
@@ -912,7 +912,7 @@ describe('yargs-parser', function () {
   })
 
   it('should set boolean and alias using explicit true', function () {
-    var aliased = [ '-h', 'true' ]
+    var aliased = ['-h', 'true']
     var aliasedArgv = parser(aliased, {
       boolean: ['h'],
       alias: {
@@ -978,12 +978,12 @@ describe('yargs-parser', function () {
     })
 
     it('should set n to the numeric value 123', function () {
-      var argv = parser([ '-n123' ])
+      var argv = parser(['-n123'])
       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' ])
+      var argv = parser(['-ab5n123'])
       argv.should.have.property('a', true)
       argv.should.have.property('b', true)
       argv.should.have.property('5', true)
@@ -992,12 +992,12 @@ describe('yargs-parser', function () {
     })
 
     it('should set n to the numeric value 123, with = as separator', function () {
-      var argv = parser([ '-n=123' ])
+      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' ])
+      var argv = parser(['-ab5n=123'])
       argv.should.have.property('a', true)
       argv.should.have.property('b', true)
       argv.should.have.property('5', true)
@@ -1008,7 +1008,7 @@ describe('yargs-parser', function () {
 
   describe('whitespace', function () {
     it('should be whitespace', function () {
-      var argv = parser([ '-x', '\t' ])
+      var argv = parser(['-x', '\t'])
       argv.should.have.property('x', '\t')
     })
   })
@@ -1049,7 +1049,7 @@ describe('yargs-parser', function () {
 
     function checkStringArg (opts, hasAlias) {
       it('should set defaults even if arg looks like a string', function () {
-        var result = parser([ '--flag', 'extra' ], opts)
+        var result = parser(['--flag', 'extra'], opts)
         result.should.have.property('flag', true)
         result.should.have.property('_').and.deep.equal(['extra'])
         if (hasAlias) {
@@ -1215,7 +1215,7 @@ describe('yargs-parser', function () {
       }
 
       it('should provide count options with dashes as camelCase properties', function () {
-        var result = parser([ '--some-option', '--some-option', '--some-option' ], {
+        var result = parser(['--some-option', '--some-option', '--some-option'], {
           count: ['some-option']
         })
 
@@ -1352,7 +1352,7 @@ describe('yargs-parser', function () {
     })
 
     it('should set - as the value of s when s is set as a string', function () {
-      var argv = parser([ '-s', '-' ], {
+      var argv = parser(['-s', '-'], {
         string: ['s']
       })
 
@@ -1406,13 +1406,13 @@ describe('yargs-parser', function () {
     })
 
     it('should not consume the next argument', function () {
-      var parsed = parser([ '-v', 'moo' ], {
+      var parsed = parser(['-v', 'moo'], {
         count: 'v'
       })
       parsed.v.should.equal(1)
       parsed.should.have.property('_').and.deep.equal(['moo'])
 
-      parsed = parser([ '--verbose', 'moomoo', '--verbose' ], {
+      parsed = parser(['--verbose', 'moomoo', '--verbose'], {
         count: 'verbose'
       })
       parsed.verbose.should.equal(2)
@@ -1709,7 +1709,7 @@ describe('yargs-parser', function () {
 
   describe('nargs', function () {
     it('should allow the number of arguments following a key to be specified', function () {
-      var result = parser([ '--foo', 'apple', 'bar' ], {
+      var result = parser(['--foo', 'apple', 'bar'], {
         narg: {
           foo: 2
         }
@@ -1744,7 +1744,7 @@ describe('yargs-parser', function () {
     })
 
     it('should apply nargs to flag arguments', function () {
-      var result = parser([ '-f', 'apple', 'bar', 'blerg' ], {
+      var result = parser(['-f', 'apple', 'bar', 'blerg'], {
         narg: {
           f: 2
         }
@@ -1793,7 +1793,7 @@ describe('yargs-parser', function () {
     })
 
     it('allows multiple nargs to be set at the same time', function () {
-      var result = parser([ '--foo', 'apple', 'bar', '--bar', 'banana', '-f' ], {
+      var result = parser(['--foo', 'apple', 'bar', '--bar', 'banana', '-f'], {
         narg: {
           foo: 2,
           bar: 1
@@ -2689,6 +2689,172 @@ describe('yargs-parser', function () {
         })
       })
     })
+
+    describe('collect-unknown-options = true', function () {
+      it('should ignore unknown options in long format separated by =', function () {
+        const argv = parser('--known-arg=1 --unknown-arg=2', {
+          number: ['known-arg'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['--unknown-arg=2'],
+          'known-arg': 1,
+          'knownArg': 1
+        })
+      })
+      it('should ignore unknown options in boolean negations', function () {
+        const argv = parser('--no-known-arg --no-unknown-arg', {
+          boolean: ['known-arg'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['--no-unknown-arg'],
+          'known-arg': false,
+          'knownArg': false
+        })
+      })
+      it('should ignore unknown options in long format separated by space', function () {
+        const argv = parser('--known-arg a --unknown-arg b', {
+          string: ['known-arg'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['--unknown-arg', 'b'],
+          'known-arg': 'a',
+          'knownArg': 'a'
+        })
+      })
+      it('should ignore unknown options in short dot format separated by equals', function () {
+        const argv = parser('-k.arg=a -u.arg=b', {
+          string: ['k.arg'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['-u.arg=b'],
+          'k': {
+            'arg': 'a'
+          }
+        })
+      })
+      it('should ignore unknown options in short dot format separated by space', function () {
+        const argv = parser('-k.arg 1 -u.arg 2', {
+          number: ['k.arg'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['-u.arg', '2'],
+          'k': {
+            'arg': 1
+          }
+        })
+      })
+      it('should ignore unknown options in short format separated by equals', function () {
+        const argv = parser('-k=a -u=b', {
+          string: ['k'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['-u=b'],
+          'k': 'a'
+        })
+      })
+      it('should ignore unknown options in short format followed by hyphen', function () {
+        const argv = parser('-k- -u-', {
+          string: ['k'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['-u-'],
+          'k': '-'
+        })
+      })
+      it('should ignore unknown options in short format separated by space', function () {
+        const argv = parser('-k 1 -u 2', {
+          number: ['k'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['-u', '2'],
+          'k': 1
+        })
+      })
+      it('should ignore unknown options in short format followed by a number', function () {
+        const argv = parser('-k1 -u2', {
+          number: ['k'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['-u2'],
+          'k': 1
+        })
+      })
+      it('should ignore unknown options in short format followed by a non-word character', function () {
+        const argv = parser('-k/1/ -u/2/', {
+          string: ['k'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['-u/2/'],
+          'k': '/1/'
+        })
+      })
+      it('should ignore unknown options in short format with multiple flags in one argument where an unknown flag is before the end', function () {
+        const argv = parser('-kuv', {
+          boolean: ['k', 'v'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: ['-kuv']
+        })
+      })
+      it('should parse known options in short format with multiple flags in one argument where no unknown flag is in the argument', function () {
+        const argv = parser('-kv', {
+          boolean: ['k', 'v'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: [],
+          k: true,
+          v: true
+        })
+      })
+      it('should parse negative numbers', function () {
+        const argv = parser('-k -33', {
+          boolean: ['k'],
+          configuration: {
+            'collect-unknown-options': true
+          }
+        })
+        argv.should.deep.equal({
+          _: [-33],
+          k: true
+        })
+      })
+    })
   })
 
   // addresses: https://github.com/yargs/yargs-parser/issues/41
@@ -2832,9 +2998,9 @@ describe('yargs-parser', function () {
           bar: fancyNumberParser
         }
       })
-      ;(typeof parsed.foo).should.equal('string')
+        ; (typeof parsed.foo).should.equal('string')
       parsed.foo.should.equal('88888889999990000998989898989898')
-      ;(typeof parsed.bar).should.equal('number')
+      ; (typeof parsed.bar).should.equal('number')
       parsed.bar.should.equal(998)
     })
 
@@ -2868,7 +3034,7 @@ describe('yargs-parser', function () {
         runcount++
         return undefined
       }
-      parser([ '--foo', 'bar' ], {
+      parser(['--foo', 'bar'], {
         alias: {
           foo: ['f', 'foo-bar', 'bar'],
           b: ['bar']
@@ -2883,7 +3049,7 @@ describe('yargs-parser', function () {
 
   // see: https://github.com/yargs/yargs-parser/issues/37
   it('normalizes all paths in array when provided via config object', function () {
-    var argv = parser([ '--foo', 'bar' ], {
+    var argv = parser(['--foo', 'bar'], {
       array: ['a'],
       normalize: ['a'],
       configObjects: [{ 'a': ['bin/../a.txt', 'bin/../b.txt'] }]
@@ -2893,17 +3059,17 @@ describe('yargs-parser', function () {
 
   // see: https://github.com/yargs/yargs/issues/963
   it('does not magically convert numeric strings larger than Number.MAX_SAFE_INTEGER', () => {
-    const argv = parser([ '--foo', '93940495950949399948393' ])
+    const argv = parser(['--foo', '93940495950949399948393'])
     argv.foo.should.equal('93940495950949399948393')
   })
 
   it('does not magically convert scientific notation larger than Number.MAX_SAFE_INTEGER', () => {
-    const argv = parser([ '--foo', '33e99999' ])
+    const argv = parser(['--foo', '33e99999'])
     argv.foo.should.equal('33e99999')
   })
 
   it('converts numeric options larger than Number.MAX_SAFE_INTEGER to number', () => {
-    const argv = parser([ '--foo', '93940495950949399948393' ], {
+    const argv = parser(['--foo', '93940495950949399948393'], {
       number: ['foo']
     })
     argv.foo.should.equal(9.39404959509494e+22)
@@ -2930,21 +3096,21 @@ describe('yargs-parser', function () {
   // see: https://github.com/yargs/yargs-parser/issues/101
   describe('dot-notation array arguments combined with string arguments', function () {
     it('parses correctly when dot-notation argument is first', function () {
-      var argv = parser([ '--foo.bar', 'baz', '--foo', 'bux' ])
+      var argv = parser(['--foo.bar', 'baz', '--foo', 'bux'])
       Array.isArray(argv.foo).should.equal(true)
       argv.foo[0].bar.should.equal('baz')
       argv.foo[1].should.equal('bux')
     })
 
     it('parses correctly when dot-notation argument is last', function () {
-      var argv = parser([ '--foo', 'bux', '--foo.bar', 'baz' ])
+      var argv = parser(['--foo', 'bux', '--foo.bar', 'baz'])
       Array.isArray(argv.foo).should.equal(true)
       argv.foo[0].should.equal('bux')
       argv.foo[1].bar.should.equal('baz')
     })
 
     it('parses correctly when there are multiple dot-notation arguments', function () {
-      var argv = parser([ '--foo.first', 'firstvalue', '--foo', 'bux', '--foo.bar', 'baz', '--foo.bla', 'banana' ])
+      var argv = parser(['--foo.first', 'firstvalue', '--foo', 'bux', '--foo.bar', 'baz', '--foo.bla', 'banana'])
       Array.isArray(argv.foo).should.equal(true)
       argv.foo.length.should.equal(4)
       argv.foo[0].first.should.equal('firstvalue')
@@ -2983,7 +3149,7 @@ describe('yargs-parser', function () {
 
   // see: https://github.com/yargs/yargs-parser/issues/144
   it('number/string types should use default when no right-hand value', () => {
-    let argv = parser([ '--foo' ], {
+    let argv = parser(['--foo'], {
       number: ['foo'],
       default: {
         foo: 99
@@ -2991,7 +3157,7 @@ describe('yargs-parser', function () {
     })
     argv.foo.should.equal(99)
 
-    argv = parser([ '-b' ], {
+    argv = parser(['-b'], {
       alias: {
         bar: 'b'
       },
@@ -3005,7 +3171,7 @@ describe('yargs-parser', function () {
 
   describe('stripping', function () {
     it('strip-dashed removes expected fields from argv', function () {
-      const argv = parser([ '--test-value', '1' ], {
+      const argv = parser(['--test-value', '1'], {
         number: ['test-value'],
         alias: {
           'test-value': ['alt-test']
@@ -3022,7 +3188,7 @@ describe('yargs-parser', function () {
     })
 
     it('strip-aliased removes expected fields from argv', function () {
-      const argv = parser([ '--test-value', '1' ], {
+      const argv = parser(['--test-value', '1'], {
         number: ['test-value'],
         alias: {
           'test-value': ['alt-test']
@@ -3039,7 +3205,7 @@ describe('yargs-parser', function () {
     })
 
     it('strip-aliased and strip-dashed combined removes expected fields from argv', function () {
-      const argv = parser([ '--test-value', '1' ], {
+      const argv = parser(['--test-value', '1'], {
         number: ['test-value'],
         alias: {
           'test-value': ['alt-test']
@@ -3056,7 +3222,7 @@ describe('yargs-parser', function () {
     })
 
     it('ignores strip-dashed if camel-case-expansion is disabled', function () {
-      const argv = parser([ '--test-value', '1' ], {
+      const argv = parser(['--test-value', '1'], {
         number: ['test-value'],
         configuration: {
           'camel-case-expansion': false,