Repository: yargs/yargs (js)

Issue:
separate demand into demandOption and demandCommand
As @ljharb was fixing #431 with #438 , we've discovered a bit of a black hole that is `demand`. 

The suggested solution at this point is to work off `demand` being separated in to two different functions: `demandOption` and `demandCommand`, clear detailed in [this comment](https://github.com/yargs/yargs/pull/438#issuecomment-200545670). 

I am currently working on separating the two. To avoid breaking too many things, I am hoping to keep both `require` and `demand` as a alias for `demandOption` until we decide to completely get rid of `demand`. 

Let's use this thread to track any discussion we might have about `demand` and the proposed separation. 


Pull request:
feat: split demand() into demandCommand()/demandOption()
Getting @lrlna's wonderful work on splitting `demand()` into `demandCommand()`/`demandOption()` over the finish line.

Everything is %99 of the way there, just addressing some final nits from @maxrimue and myself -- excited to try to get yargs down to a few less open issue.

TODO:

- [x] add a few more tests, specifically one for #438.

fixes #504, #431 follow up with #618

Patch:
diff --git a/README.md b/README.md
index ea6aea9..154ed80 100644
--- a/README.md
+++ b/README.md
@@ -160,7 +160,7 @@ area.js:
 #!/usr/bin/env node
 var argv = require('yargs')
     .usage('Usage: $0 -w [num] -h [num]')
-    .demand(['w','h'])
+    .demandOption(['w','h'])
     .argv;
 
 console.log("The area is:", argv.w * argv.h);
@@ -188,7 +188,7 @@ demand_count.js:
 ````javascript
 #!/usr/bin/env node
 var argv = require('yargs')
-    .demand(2)
+    .demandCommand(2)
     .argv;
 console.dir(argv);
 ````
@@ -298,7 +298,7 @@ var argv = require('yargs')
     .alias('f', 'file')
     .nargs('f', 1)
     .describe('f', 'Load a file')
-    .demand(1, ['f'])
+    .demandOption(['f'])
     .help('h')
     .alias('h', 'help')
     .epilog('copyright 2015')
@@ -678,7 +678,7 @@ require('yargs')
       console.log(`setting ${argv.key} to ${argv.value}`)
     }
   })
-  .demand(1)
+  .demandCommand(1)
   .help()
   .wrap(72)
   .argv
@@ -824,7 +824,7 @@ cli.js:
 #!/usr/bin/env node
 require('yargs')
   .commandDir('cmds')
-  .demand(1)
+  .demandCommand(1)
   .help()
   .argv
 ```
@@ -985,6 +985,13 @@ $ node test.js
   '$0': 'test.js' }
 ```
 
+<a name="conflicts"></a>.conflicts(x, y)
+----------------------------------------------
+
+Given the key `x` is set, the key `y` must not be set.
+
+Optionally `.conflicts()` can accept an object specifying multiple conflicting keys.
+
 <a name="count"></a>.count(key)
 ------------
 
@@ -1020,45 +1027,127 @@ displaying the value in the usage instructions:
 .default('timeout', 60000, '(one-minute)')
 ```
 
-<a name="demand"></a>.demand(key, [msg | boolean])
+<a name="demand"></a>.demand(count, [max], [msg]) [DEPRECATED]
+--------------------
+
+`demand()` has been deprecated, please instead see [`demandOption()`](#demandOption) and
+[`demandCommand()`](#demandCommand).
+
+<a name="demandOption"></a>.demandOption(key, [msg | boolean])
 ------------------------------
-.demand(count, [max], [msg])
+.demandOption(key, msg)
 ------------------------------
 
 If `key` is a string, show the usage information and exit if `key` wasn't
 specified in `process.argv`.
 
-If `key` is a number, demand at least as many non-option arguments, which show
-up in `argv._`. A second number can also optionally be provided, which indicates
-the maximum number of non-option arguments.
-
 If `key` is an array, demand each element.
 
-If a `msg` string is given, it will be printed when the argument is missing,
-instead of the standard error message. This is especially helpful for the non-option arguments in `argv._`.
+If a `msg` string is given, it will be printed when the argument is missing, instead of the standard error message.
+
+```javascript
+// demand an array of keys to be provided
+require('yargs')
+  .option('run', {
+    alias: 'r',
+    describe: 'run your program'
+  })
+  .option('path', {
+    alias: 'p',
+    describe: 'provide a path to file'
+  })
+  .option('spec', {
+    alias: 's',
+    describe: 'program specifications'
+  })
+  .demandOption(['run', 'path'], 'Please provide both run and path arguments to work with this tool')
+  .help()
+  .argv
+```
+which will provide the following output:
+```bash
+Options:
+  --run, -r   run your program                [required]
+  --path, -p  provide a path to file          [required]
+  --spec, -s  program specifications
+  --help      Show help                        [boolean]
+
+  Missing required arguments: run, path
+  Please provide both run and path arguments to work with this tool
+```
 
 If a `boolean` value is given, it controls whether the option is demanded;
 this is useful when using `.options()` to specify command line parameters.
 
-A combination of `.demand(1)` and `.strict()` will allow you to require a user to pass at least one command:
-
-```js
-var argv = require('yargs')
-  .command('install', 'tis a mighty fine package to install')
-  .demand(1)
-  .strict()
+```javascript
+// demand individual options within the option constructor
+require('yargs')
+  .options({
+    'run': {
+      alias: 'r',
+      describe: 'run your program',
+      demand: true
+    },
+    'path': {
+      alias: 'p',
+      describe: 'provide a path to file',
+      demand: true
+    },
+    'spec': {
+      alias: 's',
+      describe: 'program specifications'
+    }
+  })
+  .help()
   .argv
 ```
+which will provide the following output:
+```bash
+Options:
+  --run, -r   run your program                                       [required]
+  --path, -p  provide a path to file                                 [required]
+  --spec, -s  program specifications
+  --help      Show help                                               [boolean]
 
-Similarly, you can require a command and arguments at the same time:
+Missing required arguments: run, path
+```
 
-```js
-var argv = require('yargs')
-  .command('install', 'tis a mighty fine package to install')
-  .demand(1, ['w', 'm'])
-  .strict()
+<a name="demandCommand"></a>.demandCommand(min, [minMsg])
+------------------------------
+.demandCommand(min, [max], [minMsg], [maxMsg])
+------------------------------
+
+Demand in context of commands. You can demand a minimum and a maximum number a user can have within your program, as well as provide corresponding error messages if either of the demands is not met.
+```javascript
+require('yargs')
+  .command({
+    command: 'configure <key> [value]',
+    aliases: ['config', 'cfg'],
+    desc: 'Set a config variable',
+    builder: (yargs) => yargs.default('value', 'true'),
+    handler: (argv) => {
+      console.log(`setting ${argv.key} to ${argv.value}`)
+    }
+  })
+  // provide a minimum demand and a minimum demand message
+  .demandCommand(1, 'You need at least one command before moving on')
+  .help()
   .argv
 ```
+which will provide the following output:
+```bash
+Commands:
+  configure <key> [value]  Set a config variable         [aliases: config, cfg]
+
+Options:
+  --help  Show help                                                   [boolean]
+
+You need at least one command before moving on
+```
+
+_Note: in `minMsg` and `maxMsg`, every occurrence of `$0` will be replaced
+with the observed value, and every instance of `$1` will be replaced with the
+expected value._
 
 <a name="describe"></a>.describe(key, desc)
 --------------------
@@ -1433,7 +1522,7 @@ var argv = require('yargs')
 
 This method can be used to make yargs aware of options that _could_
 exist. You can also pass an `opt` object which can hold further
-customization, like `.alias()`, `.demand()` etc. for that option.
+customization, like `.alias()`, `.demandOption()` etc. for that option.
 
 For example:
 
@@ -1455,7 +1544,7 @@ is the same as
 ````javascript
 var argv = require('yargs')
     .alias('f', 'file')
-    .demand('f')
+    .demandOption('f')
     .default('f', '/etc/passwd')
     .describe('f', 'x marks the spot')
     .string('f')
@@ -1492,7 +1581,7 @@ Valid `opt` keys include:
 - `count`: boolean, interpret option as a count of boolean flags, see [`count()`](#count)
 - `default`: value, set a default value for the option, see [`default()`](#default)
 - `defaultDescription`: string, use this description for the default value in help content, see [`default()`](#default)
-- `demand`/`require`/`required`: boolean or string, demand the option be given, with optional error message, see [`demand()`](#demand)
+- `demandOption`: boolean or string, demand the option be given, with optional error message, see [`demandOption()`](#demandOption)
 - `desc`/`describe`/`description`: string, the option description for help content, see [`describe()`](#describe)
 - `global`: boolean, indicate that this key should not be [reset](#reset) when a command is invoked, see [`global()`](#global)
 - `group`: string, when displaying usage instructions place the option under an alternative group heading, see [`group()`](#group)
@@ -1594,7 +1683,7 @@ var yargs = require('yargs')
   .usage('$0 command')
   .command('hello', 'hello command')
   .command('world', 'world command')
-  .demand(1, 'must provide a valid command'),
+  .demandCommand(1, 'must provide a valid command'),
   argv = yargs.argv,
   command = argv._[0];
 
@@ -1661,7 +1750,7 @@ line_count.js:
 #!/usr/bin/env node
 var argv = require('yargs')
     .usage('Count the lines in a file.\nUsage: $0 -f <file>')
-    .demand('f')
+    .demandOption('f')
     .alias('f', 'file')
     .describe('f', 'Load a file')
     .string('f')
diff --git a/lib/usage.js b/lib/usage.js
index 7f62275..ad6ce00 100644
--- a/lib/usage.js
+++ b/lib/usage.js
@@ -128,12 +128,15 @@ module.exports = function (yargs, y18n) {
   self.help = function () {
     normalizeAliases()
 
-    var demanded = yargs.getDemanded()
+    // handle old demanded API
+    var demandedOptions = yargs.getDemandedOptions()
+    var demandedCommands = yargs.getDemandedCommands()
     var groups = yargs.getGroups()
     var options = yargs.getOptions()
     var keys = Object.keys(
       Object.keys(descriptions)
-      .concat(Object.keys(demanded))
+      .concat(Object.keys(demandedOptions))
+      .concat(Object.keys(demandedCommands))
       .concat(Object.keys(options.default))
       .reduce(function (acc, key) {
         if (key !== '_') acc[key] = true
@@ -231,7 +234,7 @@ module.exports = function (yargs, y18n) {
 
         var extra = [
           type,
-          demanded[key] ? '[' + __('required') + ']' : null,
+          demandedOptions[key] ? '[' + __('required') + ']' : null,
           options.choices && options.choices[key] ? '[' + __('choices:') + ' ' +
             self.stringifiedValues(options.choices[key]) + ']' : null,
           defaultString(options.default[key], options.defaultDescription[key])
@@ -303,7 +306,8 @@ module.exports = function (yargs, y18n) {
   // make sure any options set for aliases,
   // are copied to the keys being aliased.
   function normalizeAliases () {
-    var demanded = yargs.getDemanded()
+    // handle old demanded API
+    var demandedOptions = yargs.getDemandedOptions()
     var options = yargs.getOptions()
 
     ;(Object.keys(options.alias) || []).forEach(function (key) {
@@ -311,7 +315,7 @@ module.exports = function (yargs, y18n) {
         // copy descriptions.
         if (descriptions[alias]) self.describe(key, descriptions[alias])
         // copy demanded.
-        if (demanded[alias]) yargs.demand(key, demanded[alias].msg)
+        if (demandedOptions[alias]) yargs.demandOption(key, demandedOptions[alias].msg)
         // type messages.
         if (~options.boolean.indexOf(alias)) yargs.boolean(key)
         if (~options.count.indexOf(alias)) yargs.count(key)
diff --git a/lib/validation.js b/lib/validation.js
index 9537575..328d83a 100644
--- a/lib/validation.js
+++ b/lib/validation.js
@@ -10,21 +10,33 @@ module.exports = function (yargs, usage, y18n) {
   // validate appropriate # of non-option
   // arguments were provided, i.e., '_'.
   self.nonOptionCount = function (argv) {
-    const demanded = yargs.getDemanded()
+    const demandedCommands = yargs.getDemandedCommands()
     // don't count currently executing commands
     const _s = argv._.length - yargs.getContext().commands.length
 
-    if (demanded._ && (_s < demanded._.count || _s > demanded._.max)) {
-      if (demanded._.msg !== undefined) {
-        usage.fail(demanded._.msg)
-      } else if (_s < demanded._.count) {
-        usage.fail(
-          __('Not enough non-option arguments: got %s, need at least %s', _s, demanded._.count)
-        )
-      } else {
-        usage.fail(
-          __('Too many non-option arguments: got %s, maximum of %s', _s, demanded._.max)
-        )
+    if (demandedCommands._ && (_s < demandedCommands._.min || _s > demandedCommands._.max)) {
+      if (_s < demandedCommands._.min) {
+        if (demandedCommands._.minMsg !== undefined) {
+          usage.fail(
+            // replace $0 with observed, $1 with expected.
+            demandedCommands._.minMsg ? demandedCommands._.minMsg.replace(/\$0/g, _s).replace(/\$1/, demandedCommands._.min) : null
+          )
+        } else {
+          usage.fail(
+            __('Not enough non-option arguments: got %s, need at least %s', _s, demandedCommands._.min)
+          )
+        }
+      } else if (_s > demandedCommands._.max) {
+        if (demandedCommands._.maxMsg !== undefined) {
+          usage.fail(
+            // replace $0 with observed, $1 with expected.
+            demandedCommands._.maxMsg ? demandedCommands._.maxMsg.replace(/\$0/g, _s).replace(/\$1/, demandedCommands._.max) : null
+          )
+        } else {
+          usage.fail(
+          __('Too many non-option arguments: got %s, maximum of %s', _s, demandedCommands._.max)
+          )
+        }
       }
     }
   }
@@ -73,13 +85,13 @@ module.exports = function (yargs, usage, y18n) {
 
   // make sure all the required arguments are present.
   self.requiredArguments = function (argv) {
-    const demanded = yargs.getDemanded()
+    const demandedOptions = yargs.getDemandedOptions()
     var missing = null
 
-    Object.keys(demanded).forEach(function (key) {
+    Object.keys(demandedOptions).forEach(function (key) {
       if (!argv.hasOwnProperty(key)) {
         missing = missing || {}
-        missing[key] = demanded[key]
+        missing[key] = demandedOptions[key]
       }
     })
 
@@ -107,7 +119,7 @@ module.exports = function (yargs, usage, y18n) {
   self.unknownArguments = function (argv, aliases) {
     const aliasLookup = {}
     const descriptions = usage.getDescriptions()
-    const demanded = yargs.getDemanded()
+    const demandedOptions = yargs.getDemandedOptions()
     const commandKeys = yargs.getCommandInstance().getCommands()
     const unknown = []
     const currentContext = yargs.getContext()
@@ -121,7 +133,7 @@ module.exports = function (yargs, usage, y18n) {
     Object.keys(argv).forEach(function (key) {
       if (key !== '$0' && key !== '_' &&
         !descriptions.hasOwnProperty(key) &&
-        !demanded.hasOwnProperty(key) &&
+        !demandedOptions.hasOwnProperty(key) &&
         !aliasLookup.hasOwnProperty(key)) {
         unknown.push(key)
       }
@@ -271,6 +283,30 @@ module.exports = function (yargs, usage, y18n) {
     }
   }
 
+  var conflicting = {}
+  self.conflicts = function (key, value) {
+    if (typeof key === 'object') {
+      Object.keys(key).forEach(function (k) {
+        self.conflicts(k, key[k])
+      })
+    } else {
+      conflicting[key] = value
+    }
+  }
+  self.getConflicting = function () {
+    return conflicting
+  }
+
+  self.conflicting = function (argv) {
+    var args = Object.getOwnPropertyNames(argv)
+
+    args.forEach(function (arg) {
+      if (conflicting[arg] && args.indexOf(conflicting[arg]) !== -1) {
+        usage.fail(__('Arguments %s and %s are mutually exclusive', arg, conflicting[arg]))
+      }
+    })
+  }
+
   self.recommendCommands = function (cmd, potentialCommands) {
     const distance = require('./levenshtein')
     const threshold = 3 // if it takes more than three edits, let's move on.
@@ -293,6 +329,7 @@ module.exports = function (yargs, usage, y18n) {
       return globalLookup[k]
     })
     checks = []
+    conflicting = {}
     return self
   }
 
@@ -301,10 +338,12 @@ module.exports = function (yargs, usage, y18n) {
     frozen = {}
     frozen.implied = implied
     frozen.checks = checks
+    frozen.conflicting = conflicting
   }
   self.unfreeze = function () {
     implied = frozen.implied
     checks = frozen.checks
+    conflicting = frozen.conflicting
     frozen = undefined
   }
 
diff --git a/locales/en.json b/locales/en.json
index 4917664..4a618e7 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -35,5 +35,6 @@
   "Path to JSON config file": "Path to JSON config file",
   "Show help": "Show help",
   "Show version number": "Show version number",
-  "Did you mean %s?": "Did you mean %s?"
+  "Did you mean %s?": "Did you mean %s?",
+  "Arguments %s and %s are mutually exclusive" : "Arguments %s and %s are mutually exclusive"
 }
diff --git a/locales/pt.json b/locales/pt.json
index 0b9ea25..75c3921 100644
--- a/locales/pt.json
+++ b/locales/pt.json
@@ -33,5 +33,6 @@
   "Invalid JSON config file: %s": "Arquivo de configuração em JSON esta inválido: %s",
   "Path to JSON config file": "Caminho para o arquivo de configuração em JSON",
   "Show help": "Mostra ajuda",
-  "Show version number": "Mostra número de versão"
+  "Show version number": "Mostra número de versão",
+  "Arguments %s and %s are mutually exclusive" : "Argumentos %s e %s são mutualmente exclusivos"
 }
diff --git a/locales/pt_BR.json b/locales/pt_BR.json
index 96d31e7..60e5ffa 100644
--- a/locales/pt_BR.json
+++ b/locales/pt_BR.json
@@ -35,5 +35,6 @@
   "Path to JSON config file": "Caminho para o arquivo JSON de configuração",
   "Show help": "Exibe ajuda",
   "Show version number": "Exibe a versão",
-  "Did you mean %s?": "Você quis dizer %s?"
+  "Did you mean %s?": "Você quis dizer %s?",
+  "Arguments %s and %s are mutually exclusive" : "Argumentos %s e %s são mutualmente exclusivos"
 }
diff --git a/yargs.js b/yargs.js
index 95cce8f..b465692 100644
--- a/yargs.js
+++ b/yargs.js
@@ -98,7 +98,7 @@ function Yargs (processArgs, cwd, parentRequire) {
 
     var objectOptions = [
       'narg', 'key', 'alias', 'default', 'defaultDescription',
-      'config', 'choices', 'demanded', 'coerce'
+      'config', 'choices', 'demandedOptions', 'demandedCommands', 'coerce'
     ]
 
     arrayOptions.forEach(function (k) {
@@ -301,14 +301,15 @@ function Yargs (processArgs, cwd, parentRequire) {
     return self
   }
 
+  // deprecated: the demand API is too overloaded, and is being
+  // deprecated in favor of .demandCommand() .demandOption().
   self.demand = self.required = self.require = function (keys, max, msg) {
     // you can optionally provide a 'max' key,
     // which will raise an exception if too many '_'
     // options are provided.
-
     if (Array.isArray(max)) {
       max.forEach(function (key) {
-        self.demand(key, msg)
+        self.demandOption(key, msg)
       })
       max = Infinity
     } else if (typeof max !== 'number') {
@@ -317,26 +318,61 @@ function Yargs (processArgs, cwd, parentRequire) {
     }
 
     if (typeof keys === 'number') {
-      if (!options.demanded._) options.demanded._ = { count: 0, msg: null, max: max }
-      options.demanded._.count = keys
-      options.demanded._.msg = msg
+      self.demandCommand(keys, max, msg)
     } else if (Array.isArray(keys)) {
       keys.forEach(function (key) {
-        self.demand(key, msg)
+        self.demandOption(key, msg)
       })
     } else {
       if (typeof msg === 'string') {
-        options.demanded[keys] = { msg: msg }
+        self.demandOption(keys, msg)
       } else if (msg === true || typeof msg === 'undefined') {
-        options.demanded[keys] = { msg: undefined }
+        self.demandOption(keys)
       }
     }
 
     return self
   }
 
-  self.getDemanded = function () {
-    return options.demanded
+  self.demandOption = function (key, msg) {
+    if (Array.isArray(key)) {
+      key.forEach(function (key) {
+        self.demandOption(key, msg)
+      })
+    } else {
+      if (typeof msg === 'string') {
+        options.demandedOptions[key] = { msg: msg }
+      // allow edge-case of options: {a: {demand: true}, b: {demand: false}}
+      } else if (msg === true || typeof msg === 'undefined') {
+        options.demandedOptions[key] = { msg: undefined }
+      }
+    }
+
+    return self
+  }
+
+  self.demandCommand = function (min, max, minMsg, maxMsg) {
+    if (typeof max !== 'number') {
+      minMsg = max
+      max = Infinity
+    }
+
+    options.demandedCommands._ = {
+      min: min,
+      max: max,
+      minMsg: minMsg,
+      maxMsg: maxMsg
+    }
+
+    return self
+  }
+
+  self.getDemandedOptions = function () {
+    return options.demandedOptions
+  }
+
+  self.getDemandedCommands = function () {
+    return options.demandedCommands
   }
 
   self.requiresArg = function (requiresArgs) {
@@ -354,6 +390,11 @@ function Yargs (processArgs, cwd, parentRequire) {
     return self
   }
 
+  self.conflicts = function (key1, key2) {
+    validation.conflicts(key1, key2)
+    return self
+  }
+
   self.usage = function (msg, opts) {
     if (!opts && typeof msg === 'object') {
       opts = msg
@@ -486,6 +527,10 @@ function Yargs (processArgs, cwd, parentRequire) {
         self.demand(key, demand)
       }
 
+      if ('demandOption' in opt) {
+        self.demandOption(key, opt.demandOption)
+      }
+
       if ('config' in opt) {
         self.config(key, opt.configParser)
       }
@@ -952,6 +997,7 @@ function Yargs (processArgs, cwd, parentRequire) {
         validation.customChecks(argv, aliases)
         validation.limitedChoices(argv)
         validation.implications(argv)
+        validation.conflicting(argv)
       }
     }
 


Test patch:
diff --git a/test/validation.js b/test/validation.js
index e7b3bc0..14d864b 100644
--- a/test/validation.js
+++ b/test/validation.js
@@ -1,8 +1,11 @@
 /* global describe, it, beforeEach */
 
+var checkUsage = require('./helpers/utils').checkOutput
 var expect = require('chai').expect
 var yargs = require('../')
 
+require('chai').should()
+
 describe('validation tests', function () {
   beforeEach(function () {
     yargs.reset()
@@ -123,6 +126,54 @@ describe('validation tests', function () {
     })
   })
 
+  describe('conflicts', function () {
+    it('fails if both arguments are supplied', function (done) {
+      yargs(['-f', '-b'])
+          .conflicts('f', 'b')
+          .fail(function (msg) {
+            msg.should.equal('Arguments f and b are mutually exclusive')
+            return done()
+          })
+          .argv
+    })
+
+    it('should not fail if no conflicting arguments are provided', function () {
+      yargs(['-b', '-c'])
+        .conflicts('f', 'b')
+        .fail(function (msg) {
+          expect.fail()
+        })
+        .argv
+    })
+
+    it('allows an object to be provided defining conflicting option pairs', function (done) {
+      yargs(['-t', '-s'])
+        .conflicts({
+          'c': 'a',
+          's': 't'
+        })
+        .fail(function (msg) {
+          msg.should.equal('Arguments s and t are mutually exclusive')
+          return done()
+        })
+        .argv
+    })
+
+    it('takes into account aliases when applying conflicts logic', function (done) {
+      yargs(['-t', '-c'])
+        .conflicts({
+          'c': 'a',
+          's': 't'
+        })
+        .alias('c', 's')
+        .fail(function (msg) {
+          msg.should.equal('Arguments s and t are mutually exclusive')
+          return done()
+        })
+        .argv
+    })
+  })
+
   describe('demand', function () {
     it('fails with standard error message if msg is not defined', function (done) {
       yargs([])
@@ -311,7 +362,6 @@ describe('validation tests', function () {
     })
 
     it('should be displayed in the help message', function () {
-      var checkUsage = require('./helpers/utils').checkOutput
       var r = checkUsage(function () {
         return yargs(['--help'])
           .config('settings')
@@ -439,4 +489,70 @@ describe('validation tests', function () {
         .argv
     })
   })
+
+  describe('demandOption', function () {
+    it('allows an array of options to be demanded', function (done) {
+      yargs('-a 10 marsupial')
+        .demandOption(['a', 'b'])
+        .fail(function (msg) {
+          msg.should.equal('Missing required argument: b')
+          return done()
+        })
+        .argv
+    })
+
+    it('allows demandOption in option shorthand', function (done) {
+      yargs('-a 10 marsupial')
+        .option('c', {
+          demandOption: true
+        })
+        .fail(function (msg) {
+          msg.should.equal('Missing required argument: c')
+          return done()
+        })
+        .argv
+    })
+  })
+
+  describe('demandCommand', function () {
+    it('should return a custom failure message when too many non-hyphenated arguments are found after a demand count', function () {
+      var r = checkUsage(function () {
+        return yargs(['src', 'dest'])
+          .usage('Usage: $0 [x] [y] [z] {OPTIONS} <src> <dest> [extra_files...]')
+          .demandCommand(0, 1, 'src and dest files are both required', 'too many arguments are provided')
+          .wrap(null)
+          .argv
+      })
+      r.should.have.property('result')
+      r.should.have.property('logs').with.length(0)
+      r.should.have.property('exit').and.be.ok
+      r.result.should.have.property('_').and.deep.equal(['src', 'dest'])
+      r.errors.join('\n').split(/\n+/).should.deep.equal([
+        'Usage: ./usage [x] [y] [z] {OPTIONS} <src> <dest> [extra_files...]',
+        'too many arguments are provided'
+      ])
+    })
+
+    // see: https://github.com/yargs/yargs/pull/438
+    it('allows a custom min message to be provided', function (done) {
+      yargs('-a 10 marsupial')
+        .demandCommand(2, 'totes got $0 totes expected $1')
+        .fail(function (msg) {
+          msg.should.equal('totes got 1 totes expected 2')
+          return done()
+        })
+        .argv
+    })
+
+    // see: https://github.com/yargs/yargs/pull/438
+    it('allows a custom min and max message to be provided', function (done) {
+      yargs('-a 10 marsupial mammal bro')
+        .demandCommand(1, 2, 'totes too few, got $0 totes expected $1', 'totes too many, got $0 totes expected $1')
+        .fail(function (msg) {
+          msg.should.equal('totes too many, got 3 totes expected 2')
+          return done()
+        })
+        .argv
+    })
+  })
 })
diff --git a/test/yargs.js b/test/yargs.js
index 68d6679..b8777e7 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -210,13 +210,15 @@ describe('yargs dsl tests', function () {
         .command('foo', 'bar', function () {})
         .default('foo', 'bar')
         .describe('foo', 'foo variable')
-        .demand('foo')
+        .demandCommand(1)
+        .demandOption('foo')
         .string('foo')
         .alias('foo', 'bar')
         .string('foo')
         .choices('foo', ['bar', 'baz'])
         .coerce('foo', function (foo) { return foo + 'bar' })
         .implies('foo', 'snuh')
+        .conflicts('qux', 'xyzzy')
         .group('foo', 'Group:')
         .strict()
         .exitProcess(false)  // defaults to true.
@@ -243,16 +245,19 @@ describe('yargs dsl tests', function () {
         configObjects: [],
         envPrefix: 'YARGS', // preserved as global
         global: ['help'],
-        demanded: {}
+        demandedCommands: {},
+        demandedOptions: {}
       }
 
       expect(y.getOptions()).to.deep.equal(emptyOptions)
       expect(y.getUsageInstance().getDescriptions()).to.deep.equal({help: '__yargsString__:Show help'})
       expect(y.getValidationInstance().getImplied()).to.deep.equal({})
+      expect(y.getValidationInstance().getConflicting()).to.deep.equal({})
       expect(y.getCommandInstance().getCommandHandlers()).to.deep.equal({})
       expect(y.getExitProcess()).to.equal(false)
       expect(y.getStrict()).to.equal(false)
-      expect(y.getDemanded()).to.deep.equal({})
+      expect(y.getDemandedOptions()).to.deep.equal({})
+      expect(y.getDemandedCommands()).to.deep.equal({})
       expect(y.getGroups()).to.deep.equal({})
     })
 
@@ -261,7 +266,7 @@ describe('yargs dsl tests', function () {
         .demand('cake')
 
       y.parse('hello', function (err) {
-        err.message.should.match(/Missing required argumen/)
+        err.message.should.match(/Missing required argument/)
       })
       y.reset()
       y.parse('cake', function (err) {
@@ -1243,11 +1248,11 @@ describe('yargs dsl tests', function () {
 
       options.key.foo.should.equal(true)
       options.string.should.include('awesome-sauce')
-      Object.keys(options.demanded).should.include('awesomeSauce')
+      Object.keys(options.demandedOptions).should.include('awesomeSauce')
 
       expect(options.key.bar).to.equal(undefined)
       options.string.should.not.include('bar')
-      Object.keys(options.demanded).should.not.include('bar')
+      Object.keys(options.demandedOptions).should.not.include('bar')
     })
 
     it('should set help to global option by default', function () {