Repository: yargs/yargs
Pull request #646 (2016-10-02 20:57:50)

feat: reworking yargs API to make it easier to run in headless environments, e.g., Slack
after discussion today, @nexdrew and I settled on what I think is a pretty clever extension to the `parse()` function, as a simple API extension that supports running yargs in headless mode (yargs with no terminal output).

----

A callback `fn` can optionally be provided as the second argument 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.
yargs()
  .command('lunch-train <restaurant> <time>', function () {}, function (argv) {
    api.scheduleLunch(argv.restaurant, moment(argv.time))
  })
  .parse(bot.userText, function (err, argv, output) {
    if (output) bot.respond(output)
  })
```

----

Long story short, `parse()` now accepts an optional `fn`, which if provided will:

1. prevent text from being output to the terminal.
2. prevent commands from being invoked if an error is thrown, or text has been output to the terminal.
3. get invoked with `err`, `argv`, `output` (allowing output to be routed somewhere elsewhere, .e.g., a chat-room).

reviewers: @nexdrew, @maxrimue, @lrlna

nexdrew: I think an API that's more "transactional", i.e. exchange one input for one output, would be more ideal than registering a custom logger and exit handler. My idea is that the input is a string or array representing a message or args to parse/execute, and the output is an object containing the following (somewhat similar to the way the `checkOutput()` test helper function works):

1. an overall success/fail indicator, like an integer exit code
2. an `Error`, if one was thrown (could possibly be used instead of an exit code)
3. a parsed `argv` object
4. buffered text (that would typically be output to the console/terminal) as a string or array of strings

@bcoe What do you think? How would you like to proceed?
lrlna: @bcoe this is so ace 💯 think of all the things people can build!! 

Patch:
diff --git a/README.md b/README.md
index 660661c..6451af3 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
@@ -1488,13 +1488,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..128b750 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
@@ -161,10 +162,9 @@ 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()
@@ -185,6 +185,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 +195,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 +213,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..9b3e3e1 100644
--- a/lib/usage.js
+++ b/lib/usage.js
@@ -30,6 +30,8 @@ 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)
@@ -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/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/ru.json b/locales/ru.json
new file mode 100644
index 0000000..304e648
--- /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": "Not enough arguments following: %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..66f2c72 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
     "LICENSE"
   ],
   "dependencies": {
+    "camelcase": "^3.0.0",
     "cliui": "^3.2.0",
     "decamelize": "^1.1.1",
     "get-caller-file": "^1.0.1",
diff --git a/yargs.js b/yargs.js
index abe2ec8..373bfbe 100644
--- a/yargs.js
+++ b/yargs.js
@@ -16,6 +16,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 +65,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 +122,42 @@ 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.exitProcess = exitProcess
+    frozen.groups = groups
+    usage.freeze()
+    validation.freeze()
+    command.freeze()
+    frozen.strict = strict
+    frozen.completionCommand = completionCommand
+  }
+  function unfreeze () {
+    options = frozen.options
+    exitProcess = frozen.exitProcess
+    groups = frozen.groups
+    usage.unfreeze()
+    validation.unfreeze()
+    command.unfreeze()
+    strict = frozen.strict
+    completionCommand = frozen.completionCommand
+    frozen = undefined
+  }
+
   self.boolean = function (bools) {
     options.boolean.push.apply(options.boolean, [].concat(bools))
     return self
@@ -382,9 +411,43 @@ function Yargs (processArgs, cwd, parentRequire) {
     return pkgs[npath]
   }
 
-  self.parse = function (args, shortCircuit) {
+  var parseFn = 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') {
+      self.config(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)
+      self.reset()
+      unfreeze()
+      parseFn = null
+    }
+
+    return parsed
+  }
+
+  self._hasParseCallback = function () {
+    return !!parseFn
   }
 
   self.option = self.options = function (key, opt) {
@@ -636,7 +699,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 +732,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
@@ -772,9 +872,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 +886,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 +903,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 +938,7 @@ function Yargs (processArgs, cwd, parentRequire) {
       }
     }
 
-    setPlaceholderKeys(argv)
-
-    return argv
+    return setPlaceholderKeys(argv)
   }
 
   function guessLocale () {
@@ -870,6 +960,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/yargs.js b/test/yargs.js
index 75d129d..71f5ef0 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,286 @@ 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\]/)
+    })
+
+    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)
+      })
+
+      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 +1730,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