Repository: bcoe/yargs
Pull request #368 (2016-02-07 02:06:30)

working on a new API for commands that creates less redundant code
I'm working on a new API for commands that makes writing them less redundant:

* commands now accept two functions a `builder` for describing command-specific help, and a `handler` that will get argv passed to it.
* the concept of `global` options is being introduced, to eliminate the need for setting the same option over and over again on each command.
* positional argument parsing: `foo <foo> [bar]` is being introduced.

- [x] **Builder/Handler Pattern.**

The definition of commands has now been split into a:

* `builder`: which is used to describe a command.
* `handler`: which is used to handle a command when it is invoked.

```js
var argv = require('yargs')
  .command(
    'push', 
    'push to a URL', 
    function (yargs) {
      // build command specific yargs object.
    },
    function (argv) {
      // handle the parsed options.
   })
   .help('help')
  .argv
```

- [x] **Providing a Module For Commands**

I would like to make it so that you could also provide a module to `command`, you would write a module like this:

```js
exports.builder = function (yargs) {
  return yargs.option('f', {})
}
exports.handler = function (argv) {
// do something with argv.
}
```

And would provide it to a command like this:

```js
yargs.command('foo', 'my awesome foo command', require('./test-module')).
```

I think this is pretty killer, and splitting out the builder and handler makes this possible what do you think @nexdrew?

- [x] **Global Options**

You can now specify `global` options using `.global()` or the `global: true` shorthand on options.

These options will not be reset when passed to commands, by default `help`, `version`, are treated this way (since this is the behavior folks usually want).

- [x] **Parsing Positional Arguments**

see: #363, #362 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d503f5f..8278c67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,11 @@
 
 ### 4.x
 
-- [#330](https://github.com/bcoe/yargs/pull/330) **breaking change**, fix inconsistencies with `.version()` API.
+- [#368](https://github.com/bcoe/yargs/pull/368) **breaking change**, overhaul to command handling API:
+  introducing named positional arguments, commands as modules, introduces the concept of global options (options that don't reset). (@nexdrew, @bcoe).
+- [#364](https://github.com/bcoe/yargs/pull/364) add the slick new yargs website to the package.json (@iarna).
+- [#356](https://github.com/bcoe/yargs/pull/356) pull the parsing bits of yargs into the separate module yargs-parser. Various parsing options can now be turned on and off using configuration (@bcoe).
+- [#330](https://github.com/bcoe/yargs/pull/330) **breaking change**, fix inconsistencies with `.version()` API. (@maxrimue).
 
 ### v3.32.0 (2016/1/14 10:13 +07:00)
 
diff --git a/README.md b/README.md
index d4f9711..32ca098 100644
--- a/README.md
+++ b/README.md
@@ -452,7 +452,9 @@ var argv = require('yargs')
   .argv
 ```
 
-.command(cmd, desc, [fn])
+.command(cmd, desc, [builder], [handler])
+-------------------
+.command(cmd, desc, [module])
 -------------------
 
 Document the commands exposed by your application.
@@ -462,32 +464,99 @@ values stored in `argv._`).  Set `desc` to `false` to create a hidden command.
 Hidden commands don't show up in the help output and aren't available for
 completion.
 
-Optionally, you can provide a handler `fn` which will be executed when
-a given command is provided. The handler will be called with `yargs` and
-`argv` as arguments.
-
-`yargs` is a blank instance of yargs, which can be used to compose a nested
-hierarchy of options handlers.
+Optionally, you can provide a `builder` object to give hints about the
+options that your command accepts:
 
-`argv` represents the arguments parsed prior to the
-command being executed (those described in the outer yargs instance).
+```js
+yargs.command('get', 'make a get HTTP request', {
+    url: {
+      alias: 'u',
+      default: 'http://yargs.js.org/'
+    }
+  })
+  .help()
+  .argv
+```
 
-Here's an example of top-level and nested commands in action:
+`builder` can also be a function. This function is executed
+with a `yargs` instance, and can be used to provide _advanced_ command specific help:
 
 ```js
-var argv = require('yargs')
-  .usage('npm <command>')
-  .command('install', 'tis a mighty fine package to install')
-  .command('publish', 'shiver me timbers, should you be sharing all that', function (yargs, argv) {
-    argv = yargs.option('f', {
-      alias: 'force',
-      description: 'yar, it usually be a bad idea'
+yargs.command('get', 'make a get HTTP request', function (yargs) {
+    return yargs.option('url', {
+      alias: 'u',
+      default: 'http://yargs.js.org/'
     })
-    .help('help')
-    .argv
   })
-  .help('help')
-  .argv;
+  .help()
+  .argv
+```
+
+You can also provide a handler function, which will be executed with the
+parsed `argv` object:
+
+```js
+yargs
+  .command(
+    'get',
+    'make a get HTTP request',
+    function (yargs) {
+      return yargs.option('u', {
+        alias: 'url',
+        describe: 'the URL to make an HTTP request to'
+      })
+    },
+    function (argv) {
+      console.log(argv.url)
+    }
+  )
+  .help()
+  .argv
+```
+
+### Positional Arguments
+
+Commands can accept _optional_ and _required_ positional arguments. Required
+positional arguments take the form `<foo>`, and optional arguments
+take the form `[bar]`. The parsed positional arguments will be populated in
+`argv`:
+
+```js
+yargs.command('get <source> [proxy]', 'make a get HTTP request')
+  .help()
+  .argv
+```
+
+### Providing a Command Module
+
+For complicated commands you can pull the logic into a module. A module
+simply needs to export:
+
+* `exports.builder`: which describes the options that a command accepts.
+* `exports.handler`: a function which will be passed the parsed argv.
+
+```js
+// my-module.js
+exports.builder = {
+  banana: {
+    default: 'cool'
+  },
+  batman: {
+    default: 'sad'
+  }
+}
+
+exports.handler = function (argv) {
+  // do something with argv.
+}
+```
+
+You then register the module like so:
+
+```js
+yargs.command('get <source> [proxy]', 'make a get HTTP request', require('my-module'))
+  .help()
+  .argv
 ```
 
 .completion(cmd, [description], [fn]);
@@ -725,6 +794,37 @@ Method to execute when a failure occurs, rather than printing the failure messag
 
 `fn` is called with the failure message that would have been printed.
 
+<a name="global"></a>.global(globals)
+------------
+
+Indicate that an option (or group of options) should not be reset when a command
+is executed, as an example:
+
+```js
+var argv = require('yargs')
+  .option('a', {
+    alias: 'all',
+    default: true
+  })
+  .option('n', {
+    alias: 'none',
+    default: true
+  })
+  .command('foo', 'foo command', function (yargs) {
+    return yargs.option('b', {
+      alias: 'bar'
+    })
+  })
+  .help('help')
+  .global('a')
+  .argv
+```
+
+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.
+
 <a name="group"></a>.group(key(s), groupName)
 --------------------
 
@@ -936,6 +1036,7 @@ Valid `opt` keys include:
 - `defaultDescription`: string, use this description for the default value in help content, see [`default()`](#default)
 - `demand`/`require`/`required`: boolean or string, demand the option be given, with optional error message, see [`demand()`](#demand)
 - `desc`/`describe`/`description`: string, the option description for help content, see [`describe()`](#describe)
+- `global`: boolean, indicate that this key should not be [reset](#reset) when a command is invoked, see [`global()`](#global)
 - `group`: string, when displaying usage instructions place the option under an alternative group heading, see [`group()`](#group)
 - `nargs`: number, specify how many arguments should be consumed for the option, see [`nargs()`](#nargs)
 - `normalize`: The option should be normalized, see [`normalize()`](#normalize)
@@ -971,11 +1072,12 @@ usage information and exit.
 The default behavior is to set the value of any key not followed by an
 option value to `true`.
 
-.reset()
+<a name="reset"></a>.reset()
 --------
 
 Reset the argument object built up so far. This is useful for
-creating nested command line interfaces.
+creating nested command line interfaces. Use [global](#global)
+to specify keys that should not be reset.
 
 ```js
 var yargs = require('yargs')
diff --git a/index.js b/index.js
index 9034463..35e7550 100644
--- a/index.js
+++ b/index.js
@@ -1,4 +1,5 @@
 var assert = require('assert')
+var Command = require('./lib/command')
 var Completion = require('./lib/completion')
 var Parser = require('yargs-parser')
 var path = require('path')
@@ -8,6 +9,7 @@ var Y18n = require('y18n')
 var readPkgUp = require('read-pkg-up')
 var pkgConf = require('pkg-conf')
 var requireMainFilename = require('require-main-filename')
+var objFilter = require('./lib/obj-filter')
 
 Argv(process.argv.slice(2))
 
@@ -16,6 +18,7 @@ function Argv (processArgs, cwd) {
   processArgs = processArgs || [] // handle calling yargs().
 
   var self = {}
+  var command = null
   var completion = null
   var usage = null
   var validation = null
@@ -43,40 +46,65 @@ function Argv (processArgs, cwd) {
     )
   }
 
+  // puts yargs back into an initial state. any keys
+  // that have been set to "global" will not be reset
+  // by this action.
   var options
-  self.resetOptions = self.reset = function () {
-    // put yargs back into its initial
-    // state, this is useful for creating a
-    // nested CLI.
-    options = {
-      array: [],
-      boolean: [],
-      string: [],
-      narg: {},
-      key: {},
-      alias: {},
-      default: {},
-      defaultDescription: {},
-      choices: {},
-      requiresArg: [],
-      count: [],
-      normalize: [],
-      config: {},
-      envPrefix: undefined
-    }
+  self.resetOptions = self.reset = function (aliases) {
+    aliases = aliases || {}
+    options = options || {}
+    // put yargs back into an initial state, this
+    // logic is used to build a nested command
+    // hierarchy.
+    var tmpOptions = {}
+    tmpOptions.global = options.global ? options.global : []
+
+    // 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
+      })
+    })
 
-    usage = Usage(self, y18n) // handle usage output.
-    validation = Validation(self, usage, y18n) // handle arg validation.
-    completion = Completion(self, usage)
+    var arrayOptions = [
+      'array', 'boolean', 'string', 'requiresArg',
+      'count', 'requiresArg', 'count', 'normalize'
+    ]
 
-    demanded = {}
-    groups = {}
+    var objectOptions = [
+      'narg', 'key', 'alias', 'default', 'defaultDescription',
+      'config', 'choices', 'demanded'
+    ]
+
+    arrayOptions.forEach(function (k) {
+      tmpOptions[k] = (options[k] || []).filter(function (k) {
+        return globalLookup[k]
+      })
+    })
+
+    objectOptions.forEach(function (k) {
+      tmpOptions[k] = objFilter(options[k], function (k, v) {
+        return globalLookup[k]
+      })
+    })
+
+    tmpOptions.envPrefix = undefined
+    options = tmpOptions
+
+    // 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)
+    command = command ? command.reset() : Command(self, usage, validation)
+    if (!completion) completion = Completion(self, usage, command)
 
     exitProcess = true
     strict = false
-    helpOpt = null
-    versionOpt = null
-    commandHandlers = {}
+    groups = {}
+    completionCommand = null
     self.parsed = false
 
     return self
@@ -137,19 +165,11 @@ function Argv (processArgs, cwd) {
     return self
   }
 
-  self.command = function (cmd, description, fn) {
-    if (description !== false) {
-      usage.command(cmd, description)
-    }
-    if (fn) commandHandlers[cmd] = fn
+  self.command = function (cmd, description, builder, handler) {
+    command.addHandler(cmd, description, builder, handler)
     return self
   }
 
-  var commandHandlers = {}
-  self.getCommandHandlers = function () {
-    return commandHandlers
-  }
-
   self.string = function (strings) {
     options.string.push.apply(options.string, [].concat(strings))
     return self
@@ -187,7 +207,6 @@ function Argv (processArgs, cwd) {
     return self
   }
 
-  var demanded = {}
   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 '_'
@@ -198,25 +217,25 @@ function Argv (processArgs, cwd) {
     }
 
     if (typeof keys === 'number') {
-      if (!demanded._) demanded._ = { count: 0, msg: null, max: max }
-      demanded._.count = keys
-      demanded._.msg = msg
+      if (!options.demanded._) options.demanded._ = { count: 0, msg: null, max: max }
+      options.demanded._.count = keys
+      options.demanded._.msg = msg
     } else if (Array.isArray(keys)) {
       keys.forEach(function (key) {
         self.demand(key, msg)
       })
     } else {
       if (typeof msg === 'string') {
-        demanded[keys] = { msg: msg }
+        options.demanded[keys] = { msg: msg }
       } else if (msg === true || typeof msg === 'undefined') {
-        demanded[keys] = { msg: undefined }
+        options.demanded[keys] = { msg: undefined }
       }
     }
 
     return self
   }
   self.getDemanded = function () {
-    return demanded
+    return options.demanded
   }
 
   self.requiresArg = function (requiresArgs) {
@@ -265,6 +284,11 @@ function Argv (processArgs, cwd) {
     return self
   }
 
+  self.global = function (globals) {
+    options.global.push.apply(options.global, [].concat(globals))
+    return self
+  }
+
   self.parse = function (args) {
     return parseArgs(args)
   }
@@ -297,6 +321,8 @@ function Argv (processArgs, cwd) {
         self.choices(key, opt.choices)
       } if ('group' in opt) {
         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)
@@ -386,6 +412,7 @@ function Argv (processArgs, cwd) {
 
     usage.version(ver || undefined)
     self.boolean(versionOpt)
+    self.global(versionOpt)
     self.describe(versionOpt, msg)
     return self
   }
@@ -402,6 +429,7 @@ function Argv (processArgs, cwd) {
   self.addHelpOpt = function (opt, msg) {
     helpOpt = opt
     self.boolean(opt)
+    self.global(opt)
     self.describe(opt, msg || usage.deferY18nLookup('Show help'))
     return self
   }
@@ -493,6 +521,10 @@ function Argv (processArgs, cwd) {
     return validation
   }
 
+  self.getCommandInstance = function () {
+    return command
+  }
+
   self.terminalWidth = function () {
     return require('window-size').width
   }
@@ -538,11 +570,11 @@ function Argv (processArgs, cwd) {
 
     // if there's a handler associated with a
     // command defer processing to it.
-    var handlerKeys = Object.keys(self.getCommandHandlers())
-    for (var i = 0, command; (command = handlerKeys[i]) !== undefined; i++) {
-      if (~argv._.indexOf(command)) {
-        runCommand(command, self, argv)
-        return self.argv
+    var handlerKeys = command.getCommands()
+    for (var i = 0, cmd; (cmd = handlerKeys[i]) !== undefined; i++) {
+      if (~argv._.indexOf(cmd) && cmd !== completionCommand) {
+        setPlaceholderKeys(argv)
+        return command.runCommand(cmd, self, parsed)
       }
     }
 
@@ -623,11 +655,6 @@ function Argv (processArgs, cwd) {
     }
   }
 
-  function runCommand (command, yargs, argv) {
-    setPlaceholderKeys(argv)
-    yargs.getCommandHandlers()[command](yargs.reset(), argv)
-  }
-
   function setPlaceholderKeys (argv) {
     Object.keys(options.key).forEach(function (key) {
       // don't set placeholder keys for dot
diff --git a/lib/command.js b/lib/command.js
new file mode 100644
index 0000000..f43a6b7
--- /dev/null
+++ b/lib/command.js
@@ -0,0 +1,111 @@
+// handles parsing positional arguments,
+// and populating argv with said positional
+// arguments.
+module.exports = function (yargs, usage, validation) {
+  var self = {}
+
+  var handlers = {}
+  self.addHandler = function (cmd, description, builder, handler) {
+    if (description !== false) {
+      usage.command(cmd, description)
+    }
+
+    // allow a module to be provided for a command.
+    if (typeof builder === 'object' && builder.builder && typeof builder.handler === 'function') {
+      self.addHandler(cmd, description, builder.builder, builder.handler)
+      return
+    }
+
+    // we should not register a handler if no
+    // builder is provided, e.g., user will
+    // handle command themselves with '_'.
+    var parsedCommand = parseCommand(cmd)
+    handlers[parsedCommand.cmd] = {
+      original: cmd,
+      handler: handler,
+      builder: builder,
+      demanded: parsedCommand.demanded,
+      optional: parsedCommand.optional
+    }
+  }
+
+  function parseCommand (cmd) {
+    var splitCommand = cmd.split(/\s/)
+    var bregex = /[\][<>]/g
+    var parsedCommand = {
+      cmd: (splitCommand.shift()).replace(bregex, ''),
+      demanded: [],
+      optional: []
+    }
+    splitCommand.forEach(function (cmd) {
+      if (/^\[/.test(cmd)) parsedCommand.optional.push(cmd.replace(bregex, ''))
+      else parsedCommand.demanded.push(cmd.replace(bregex, ''))
+    })
+    return parsedCommand
+  }
+
+  self.getCommands = function () {
+    return Object.keys(handlers)
+  }
+
+  self.getCommandHandlers = function () {
+    return handlers
+  }
+
+  self.runCommand = function (command, yargs, parsed) {
+    var argv = parsed.argv
+    var commandHandler = handlers[command]
+    var innerArgv = argv
+    if (commandHandler.builder && typeof commandHandler.builder === 'function') {
+      // a function can be provided, which interacts which builds
+      // up a yargs chain and returns it.
+      innerArgv = commandHandler.builder(yargs.reset(parsed.aliases))
+      innerArgv = innerArgv ? innerArgv.argv : argv
+    } else if (commandHandler.builder && 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 ' + commandHandler.original)
+      Object.keys(commandHandler.builder).forEach(function (key) {
+        innerArgv.option(key, commandHandler.builder[key])
+      })
+      innerArgv = innerArgv.argv
+    }
+
+    populatePositional(commandHandler, innerArgv)
+
+    if (commandHandler.handler) {
+      commandHandler.handler(innerArgv)
+    }
+    return innerArgv
+  }
+
+  function populatePositional (commandHandler, argv) {
+    var cmd = argv._.shift() // nuke the first argument (the current command)
+    var demanded = commandHandler.demanded.slice(0)
+    var optional = commandHandler.optional.slice(0)
+
+    validation.positionalCount(demanded.length, argv._.length)
+
+    while (demanded.length) {
+      if (!argv._.length) break
+      var demand = demanded.shift()
+      argv[demand] = argv._.shift()
+    }
+
+    while (optional.length) {
+      if (!argv._.length) break
+      var maybe = optional.shift()
+      argv[maybe] = argv._.shift()
+    }
+
+    argv._.unshift(cmd)
+  }
+
+  self.reset = function () {
+    handlers = {}
+    return self
+  }
+
+  return self
+}
diff --git a/lib/completion.js b/lib/completion.js
index 576012f..8f917c4 100644
--- a/lib/completion.js
+++ b/lib/completion.js
@@ -3,7 +3,7 @@ var path = require('path')
 
 // add bash completions to your
 //  yargs-powered applications.
-module.exports = function (yargs, usage) {
+module.exports = function (yargs, usage, command) {
   var self = {
     completionKey: 'get-yargs-completions'
   }
@@ -40,10 +40,10 @@ module.exports = function (yargs, usage) {
       }
     }
 
-    var handlers = yargs.getCommandHandlers()
+    var handlers = command.getCommandHandlers()
     for (var i = 0, ii = previous.length; i < ii; ++i) {
-      if (handlers[previous[i]]) {
-        return handlers[previous[i]](yargs.reset())
+      if (handlers[previous[i]] && handlers[previous[i]].builder) {
+        return handlers[previous[i]].builder(yargs.reset()).argv
       }
     }
 
diff --git a/lib/obj-filter.js b/lib/obj-filter.js
new file mode 100644
index 0000000..d5f8011
--- /dev/null
+++ b/lib/obj-filter.js
@@ -0,0 +1,10 @@
+module.exports = function (original, filter) {
+  var obj = {}
+  filter = filter || function (k, v) { return true }
+  Object.keys(original || {}).forEach(function (key) {
+    if (filter(key, original[key])) {
+      obj[key] = original[key]
+    }
+  })
+  return obj
+}
diff --git a/lib/usage.js b/lib/usage.js
index a1c18ec..588117f 100644
--- a/lib/usage.js
+++ b/lib/usage.js
@@ -4,6 +4,7 @@ var cliui = require('cliui')
 var decamelize = require('decamelize')
 var stringWidth = require('string-width')
 var wsize = require('window-size')
+var objFilter = require('./obj-filter')
 
 module.exports = function (yargs, y18n) {
   var __ = y18n.__
@@ -379,5 +380,20 @@ module.exports = function (yargs, y18n) {
     else console.log(version)
   }
 
+  self.reset = function (globalLookup) {
+    fails = []
+    failMessage = null
+    failureOutput = false
+    usage = undefined
+    epilog = undefined
+    examples = []
+    commands = []
+    wrap = windowWidth()
+    descriptions = objFilter(descriptions, function (k, v) {
+      return globalLookup[k]
+    })
+    return self
+  }
+
   return self
 }
diff --git a/lib/validation.js b/lib/validation.js
index 514bbff..0394699 100644
--- a/lib/validation.js
+++ b/lib/validation.js
@@ -1,3 +1,5 @@
+var objFilter = require('./obj-filter')
+
 // validation-type-stuff, missing params,
 // bad implications, custom checks.
 module.exports = function (yargs, usage, y18n) {
@@ -26,6 +28,16 @@ module.exports = function (yargs, usage, y18n) {
     }
   }
 
+  // validate the appropriate # of <required>
+  // positional arguments were provided:
+  self.positionalCount = function (required, observed) {
+    if (observed < required) {
+      usage.fail(
+        __('Not enough non-option arguments: got %s, need at least %s', observed, required)
+      )
+    }
+  }
+
   // make sure that any args that require an
   // value (--foo=bar), have a value.
   self.missingArgumentValue = function (argv) {
@@ -245,5 +257,13 @@ module.exports = function (yargs, usage, y18n) {
     }
   }
 
+  self.reset = function (globalLookup) {
+    implied = objFilter(implied, function (k, v) {
+      return globalLookup[k]
+    })
+    checks = []
+    return self
+  }
+
   return self
 }


Test patch:
diff --git a/test/command.js b/test/command.js
new file mode 100644
index 0000000..3e5a017
--- /dev/null
+++ b/test/command.js
@@ -0,0 +1,68 @@
+/* global describe, it, beforeEach */
+var yargs = require('../')
+var expect = require('chai').expect
+
+require('chai').should()
+
+describe('Command', function () {
+  beforeEach(function () {
+    yargs.reset()
+  })
+
+  describe('positional arguments', function () {
+    it('parses command string and populates optional and required positional arguments', function () {
+      var y = yargs([])
+        .command('foo <bar> [awesome]', 'my awesome command', function (yargs) {
+          return yargs
+        })
+      var command = y.getCommandInstance()
+      var handlers = command.getCommandHandlers()
+      handlers.foo.demanded.should.include('bar')
+      handlers.foo.optional.should.include('awesome')
+    })
+
+    it('populates inner argv with positional arguments', function (done) {
+      yargs('foo hello world')
+        .command('foo <bar> [awesome]', 'my awesome command', function () {}, function (argv) {
+          argv._.should.include('foo')
+          argv.bar.should.equal('hello')
+          argv.awesome.should.equal('world')
+          return done()
+        })
+        .argv
+    })
+
+    it('populates outer argv with positional arguments', function () {
+      var argv = yargs('foo hello world')
+        .command('foo <bar> [awesome]')
+        .argv
+
+      argv._.should.include('foo')
+      argv.bar.should.equal('hello')
+      argv.awesome.should.equal('world')
+    })
+  })
+
+  describe('missing positional arguments', function () {
+    it('fails if a required argument is missing', function (done) {
+      var argv = yargs('foo hello')
+        .command('foo <bar> <awesome>')
+        .fail(function (err) {
+          err.should.match(/got 1, need at least 2/)
+          return done()
+        })
+        .argv
+
+      argv.bar.should.equal('hello')
+    })
+
+    it('does not fail if optional argument is missing', function () {
+      var argv = yargs('foo hello')
+        .command('foo <bar> [awesome]')
+        .argv
+
+      expect(argv.awesome).to.equal(undefined)
+      argv.bar.should.equal('hello')
+    })
+  })
+})
diff --git a/test/completion.js b/test/completion.js
index 6839c0e..017e8c7 100644
--- a/test/completion.js
+++ b/test/completion.js
@@ -42,13 +42,12 @@ describe('Completion', function () {
       var r = checkUsage(function () {
         return yargs(['--get-yargs-completions'])
           .command('foo', 'foo command', function (subYargs) {
-            subYargs.options({
+            return subYargs.options({
               bar: {
                 describe: 'bar option'
               }
             })
             .help('help')
-            .argv
           })
           .completion()
           .argv
diff --git a/test/fixtures/command.js b/test/fixtures/command.js
new file mode 100644
index 0000000..92cf525
--- /dev/null
+++ b/test/fixtures/command.js
@@ -0,0 +1,12 @@
+exports.builder = {
+  banana: {
+    default: 'cool'
+  },
+  batman: {
+    default: 'sad'
+  }
+}
+
+exports.handler = function (argv) {
+  global.commandHandlerCalledWith = argv
+}
diff --git a/test/integration.js b/test/integration.js
index 9d02ca8..c514175 100644
--- a/test/integration.js
+++ b/test/integration.js
@@ -50,57 +50,59 @@ describe('integration tests', function () {
     })
   })
 
-  describe('load root package.json', function () {
-    before(function (done) {
-      this.timeout(10000)
-      // create a symlinked, and a physical copy of yargs in
-      // our fixtures directory, so that we can test that the
-      // nearest package.json is appropriately loaded.
-      cpr('./', './test/fixtures/yargs', {
-        filter: /node_modules|example|test|package\.json/
-      }, function () {
-        fs.symlinkSync(process.cwd(), './test/fixtures/yargs-symlink')
-        return done()
-      })
-    })
-
-    describe('version #', function () {
-      it('defaults to appropriate version # when yargs is installed normally', function (done) {
-        testCmd('./normal-bin.js', [ '--version' ], function (buf) {
-          buf.should.match(/9\.9\.9/)
+  if (process.platform !== 'win32') {
+    describe('load root package.json', function () {
+      before(function (done) {
+        this.timeout(10000)
+        // create a symlinked, and a physical copy of yargs in
+        // our fixtures directory, so that we can test that the
+        // nearest package.json is appropriately loaded.
+        cpr('./', './test/fixtures/yargs', {
+          filter: /node_modules|example|test|package\.json/
+        }, function () {
+          fs.symlinkSync(process.cwd(), './test/fixtures/yargs-symlink')
           return done()
         })
       })
 
-      it('defaults to appropriate version # when yargs is symlinked', function (done) {
-        testCmd('./symlink-bin.js', [ '--version' ], function (buf) {
-          buf.should.match(/9\.9\.9/)
-          return done()
+      describe('version #', function () {
+        it('defaults to appropriate version # when yargs is installed normally', function (done) {
+          testCmd('./normal-bin.js', [ '--version' ], function (buf) {
+            buf.should.match(/9\.9\.9/)
+            return done()
+          })
         })
-      })
-    })
 
-    describe('parser settings', function (done) {
-      it('reads parser config settings when yargs is installed normally', function (done) {
-        testCmd('./normal-bin.js', [ '--foo.bar' ], function (buf) {
-          buf.should.match(/foo\.bar/)
-          return done()
+        it('defaults to appropriate version # when yargs is symlinked', function (done) {
+          testCmd('./symlink-bin.js', [ '--version' ], function (buf) {
+            buf.should.match(/9\.9\.9/)
+            return done()
+          })
         })
       })
 
-      it('reads parser config settings when yargs is installed as a symlink', function (done) {
-        testCmd('./symlink-bin.js', [ '--foo.bar' ], function (buf) {
-          buf.should.match(/foo\.bar/)
-          return done()
+      describe('parser settings', function (done) {
+        it('reads parser config settings when yargs is installed normally', function (done) {
+          testCmd('./normal-bin.js', [ '--foo.bar' ], function (buf) {
+            buf.should.match(/foo\.bar/)
+            return done()
+          })
+        })
+
+        it('reads parser config settings when yargs is installed as a symlink', function (done) {
+          testCmd('./symlink-bin.js', [ '--foo.bar' ], function (buf) {
+            buf.should.match(/foo\.bar/)
+            return done()
+          })
         })
       })
-    })
 
-    after(function () {
-      rimraf.sync('./test/fixtures/yargs')
-      fs.unlinkSync('./test/fixtures/yargs-symlink')
+      after(function () {
+        rimraf.sync('./test/fixtures/yargs')
+        fs.unlinkSync('./test/fixtures/yargs-symlink')
+      })
     })
-  })
+  }
 })
 
 function testCmd (cmd, args, done) {
diff --git a/test/usage.js b/test/usage.js
index bd94795..b31582f 100644
--- a/test/usage.js
+++ b/test/usage.js
@@ -1214,15 +1214,14 @@ describe('usage tests', function () {
     it('resets groups for a command handler, respecting order', function () {
       var r = checkUsage(function () {
         return yargs(['upload', '-h'])
-          .command('upload', 'upload something', function (yargs, argv) {
-            argv = yargs
+          .command('upload', 'upload something', function (yargs) {
+            return yargs
               .option('q', {
                 type: 'boolean',
                 group: 'Flags:'
               })
               .help('h').group('h', 'Global Flags:')
               .wrap(null)
-              .argv
           })
           .help('h').group('h', 'Global Flags:')
           .wrap(null)
diff --git a/test/yargs.js b/test/yargs.js
index 6d6c5db..a9b159d 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -43,7 +43,7 @@ describe('yargs dsl tests', function () {
   it('populates argv with placeholder keys when passed into command handler', function (done) {
     yargs(['blerg'])
       .option('cool', {})
-      .command('blerg', 'handle blerg things', function (yargs, argv) {
+      .command('blerg', 'handle blerg things', function () {}, function (argv) {
         Object.keys(argv).should.include('cool')
         return done()
       })
@@ -223,11 +223,11 @@ describe('yargs dsl tests', function () {
 
       var emptyOptions = {
         array: [],
-        boolean: [],
+        boolean: ['help'],
         string: [],
         alias: {},
         default: {},
-        key: {},
+        key: {help: true},
         narg: {},
         defaultDescription: {},
         choices: {},
@@ -235,48 +235,55 @@ describe('yargs dsl tests', function () {
         count: [],
         normalize: [],
         config: {},
-        envPrefix: undefined
+        envPrefix: undefined,
+        global: ['help'],
+        demanded: {}
       }
 
       expect(y.getOptions()).to.deep.equal(emptyOptions)
-      expect(y.getUsageInstance().getDescriptions()).to.deep.equal({})
+      expect(y.getUsageInstance().getDescriptions()).to.deep.equal({help: '__yargsString__:Show help'})
       expect(y.getValidationInstance().getImplied()).to.deep.equal({})
+      expect(y.getCommandInstance().getCommandHandlers()).to.deep.equal({})
       expect(y.getExitProcess()).to.equal(true)
       expect(y.getStrict()).to.equal(false)
       expect(y.getDemanded()).to.deep.equal({})
-      expect(y.getCommandHandlers()).to.deep.equal({})
       expect(y.getGroups()).to.deep.equal({})
     })
   })
 
   describe('command', function () {
-    it('allows a function to be associated with a command', function (done) {
+    it('executes command handler with parsed argv', function (done) {
       yargs(['blerg'])
-        .command('blerg', 'handle blerg things', function (yargs, argv) {
-          // a fresh yargs instance for performing command-specific parsing,
-          // and an argv instance containing the parsing performed thus far
-          // should be passed to the command handler.
-          (typeof yargs.option).should.equal('function')
-          // we should get the argv from the prior yargs.
-          argv._[0].should.equal('blerg')
-
-          // the yargs instance has been reset.
-          yargs.getCommandHandlers().should.deep.equal({})
-          return done()
-        })
+        .command(
+          'blerg',
+          'handle blerg things',
+          function () {},
+          function (argv) {
+            // we should get the argv from the prior yargs.
+            argv._[0].should.equal('blerg')
+            return done()
+          }
+        )
         .exitProcess(false) // defaults to true.
         .argv
     })
 
-    it('does not execute top-level help if a handled command is provided', function () {
+    it("skips executing top-level command if builder's help is executed", function () {
       var r = checkOutput(function () {
         yargs(['blerg', '-h'])
-          .command('blerg', 'handle blerg things', function (yargs) {
-            yargs.command('snuh', 'snuh command')
-              .help('h')
-              .wrap(null)
-              .argv
-          })
+          .command(
+            'blerg',
+            'handle blerg things',
+            function (yargs) {
+              return yargs
+                .command('snuh', 'snuh command')
+                .help('h')
+                .wrap(null)
+            },
+            function () {
+              throw Error('should not happen')
+            }
+          )
           .help('h')
           .argv
       })
@@ -295,7 +302,8 @@ describe('yargs dsl tests', function () {
       var r = checkOutput(function () {
         yargs(['snuh', '-h'])
           .command('blerg', 'handle blerg things', function (yargs) {
-            yargs.command('snuh', 'snuh command')
+            return yargs
+              .command('snuh', 'snuh command')
               .help('h')
               .argv
           })
@@ -313,6 +321,43 @@ describe('yargs dsl tests', function () {
         ''
       ])
     })
+
+    it("accepts an object for describing a command's options", function () {
+      var r = checkOutput(function () {
+        yargs(['blerg', '-h'])
+          .command('blerg <foo>', 'handle blerg things', {
+            foo: {
+              default: 99
+            },
+            bar: {
+              default: 'hello world'
+            }
+          })
+          .help('h')
+          .wrap(null)
+          .argv
+      })
+
+      var usageString = r.logs[0]
+      usageString.should.match(/usage blerg <foo>/)
+      usageString.should.match(/--foo.*default: 99/)
+      usageString.should.match(/--bar.*default: "hello world"/)
+    })
+
+    it("accepts a module with a 'builder' and 'handler' key", function () {
+      var argv = yargs(['blerg', 'bar'])
+        .command('blerg <foo>', 'handle blerg things', require('./fixtures/command'))
+        .argv
+
+      argv.banana.should.equal('cool')
+      argv.batman.should.equal('sad')
+      argv.foo.should.equal('bar')
+
+      global.commandHandlerCalledWith.banana.should.equal('cool')
+      global.commandHandlerCalledWith.batman.should.equal('sad')
+      global.commandHandlerCalledWith.foo.should.equal('bar')
+      delete global.commandHandlerCalledWith
+    })
   })
 
   describe('terminalWidth', function () {
@@ -661,4 +706,104 @@ describe('yargs dsl tests', function () {
       argv._.should.deep.equal(['c'])
     })
   })
+
+  describe('global', function () {
+    it('does not reset a global options when reset is called', function () {
+      var y = yargs('--foo a b c')
+        .option('foo', {
+          nargs: 2
+        })
+        .option('bar', {
+          nargs: 2
+        })
+        .global('foo')
+        .reset()
+      var options = y.getOptions()
+      options.key.foo.should.equal(true)
+      expect(options.key.bar).to.equal(undefined)
+    })
+
+    it('does not reset alias of global option', function () {
+      var y = yargs('--foo a b c')
+        .option('foo', {
+          nargs: 2,
+          alias: 'awesome-sauce'
+        })
+        .string('awesome-sauce')
+        .demand('awesomeSauce')
+        .option('bar', {
+          nargs: 2,
+          string: true,
+          demand: true
+        })
+        .global('foo')
+        .reset({
+          foo: ['awesome-sauce', 'awesomeSauce']
+        })
+      var options = y.getOptions()
+
+      options.key.foo.should.equal(true)
+      options.string.should.include('awesome-sauce')
+      Object.keys(options.demanded).should.include('awesomeSauce')
+
+      expect(options.key.bar).to.equal(undefined)
+      options.string.should.not.include('bar')
+      Object.keys(options.demanded).should.not.include('bar')
+    })
+
+    it('should set help to global option by default', function () {
+      var y = yargs('--foo')
+        .help('help')
+      var options = y.getOptions()
+      options.global.should.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')
+    })
+
+    it('should not reset usage descriptions of global options', function () {
+      var y = yargs('--foo')
+        .describe('bar', 'my awesome bar option')
+        .describe('foo', 'my awesome foo option')
+        .global('foo')
+        .reset()
+      var descriptions = y.getUsageInstance().getDescriptions()
+      Object.keys(descriptions).should.include('foo')
+      Object.keys(descriptions).should.not.include('bar')
+    })
+
+    it('should not reset implications of global options', function () {
+      var y = yargs(['--x=33'])
+        .implies({
+          x: 'y'
+        })
+        .implies({
+          z: 'w'
+        })
+        .global(['x'])
+        .reset()
+      var implied = y.getValidationInstance().getImplied()
+      Object.keys(implied).should.include('x')
+      Object.keys(implied).should.not.include('z')
+    })
+
+    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
+        })
+        .option('bar', {
+          nargs: 2
+        })
+        .reset()
+      var options = y.getOptions()
+      options.key.foo.should.equal(true)
+      expect(options.key.bar).to.equal(undefined)
+    })
+  })
 })