Repository: yargs/yargs
Pull request #766 (2017-01-22 02:10:07)

feat: rethink how options are inherited by commands
BREAKING CHANGE: by default options, and many of yargs' parsing helpers will now default to being applied globally; such that they are no-longer reset before being passed into commands.

This is to address the fact that the existing behavior has created a lot of confusion: #659, #762, #764, etc.

This is going to be a pretty major change, and would appreciate as much feedback as possible as it's built out.

@nexdrew I'm going to merge your work from #734 into this.


Patch:
diff --git a/README.md b/README.md
index c14cb29..c4c006c 100644
--- a/README.md
+++ b/README.md
@@ -408,7 +408,7 @@ explicitly set.
 
 If `key` is an array, interpret all the elements as booleans.
 
-.check(fn)
+.check(fn, [global=true])
 ----------
 
 Check that certain conditions are met in the provided arguments.
@@ -418,6 +418,9 @@ Check that certain conditions are met in the provided arguments.
 If `fn` throws or returns a non-truthy value, show the thrown error, usage information, and
 exit.
 
+`global` indicates whether `check()` should be enabled both
+at the top-level and for each sub-command.
+
 <a name="choices"></a>.choices(key, choices)
 ----------------------
 
@@ -551,14 +554,6 @@ yargs
   .argv
 ```
 
-Note that commands will not automatically inherit configuration _or_ options
-of their parent context. This means you'll have to re-apply configuration
-if necessary, and make options global manually using the [global](#global) method.
-
-Additionally, the [`help`](#help) and [`version`](#version)
-options (if used) **always** apply globally, just like the
-[`.wrap()`](#wrap) configuration.
-
 `builder` can also be a function. This function is executed
 with a `yargs` instance, and can be used to provide _advanced_ command specific help:
 
@@ -678,7 +673,7 @@ require('yargs')
       console.log(`setting ${argv.key} to ${argv.value}`)
     }
   })
-  .demandCommand(1)
+  .demandCommand()
   .help()
   .wrap(72)
   .argv
@@ -824,7 +819,7 @@ cli.js:
 #!/usr/bin/env node
 require('yargs')
   .commandDir('cmds')
-  .demandCommand(1)
+  .demandCommand()
   .help()
   .argv
 ```
@@ -1112,9 +1107,9 @@ Options:
 Missing required arguments: run, path
 ```
 
-<a name="demandCommand"></a>.demandCommand(min, [minMsg])
+<a name="demandCommand"></a>.demandCommand([min=1], [minMsg])
 ------------------------------
-.demandCommand(min, [max], [minMsg], [maxMsg])
+.demandCommand([min=1], [max], [minMsg], [maxMsg])
 ------------------------------
 
 Demand in context of commands. You can demand a minimum and a maximum number a user can have within your program, as well as provide corresponding error messages if either of the demands is not met.
@@ -1134,7 +1129,9 @@ require('yargs')
   .help()
   .argv
 ```
+
 which will provide the following output:
+
 ```bash
 Commands:
   configure <key> [value]  Set a config variable         [aliases: config, cfg]
@@ -1293,7 +1290,7 @@ require('yargs')
 
 Outputs the same completion choices as `./test.js --foo`<kbd>TAB</kbd>: `--foobar` and `--foobaz`
 
-<a name="global"></a>.global(globals)
+<a name="global"></a>.global(globals, [global=true])
 ------------
 
 Indicate that an option (or group of options) should not be reset when a command
@@ -1303,11 +1300,13 @@ is executed, as an example:
 var argv = require('yargs')
   .option('a', {
     alias: 'all',
-    default: true
+    default: true,
+    global: false
   })
   .option('n', {
     alias: 'none',
-    default: true
+    default: true,
+    global: false
   })
   .command('foo', 'foo command', function (yargs) {
     return yargs.option('b', {
@@ -1322,7 +1321,7 @@ var argv = require('yargs')
 If the `foo` command is executed the `all` option will remain, but the `none`
 option will have been eliminated.
 
-`help`, `version`, and `completion` options default to being global.
+Options default to being global.
 
 <a name="group"></a>.group(key(s), groupName)
 --------------------
@@ -1778,12 +1777,15 @@ Specify --help for available options
 Specifies either a single option key (string), or an array of options.
 If any of the options is present, yargs validation is skipped.
 
-.strict()
+.strict([global=true])
 ---------
 
 Any command-line argument given that is not demanded, or does not have a
 corresponding description, will be reported as an error.
 
+`global` indicates whether `strict()` should be enabled both
+at the top-level and for each sub-command.
+
 <a name="string"></a>.string(key)
 ------------
 
diff --git a/lib/command.js b/lib/command.js
index c1afc1c..4b4cbea 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -131,47 +131,62 @@ module.exports = function (yargs, usage, validation) {
   }
 
   self.runCommand = function (command, yargs, parsed) {
-    var argv = parsed.argv
+    var aliases = parsed.aliases
     var commandHandler = handlers[command] || handlers[aliasMap[command]]
-    var innerArgv = argv
     var currentContext = yargs.getContext()
     var numFiles = currentContext.files.length
     var parentCommands = currentContext.commands.slice()
+
+    // what does yargs look like after the buidler is run?
+    var innerArgv = parsed.argv
+    var innerYargs = null
+
     currentContext.commands.push(command)
     if (typeof commandHandler.builder === 'function') {
       // a function can be provided, which builds
       // up a yargs chain and possibly returns it.
-      innerArgv = commandHandler.builder(yargs.reset(parsed.aliases))
+      innerYargs = commandHandler.builder(yargs.reset(parsed.aliases))
       // if the builder function did not yet parse argv with reset yargs
       // and did not explicitly set a usage() string, then apply the
       // original command string as usage() for consistent behavior with
-      // options object below
+      // options object below.
       if (yargs.parsed === false) {
         if (typeof yargs.getUsageInstance().getUsage() === 'undefined') {
           yargs.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
         }
-        innerArgv = innerArgv ? innerArgv.argv : yargs.argv
+        innerArgv = innerYargs ? innerYargs._parseArgs(null, null, true) : yargs._parseArgs(null, null, true)
       } else {
         innerArgv = yargs.parsed.argv
       }
+
+      if (innerYargs && yargs.parsed === false) aliases = innerYargs.parsed.aliases
+      else aliases = yargs.parsed.aliases
     } else if (typeof commandHandler.builder === 'object') {
       // as a short hand, an object can instead be provided, specifying
       // the options that a command takes.
-      innerArgv = yargs.reset(parsed.aliases)
-      innerArgv.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
+      innerYargs = yargs.reset(parsed.aliases)
+      innerYargs.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
       Object.keys(commandHandler.builder).forEach(function (key) {
-        innerArgv.option(key, commandHandler.builder[key])
+        innerYargs.option(key, commandHandler.builder[key])
       })
-      innerArgv = innerArgv.argv
+      innerArgv = innerYargs._parseArgs(null, null, true)
+      aliases = innerYargs.parsed.aliases
     }
+
     if (!yargs._hasOutput()) populatePositionals(commandHandler, innerArgv, currentContext, yargs)
 
     if (commandHandler.handler && !yargs._hasOutput()) {
       commandHandler.handler(innerArgv)
     }
+
+    // we apply validation post-hoc, so that custom
+    // checks get passed populated positional arguments.
+    yargs._runValidation(innerArgv, aliases)
+
     currentContext.commands.pop()
     numFiles = currentContext.files.length - numFiles
     if (numFiles > 0) currentContext.files.splice(numFiles * -1, numFiles)
+
     return innerArgv
   }
 
diff --git a/lib/usage.js b/lib/usage.js
index 2e503ac..0efabe8 100644
--- a/lib/usage.js
+++ b/lib/usage.js
@@ -436,7 +436,7 @@ module.exports = function (yargs, y18n) {
     else logger.log(version)
   }
 
-  self.reset = function (globalLookup) {
+  self.reset = function (localLookup) {
     // do not reset wrap here
     // do not reset fails here
     failMessage = null
@@ -446,7 +446,7 @@ module.exports = function (yargs, y18n) {
     examples = []
     commands = []
     descriptions = objFilter(descriptions, function (k, v) {
-      return globalLookup[k]
+      return !localLookup[k]
     })
     return self
   }
diff --git a/lib/validation.js b/lib/validation.js
index 328d83a..475ee31 100644
--- a/lib/validation.js
+++ b/lib/validation.js
@@ -194,22 +194,26 @@ module.exports = function (yargs, usage, y18n) {
 
   // custom checks, added using the `check` option on yargs.
   var checks = []
-  self.check = function (f) {
-    checks.push(f)
+  self.check = function (f, global) {
+    checks.push({
+      func: f,
+      global: global
+    })
   }
 
   self.customChecks = function (argv, aliases) {
     for (var i = 0, f; (f = checks[i]) !== undefined; i++) {
+      var func = f.func
       var result = null
       try {
-        result = f(argv, aliases)
+        result = func(argv, aliases)
       } catch (err) {
         usage.fail(err.message ? err.message : err, err)
         continue
       }
 
       if (!result) {
-        usage.fail(__('Argument check failed: %s', f.toString()))
+        usage.fail(__('Argument check failed: %s', func.toString()))
       } else if (typeof result === 'string' || result instanceof Error) {
         usage.fail(result.toString(), result)
       }
@@ -224,6 +228,7 @@ module.exports = function (yargs, usage, y18n) {
         self.implies(k, key[k])
       })
     } else {
+      yargs.global(key)
       implied[key] = value
     }
   }
@@ -290,6 +295,7 @@ module.exports = function (yargs, usage, y18n) {
         self.conflicts(k, key[k])
       })
     } else {
+      yargs.global(key)
       conflicting[key] = value
     }
   }
@@ -324,12 +330,16 @@ module.exports = function (yargs, usage, y18n) {
     if (recommended) usage.fail(__('Did you mean %s?', recommended))
   }
 
-  self.reset = function (globalLookup) {
+  self.reset = function (localLookup) {
     implied = objFilter(implied, function (k, v) {
-      return globalLookup[k]
+      return !localLookup[k]
+    })
+    conflicting = objFilter(conflicting, function (k, v) {
+      return !localLookup[k]
+    })
+    checks = checks.filter(function (c) {
+      return c.global
     })
-    checks = []
-    conflicting = {}
     return self
   }
 
diff --git a/yargs.js b/yargs.js
index bbb3d64..6e9ea90 100644
--- a/yargs.js
+++ b/yargs.js
@@ -66,23 +66,23 @@ function Yargs (processArgs, cwd, parentRequire) {
     // logic is used to build a nested command
     // hierarchy.
     var tmpOptions = {}
-    tmpOptions.global = options.global ? options.global : []
+    tmpOptions.local = options.local ? options.local : []
     tmpOptions.configObjects = options.configObjects ? options.configObjects : []
 
-    // if a key has been set as a global, we
-    // do not want to reset it or its aliases.
-    var globalLookup = {}
-    tmpOptions.global.forEach(function (g) {
-      globalLookup[g] = true
-      ;(aliases[g] || []).forEach(function (a) {
-        globalLookup[a] = true
+    // if a key has been explicitly set as local,
+    // we should reset it before passing options to command.
+    var localLookup = {}
+    tmpOptions.local.forEach(function (l) {
+      localLookup[l] = true
+      ;(aliases[l] || []).forEach(function (a) {
+        localLookup[a] = true
       })
     })
 
-    // preserve groups containing global keys
+    // preserve all groups not set to local.
     preservedGroups = Object.keys(groups).reduce(function (acc, groupName) {
       var keys = groups[groupName].filter(function (key) {
-        return key in globalLookup
+        return !(key in localLookup)
       })
       if (keys.length > 0) {
         acc[groupName] = keys
@@ -104,13 +104,13 @@ function Yargs (processArgs, cwd, parentRequire) {
 
     arrayOptions.forEach(function (k) {
       tmpOptions[k] = (options[k] || []).filter(function (k) {
-        return globalLookup[k]
+        return !localLookup[k]
       })
     })
 
     objectOptions.forEach(function (k) {
       tmpOptions[k] = objFilter(options[k], function (k, v) {
-        return globalLookup[k]
+        return !localLookup[k]
       })
     })
 
@@ -119,12 +119,12 @@ function Yargs (processArgs, cwd, parentRequire) {
 
     // if this is the first time being executed, create
     // instances of all our helpers -- otherwise just reset.
-    usage = usage ? usage.reset(globalLookup) : Usage(self, y18n)
-    validation = validation ? validation.reset(globalLookup) : Validation(self, usage, y18n)
+    usage = usage ? usage.reset(localLookup) : Usage(self, y18n)
+    validation = validation ? validation.reset(localLookup) : Validation(self, usage, y18n)
     command = command ? command.reset() : Command(self, usage, validation)
     if (!completion) completion = Completion(self, usage, command)
 
-    strict = false
+    if (!strictGlobal) strict = false
     completionCommand = null
     output = ''
     exitError = null
@@ -172,56 +172,127 @@ function Yargs (processArgs, cwd, parentRequire) {
     frozen = undefined
   }
 
-  self.boolean = function (bools) {
-    options.boolean.push.apply(options.boolean, [].concat(bools))
+  self.boolean = function (keys) {
+    populateParserHintArray('boolean', keys)
     return self
   }
 
-  self.array = function (arrays) {
-    options.array.push.apply(options.array, [].concat(arrays))
+  self.array = function (keys) {
+    populateParserHintArray('array', keys)
     return self
   }
 
-  self.nargs = function (key, n) {
-    if (typeof key === 'object') {
-      Object.keys(key).forEach(function (k) {
-        self.nargs(k, key[k])
-      })
-    } else {
-      options.narg[key] = n
-    }
+  self.number = function (keys) {
+    populateParserHintArray('number', keys)
     return self
   }
 
-  self.number = function (numbers) {
-    options.number.push.apply(options.number, [].concat(numbers))
+  self.normalize = function (keys) {
+    populateParserHintArray('normalize', keys)
     return self
   }
 
-  self.choices = function (key, values) {
-    if (typeof key === 'object') {
-      Object.keys(key).forEach(function (k) {
-        self.choices(k, key[k])
-      })
-    } else {
-      options.choices[key] = (options.choices[key] || []).concat(values)
+  self.count = function (keys) {
+    populateParserHintArray('count', keys)
+    return self
+  }
+
+  self.string = function (keys) {
+    populateParserHintArray('string', keys)
+    return self
+  }
+
+  self.requiresArg = function (keys) {
+    populateParserHintArray('requiresArg', keys)
+    return self
+  }
+
+  self.skipValidation = function (keys) {
+    populateParserHintArray('skipValidation', keys)
+    return self
+  }
+
+  function populateParserHintArray (type, keys, value) {
+    keys = [].concat(keys)
+    keys.forEach(function (key) {
+      options[type].push(key)
+    })
+  }
+
+  self.nargs = function (key, value) {
+    populateParserHintObject(self.nargs, false, 'narg', key, value)
+    return self
+  }
+
+  self.choices = function (key, value) {
+    populateParserHintObject(self.choices, true, 'choices', key, value)
+    return self
+  }
+
+  self.alias = function (key, value) {
+    populateParserHintObject(self.alias, true, 'alias', key, value)
+    return self
+  }
+
+  // TODO: actually deprecate self.defaults.
+  self.default = self.defaults = function (key, value, defaultDescription) {
+    if (defaultDescription) options.defaultDescription[key] = defaultDescription
+    if (typeof value === 'function') {
+      if (!options.defaultDescription[key]) options.defaultDescription[key] = usage.functionDescription(value)
+      value = value.call()
     }
+    populateParserHintObject(self.default, false, 'default', key, value)
     return self
   }
 
-  self.normalize = function (strings) {
-    options.normalize.push.apply(options.normalize, [].concat(strings))
+  self.describe = function (key, desc) {
+    populateParserHintObject(self.describe, false, 'key', key, true)
+    usage.describe(key, desc)
     return self
   }
 
+  self.demandOption = function (keys, msg) {
+    var value = { msg: typeof msg === 'string' ? msg : undefined }
+    populateParserHintObject(self.demandOption, false, 'demandedOptions', keys, value)
+    return self
+  }
+
+  self.coerce = function (keys, value) {
+    populateParserHintObject(self.coerce, false, 'coerce', keys, value)
+    return self
+  }
+
+  function populateParserHintObject (builder, isArray, type, key, value) {
+    if (Array.isArray(key)) {
+      // an array of keys with one value ['x', 'y', 'z'], function parse () {}
+      var temp = {}
+      key.forEach(function (k) {
+        temp[k] = value
+      })
+      builder(temp)
+    } else if (typeof key === 'object') {
+      // an object of key value pairs: {'x': parse () {}, 'y': parse() {}}
+      Object.keys(key).forEach(function (k) {
+        builder(k, key[k])
+      })
+    } else {
+      // a single key value pair 'x', parse() {}
+      if (isArray) {
+        options[type][key] = (options[type][key] || []).concat(value)
+      } else {
+        options[type][key] = value
+      }
+    }
+  }
+
   self.config = function (key, msg, parseFn) {
-    // allow to pass a configuration object
+    // allow a config object to be provided directly.
     if (typeof key === 'object') {
       options.configObjects = (options.configObjects || []).concat(key)
       return self
     }
 
-    // allow to provide a parsing function
+    // allow for a custom parsing function.
     if (typeof msg === 'function') {
       parseFn = msg
       msg = null
@@ -251,59 +322,8 @@ function Yargs (processArgs, cwd, parentRequire) {
     return self
   }
 
-  self.string = function (strings) {
-    options.string.push.apply(options.string, [].concat(strings))
-    return self
-  }
-
-  // The 'defaults' alias is deprecated. It will be removed in the next major version.
-  self.default = self.defaults = function (key, value, defaultDescription) {
-    if (typeof key === 'object') {
-      Object.keys(key).forEach(function (k) {
-        self.default(k, key[k])
-      })
-    } else {
-      if (defaultDescription) options.defaultDescription[key] = defaultDescription
-      if (typeof value === 'function') {
-        if (!options.defaultDescription[key]) options.defaultDescription[key] = usage.functionDescription(value)
-        value = value.call()
-      }
-      options.default[key] = value
-    }
-    return self
-  }
-
-  self.alias = function (x, y) {
-    if (typeof x === 'object') {
-      Object.keys(x).forEach(function (key) {
-        self.alias(key, x[key])
-      })
-    } else {
-      options.alias[x] = (options.alias[x] || []).concat(y)
-    }
-    return self
-  }
-
-  self.coerce = function (key, fn) {
-    if (typeof key === 'object' && !Array.isArray(key)) {
-      Object.keys(key).forEach(function (k) {
-        self.coerce(k, key[k])
-      })
-    } else {
-      [].concat(key).forEach(function (k) {
-        options.coerce[k] = fn
-      })
-    }
-    return self
-  }
-
-  self.count = function (counts) {
-    options.count.push.apply(options.count, [].concat(counts))
-    return self
-  }
-
-  // deprecated: the demand API is too overloaded, and is being
-  // deprecated in favor of .demandCommand() .demandOption().
+  // TODO: deprecate self.demand in favor of
+  // .demandCommand() .demandOption().
   self.demand = self.required = self.require = function (keys, max, msg) {
     // you can optionally provide a 'max' key,
     // which will raise an exception if too many '_'
@@ -335,29 +355,16 @@ function Yargs (processArgs, cwd, parentRequire) {
     return self
   }
 
-  self.demandOption = function (key, msg) {
-    if (Array.isArray(key)) {
-      key.forEach(function (key) {
-        self.demandOption(key, msg)
-      })
-    } else {
-      if (typeof msg === 'string') {
-        options.demandedOptions[key] = { msg: msg }
-      // allow edge-case of options: {a: {demand: true}, b: {demand: false}}
-      } else if (msg === true || typeof msg === 'undefined') {
-        options.demandedOptions[key] = { msg: undefined }
-      }
-    }
-
-    return self
-  }
-
   self.demandCommand = function (min, max, minMsg, maxMsg) {
+    if (typeof min === 'undefined') min = 1
+
     if (typeof max !== 'number') {
       minMsg = max
       max = Infinity
     }
 
+    self.global('_', false)
+
     options.demandedCommands._ = {
       min: min,
       max: max,
@@ -376,16 +383,6 @@ function Yargs (processArgs, cwd, parentRequire) {
     return options.demandedCommands
   }
 
-  self.requiresArg = function (requiresArgs) {
-    options.requiresArg.push.apply(options.requiresArg, [].concat(requiresArgs))
-    return self
-  }
-
-  self.skipValidation = function (skipValidations) {
-    options.skipValidation.push.apply(options.skipValidation, [].concat(skipValidations))
-    return self
-  }
-
   self.implies = function (key, value) {
     validation.implies(key, value)
     return self
@@ -419,31 +416,27 @@ function Yargs (processArgs, cwd, parentRequire) {
     return self
   }
 
-  self.check = function (f) {
-    validation.check(f)
+  self.check = function (f, _global) {
+    validation.check(f, _global !== false)
     return self
   }
 
-  self.describe = function (key, desc) {
-    if (typeof key === 'object') {
-      Object.keys(key).forEach(function (k) {
-        options.key[k] = true
+  self.global = function (globals, global) {
+    globals = [].concat(globals)
+    if (global !== false) {
+      options.local = options.local.filter(function (l) {
+        return globals.indexOf(l) === -1
       })
     } else {
-      options.key[key] = true
+      globals.forEach(function (g) {
+        if (options.local.indexOf(g) === -1) options.local.push(g)
+      })
     }
-    usage.describe(key, desc)
-    return self
-  }
-
-  self.global = function (globals) {
-    options.global.push.apply(options.global, [].concat(globals))
     return self
   }
 
   self.pkgConf = function (key, path) {
     var conf = null
-
     var obj = pkgUp(path)
 
     // If an object exists in the key, add it to options.configObjects
@@ -497,7 +490,7 @@ function Yargs (processArgs, cwd, parentRequire) {
     freeze()
     if (parseFn) exitProcess = false
 
-    var parsed = parseArgs(args, shortCircuit)
+    var parsed = self._parseArgs(args, shortCircuit)
     if (parseFn) parseFn(exitError, parsed, output)
     unfreeze()
 
@@ -568,10 +561,6 @@ function Yargs (processArgs, cwd, parentRequire) {
         self.group(key, opt.group)
       }
 
-      if (opt.global) {
-        self.global(key)
-      }
-
       if (opt.boolean || opt.type === 'boolean') {
         self.boolean(key)
         if (opt.alias) self.boolean(opt.alias)
@@ -596,6 +585,10 @@ function Yargs (processArgs, cwd, parentRequire) {
         self.count(key)
       }
 
+      if (typeof opt.global === 'boolean') {
+        self.global(key, opt.global)
+      }
+
       if (opt.defaultDescription) {
         options.defaultDescription[key] = opt.defaultDescription
       }
@@ -623,8 +616,7 @@ function Yargs (processArgs, cwd, parentRequire) {
   self.group = function (opts, groupName) {
     var existing = preservedGroups[groupName] || groups[groupName]
     if (preservedGroups[groupName]) {
-      // the preserved group will be moved to the set of explicitly declared
-      // groups
+      // we now only need to track this group name in groups.
       delete preservedGroups[groupName]
     }
 
@@ -654,8 +646,10 @@ function Yargs (processArgs, cwd, parentRequire) {
   }
 
   var strict = false
-  self.strict = function () {
+  var strictGlobal = false
+  self.strict = function (global) {
     strict = true
+    strictGlobal = global !== false
     return self
   }
   self.getStrict = function () {
@@ -663,7 +657,7 @@ function Yargs (processArgs, cwd, parentRequire) {
   }
 
   self.showHelp = function (level) {
-    if (!self.parsed) parseArgs(processArgs) // run parser, if it has not already been executed.
+    if (!self.parsed) self._parseArgs(processArgs) // run parser, if it has not already been executed.
     usage.showHelp(level)
     return self
   }
@@ -685,7 +679,6 @@ function Yargs (processArgs, cwd, parentRequire) {
 
     usage.version(ver || undefined)
     self.boolean(versionOpt)
-    self.global(versionOpt)
     self.describe(versionOpt, msg)
     return self
   }
@@ -722,7 +715,6 @@ function Yargs (processArgs, cwd, parentRequire) {
     // use arguments, fallback to defaults for opt and msg
     helpOpt = opt || 'help'
     self.boolean(helpOpt)
-    self.global(helpOpt)
     self.describe(helpOpt, msg || usage.deferY18nLookup('Show help'))
     return self
   }
@@ -866,7 +858,7 @@ function Yargs (processArgs, cwd, parentRequire) {
       var args = null
 
       try {
-        args = parseArgs(processArgs)
+        args = self._parseArgs(processArgs)
       } catch (err) {
         if (err instanceof YError) usage.fail(err.message, err)
         else throw err
@@ -877,7 +869,10 @@ function Yargs (processArgs, cwd, parentRequire) {
     enumerable: true
   })
 
-  function parseArgs (args, shortCircuit) {
+  self._parseArgs = function (args, shortCircuit, _skipValidation) {
+    var skipValidation = !!_skipValidation
+    args = args || processArgs
+
     options.__ = y18n.__
     options.configuration = pkgUp()['yargs'] || {}
     const parsed = Parser.detailed(args, options)
@@ -966,8 +961,6 @@ function Yargs (processArgs, cwd, parentRequire) {
       return setPlaceholderKeys(argv)
     }
 
-    var skipValidation = false
-
     // Handle 'help' and 'version' options
     Object.keys(argv).forEach(function (key) {
       if (key === helpOpt && argv[key]) {
@@ -993,27 +986,31 @@ function Yargs (processArgs, cwd, parentRequire) {
     }
 
     // If the help or version options where used and exitProcess is false,
-    // or if explicitly skipped, we won't run validations
+    // or if explicitly skipped, we won't run validations.
     if (!skipValidation) {
       if (parsed.error) throw new YError(parsed.error.message)
 
       // if we're executed via bash completion, don't
       // bother with validation.
       if (!argv[completion.completionKey]) {
-        validation.nonOptionCount(argv)
-        validation.missingArgumentValue(argv)
-        validation.requiredArguments(argv)
-        if (strict) validation.unknownArguments(argv, aliases)
-        validation.customChecks(argv, aliases)
-        validation.limitedChoices(argv)
-        validation.implications(argv)
-        validation.conflicting(argv)
+        self._runValidation(argv, aliases)
       }
     }
 
     return setPlaceholderKeys(argv)
   }
 
+  self._runValidation = function (argv, aliases) {
+    validation.nonOptionCount(argv)
+    validation.missingArgumentValue(argv)
+    validation.requiredArguments(argv)
+    if (strict) validation.unknownArguments(argv, aliases)
+    validation.customChecks(argv, aliases)
+    validation.limitedChoices(argv)
+    validation.implications(argv)
+    validation.conflicting(argv)
+  }
+
   function guessLocale () {
     if (!detectLocale) return
 


Test patch:
diff --git a/test/command.js b/test/command.js
index be4f0dc..ebdf881 100644
--- a/test/command.js
+++ b/test/command.js
@@ -839,4 +839,300 @@ describe('Command', function () {
       argv.sins.should.deep.equal([113993, 112888])
     })
   })
+
+  describe('global parsing hints', function () {
+    describe('config', function () {
+      it('does not load config for command if global is false', function (done) {
+        yargs('command --foo ./package.json')
+          .command('command', 'a command', {}, function (argv) {
+            expect(argv.license).to.equal(undefined)
+            return done()
+          })
+          .config('foo')
+          .global('foo', false)
+          .argv
+      })
+
+      it('loads config for command by default', function (done) {
+        yargs('command --foo ./package.json')
+          .command('command', 'a command', {}, function (argv) {
+            argv.license.should.equal('MIT')
+            return done()
+          })
+          .config('foo')
+          .argv
+      })
+    })
+
+    describe('validation', function () {
+      it('resets implies logic for command if global is false', function (done) {
+        yargs('command --foo 99')
+          .command('command', 'a command', {}, function (argv) {
+            argv.foo.should.equal(99)
+            return done()
+          })
+          .implies('foo', 'bar')
+          .global('foo', false)
+          .argv
+      })
+
+      it('applies conflicts logic for command by default', function (done) {
+        yargs('command --foo --bar')
+          .command('command', 'a command', {}, function (argv) {})
+          .fail(function (msg) {
+            msg.should.match(/mutually exclusive/)
+            return done()
+          })
+          .conflicts('foo', 'bar')
+          .argv
+      })
+
+      it('resets conflicts logic for command if global is false', function (done) {
+        yargs('command --foo --bar')
+          .command('command', 'a command', {}, function (argv) {
+            argv.foo.should.equal(true)
+            argv.bar.should.equal(true)
+            return done()
+          })
+          .conflicts('foo', 'bar')
+          .global('foo', false)
+          .argv
+      })
+
+      it('applies custom checks globally by default', function (done) {
+        yargs('command blerg --foo')
+          .command('command <snuh>', 'a command')
+          .check(function (argv) {
+            argv.snuh.should.equal('blerg')
+            argv.foo.should.equal(true)
+            argv._.should.include('command')
+            done()
+            return true
+          })
+          .argv
+      })
+
+      it('resets custom check if global is false', function () {
+        var checkCalled = false
+        yargs('command blerg --foo')
+          .command('command <snuh>', 'a command')
+          .check(function (argv) {
+            checkCalled = true
+            return true
+          }, false)
+          .argv
+        checkCalled.should.equal(false)
+      })
+
+      it('applies demandOption globally', function (done) {
+        yargs('command blerg --foo')
+          .command('command <snuh>', 'a command')
+          .fail(function (msg) {
+            msg.should.match(/Missing required argument: bar/)
+            return done()
+          })
+          .demandOption('bar')
+          .argv
+      })
+    })
+
+    describe('strict', function () {
+      it('defaults to false when not called', function () {
+        var commandCalled = false
+        yargs('hi')
+          .command('hi', 'The hi command', function (innerYargs) {
+            commandCalled = true
+            innerYargs.getStrict().should.be.false
+          })
+        yargs.getStrict().should.be.false
+        yargs.argv // parse and run command
+        commandCalled.should.be.true
+      })
+
+      it('can be enabled just for a command', function () {
+        var commandCalled = false
+        yargs('hi')
+          .command('hi', 'The hi command', function (innerYargs) {
+            commandCalled = true
+            innerYargs.strict().getStrict().should.be.true
+          })
+        yargs.getStrict().should.be.false
+        yargs.argv // parse and run command
+        commandCalled.should.be.true
+      })
+
+      it('applies strict globally by default', function () {
+        var commandCalled = false
+        yargs('hi')
+          .strict()
+          .command('hi', 'The hi command', function (innerYargs) {
+            commandCalled = true
+            innerYargs.getStrict().should.be.true
+          })
+        yargs.getStrict().should.be.true
+        yargs.argv // parse and run command
+        commandCalled.should.be.true
+      })
+
+      it('does not apply strict globally when passed value of `false`', function () {
+        var commandCalled = false
+        yargs('hi')
+          .strict(false)
+          .command('hi', 'The hi command', function (innerYargs) {
+            commandCalled = true
+            innerYargs.getStrict().should.be.false
+          })
+        yargs.getStrict().should.be.true
+        yargs.argv // parse and run command
+        commandCalled.should.be.true
+      })
+    })
+
+    describe('types', function () {
+      it('applies array type globally', function () {
+        const argv = yargs('command --foo 1 2')
+          .command('command', 'a command')
+          .array('foo')
+          .argv
+        argv.foo.should.eql([1, 2])
+      })
+
+      it('allows global setting to be disabled for array type', function () {
+        const argv = yargs('command --foo 1 2')
+          .command('command', 'a command')
+          .array('foo')
+          .global('foo', false)
+          .argv
+        argv.foo.should.eql(1)
+      })
+
+      it('applies choices type globally', function (done) {
+        yargs('command --foo 99')
+          .command('command', 'a command')
+          .choices('foo', [33, 88])
+          .fail(function (msg) {
+            msg.should.match(/Choices: 33, 88/)
+            return done()
+          })
+          .argv
+      })
+    })
+
+    describe('aliases', function () {
+      it('defaults to applying aliases globally', function (done) {
+        yargs('command blerg --foo 22')
+          .command('command <snuh>', 'a command', {}, function (argv) {
+            argv.foo.should.equal(22)
+            argv.bar.should.equal(22)
+            argv.snuh.should.equal('blerg')
+            return done()
+          })
+          .alias('foo', 'bar')
+          .argv
+      })
+
+      it('allows global application of alias to be disabled', function (done) {
+        yargs('command blerg --foo 22')
+          .command('command <snuh>', 'a command', {}, function (argv) {
+            argv.foo.should.equal(22)
+            expect(argv.bar).to.equal(undefined)
+            argv.snuh.should.equal('blerg')
+            return done()
+          })
+          .option('foo', {
+            alias: 'bar',
+            global: false
+          })
+          .argv
+      })
+    })
+
+    describe('coerce', function () {
+      it('defaults to applying coerce rules globally', function (done) {
+        yargs('command blerg --foo 22')
+          .command('command <snuh>', 'a command', {}, function (argv) {
+            argv.foo.should.equal(44)
+            argv.snuh.should.equal('blerg')
+            return done()
+          })
+          .coerce('foo', function (arg) {
+            return arg * 2
+          })
+          .argv
+      })
+    })
+
+    describe('defaults', function () {
+      it('applies defaults globally', function (done) {
+        yargs('command --foo 22')
+          .command('command [snuh]', 'a command', {}, function (argv) {
+            argv.foo.should.equal(22)
+            argv.snuh.should.equal(55)
+            return done()
+          })
+          .default('snuh', 55)
+          .argv
+      })
+    })
+
+    describe('describe', function () {
+      it('flags an option as global if a description is set', function (done) {
+        yargs()
+          .command('command [snuh]', 'a command')
+          .describe('foo', 'an awesome argument')
+          .help()
+          .parse('command --help', function (err, argv, output) {
+            if (err) return done(err)
+            output.should.not.match(/Commands:/)
+            output.should.match(/an awesome argument/)
+            return done()
+          })
+      })
+    })
+
+    describe('help', function () {
+      it('applies help globally', function (done) {
+        yargs()
+          .command('command [snuh]', 'a command')
+          .describe('foo', 'an awesome argument')
+          .help('hellllllp')
+          .parse('command --hellllllp', function (err, argv, output) {
+            if (err) return done(err)
+            output.should.match(/--hellllllp {2}Show help/)
+            return done()
+          })
+      })
+    })
+
+    describe('version', function () {
+      it('applies version globally', function (done) {
+        yargs()
+          .command('command [snuh]', 'a command')
+          .describe('foo', 'an awesome argument')
+          .version('ver', 'show version', '9.9.9')
+          .parse('command --ver', function (err, argv, output) {
+            if (err) return done(err)
+            output.should.equal('9.9.9')
+            return done()
+          })
+      })
+    })
+
+    describe('groups', function () {
+      it('should apply custom option groups globally', function (done) {
+        yargs()
+          .command('command [snuh]', 'a command')
+          .group('foo', 'Bad Variable Names:')
+          .group('snuh', 'Bad Variable Names:')
+          .describe('foo', 'foo option')
+          .describe('snuh', 'snuh positional')
+          .help()
+          .parse('command --help', function (err, argv, output) {
+            if (err) return done(err)
+            output.should.match(/Bad Variable Names:\W*--foo/)
+            return done()
+          })
+      })
+    })
+  })
 })
diff --git a/test/usage.js b/test/usage.js
index b3f532d..87635aa 100644
--- a/test/usage.js
+++ b/test/usage.js
@@ -84,16 +84,16 @@ describe('usage tests', function () {
         r.exit.should.be.ok
       })
 
-      it('no failure occurs if the required arguments and the required number of commands are provided.', function () {
+      it('no failure occurs if the required arguments and the required number of commands are provided', function () {
         var r = checkUsage(function () {
           return yargs('wombat -w 10 -m 10')
             .usage('Usage: $0 -w NUM -m NUM')
             .command('wombat', 'wombat handlers')
             .demand(1, ['w', 'm'])
-            .strict()
             .wrap(null)
             .argv
         })
+
         r.result.should.have.property('w', 10)
         r.result.should.have.property('m', 10)
         r.result.should.have.property('_').with.length(1)
@@ -170,24 +170,6 @@ describe('usage tests', function () {
         r.logs.should.have.length(0)
         r.exit.should.be.ok
       })
-
-      it('no failure occurs if the required arguments and the required number of commands are provided.', function () {
-        var r = checkUsage(function () {
-          return yargs('wombat -w 10 -m 10')
-            .usage('Usage: $0 -w NUM -m NUM')
-            .command('wombat', 'wombat handlers')
-            .require(1, ['w', 'm'])
-            .strict()
-            .wrap(null)
-            .argv
-        })
-        r.result.should.have.property('w', 10)
-        r.result.should.have.property('m', 10)
-        r.result.should.have.property('_').with.length(1)
-        r.should.have.property('errors').with.length(0)
-        r.should.have.property('logs').with.length(0)
-        r.should.have.property('exit', false)
-      })
     })
 
     it('should show an error along with a custom message on demand fail', function () {
@@ -1598,7 +1580,7 @@ describe('usage tests', function () {
       ])
     })
 
-    it('preserves groups with global keys', function () {
+    it('allows global option to be disabled', function () {
       var r = checkUsage(function () {
         return yargs(['upload', '-h'])
           .command('upload', 'upload something', function (yargs) {
diff --git a/test/validation.js b/test/validation.js
index f08080a..a831dea 100644
--- a/test/validation.js
+++ b/test/validation.js
@@ -589,5 +589,15 @@ describe('validation tests', function () {
         })
         .argv
     })
+
+    it('defaults to demanding 1 command', function (done) {
+      yargs('-a 10')
+        .demandCommand()
+        .fail(function (msg) {
+          msg.should.equal('Not enough non-option arguments: got 0, need at least 1')
+          return done()
+        })
+        .argv
+    })
   })
 })
diff --git a/test/yargs.js b/test/yargs.js
index b8777e7..5d5771d 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -220,8 +220,10 @@ describe('yargs dsl tests', function () {
         .implies('foo', 'snuh')
         .conflicts('qux', 'xyzzy')
         .group('foo', 'Group:')
-        .strict()
+        .strict(false)
         .exitProcess(false)  // defaults to true.
+        .global('foo', false)
+        .global('qux', false)
         .env('YARGS')
         .reset()
 
@@ -244,9 +246,13 @@ describe('yargs dsl tests', function () {
         config: {},
         configObjects: [],
         envPrefix: 'YARGS', // preserved as global
-        global: ['help'],
         demandedCommands: {},
-        demandedOptions: {}
+        demandedOptions: {},
+        local: [
+          '_',
+          'foo',
+          'qux'
+        ]
       }
 
       expect(y.getOptions()).to.deep.equal(emptyOptions)
@@ -261,9 +267,10 @@ describe('yargs dsl tests', function () {
       expect(y.getGroups()).to.deep.equal({})
     })
 
-    it('does not invoke parse with an error if reset has been called', function (done) {
+    it('does not invoke parse with an error if reset has been called and option is not global', function (done) {
       var y = yargs()
         .demand('cake')
+        .global('cake', false)
 
       y.parse('hello', function (err) {
         err.message.should.match(/Missing required argument/)
@@ -1123,6 +1130,7 @@ describe('yargs dsl tests', function () {
         .config('config', function (path) {
           return JSON.parse(fs.readFileSync(path))
         })
+        .global('config', false)
         .argv
 
       argv.foo.should.equal('baz')
@@ -1131,7 +1139,8 @@ describe('yargs dsl tests', function () {
     it('allows key to be specified with option shorthand', function () {
       var argv = yargs('--config ./test/fixtures/config.json')
         .option('config', {
-          config: true
+          config: true,
+          global: false
         })
         .argv
 
@@ -1218,7 +1227,8 @@ describe('yargs dsl tests', function () {
           nargs: 2
         })
         .option('bar', {
-          nargs: 2
+          nargs: 2,
+          global: false
         })
         .global('foo')
         .reset()
@@ -1238,7 +1248,8 @@ describe('yargs dsl tests', function () {
         .option('bar', {
           nargs: 2,
           string: true,
-          demand: true
+          demand: true,
+          global: false
         })
         .global('foo')
         .reset({
@@ -1259,14 +1270,14 @@ describe('yargs dsl tests', function () {
       var y = yargs('--foo')
         .help('help')
       var options = y.getOptions()
-      options.global.should.include('help')
+      options.local.should.not.include('help')
     })
 
     it('should set version to global option by default', function () {
       var y = yargs('--foo')
         .version()
       var options = y.getOptions()
-      options.global.should.include('version')
+      options.local.should.not.include('version')
     })
 
     it('should not reset usage descriptions of global options', function () {
@@ -1274,6 +1285,7 @@ describe('yargs dsl tests', function () {
         .describe('bar', 'my awesome bar option')
         .describe('foo', 'my awesome foo option')
         .global('foo')
+        .global('bar', false)
         .reset()
       var descriptions = y.getUsageInstance().getDescriptions()
       Object.keys(descriptions).should.include('foo')
@@ -1288,7 +1300,7 @@ describe('yargs dsl tests', function () {
         .implies({
           z: 'w'
         })
-        .global(['x'])
+        .global(['z'], false)
         .reset()
       var implied = y.getValidationInstance().getImplied()
       Object.keys(implied).should.include('x')
@@ -1298,11 +1310,11 @@ describe('yargs dsl tests', function () {
     it('should expose an options short-hand for declaring global options', function () {
       var y = yargs('--foo a b c')
         .option('foo', {
-          nargs: 2,
-          global: true
+          nargs: 2
         })
         .option('bar', {
-          nargs: 2
+          nargs: 2,
+          global: false
         })
         .reset()
       var options = y.getOptions()