Repository: yargs/yargs
Pull request #613 (2016-08-31 01:18:11)

Also pass yargs to the fail handler
This is needed to display a complete help message for a command that failed in a top-level `fail` handler


bcoe: @mkawalec love it. 👍 
mkawalec: @bcoe: Should I change something, or can we merge this? :)
bcoe: @mkawalec sorry for the slow turnaround! looks great, but could I perhaps get you to add a test? Partially because it would be neat to see how you plan on using this functionality 👍 
mkawalec: I'll add all of that early next week, thanks for the review.
bcoe: @mkawalec would love to land this, any news on that test :)
mkawalec: @bcoe: I've added the test, but it fails. The reason for that is that the exception thrown in command handler is not caught and so `fail` is never called. However, in a program I have that uses yargs the `fail` handler is called if an exception is thrown in the command handler. Is this a problem with the test harness or my mistake somewhere?
bcoe: @mkawalec when I have some time this weekend I'll take a look at the harness 👍 
bcoe: @mkawalec _this weekend_ has quickly become _this following weekend_, for which I apologize; I haven't forgotten about this pull.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cbc3791..d99acdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,70 @@
 
 All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
 
+<a name="6.3.0"></a>
+# [6.3.0](https://github.com/yargs/yargs/compare/v6.2.0...v6.3.0) (2016-10-19)
+
+
+### Bug Fixes
+
+* **command:** subcommands via commandDir() now supported for parse(msg, cb) ([#678](https://github.com/yargs/yargs/issues/678)) ([6b85cc6](https://github.com/yargs/yargs/commit/6b85cc6))
+
+
+### Features
+
+* **locales:** Add Thai locale file ([#679](https://github.com/yargs/yargs/issues/679)) ([c05e36b](https://github.com/yargs/yargs/commit/c05e36b))
+
+
+
+<a name="6.2.0"></a>
+# [6.2.0](https://github.com/yargs/yargs/compare/v6.1.1...v6.2.0) (2016-10-16)
+
+
+### Bug Fixes
+
+* stop applying parser to context object ([#675](https://github.com/yargs/yargs/issues/675)) ([3fe9b8f](https://github.com/yargs/yargs/commit/3fe9b8f))
+
+
+### Features
+
+* add new pt_BR translations ([#674](https://github.com/yargs/yargs/issues/674)) ([5615a82](https://github.com/yargs/yargs/commit/5615a82))
+* Italian translations for 'did you mean' and 'aliases' ([#673](https://github.com/yargs/yargs/issues/673)) ([81984e6](https://github.com/yargs/yargs/commit/81984e6))
+
+
+
+<a name="6.1.1"></a>
+## [6.1.1](https://github.com/yargs/yargs/compare/v6.1.0...v6.1.1) (2016-10-15)
+
+
+### Bug Fixes
+
+* freeze was not resetting configObjects to initial state; addressed performance issue raised by [@nexdrew](https://github.com/nexdrew). ([#670](https://github.com/yargs/yargs/issues/670)) ([ae4bcd4](https://github.com/yargs/yargs/commit/ae4bcd4))
+
+
+
+<a name="6.1.0"></a>
+# [6.1.0](https://github.com/yargs/yargs/compare/v6.0.0...v6.1.0) (2016-10-15)
+
+
+### Bug Fixes
+
+* **locales:** change some translations ([#667](https://github.com/yargs/yargs/issues/667)) ([aa966c5](https://github.com/yargs/yargs/commit/aa966c5))
+* **locales:** conform hi locale to y18n.__n expectations ([#666](https://github.com/yargs/yargs/issues/666)) ([22adb18](https://github.com/yargs/yargs/commit/22adb18))
+
+
+### Features
+
+* initial support for command aliases ([#647](https://github.com/yargs/yargs/issues/647)) ([127a040](https://github.com/yargs/yargs/commit/127a040))
+* **command:** add camelcase commands to argv ([#658](https://github.com/yargs/yargs/issues/658)) ([b1cabae](https://github.com/yargs/yargs/commit/b1cabae))
+* **locales:** add Hindi translations ([9290912](https://github.com/yargs/yargs/commit/9290912))
+* **locales:** add Hungarian translations  ([be92327](https://github.com/yargs/yargs/commit/be92327))
+* **locales:** Japanese translations for 'did you mean' and 'aliases'  ([#651](https://github.com/yargs/yargs/issues/651)) ([5eb78fc](https://github.com/yargs/yargs/commit/5eb78fc))
+* **locales:** Polish translations for 'did you mean' and 'aliases' ([#650](https://github.com/yargs/yargs/issues/650)) ([c951c0e](https://github.com/yargs/yargs/commit/c951c0e))
+* reworking yargs API to make it easier to run in headless environments, e.g., Slack ([#646](https://github.com/yargs/yargs/issues/646)) ([f284c29](https://github.com/yargs/yargs/commit/f284c29))
+* Turkish translations for 'did you mean' and 'aliases' ([#660](https://github.com/yargs/yargs/issues/660)) ([072fd45](https://github.com/yargs/yargs/commit/072fd45))
+
+
+
 <a name="6.0.0"></a>
 # [6.0.0](https://github.com/yargs/yargs/compare/v5.0.0...v6.0.0) (2016-09-30)
 
@@ -53,7 +117,6 @@ All notable changes to this project will be documented in this file. See [standa
 * adds recommendCommands() for command suggestions ([#580](https://github.com/yargs/yargs/issues/580)) ([59474dc](https://github.com/yargs/yargs/commit/59474dc))
 * apply .env() globally ([#553](https://github.com/yargs/yargs/issues/553)) ([be65728](https://github.com/yargs/yargs/commit/be65728))
 * apply default builder to command() and apply fail() handlers globally ([#583](https://github.com/yargs/yargs/issues/583)) ([0aaa68b](https://github.com/yargs/yargs/commit/0aaa68b))
-* interpret demand() numbers as relative to executing command ([#582](https://github.com/yargs/yargs/issues/582)) ([927810c](https://github.com/yargs/yargs/commit/927810c))
 * update yargs-parser to version 3.1.0 ([#581](https://github.com/yargs/yargs/issues/581)) ([882a127](https://github.com/yargs/yargs/commit/882a127))
 
 
@@ -69,6 +132,7 @@ All notable changes to this project will be documented in this file. See [standa
 * yargs-parser now better handles negative integer values, at the cost of handling numeric option names, e.g., -1 hello
 * default: removed undocumented `defaults` alias for `default`.
 * introduces a default `help` command which outputs help, as an alternative to a help flag.
+* interpret demand() numbers as relative to executing command ([#582](https://github.com/yargs/yargs/issues/582)) ([927810c](https://github.com/yargs/yargs/commit/927810c))
 
 
 
diff --git a/README.md b/README.md
index 660661c..a0b9ced 100644
--- a/README.md
+++ b/README.md
@@ -1137,7 +1137,7 @@ Give some example invocations of your program. Inside `cmd`, the string
 present script similar to how `$0` works in bash or perl.
 Examples will be printed out as part of the help message.
 
-.exitProcess(enable)
+<a name="exitprocess"></a>.exitProcess(enable)
 ----------------------------------
 
 By default, yargs exits the process when the user passes a help flag, uses the
@@ -1150,15 +1150,17 @@ yargs have been validated.
 
 Method to execute when a failure occurs, rather than printing the failure message.
 
-`fn` is called with the failure message that would have been printed and the
-`Error` instance originally thrown, if any.
+`fn` is called with the failure message that would have been printed, the
+`Error` instance originally thrown and yargs state when the failure
+occured. 
 
 ```js
 var argv = require('yargs')
-  .fail(function (msg, err) {
+  .fail(function (msg, err, yargs) {
     if (err) throw err // preserve stack
     console.error('You broke it!')
     console.error(msg)
+    console.error('You should be doing', yargs.help())
     process.exit(1)
   })
   .argv
@@ -1341,6 +1343,8 @@ Locales currently supported:
 * **en:** American English.
 * **es:** Spanish.
 * **fr:** French.
+* **hi:** Hindi.
+* **hu:** Hungarian.
 * **id:** Indonesian.
 * **it:** Italian.
 * **ja:** Japanese.
@@ -1350,8 +1354,10 @@ Locales currently supported:
 * **pl:** Polish.
 * **pt:** Portuguese.
 * **pt_BR:** Brazilian Portuguese.
+* **ru:** Russian.
+* **th:** Thai.
 * **tr:** Turkish.
-* **zh:** Chinese.
+* **zh_CN:** Chinese.
 
 To submit a new translation for yargs:
 
@@ -1488,13 +1494,47 @@ Valid `opt` keys include:
     - `'number'`: synonymous for `number: true`, see [`number()`](#number)
     - `'string'`: synonymous for `string: true`, see [`string()`](#string)
 
-.parse(args)
+.parse(args, [context], [parseCallback])
 ------------
 
 Parse `args` instead of `process.argv`. Returns the `argv` object.
-
 `args` may either be a pre-processed argv array, or a raw argument string.
 
+A `context` object can optionally be given as the second argument to `parse()`, providing a
+useful mechanism for passing state information to commands:
+
+```js
+const parser = yargs
+  .command('lunch-train <restaurant>', 'start lunch train', function () {}, function (argv) {
+    console.log(argv.restaurant, argv.time)
+  })
+  .parse("lunch-train rudy's", {time: '12:15'})
+```
+
+A `parseCallback` can also be provided to `.parse()`. If a callback is given, it will be invoked with three arguments:
+
+1. `err`: populated if any validation errors raised while parsing.
+2. `argv`: the parsed argv object.
+3. `output`: any text that would have been output to the terminal, had a
+  callback not been provided.
+
+```js
+// providing the `fn` argument to `parse()` runs yargs in headless mode, this
+// makes it easy to use yargs in contexts other than the CLI, e.g., writing
+// a chat-bot.
+const parser = yargs
+  .command('lunch-train <restaurant> <time>', 'start lunch train', function () {}, function (argv) {
+    api.scheduleLunch(argv.restaurant, moment(argv.time))
+  })
+  .help()
+
+parser.parse(bot.userText, function (err, argv, output) {
+  if (output) bot.respond(output)
+})
+```
+
+***Note:*** Providing a callback to `parse()` disables the [`exitProcess` setting](#exitprocess) until after the callback is invoked.
+
 .pkgConf(key, [cwd])
 ------------
 
diff --git a/lib/command.js b/lib/command.js
index 35b3077..e73340d 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -1,5 +1,6 @@
 const path = require('path')
 const inspect = require('util').inspect
+const camelCase = require('camelcase')
 
 // handles parsing positional arguments,
 // and populating argv with said positional
@@ -133,6 +134,7 @@ module.exports = function (yargs, usage, validation) {
     var commandHandler = handlers[command] || handlers[aliasMap[command]]
     var innerArgv = argv
     var currentContext = yargs.getContext()
+    var numFiles = currentContext.files.length
     var parentCommands = currentContext.commands.slice()
     currentContext.commands.push(command)
     if (typeof commandHandler.builder === 'function') {
@@ -161,13 +163,14 @@ module.exports = function (yargs, usage, validation) {
       })
       innerArgv = innerArgv.argv
     }
+    if (!yargs._hasOutput()) populatePositional(commandHandler, innerArgv, currentContext, yargs)
 
-    populatePositional(commandHandler, innerArgv, currentContext, yargs)
-
-    if (commandHandler.handler) {
+    if (commandHandler.handler && !yargs._hasOutput()) {
       commandHandler.handler(innerArgv)
     }
     currentContext.commands.pop()
+    numFiles = currentContext.files.length - numFiles
+    if (numFiles > 0) currentContext.files.splice(numFiles * -1, numFiles)
     return innerArgv
   }
 
@@ -185,6 +188,7 @@ module.exports = function (yargs, usage, validation) {
       if (demand.variadic) argv[demand.cmd] = argv._.splice(0)
       else argv[demand.cmd] = argv._.shift()
       postProcessPositional(yargs, argv, demand.cmd)
+      addCamelCaseExpansions(argv, demand.cmd)
     }
 
     while (optional.length) {
@@ -194,6 +198,7 @@ module.exports = function (yargs, usage, validation) {
       if (maybe.variadic) argv[maybe.cmd] = argv._.splice(0)
       else argv[maybe.cmd] = argv._.shift()
       postProcessPositional(yargs, argv, maybe.cmd)
+      addCamelCaseExpansions(argv, maybe.cmd)
     }
 
     argv._ = context.commands.concat(argv._)
@@ -211,11 +216,35 @@ module.exports = function (yargs, usage, validation) {
     }
   }
 
+  function addCamelCaseExpansions (argv, option) {
+    if (/-/.test(option)) {
+      const cc = camelCase(option)
+      if (typeof argv[option] === 'object') argv[cc] = argv[option].slice(0)
+      else argv[cc] = argv[option]
+    }
+  }
+
   self.reset = function () {
     handlers = {}
     aliasMap = {}
     return self
   }
 
+  // used by yargs.parse() to freeze
+  // the state of commands such that
+  // we can apply .parse() multiple times
+  // with the same yargs instance.
+  var frozen
+  self.freeze = function () {
+    frozen = {}
+    frozen.handlers = handlers
+    frozen.aliasMap = aliasMap
+  }
+  self.unfreeze = function () {
+    handlers = frozen.handlers
+    aliasMap = frozen.aliasMap
+    frozen = undefined
+  }
+
   return self
 }
diff --git a/lib/usage.js b/lib/usage.js
index 6d989ef..7f62275 100644
--- a/lib/usage.js
+++ b/lib/usage.js
@@ -30,9 +30,11 @@ module.exports = function (yargs, y18n) {
 
   var failureOutput = false
   self.fail = function (msg, err) {
+    const logger = yargs._getLoggerInstance()
+
     if (fails.length) {
       for (var i = fails.length - 1; i >= 0; --i) {
-        fails[i](msg, err)
+        fails[i](msg, err, self)
       }
     } else {
       if (yargs.getExitProcess()) setBlocking(true)
@@ -41,16 +43,20 @@ module.exports = function (yargs, y18n) {
       if (!failureOutput) {
         failureOutput = true
         if (showHelpOnFail) yargs.showHelp('error')
-        if (msg) console.error(msg)
+        if (msg) logger.error(msg)
         if (failMessage) {
-          if (msg) console.error('')
-          console.error(failMessage)
+          if (msg) logger.error('')
+          logger.error(failMessage)
         }
       }
+
+      err = err || new Error(msg)
       if (yargs.getExitProcess()) {
-        process.exit(1)
+        return yargs.exit(1)
+      } else if (yargs._hasParseCallback()) {
+        return yargs.exit(1, err)
       } else {
-        throw err || new Error(msg)
+        throw err
       }
     }
   }
@@ -276,7 +282,7 @@ module.exports = function (yargs, y18n) {
     var width = 0
 
     // table might be of the form [leftColumn],
-    // or {key: leftColumn}}
+    // or {key: leftColumn}
     if (!Array.isArray(table)) {
       table = Object.keys(table).map(function (key) {
         return [table[key]]
@@ -338,8 +344,9 @@ module.exports = function (yargs, y18n) {
   }
 
   self.showHelp = function (level) {
+    const logger = yargs._getLoggerInstance()
     if (!level) level = 'error'
-    var emit = typeof level === 'function' ? level : console[ level ]
+    var emit = typeof level === 'function' ? level : logger[level]
     emit(self.help())
   }
 
@@ -401,8 +408,9 @@ module.exports = function (yargs, y18n) {
   }
 
   self.showVersion = function () {
-    if (typeof version === 'function') console.log(version())
-    else console.log(version)
+    const logger = yargs._getLoggerInstance()
+    if (typeof version === 'function') logger.log(version())
+    else logger.log(version)
   }
 
   self.reset = function (globalLookup) {
@@ -420,5 +428,27 @@ module.exports = function (yargs, y18n) {
     return self
   }
 
+  var frozen
+  self.freeze = function () {
+    frozen = {}
+    frozen.failMessage = failMessage
+    frozen.failureOutput = failureOutput
+    frozen.usage = usage
+    frozen.epilog = epilog
+    frozen.examples = examples
+    frozen.commands = commands
+    frozen.descriptions = descriptions
+  }
+  self.unfreeze = function () {
+    failMessage = frozen.failMessage
+    failureOutput = frozen.failureOutput
+    usage = frozen.usage
+    epilog = frozen.epilog
+    examples = frozen.examples
+    commands = frozen.commands
+    descriptions = frozen.descriptions
+    frozen = undefined
+  }
+
   return self
 }
diff --git a/lib/validation.js b/lib/validation.js
index cc45378..0851cb1 100644
--- a/lib/validation.js
+++ b/lib/validation.js
@@ -187,18 +187,21 @@ module.exports = function (yargs, usage, y18n) {
   }
 
   self.customChecks = function (argv, aliases) {
-    checks.forEach(function (f) {
+    for (var i = 0, f; (f = checks[i]) !== undefined; i++) {
+      var result = null
       try {
-        const result = f(argv, aliases)
-        if (!result) {
-          usage.fail(__('Argument check failed: %s', f.toString()))
-        } else if (typeof result === 'string') {
-          usage.fail(result)
-        }
+        result = f(argv, aliases)
       } catch (err) {
         usage.fail(err.message ? err.message : err, err)
+        continue
       }
-    })
+
+      if (!result) {
+        usage.fail(__('Argument check failed: %s', f.toString()))
+      } else if (typeof result === 'string' || result instanceof Error) {
+        usage.fail(result.toString(), result)
+      }
+    }
   }
 
   // check implications, argument foo implies => argument bar.
@@ -299,5 +302,17 @@ module.exports = function (yargs, usage, y18n) {
     return self
   }
 
+  var frozen
+  self.freeze = function () {
+    frozen = {}
+    frozen.implied = implied
+    frozen.checks = checks
+  }
+  self.unfreeze = function () {
+    implied = frozen.implied
+    checks = frozen.checks
+    frozen = undefined
+  }
+
   return self
 }
diff --git a/locales/be.json b/locales/be.json
new file mode 100644
index 0000000..141ebe1
--- /dev/null
+++ b/locales/be.json
@@ -0,0 +1,39 @@
+{
+  "Commands:": "Каманды:",
+  "Options:": "Опцыі:",
+  "Examples:": "Прыклады:",
+  "boolean": "булевы тып",
+  "count": "падлік",
+  "string": "радковы тып",
+  "number": "лік",
+  "array": "масіў",
+  "required": "неабходна",
+  "default:": "па змаўчанні:",
+  "choices:": "магчымасці:",
+  "aliases:": "аліасы:",
+  "generated-value": "згенераванае значэнне",
+  "Not enough non-option arguments: got %s, need at least %s": "Недастаткова неапцыйных аргументаў: ёсць %s, трэба як мінімум %s",
+  "Too many non-option arguments: got %s, maximum of %s": "Занадта шмат неапцыйных аргументаў: ёсць %s, максімум дапушчальна %s",
+  "Missing argument value: %s": {
+    "one": "Не хапае значэння аргументу: %s",
+    "other": "Не хапае значэнняў аргументаў: %s"
+  },
+  "Missing required argument: %s": {
+    "one": "Не хапае неабходнага аргументу: %s",
+    "other": "Не хапае неабходных аргументаў: %s"
+  },
+  "Unknown argument: %s": {
+    "one": "Невядомы аргумент: %s",
+    "other": "Невядомыя аргументы: %s"
+  },
+  "Invalid values:": "Несапраўдныя значэння:",
+  "Argument: %s, Given: %s, Choices: %s": "Аргумент: %s, Дадзенае значэнне: %s, Магчымасці: %s",
+  "Argument check failed: %s": "Праверка аргументаў не ўдалася: %s",
+  "Implications failed:": "Дадзены аргумент патрабуе наступны дадатковы аргумент:",
+  "Not enough arguments following: %s": "Недастаткова наступных аргументаў: %s",
+  "Invalid JSON config file: %s": "Несапраўдны файл канфігурацыі JSON: %s",
+  "Path to JSON config file": "Шлях да файла канфігурацыі JSON",
+  "Show help": "Паказаць дапамогу",
+  "Show version number": "Паказаць нумар версіі",
+  "Did you mean %s?": "Вы мелі на ўвазе %s?"
+}
diff --git a/locales/es.json b/locales/es.json
index f6bdb69..d7c8af9 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -2,12 +2,12 @@
   "Commands:": "Comandos:",
   "Options:": "Opciones:",
   "Examples:": "Ejemplos:",
-  "boolean": "boolean",
+  "boolean": "booleano",
   "count": "cuenta",
   "string": "cadena de caracteres",
   "number": "número",
   "array": "tabla",
-  "required": "requisito",
+  "required": "requerido",
   "default:": "defecto:",
   "choices:": "selección:",
   "aliases:": "alias:",
@@ -27,9 +27,9 @@
     "other": "Argumentos desconocidos: %s"
   },
   "Invalid values:": "Valores inválidos:",
-  "Argument: %s, Given: %s, Choices: %s": "Argumento: %s, Recibido: %s, Selección: %s",
-  "Argument check failed: %s": "Verificación de argumento ha fracasado: %s",
-  "Implications failed:": "Implicaciones fracasadas:",
+  "Argument: %s, Given: %s, Choices: %s": "Argumento: %s, Recibido: %s, Seleccionados: %s",
+  "Argument check failed: %s": "Verificación de argumento ha fallado: %s",
+  "Implications failed:": "Implicaciones fallidas:",
   "Not enough arguments following: %s": "No hay suficientes argumentos después de: %s",
   "Invalid JSON config file: %s": "Archivo de configuración JSON inválido: %s",
   "Path to JSON config file": "Ruta al archivo de configuración JSON",
diff --git a/locales/hi.json b/locales/hi.json
new file mode 100644
index 0000000..2a9826d
--- /dev/null
+++ b/locales/hi.json
@@ -0,0 +1,39 @@
+{
+  "Commands:": "आदेश:",
+  "Options:": "विकल्प:",
+  "Examples:": "उदाहरण:",
+  "boolean": "सत्यता",
+  "count": "संख्या",
+  "string": "वर्णों का तार ",
+  "number": "अंक",
+  "array": "सरणी",
+  "required": "आवश्यक",
+  "default:": "डिफॉल्ट:",
+  "choices:": "विकल्प:",
+  "aliases:": "उपनाम:",
+  "generated-value": "उत्पन्न-मूल्य",
+  "Not enough non-option arguments: got %s, need at least %s": "पर्याप्त गैर-विकल्प तर्क प्राप्त नहीं: %s प्राप्त, कम से कम %s की आवश्यकता है",
+  "Too many non-option arguments: got %s, maximum of %s": "बहुत सारे गैर-विकल्प तर्क: %s प्राप्त, अधिकतम %s मान्य",
+  "Missing argument value: %s": {
+    "one": "कुछ तर्को के मूल्य गुम हैं: %s",
+    "other": "कुछ तर्को के मूल्य गुम हैं: %s"
+  },
+  "Missing required argument: %s": {
+    "one": "आवश्यक तर्क गुम हैं: %s",
+    "other": "आवश्यक तर्क गुम हैं: %s"
+  },
+  "Unknown argument: %s": {
+    "one": "अज्ञात तर्क प्राप्त: %s",
+    "other": "अज्ञात तर्क प्राप्त: %s"
+  },
+  "Invalid values:": "अमान्य मूल्य:",
+  "Argument: %s, Given: %s, Choices: %s": "तर्क: %s, प्राप्त: %s, विकल्प: %s",
+  "Argument check failed: %s": "तर्क जांच विफल: %s",
+  "Implications failed:": "दिए गए तर्क के लिए अतिरिक्त तर्क की अपेक्षा है:",
+  "Not enough arguments following: %s": "निम्नलिखित के बाद पर्याप्त तर्क नहीं प्राप्त: %s",
+  "Invalid JSON config file: %s": "अमान्य JSON config फाइल: %s",
+  "Path to JSON config file": "JSON config फाइल का पथ",
+  "Show help": "सहायता दिखाएँ",
+  "Show version number": "Version संख्या दिखाएँ",
+  "Did you mean %s?": "क्या आपका मतलब है %s?"
+}
diff --git a/locales/hu.json b/locales/hu.json
new file mode 100644
index 0000000..7b7d166
--- /dev/null
+++ b/locales/hu.json
@@ -0,0 +1,39 @@
+{
+  "Commands:": "Parancsok:",
+  "Options:": "Opciók:",
+  "Examples:": "Példák:",
+  "boolean": "boolean",
+  "count": "számláló",
+  "string": "szöveg",
+  "number": "szám",
+  "array": "tömb",
+  "required": "kötelező",
+  "default:": "alapértelmezett:",
+  "choices:": "lehetőségek:",
+  "aliases:": "aliaszok:",
+  "generated-value": "generált-érték",
+  "Not enough non-option arguments: got %s, need at least %s": "Nincs elég nem opcionális argumentum: %s van, legalább %s kell",
+  "Too many non-option arguments: got %s, maximum of %s": "Túl sok nem opciánlis argumentum van: %s van, maximum %s lehet",
+  "Missing argument value: %s": {
+    "one": "Hiányzó argumentum érték: %s",
+    "other": "Hiányzó argumentum értékek: %s"
+  },
+  "Missing required argument: %s": {
+    "one": "Hiányzó kötelező argumentum: %s",
+    "other": "Hiányzó kötelező argumentumok: %s"
+  },
+  "Unknown argument: %s": {
+    "one": "Ismeretlen argumentum: %s",
+    "other": "Ismeretlen argumentumok: %s"
+  },
+  "Invalid values:": "Érvénytelen érték:",
+  "Argument: %s, Given: %s, Choices: %s": "Argumentum: %s, Megadott: %s, Lehetőségek: %s",
+  "Argument check failed: %s": "Argumentum ellenőrzés sikertelen: %s",
+  "Implications failed:": "Implikációk sikertelenek:",
+  "Not enough arguments following: %s": "Nem elég argumentum követi: %s",
+  "Invalid JSON config file: %s": "Érvénytelen JSON konfigurációs file: %s",
+  "Path to JSON config file": "JSON konfigurációs file helye",
+  "Show help": "Súgo megjelenítése",
+  "Show version number": "Verziószám megjelenítése",
+  "Did you mean %s?": "Erre gondoltál %s?"
+}
diff --git a/locales/id.json b/locales/id.json
index abe317f..a06e860 100644
--- a/locales/id.json
+++ b/locales/id.json
@@ -35,6 +35,6 @@
   "Invalid JSON config file: %s": "Berkas konfigurasi JSON tidak valid: %s",
   "Path to JSON config file": "Alamat berkas konfigurasi JSON",
   "Show help": "Lihat bantuan",
-  "Did you mean %s?": "Maksud Anda: %s?",
-  "Show version number": "Lihat nomor versi"
+  "Show version number": "Lihat nomor versi",
+  "Did you mean %s?": "Maksud Anda: %s?"
 }
diff --git a/locales/it.json b/locales/it.json
index bd9573a..f9eb375 100644
--- a/locales/it.json
+++ b/locales/it.json
@@ -10,6 +10,7 @@
   "required": "richiesto",
   "default:": "predefinito:",
   "choices:": "scelte:",
+  "aliases:": "alias:",
   "generated-value": "valore generato",
   "Not enough non-option arguments: got %s, need at least %s": "Numero insufficiente di argomenti non opzione: inseriti %s, richiesti almeno %s",
   "Too many non-option arguments: got %s, maximum of %s": "Troppi argomenti non opzione: inseriti %s, massimo possibile %s",
@@ -33,5 +34,6 @@
   "Invalid JSON config file: %s": "File di configurazione JSON non valido: %s",
   "Path to JSON config file": "Percorso del file di configurazione JSON",
   "Show help": "Mostra la schermata di aiuto",
-  "Show version number": "Mostra il numero di versione"
+  "Show version number": "Mostra il numero di versione",
+  "Did you mean %s?": "Intendi forse %s?"
 }
diff --git a/locales/nl.json b/locales/nl.json
new file mode 100644
index 0000000..fc029db
--- /dev/null
+++ b/locales/nl.json
@@ -0,0 +1,39 @@
+{
+  "Commands:": "Opdrachten:",
+  "Options:": "Opties:",
+  "Examples:": "Voorbeelden:",
+  "boolean": "boolean",
+  "count": "aantal",
+  "string": "text",
+  "number": "nummer",
+  "array": "lijst",
+  "required": "verplicht",
+  "default:": "standaard:",
+  "choices:": "keuzes:",
+  "aliases:": "aliassen:",
+  "generated-value": "gegenereerde waarde",
+  "Not enough non-option arguments: got %s, need at least %s": "Niet genoeg non-optie argumenten. Gekregen: %s, minstens nodig: %s",
+  "Too many non-option arguments: got %s, maximum of %s": "Te veel non-optie argumenten. Gekregen: %s, maximum: %s",
+  "Missing argument value: %s": {
+    "one": "Missing argument value: %s",
+    "other": "Missing argument values: %s"
+  },
+  "Missing required argument: %s": {
+    "one": "Missend verplichte argument: %s",
+    "other": "Missende verplichte argumenten: %s"
+  },
+  "Unknown argument: %s": {
+    "one": "Onbekend argument: %s",
+    "other": "Onbekende argumenten: %s"
+  },
+  "Invalid values:": "Ongeldige waardes:",
+  "Argument: %s, Given: %s, Choices: %s": "Argument: %s, Gegeven: %s, Keuzes: %s",
+  "Argument check failed: %s": "Argument check mislukt: %s",
+  "Implications failed:": "Implicaties mislukt:",
+  "Not enough arguments following: %s": "Niet genoeg argumenten na: %s",
+  "Invalid JSON config file: %s": "Ongeldig JSON configuratiebestand: %s",
+  "Path to JSON config file": "Pad naar JSON configuratiebestand",
+  "Show help": "Toon help",
+  "Show version number": "Toon versie nummer",
+  "Did you mean %s?": "Bedoelde u misschien %s?"
+}
diff --git a/locales/pt_BR.json b/locales/pt_BR.json
index f0aec14..96d31e7 100644
--- a/locales/pt_BR.json
+++ b/locales/pt_BR.json
@@ -10,6 +10,7 @@
   "required": "obrigatório",
   "default:": "padrão:",
   "choices:": "opções:",
+  "aliases:": "sinônimos:",
   "generated-value": "valor-gerado",
   "Not enough non-option arguments: got %s, need at least %s": "Argumentos insuficientes: Argumento %s, necessário pelo menos %s",
   "Too many non-option arguments: got %s, maximum of %s": "Excesso de argumentos: recebido %s, máximo de %s",
@@ -33,5 +34,6 @@
   "Invalid JSON config file: %s": "Arquivo JSON de configuração inválido: %s",
   "Path to JSON config file": "Caminho para o arquivo JSON de configuração",
   "Show help": "Exibe ajuda",
-  "Show version number": "Exibe a versão"
+  "Show version number": "Exibe a versão",
+  "Did you mean %s?": "Você quis dizer %s?"
 }
diff --git a/locales/ru.json b/locales/ru.json
new file mode 100644
index 0000000..cb7b88b
--- /dev/null
+++ b/locales/ru.json
@@ -0,0 +1,39 @@
+{
+  "Commands:": "Команды:",
+  "Options:": "Опции:",
+  "Examples:": "Примеры:",
+  "boolean": "булевый тип",
+  "count": "подсчет",
+  "string": "строковой тип",
+  "number": "число",
+  "array": "массив",
+  "required": "необходимо",
+  "default:": "по умолчанию:",
+  "choices:": "возможности:",
+  "aliases:": "алиасы:",
+  "generated-value": "генерированное значение",
+  "Not enough non-option arguments: got %s, need at least %s": "Недостаточно неопционных аргументов: есть %s, нужно как минимум %s",
+  "Too many non-option arguments: got %s, maximum of %s": "Слишком много неопционных аргументов: есть %s, максимум допустимо %s",
+  "Missing argument value: %s": {
+    "one": "Не хватает значения аргумента: %s",
+    "other": "Не хватает значений аргументов: %s"
+  },
+  "Missing required argument: %s": {
+    "one": "Не хватает необходимого аргумента: %s",
+    "other": "Не хватает необходимых аргументов: %s"
+  },
+  "Unknown argument: %s": {
+    "one": "Неизвестный аргумент: %s",
+    "other": "Неизвестные аргументы: %s"
+  },
+  "Invalid values:": "Недействительные значения:",
+  "Argument: %s, Given: %s, Choices: %s": "Аргумент: %s, Данное значение: %s, Возможности: %s",
+  "Argument check failed: %s": "Проверка аргументов не удалась: %s",
+  "Implications failed:": "Данный аргумент требует следующий дополнительный аргумент:",
+  "Not enough arguments following: %s": "Недостаточно следующих аргументов: %s",
+  "Invalid JSON config file: %s": "Недействительный файл конфигурации JSON: %s",
+  "Path to JSON config file": "Путь к файлу конфигурации JSON",
+  "Show help": "Показать помощь",
+  "Show version number": "Показать номер версии",
+  "Did you mean %s?": "Вы имели в виду %s?"
+}
diff --git a/locales/th.json b/locales/th.json
new file mode 100644
index 0000000..3f08dcd
--- /dev/null
+++ b/locales/th.json
@@ -0,0 +1,39 @@
+{
+  "Commands:": "คอมมาน",
+  "Options:": "ออฟชั่น",
+  "Examples:": "ตัวอย่าง",
+  "boolean": "บูลีน",
+  "count": "นับ",
+  "string": "สตริง",
+  "number": "ตัวเลข",
+  "array": "อาเรย์",
+  "required": "จำเป็น",
+  "default:": "ค่าเริ่มต้น",
+  "choices:": "ตัวเลือก",
+  "aliases:": "เอเลียส",
+  "generated-value": "ค่าที่ถูกสร้างขึ้น",
+  "Not enough non-option arguments: got %s, need at least %s": "ใส่อาร์กิวเมนต์ไม่ครบตามจำนวนที่กำหนด: ใส่ค่ามาจำนวน %s ค่า, แต่ต้องการอย่างน้อย %s ค่า",
+  "Too many non-option arguments: got %s, maximum of %s": "ใส่อาร์กิวเมนต์เกินจำนวนที่กำหนด: ใส่ค่ามาจำนวน %s ค่า, แต่ต้องการมากที่สุด %s ค่า",
+  "Missing argument value: %s": {
+    "one": "ค่าอาร์กิวเมนต์ที่ขาดไป: %s",
+    "other": "ค่าอาร์กิวเมนต์ที่ขาดไป: %s"
+  },
+  "Missing required argument: %s": {
+    "one": "อาร์กิวเมนต์จำเป็นที่ขาดไป: %s",
+    "other": "อาร์กิวเมนต์จำเป็นที่ขาดไป: %s"
+  },
+  "Unknown argument: %s": {
+    "one": "อาร์กิวเมนต์ที่ไม่รู้จัก: %s",
+    "other": "อาร์กิวเมนต์ที่ไม่รู้จัก: %s"
+  },
+  "Invalid values:": "ค่าไม่ถูกต้อง:",
+  "Argument: %s, Given: %s, Choices: %s": "อาร์กิวเมนต์: %s, ได้รับ: %s, ตัวเลือก: %s",
+  "Argument check failed: %s": "ตรวจสอบพบอาร์กิวเมนต์ที่ไม่ถูกต้อง: %s",
+  "Implications failed:": "Implications ไม่สำเร็จ:",
+  "Not enough arguments following: %s": "ใส่อาร์กิวเมนต์ไม่ครบ: %s",
+  "Invalid JSON config file: %s": "ไฟล์คอนฟิค JSON ไม่ถูกต้อง: %s",
+  "Path to JSON config file": "พาทไฟล์คอนฟิค JSON",
+  "Show help": "ขอความช่วยเหลือ",
+  "Show version number": "แสดงตัวเลขเวอร์ชั่น",
+  "Did you mean %s?": "คุณหมายถึง %s?"
+}
diff --git a/locales/tr.json b/locales/tr.json
index 5b5c91b..000060b 100644
--- a/locales/tr.json
+++ b/locales/tr.json
@@ -10,6 +10,7 @@
   "required": "zorunlu",
   "default:": "varsayılan:",
   "choices:": "seçimler:",
+  "aliases:": "takma adlar:",
   "generated-value": "oluşturulan-değer",
   "Not enough non-option arguments: got %s, need at least %s": "Seçenek dışı argümanlar yetersiz: %s bulundu, %s gerekli",
   "Too many non-option arguments: got %s, maximum of %s": "Seçenek dışı argümanlar gereğinden fazla: %s bulundu, azami %s",
@@ -33,5 +34,6 @@
   "Invalid JSON config file: %s": "Geçersiz JSON yapılandırma dosyası: %s",
   "Path to JSON config file": "JSON yapılandırma dosya konumu",
   "Show help": "Yardım detaylarını göster",
-  "Show version number": "Versiyon detaylarını göster"
+  "Show version number": "Versiyon detaylarını göster",
+  "Did you mean %s?": "Bunu mu demek istediniz: %s?"
 }
diff --git a/package.json b/package.json
index c06b0bd..eaa2d76 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "yargs",
-  "version": "6.0.0",
+  "version": "6.3.0",
   "description": "yargs the modern, pirate-themed, successor to optimist.",
   "main": "./index.js",
   "files": [
@@ -12,6 +12,7 @@
     "LICENSE"
   ],
   "dependencies": {
+    "camelcase": "^3.0.0",
     "cliui": "^3.2.0",
     "decamelize": "^1.1.1",
     "get-caller-file": "^1.0.1",
@@ -24,18 +25,18 @@
     "which-module": "^1.0.0",
     "window-size": "^0.2.0",
     "y18n": "^3.2.1",
-    "yargs-parser": "^4.0.2"
+    "yargs-parser": "^4.1.0"
   },
   "devDependencies": {
     "chai": "^3.4.1",
     "chalk": "^1.1.3",
     "coveralls": "^2.11.11",
     "cpr": "^2.0.0",
-    "cross-spawn": "^4.0.0",
+    "cross-spawn": "^5.0.1",
     "es6-promise": "^4.0.2",
     "hashish": "0.0.4",
     "mocha": "^3.0.1",
-    "nyc": "^8.1.0",
+    "nyc": "^8.3.2",
     "rimraf": "^2.5.0",
     "standard": "^8.2.0",
     "standard-version": "^3.0.0",
@@ -72,7 +73,8 @@
   },
   "greenkeeper": {
     "ignore": [
-      "string-width"
+      "string-width",
+      "read-pkg-up"
     ]
   }
 }
diff --git a/yargs.js b/yargs.js
index abe2ec8..4c53ed3 100644
--- a/yargs.js
+++ b/yargs.js
@@ -1,3 +1,4 @@
+const assign = require('./lib/assign')
 const Command = require('./lib/command')
 const Completion = require('./lib/completion')
 const Parser = require('yargs-parser')
@@ -16,6 +17,7 @@ function Yargs (processArgs, cwd, parentRequire) {
   var command = null
   var completion = null
   var groups = {}
+  var output = ''
   var preservedGroups = {}
   var usage = null
   var validation = null
@@ -64,6 +66,7 @@ function Yargs (processArgs, cwd, parentRequire) {
     // hierarchy.
     var tmpOptions = {}
     tmpOptions.global = options.global ? options.global : []
+    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.
@@ -120,15 +123,54 @@ function Yargs (processArgs, cwd, parentRequire) {
     command = command ? command.reset() : Command(self, usage, validation)
     if (!completion) completion = Completion(self, usage, command)
 
-    exitProcess = true
     strict = false
     completionCommand = null
+    output = ''
+    exitError = null
+    hasOutput = false
     self.parsed = false
 
     return self
   }
   self.resetOptions()
 
+  // temporary hack: allow "freezing" of reset-able state for parse(msg, cb)
+  var frozen
+  function freeze () {
+    frozen = {}
+    frozen.options = options
+    frozen.configObjects = options.configObjects.slice(0)
+    frozen.exitProcess = exitProcess
+    frozen.groups = groups
+    usage.freeze()
+    validation.freeze()
+    command.freeze()
+    frozen.strict = strict
+    frozen.completionCommand = completionCommand
+    frozen.output = output
+    frozen.exitError = exitError
+    frozen.hasOutput = hasOutput
+    frozen.parsed = self.parsed
+  }
+  function unfreeze () {
+    options = frozen.options
+    options.configObjects = frozen.configObjects
+    exitProcess = frozen.exitProcess
+    groups = frozen.groups
+    output = frozen.output
+    exitError = frozen.exitError
+    hasOutput = frozen.hasOutput
+    self.parsed = frozen.parsed
+    usage.unfreeze()
+    validation.unfreeze()
+    command.unfreeze()
+    strict = frozen.strict
+    completionCommand = frozen.completionCommand
+    parseFn = null
+    parseContext = null
+    frozen = undefined
+  }
+
   self.boolean = function (bools) {
     options.boolean.push.apply(options.boolean, [].concat(bools))
     return self
@@ -382,9 +424,43 @@ function Yargs (processArgs, cwd, parentRequire) {
     return pkgs[npath]
   }
 
-  self.parse = function (args, shortCircuit) {
+  var parseFn = null
+  var parseContext = null
+  self.parse = function (args, shortCircuit, _parseFn) {
+    // a context object can optionally be provided, this allows
+    // additional information to be passed to a command handler.
+    if (typeof shortCircuit === 'object') {
+      parseContext = shortCircuit
+      shortCircuit = _parseFn
+    }
+
+    // by providing a function as a second argument to
+    // parse you can capture output that would otherwise
+    // default to printing to stdout/stderr.
+    if (typeof shortCircuit === 'function') {
+      parseFn = shortCircuit
+      shortCircuit = null
+    }
+    // completion short-circuits the parsing process,
+    // skipping validation, etc.
     if (!shortCircuit) processArgs = args
-    return parseArgs(args, shortCircuit)
+
+    if (parseFn) {
+      freeze()
+      exitProcess = false
+    }
+
+    var parsed = parseArgs(args, shortCircuit)
+    if (parseFn) {
+      parseFn(exitError, parsed, output)
+      unfreeze()
+    }
+
+    return parsed
+  }
+
+  self._hasParseCallback = function () {
+    return !!parseFn
   }
 
   self.option = self.options = function (key, opt) {
@@ -504,7 +580,7 @@ function Yargs (processArgs, cwd, parentRequire) {
   }
   self.getGroups = function () {
     // combine explicit and preserved groups. explicit groups should be first
-    return require('./lib/assign')(groups, preservedGroups)
+    return assign(groups, preservedGroups)
   }
 
   // as long as options.envPrefix is not undefined,
@@ -636,7 +712,7 @@ function Yargs (processArgs, cwd, parentRequire) {
 
   self.showCompletionScript = function ($0) {
     $0 = $0 || self.$0
-    console.log(completion.generateCompletionScript($0))
+    _logger.log(completion.generateCompletionScript($0))
     return self
   }
 
@@ -669,6 +745,43 @@ function Yargs (processArgs, cwd, parentRequire) {
     return detectLocale
   }
 
+  var hasOutput = false
+  var exitError = null
+  // maybe exit, always capture
+  // context about why we wanted to exit.
+  self.exit = function (code, err) {
+    hasOutput = true
+    exitError = err
+    if (exitProcess) process.exit(code)
+  }
+
+  // we use a custom logger that buffers output,
+  // so that we can print to non-CLIs, e.g., chat-bots.
+  var _logger = {
+    log: function () {
+      var args = Array.prototype.slice.call(arguments)
+      if (!self._hasParseCallback()) console.log.apply(console, args)
+      hasOutput = true
+      if (output.length) output += '\n'
+      output += args.join(' ')
+    },
+    error: function () {
+      var args = Array.prototype.slice.call(arguments)
+      if (!self._hasParseCallback()) console.error.apply(console, args)
+      hasOutput = true
+      if (output.length) output += '\n'
+      output += args.join(' ')
+    }
+  }
+  self._getLoggerInstance = function () {
+    return _logger
+  }
+  // has yargs output an error our help
+  // message in the current execution context.
+  self._hasOutput = function () {
+    return hasOutput
+  }
+
   var recommendCommands
   self.recommendCommands = function () {
     recommendCommands = true
@@ -710,7 +823,8 @@ function Yargs (processArgs, cwd, parentRequire) {
     options.__ = y18n.__
     options.configuration = pkgUp(cwd)['yargs'] || {}
     const parsed = Parser.detailed(args, options)
-    const argv = parsed.argv
+    var argv = parsed.argv
+    if (parseContext) argv = assign(parseContext, argv)
     var aliases = parsed.aliases
 
     argv.$0 = self.$0
@@ -772,9 +886,7 @@ function Yargs (processArgs, cwd, parentRequire) {
       if (completionCommand && ~argv._.indexOf(completionCommand) && !argv[completion.completionKey]) {
         if (exitProcess) setBlocking(true)
         self.showCompletionScript()
-        if (exitProcess) {
-          process.exit(0)
-        }
+        self.exit(0)
       }
     }
 
@@ -788,14 +900,12 @@ function Yargs (processArgs, cwd, parentRequire) {
       var completionArgs = args.slice(args.indexOf('--' + completion.completionKey) + 1)
       completion.getCompletion(completionArgs, function (completions) {
         ;(completions || []).forEach(function (completion) {
-          console.log(completion)
+          _logger.log(completion)
         })
 
-        if (exitProcess) {
-          process.exit(0)
-        }
+        self.exit(0)
       })
-      return
+      return setPlaceholderKeys(argv)
     }
 
     var skipValidation = false
@@ -807,17 +917,13 @@ function Yargs (processArgs, cwd, parentRequire) {
 
         skipValidation = true
         self.showHelp('log')
-        if (exitProcess) {
-          process.exit(0)
-        }
+        self.exit(0)
       } else if (key === versionOpt && argv[key]) {
         if (exitProcess) setBlocking(true)
 
         skipValidation = true
         usage.showVersion()
-        if (exitProcess) {
-          process.exit(0)
-        }
+        self.exit(0)
       }
     })
 
@@ -846,9 +952,7 @@ function Yargs (processArgs, cwd, parentRequire) {
       }
     }
 
-    setPlaceholderKeys(argv)
-
-    return argv
+    return setPlaceholderKeys(argv)
   }
 
   function guessLocale () {
@@ -870,6 +974,7 @@ function Yargs (processArgs, cwd, parentRequire) {
       if (~key.indexOf('.')) return
       if (typeof argv[key] === 'undefined') argv[key] = undefined
     })
+    return argv
   }
 
   return self


Test patch:
diff --git a/test/command.js b/test/command.js
index b344e50..1dea29c 100644
--- a/test/command.js
+++ b/test/command.js
@@ -49,6 +49,30 @@ describe('Command', function () {
       argv.awesome.should.equal('world')
     })
 
+    it('populates argv with camel-case variants of arguments when possible', function () {
+      var argv = yargs('foo hello world')
+        .command('foo <foo-bar> [baz-qux]')
+        .argv
+
+      argv._.should.include('foo')
+      argv['foo-bar'].should.equal('hello')
+      argv.fooBar.should.equal('hello')
+      argv.bazQux.should.equal('world')
+      argv['baz-qux'].should.equal('world')
+    })
+
+    it('populates argv with camel-case variants of variadic args when possible', function () {
+      var argv = yargs('foo hello world !')
+        .command('foo <foo-bar> [baz-qux..]')
+        .argv
+
+      argv._.should.include('foo')
+      argv['foo-bar'].should.equal('hello')
+      argv.fooBar.should.equal('hello')
+      argv.bazQux.should.deep.equal(['world', '!'])
+      argv['baz-qux'].should.deep.equal(['world', '!'])
+    })
+
     it('populates subcommand\'s inner argv with positional arguments', function () {
       yargs('foo bar hello world')
         .command('foo', 'my awesome command', function (yargs) {
diff --git a/test/fixtures/cmddir/deep/within-a-dream.js b/test/fixtures/cmddir/deep/within-a-dream.js
index af9e7f7..b6d2f19 100644
--- a/test/fixtures/cmddir/deep/within-a-dream.js
+++ b/test/fixtures/cmddir/deep/within-a-dream.js
@@ -12,6 +12,7 @@ module.exports = {
   },
   handler: function (argv) {
     var factor = 7
+    if (argv.context) argv.context.counter++ // keep track of how many times we've invoked this handler.
     if (argv.extract) {
       if (!argv.withKick) factor -= 2
       if (!chancesLevel2(factor)) throw new Error('Something went wrong at level 2! Check your options for increased chance of success.')
diff --git a/test/usage.js b/test/usage.js
index e5e83ed..5bd9b81 100644
--- a/test/usage.js
+++ b/test/usage.js
@@ -473,6 +473,28 @@ describe('usage tests', function () {
         r.logs.should.deep.equal(['foo'])
         r.should.have.property('exit').and.be.false
       })
+
+      it('gives the ability to log a per-command error message if failure occurs in a command', function () {
+        var r = checkUsage(function () {
+          return yargs('foo')
+            .command('foo', 'desc', {
+              bar: {
+                describe: 'bar command'
+              }
+            }, function (argv) {
+              throw new Error('blah')
+            })
+            .fail(function (message, error, yargs) {
+              yargs.showHelp()
+            })
+            .exitProcess(false)
+            .wrap(null)
+            .argv
+        })
+
+        r.errors[0].should.contain('bar command')
+      })
+
       describe('when check() throws error', function () {
         it('fail() is called with the original error object as the second parameter', function () {
           var r = checkUsage(function () {
diff --git a/test/yargs.js b/test/yargs.js
index 75d129d..f2339b7 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -1,4 +1,4 @@
-/* global describe, it, beforeEach */
+/* global context, describe, it, beforeEach */
 
 var expect = require('chai').expect
 var fs = require('fs')
@@ -238,6 +238,7 @@ describe('yargs dsl tests', function () {
         normalize: [],
         number: [],
         config: {},
+        configObjects: [],
         envPrefix: 'YARGS', // preserved as global
         global: ['help'],
         demanded: {}
@@ -247,11 +248,25 @@ describe('yargs dsl tests', function () {
       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.getExitProcess()).to.equal(false)
       expect(y.getStrict()).to.equal(false)
       expect(y.getDemanded()).to.deep.equal({})
       expect(y.getGroups()).to.deep.equal({})
     })
+
+    it('does not invoke parse with an error if reset has been called', function (done) {
+      var y = yargs()
+        .demand('cake')
+
+      y.parse('hello', function (err) {
+        err.message.should.match(/Missing required argumen/)
+      })
+      y.reset()
+      y.parse('cake', function (err) {
+        expect(err).to.equal(null)
+        return done()
+      })
+    })
   })
 
   describe('command', function () {
@@ -730,6 +745,369 @@ describe('yargs dsl tests', function () {
       var parsed = yargs.help().parse('help', true)
       parsed._.should.deep.equal(['help'])
     })
+
+    it('allows an optional context object to be provided', function () {
+      var a1 = yargs.parse('-x=2 --foo=bar', {
+        context: 'look at me go!'
+      })
+      a1.x.should.equal(2)
+      a1.foo.should.equal('bar')
+      a1.context.should.equal('look at me go!')
+    })
+  })
+
+  // yargs.parse(['foo', '--bar'], function (err, argv, output) {}
+  context('function passed as second argument to parse', function () {
+    it('does not print to stdout', function () {
+      var r = checkOutput(function () {
+        yargs()
+          .help('h')
+          .parse('-h', function (_err, argv, output) {})
+      })
+
+      r.logs.length.should.equal(0)
+      r.errors.length.should.equal(0)
+    })
+
+    it('gets passed error as first argument', function () {
+      var err = null
+      var r = checkOutput(function () {
+        yargs()
+          .demand('robin')
+          .parse('batman', function (_err, argv, output) {
+            err = _err
+          })
+      })
+      r.logs.length.should.equal(0)
+      r.errors.length.should.equal(0)
+      err.should.match(/Missing required argument/)
+    })
+
+    it('gets passed argv as second argument', function () {
+      var argv = null
+      var r = checkOutput(function () {
+        yargs()
+          .demand('robin')
+          .parse('batman --foo', function (_err, _argv, output) {
+            argv = _argv
+          })
+      })
+      r.logs.length.should.equal(0)
+      r.errors.length.should.equal(0)
+      argv.foo.should.equal(true)
+    })
+
+    it('gets passed output as third argument', function () {
+      var output = null
+      var r = checkOutput(function () {
+        yargs()
+          .demand('robin')
+          .help()
+          .parse('--help', function (_err, argv, _output) {
+            output = _output
+          })
+      })
+      r.logs.length.should.equal(0)
+      r.errors.length.should.equal(0)
+      output.should.match(/--robin.*\[required\]/)
+    })
+
+    it('reinstates original exitProcess setting after invocation', function () {
+      var callbackCalled = false
+      var r = checkOutput(function () {
+        yargs
+          .exitProcess(true)
+          .help()
+          .parse('--help', function () {
+            callbackCalled = true
+            yargs.getExitProcess().should.be.false
+          })
+      })
+      r.logs.length.should.equal(0)
+      r.errors.length.should.equal(0)
+      r.exit.should.be.false
+      callbackCalled.should.be.true
+      yargs.getExitProcess().should.be.true
+    })
+
+    it('does not call callback if subsequently called without callback', function () {
+      var callbackCalled = 0
+      var callback = function () {
+        callbackCalled++
+      }
+      yargs.help()
+      var r1 = checkOutput(function () {
+        yargs.parse('--help', callback)
+      })
+      var r2 = checkOutput(function () {
+        yargs.parse('--help')
+      })
+      callbackCalled.should.equal(1)
+      r1.logs.length.should.equal(0)
+      r1.errors.length.should.equal(0)
+      r1.exit.should.be.false
+      r2.exit.should.be.true
+      r2.errors.length.should.equal(0)
+      r2.logs[0].should.match(/--help.*Show help.*\[boolean\]/)
+    })
+
+    it('resets error state between calls to parse', function () {
+      var y = yargs()
+        .demand(2)
+
+      var err1 = null
+      var out1 = null
+      var argv1 = null
+      y.parse('foo', function (err, argv, output) {
+        err1 = err
+        argv1 = argv
+        out1 = output
+      })
+
+      err1.message.should.match(/Not enough non-option arguments/)
+      argv1._.should.include('foo')
+      out1.should.match(/Not enough non-option arguments/)
+
+      var err2 = null
+      var argv2 = null
+      var out2 = null
+      y.parse('foo bar', function (err, argv, output) {
+        err2 = err
+        argv2 = argv
+        out2 = output
+      })
+
+      expect(err2).to.equal(null)
+      argv2._.should.deep.equal([
+        'foo',
+        'bar'
+      ])
+      expect(out2).to.equal('')
+    })
+
+    describe('commands', function () {
+      it('does not invoke command handler if output is populated', function () {
+        var err = null
+        var handlerCalled = false
+        var r = checkOutput(function () {
+          yargs()
+            .command('batman <api-token>', 'batman command', function () {}, function () {
+              handlerCalled = true
+            })
+            .parse('batman --what', function (_err, argv, output) {
+              err = _err
+            })
+        })
+        r.logs.length.should.equal(0)
+        r.errors.length.should.equal(0)
+        err.message.should.match(/Not enough non-option arguments/)
+        handlerCalled.should.equal(false)
+      })
+
+      it('invokes command handler normally if no output is populated', function () {
+        var argv = null
+        var output = null
+        var r = checkOutput(function () {
+          yargs()
+            .command('batman <api-token>', 'batman command', function () {}, function (_argv) {
+              argv = _argv
+            })
+            .parse('batman robin --what', function (_err, argv, _output) {
+              output = _output
+            })
+        })
+        r.logs.length.should.equal(0)
+        r.errors.length.should.equal(0)
+        output.should.equal('')
+        argv['api-token'].should.equal('robin')
+        argv.what.should.equal(true)
+      })
+
+      it('allows context object to be passed to parse', function () {
+        var argv = null
+        yargs()
+          .command('batman <api-token>', 'batman command', function () {}, function (_argv) {
+            argv = _argv
+          })
+          .parse('batman robin --what', {
+            state: 'grumpy but rich'
+          }, function (_err, argv, _output) {})
+
+        argv.state.should.equal('grumpy but rich')
+        argv['api-token'].should.equal('robin')
+        argv.what.should.equal(true)
+      })
+
+      // see: https://github.com/yargs/yargs/issues/671
+      it('does not fail if context object has cyclical reference', function () {
+        var argv = null
+        var context = {state: 'grumpy but rich'}
+        context.res = context
+        yargs()
+          .command('batman <api-token>', 'batman command', function () {}, function (_argv) {
+            argv = _argv
+          })
+          .parse('batman robin --what', context, function (_err, argv, _output) {})
+
+        argv.state.should.equal('grumpy but rich')
+        argv['api-token'].should.equal('robin')
+        argv.what.should.equal(true)
+      })
+
+      it('allows nested sub-commands to be invoked multiple times', function () {
+        var context = {counter: 0}
+
+        checkOutput(function () {
+          var parser = yargs()
+            .commandDir('fixtures/cmddir')
+
+          parser.parse('dream within-a-dream --what', {context: context}, function (_err, argv, _output) {})
+          parser.parse('dream within-a-dream --what', {context: context}, function (_err, argv, _output) {})
+          parser.parse('dream within-a-dream --what', {context: context}, function (_err, argv, _output) {})
+        })
+
+        context.counter.should.equal(3)
+      })
+
+      it('overwrites the prior context object, when parse is called multiple times', function () {
+        var argv = null
+        var parser = yargs()
+          .command('batman <api-token>', 'batman command', function () {}, function (_argv) {})
+
+        parser.parse('batman robin --what', {
+          state: 'grumpy but rich'
+        }, function (_err, _argv, _output) {})
+
+        parser.parse('batman robin --what', {
+          state: 'the hero we need'
+        }, function (_err, _argv, _output) {
+          argv = _argv
+        })
+
+        argv.state.should.equal('the hero we need')
+      })
+
+      it('populates argv appropriately when parse is called multiple times', function () {
+        var parser = yargs()
+          .command('batman <api-token>', 'batman command', function () {}, function (_argv) {})
+          .command('robin <egg>', 'robin command', function () {}, function (_argv) {})
+
+        var argv1 = null
+        parser.parse('batman abc123', function (_err, argv, _output) {
+          argv1 = argv
+        })
+        var argv2 = null
+        parser.parse('robin blue', function (_err, argv, _output) {
+          argv2 = argv
+        })
+        expect(argv1.egg).to.equal(undefined)
+        argv1['api-token'].should.equal('abc123')
+
+        expect(argv2['api-token']).to.equal(undefined)
+        argv2.egg.should.equal('blue')
+      })
+
+      it('populates output appropriately when parse is called multiple times', function () {
+        var parser = yargs()
+          .command('batman <api-token>', 'batman command', function () {}, function (_argv) {})
+          .command('robin <egg>', 'robin command', function () {}, function (_argv) {})
+          .wrap(null)
+          .help()
+
+        var output1 = null
+        parser.parse('batman help', function (_err, _argv, output) {
+          output1 = output
+        })
+        var output2 = null
+        parser.parse('robin help', function (_err, _argv, output) {
+          output2 = output
+        })
+
+        output1.split('\n').should.deep.equal([
+          'ndm batman <api-token>',
+          '',
+          'Options:',
+          '  --help  Show help  [boolean]',
+          ''
+        ])
+
+        output2.split('\n').should.deep.equal([
+          'ndm robin <egg>',
+          '',
+          'Options:',
+          '  --help  Show help  [boolean]',
+          ''
+        ])
+      })
+
+      it('resets errors when parse is called multiple times', function () {
+        var parser = yargs()
+          .command('batman <api-token>', 'batman command', function () {}, function (_argv) {})
+          .command('robin <egg>', 'robin command', function () {}, function (_argv) {})
+          .wrap(null)
+          .help()
+
+        var error1 = null
+        var output1 = null
+        parser.parse('batman', function (err, _argv, output) {
+          error1 = err
+          output1 = output
+        })
+        var error2 = null
+        var output2 = null
+        parser.parse('robin help', function (err, _argv, output) {
+          error2 = err
+          output2 = output
+        })
+
+        error1.message.should.match(/Not enough non-option arguments/)
+        output1.split('\n').should.deep.equal([
+          'ndm batman <api-token>',
+          '',
+          'Options:',
+          '  --help  Show help  [boolean]',
+          '',
+          'Not enough non-option arguments: got 0, need at least 1'
+        ])
+
+        expect(error2).to.equal(undefined)
+        output2.split('\n').should.deep.equal([
+          'ndm robin <egg>',
+          '',
+          'Options:',
+          '  --help  Show help  [boolean]',
+          ''
+        ])
+      })
+
+      it('preserves top-level config when parse is called multiple times', function () {
+        var x = 'wrong'
+        var err
+        var output
+        // set some top-level, reset-able config
+        var parser = yargs()
+          .demand(1, 'Must call a command')
+          .strict()
+          .command('one <x>', 'The one and only command')
+        // first call parse with command, which calls reset
+        parser.parse('one two', function (_, argv) {
+          x = argv.x
+        })
+        // then call parse without command, which should enforce top-level config
+        parser.parse('', function (_err, argv, _output) {
+          err = _err || {}
+          output = _output || ''
+        })
+        x.should.equal('two')
+        err.should.have.property('message').and.equal('Must call a command')
+        output.split('\n').should.deep.equal([
+          'Commands:',
+          '  one <x>  The one and only command',
+          '',
+          'Must call a command'
+        ])
+      })
+    })
   })
 
   describe('config', function () {
@@ -1435,6 +1813,34 @@ describe('yargs dsl tests', function () {
       dates[1].toString().should.equal(new Date('2016-07-18').toString())
     })
 
+    it('returns camelcase args for a command', function () {
+      var age1
+      var age2
+      var dates
+      yargs('add 30days 2016-06-13 2016-07-18')
+        .command('add <age-in-days> [dates..]', 'Testing', function (yargs) {
+          return yargs
+            .coerce('age-in-days', function (arg) {
+              return parseInt(arg, 10) * 86400000
+            })
+            .coerce('dates', function (arg) {
+              return arg.map(function (str) {
+                return new Date(str)
+              })
+            })
+        }, function (argv) {
+          age1 = argv.ageInDays
+          age2 = argv['age-in-days']
+          dates = argv.dates
+        })
+        .argv
+      expect(age1).to.equal(2592000000)
+      expect(age2).to.equal(2592000000)
+      expect(dates).to.have.lengthOf(2)
+      dates[0].toString().should.equal(new Date('2016-06-13').toString())
+      dates[1].toString().should.equal(new Date('2016-07-18').toString())
+    })
+
     it('allows an error from positional arg to be handled by fail() handler', function () {
       var msg
       var err