Repository: yargs/yargs (js)

Issue:
make command builder work without returning
I like the new commandDir feature but I have been having a bit of trouble getting a few of the other functions to work. The one I bumped into were `default` and `demand`. I am new to yargs, so I may just be 'holding it wrong'.

It starts with a cli.js
```js
#!/usr/bin/env node

const yargs = require( 'yargs' );

yargs // eslint-disable-line no-unused-expressions
  .commandDir( 'commands' )
  .help( 'h' )
  .alias( 'h', 'help' )
  .wrap( yargs.terminalWidth() )
  .version()
  .alias( 'version', 'v' )
  .argv
```

I have a folder called `commands` which holds two files: new.js and generate.js

```js
// new.js
exports.command = 'new <project>';
exports.description = 'description';
exports.builder = ( yargs ) => {
  yargs.example( 'example' )
    .option( 'B', {
      alias: 'b',
      description: 'description'
    } )
    .demand( 'B' ) //  or demand( 1, 'B' ) or demand( 1, [ 'B' ] )
    .strict();
};
exports.handler = ( argv ) => {
  // Handle
};
```
I expect it to skip the handle and present the user with the help menu when I type `cli new app`, but it goes right into the handler function (which breaks because I never expect argv.B to be empty)

In the second file, I have set the default values for two of the options but their respective keys on the argv object returned in the handler function contain no values if they aren't explicitly set. So: `cli generate file name` => `argv.p` and `argv.b` will be empty. 

```js
// generate.js
exports.command = 'generate <file> <name>';
exports.description = 'description';
exports.builder = ( yargs ) => {
  yargs.example( 'examplee' )
    .option( 'p', {
      alias: 'P',
      description: 'description',
      choices: [ 'firstChoice', 'secondChoice' ],
      default: 'firstChoice'
    } )
    .option( 'b', {
      alias: 'B',
      description: 'description',
      choices: [ 'firstChoice', 'secondChoice' ],
      default: 'firstChoice'
    } )
};

exports.handler = ( argv ) => {
  // Handle
};
```

Could I be doing something wrong here or should this work?

Thanks, Martien

Pull request description:
feat(command): builder function no longer needs to return the yargs instance
Fixes #522.

~~I'm not quite sure about the intentions of what should be allowed to be returned by the builder function. Returning the `yargs` instance or nothing should now work, but I wonder if we ever intended to allow it to return something like `yargs.argv`? If so, I think we will need a new issue for that.~~

Otherwise, this PR seems to work fine, but it could possibly result in unexpected behavior in user CLIs if they were previously not returning the `yargs` instance, either by mistake or not. For that reason, this change may be better suited for 5.x.

Note that I skipped the `populates argv with placeholder keys when passed into command handler` unit test because this change breaks it and it no longer seems to be valid (options must now be marked `global: true` in order to apply to the argv of a command handler, be it a placeholder or populated value). We should either remove it (if everyone agrees the test is no longer valid), or fix it by using `global: true` (which is tested elsewhere).

**UPDATE**: With commit 739a4f4, a builder function can also call `yargs.argv`, with or without returning. I think this is a good idea b/c I've seen a few folks [try to do this](https://github.com/yargs/yargs/issues/565) and it makes the API much less rigid. 😎 

Patch:
diff --git a/README.md b/README.md
index 8835fae..8dfdf5a 100644
--- a/README.md
+++ b/README.md
@@ -1099,15 +1099,31 @@ var yargs = require('yargs')(['--help'])
     Options:
       --help  Show help  [boolean]
 
-<a name="help"></a>.help([option, [description]])
-------------------------------
+<a name="help"></a>.help()
+-----------------------------------------
+.help([option | boolean])
+-----------------------------------------
+.help([option, [description | boolean]])
+-----------------------------------------
+.help([option, [description, [boolean]]])
+-----------------------------------------
+
+Add an option (e.g. `--help`) and implicit command that displays the usage
+string and exits the process.
 
-Add an option (e.g. `--help`) that displays the usage string and exits the
-process. If present, the `description` parameter customizes the description of
+If present, the `description` parameter customizes the description of
 the help option in the usage string.
 
-If invoked without parameters, `.help()` will make `--help` the option to trigger
-help output.
+If a boolean argument is provided, it will enable or disable the use of an
+implicit command. The implicit command is enabled by default, but it can be
+disabled by passing `false`.
+
+Note that any multi-char aliases (e.g. `help`) used for the help option will
+also be used for the implicit command. If there are no multi-char aliases (e.g.
+`h`), then all single-char aliases will be used for the command.
+
+If invoked without parameters, `.help()` will use `--help` as the option and
+`help` as the implicit command to trigger help output.
 
 Example:
 
diff --git a/lib/command.js b/lib/command.js
index da3dff6..804f385 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -130,17 +130,21 @@ module.exports = function (yargs, usage, validation) {
     var parentCommands = currentContext.commands.slice()
     currentContext.commands.push(command)
     if (commandHandler.builder && typeof commandHandler.builder === 'function') {
-      // a function can be provided, which interacts which builds
-      // up a yargs chain and returns it.
+      // a function can be provided, which builds
+      // up a yargs chain and possibly returns it.
       innerArgv = commandHandler.builder(yargs.reset(parsed.aliases))
       // if the builder function did not yet parse argv with reset yargs
       // and did not explicitly set a usage() string, then apply the
       // original command string as usage() for consistent behavior with
       // options object below
-      if (yargs.parsed === false && typeof yargs.getUsageInstance().getUsage() === 'undefined') {
-        yargs.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
+      if (yargs.parsed === false) {
+        if (typeof yargs.getUsageInstance().getUsage() === 'undefined') {
+          yargs.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
+        }
+        innerArgv = innerArgv ? innerArgv.argv : yargs.argv
+      } else {
+        innerArgv = yargs.parsed.argv
       }
-      innerArgv = innerArgv ? innerArgv.argv : argv
     } else if (commandHandler.builder && typeof commandHandler.builder === 'object') {
       // as a short hand, an object can instead be provided, specifying
       // the options that a command takes.
diff --git a/package.json b/package.json
index ab87282..a176645 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
     "cross-spawn": "^4.0.0",
     "es6-promise": "^3.0.2",
     "hashish": "0.0.4",
-    "mocha": "^3.0.0",
+    "mocha": "^3.0.1",
     "nyc": "^7.0.0",
     "rimraf": "^2.5.0",
     "standard": "^7.0.0",
diff --git a/yargs.js b/yargs.js
index ed6fd56..3377b1d 100644
--- a/yargs.js
+++ b/yargs.js
@@ -520,12 +520,33 @@ function Yargs (processArgs, cwd, parentRequire) {
   }
 
   var helpOpt = null
-  self.addHelpOpt = self.help = function (opt, msg) {
-    opt = opt || 'help'
-    helpOpt = opt
-    self.boolean(opt)
-    self.global(opt)
-    self.describe(opt, msg || usage.deferY18nLookup('Show help'))
+  var useHelpOptAsCommand = false // a call to .help() will enable this
+  self.addHelpOpt = self.help = function (opt, msg, addImplicitCmd) {
+    // argument shuffle
+    if (arguments.length === 0) {
+      useHelpOptAsCommand = true
+    } else if (arguments.length === 1) {
+      if (typeof opt === 'boolean') {
+        useHelpOptAsCommand = opt
+        opt = null
+      } else {
+        useHelpOptAsCommand = true
+      }
+    } else if (arguments.length === 2) {
+      if (typeof msg === 'boolean') {
+        useHelpOptAsCommand = msg
+        msg = null
+      } else {
+        useHelpOptAsCommand = true
+      }
+    } else {
+      useHelpOptAsCommand = Boolean(addImplicitCmd)
+    }
+    // use arguments, fallback to defaults for opt and msg
+    helpOpt = opt || 'help'
+    self.boolean(helpOpt)
+    self.global(helpOpt)
+    self.describe(helpOpt, msg || usage.deferY18nLookup('Show help'))
     return self
   }
 
@@ -654,22 +675,45 @@ function Yargs (processArgs, cwd, parentRequire) {
       return argv
     }
 
-    // if there's a handler associated with a
-    // command defer processing to it.
-    var handlerKeys = command.getCommands()
-    for (var i = 0, cmd; (cmd = argv._[i]) !== undefined; i++) {
-      if (~handlerKeys.indexOf(cmd) && cmd !== completionCommand) {
-        setPlaceholderKeys(argv)
-        return command.runCommand(cmd, self, parsed)
+    if (argv._.length) {
+      // check for helpOpt in argv._ before running commands
+      // assumes helpOpt must be valid if useHelpOptAsCommand is true
+      if (useHelpOptAsCommand) {
+        // consider any multi-char helpOpt alias as a valid help command
+        // unless all helpOpt aliases are single-char
+        // note that parsed.aliases is a normalized bidirectional map :)
+        var helpCmds = [helpOpt].concat(aliases[helpOpt])
+        var multiCharHelpCmds = helpCmds.filter(function (k) {
+          return k.length > 1
+        })
+        if (multiCharHelpCmds.length) helpCmds = multiCharHelpCmds
+        // look for and strip any helpCmds from argv._
+        argv._ = argv._.filter(function (cmd) {
+          if (~helpCmds.indexOf(cmd)) {
+            argv[helpOpt] = true
+            return false
+          }
+          return true
+        })
+      }
+
+      // if there's a handler associated with a
+      // command defer processing to it.
+      var handlerKeys = command.getCommands()
+      for (var i = 0, cmd; (cmd = argv._[i]) !== undefined; i++) {
+        if (~handlerKeys.indexOf(cmd) && cmd !== completionCommand) {
+          setPlaceholderKeys(argv)
+          return command.runCommand(cmd, self, parsed)
+        }
       }
-    }
 
-    // generate a completion script for adding to ~/.bashrc.
-    if (completionCommand && ~argv._.indexOf(completionCommand) && !argv[completion.completionKey]) {
-      if (exitProcess) setBlocking(true)
-      self.showCompletionScript()
-      if (exitProcess) {
-        process.exit(0)
+      // generate a completion script for adding to ~/.bashrc.
+      if (completionCommand && ~argv._.indexOf(completionCommand) && !argv[completion.completionKey]) {
+        if (exitProcess) setBlocking(true)
+        self.showCompletionScript()
+        if (exitProcess) {
+          process.exit(0)
+        }
       }
     }
 


Test patch:
diff --git a/test/command.js b/test/command.js
index 96ad4c4..3d0888a 100644
--- a/test/command.js
+++ b/test/command.js
@@ -470,6 +470,83 @@ describe('Command', function () {
     })
   })
 
+  describe('help command', function () {
+    it('displays command help appropriately', function () {
+      var sub = {
+        command: 'sub',
+        desc: 'Run the subcommand',
+        builder: {},
+        handler: function (argv) {}
+      }
+
+      var cmd = {
+        command: 'cmd <sub>',
+        desc: 'Try a command',
+        builder: function (yargs) {
+          return yargs.command(sub)
+        },
+        handler: function (argv) {}
+      }
+
+      var helpCmd = checkOutput(function () {
+        return yargs('help cmd')
+          .help().wrap(null)
+          .command(cmd)
+          .argv
+      }, [ './command' ])
+
+      var cmdHelp = checkOutput(function () {
+        return yargs('cmd help')
+          .help().wrap(null)
+          .command(cmd)
+          .argv
+      }, [ './command' ])
+
+      var helpCmdSub = checkOutput(function () {
+        return yargs('help cmd sub')
+          .help().wrap(null)
+          .command(cmd)
+          .argv
+      }, [ './command' ])
+
+      var cmdHelpSub = checkOutput(function () {
+        return yargs('cmd help sub')
+          .help().wrap(null)
+          .command(cmd)
+          .argv
+      }, [ './command' ])
+
+      var cmdSubHelp = checkOutput(function () {
+        return yargs('cmd sub help')
+          .help().wrap(null)
+          .command(cmd)
+          .argv
+      }, [ './command' ])
+
+      var expectedCmd = [
+        './command cmd <sub>',
+        'Commands:',
+        '  sub  Run the subcommand',
+        'Options:',
+        '  --help  Show help  [boolean]',
+        ''
+      ]
+
+      var expectedSub = [
+        './command cmd sub',
+        'Options:',
+        '  --help  Show help  [boolean]',
+        ''
+      ]
+
+      helpCmd.logs.join('\n').split(/\n+/).should.deep.equal(expectedCmd)
+      cmdHelp.logs.join('\n').split(/\n+/).should.deep.equal(expectedCmd)
+      helpCmdSub.logs.join('\n').split(/\n+/).should.deep.equal(expectedSub)
+      cmdHelpSub.logs.join('\n').split(/\n+/).should.deep.equal(expectedSub)
+      cmdSubHelp.logs.join('\n').split(/\n+/).should.deep.equal(expectedSub)
+    })
+  })
+
   // addresses https://github.com/yargs/yargs/issues/514.
   it('respects order of positional arguments when matching commands', function () {
     var output = []
@@ -493,4 +570,38 @@ describe('Command', function () {
     var argv = y.parse(['foo', 'bar'])
     argv.second.should.equal('bar')
   })
+
+  // addresses https://github.com/yargs/yargs/issues/522
+  it('does not require builder function to return', function () {
+    var argv = yargs('yo')
+      .command('yo [someone]', 'Send someone a yo', function (yargs) {
+        yargs.default('someone', 'Pat')
+      }, function (argv) {
+        argv.should.have.property('someone').and.equal('Pat')
+      })
+      .argv
+    argv.should.have.property('someone').and.equal('Pat')
+  })
+
+  it('allows builder function to parse argv without returning', function () {
+    var argv = yargs('yo Jude')
+      .command('yo <someone>', 'Send someone a yo', function (yargs) {
+        yargs.argv
+      }, function (argv) {
+        argv.should.have.property('someone').and.equal('Jude')
+      })
+      .argv
+    argv.should.have.property('someone').and.equal('Jude')
+  })
+
+  it('allows builder function to return parsed argv', function () {
+    var argv = yargs('yo Leslie')
+      .command('yo <someone>', 'Send someone a yo', function (yargs) {
+        return yargs.argv
+      }, function (argv) {
+        argv.should.have.property('someone').and.equal('Leslie')
+      })
+      .argv
+    argv.should.have.property('someone').and.equal('Leslie')
+  })
 })
diff --git a/test/yargs.js b/test/yargs.js
index aacf7b8..e491d24 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -40,17 +40,6 @@ describe('yargs dsl tests', function () {
     Object.keys(argv).should.include('cool')
   })
 
-  it('populates argv with placeholder keys when passed into command handler', function (done) {
-    yargs(['blerg'])
-      .option('cool', {})
-      .command('blerg', 'handle blerg things', function () {}, function (argv) {
-        Object.keys(argv).should.include('cool')
-        return done()
-      })
-      .exitProcess(false) // defaults to true.
-      .argv
-  })
-
   it('accepts an object for implies', function () {
     var r = checkOutput(function () {
       return yargs(['--x=33'])
@@ -686,6 +675,11 @@ describe('yargs dsl tests', function () {
       a1.why.should.equal('hello world')
       a2.why.should.equal('hello world')
     })
+
+    it('ignores implicit help command (with short-circuit)', function () {
+      var parsed = yargs.help().parse('help', true)
+      parsed._.should.deep.equal(['help'])
+    })
   })
 
   describe('config', function () {
@@ -1008,6 +1002,262 @@ describe('yargs dsl tests', function () {
       argv.koala.should.equal(true)
     })
   })
+
+  describe('.help()', function () {
+    it('enables `--help` option and `help` command without arguments', function () {
+      var option = checkOutput(function () {
+        return yargs('--help')
+          .help()
+          .wrap(null)
+          .argv
+      })
+      var command = checkOutput(function () {
+        return yargs('help')
+          .help()
+          .wrap(null)
+          .argv
+      })
+      var expected = [
+        'Options:',
+        '  --help  Show help  [boolean]',
+        ''
+      ]
+      option.logs[0].split('\n').should.deep.equal(expected)
+      command.logs[0].split('\n').should.deep.equal(expected)
+    })
+
+    it('enables `--help` option and `help` command with `true` argument', function () {
+      var option = checkOutput(function () {
+        return yargs('--help')
+          .help(true)
+          .wrap(null)
+          .argv
+      })
+      var command = checkOutput(function () {
+        return yargs('help')
+          .help(true)
+          .wrap(null)
+          .argv
+      })
+      var expected = [
+        'Options:',
+        '  --help  Show help  [boolean]',
+        ''
+      ]
+      option.logs[0].split('\n').should.deep.equal(expected)
+      command.logs[0].split('\n').should.deep.equal(expected)
+    })
+
+    it('enables only `--help` option with `false` argument', function () {
+      var option = checkOutput(function () {
+        return yargs('--help')
+          .help(false)
+          .wrap(null)
+          .argv
+      })
+      var command = checkOutput(function () {
+        return yargs('help')
+          .help(false)
+          .wrap(null)
+          .argv
+      })
+      option.logs[0].split('\n').should.deep.equal([
+        'Options:',
+        '  --help  Show help  [boolean]',
+        ''
+      ])
+      command.result.should.have.property('_').and.deep.equal(['help'])
+    })
+
+    it('enables given string as help option and command with string argument', function () {
+      var option = checkOutput(function () {
+        return yargs('--info')
+          .help('info')
+          .wrap(null)
+          .argv
+      })
+      var command = checkOutput(function () {
+        return yargs('info')
+          .help('info')
+          .wrap(null)
+          .argv
+      })
+      var helpOption = checkOutput(function () {
+        return yargs('--help')
+          .help('info')
+          .wrap(null)
+          .argv
+      })
+      var expected = [
+        'Options:',
+        '  --info  Show help  [boolean]',
+        ''
+      ]
+      option.logs[0].split('\n').should.deep.equal(expected)
+      command.logs[0].split('\n').should.deep.equal(expected)
+      helpOption.result.should.have.property('help').and.be.true
+    })
+
+    it('enables given string as help option and command with string argument and `true` argument', function () {
+      var option = checkOutput(function () {
+        return yargs('--info')
+          .help('info', true)
+          .wrap(null)
+          .argv
+      })
+      var command = checkOutput(function () {
+        return yargs('info')
+          .help('info', true)
+          .wrap(null)
+          .argv
+      })
+      var helpOption = checkOutput(function () {
+        return yargs('--help')
+          .help('info', true)
+          .wrap(null)
+          .argv
+      })
+      var expected = [
+        'Options:',
+        '  --info  Show help  [boolean]',
+        ''
+      ]
+      option.logs[0].split('\n').should.deep.equal(expected)
+      command.logs[0].split('\n').should.deep.equal(expected)
+      helpOption.result.should.have.property('help').and.be.true
+    })
+
+    it('enables given string as help option only with string argument and `false` argument', function () {
+      var option = checkOutput(function () {
+        return yargs('--info')
+          .help('info', false)
+          .wrap(null)
+          .argv
+      })
+      var command = checkOutput(function () {
+        return yargs('info')
+          .help('info', false)
+          .wrap(null)
+          .argv
+      })
+      option.logs[0].split('\n').should.deep.equal([
+        'Options:',
+        '  --info  Show help  [boolean]',
+        ''
+      ])
+      command.result.should.have.property('_').and.deep.equal(['info'])
+    })
+
+    it('enables given string as help option and command with custom description with two string arguments', function () {
+      var option = checkOutput(function () {
+        return yargs('--info')
+          .help('info', 'Display info')
+          .wrap(null)
+          .argv
+      })
+      var command = checkOutput(function () {
+        return yargs('info')
+          .help('info', 'Display info')
+          .wrap(null)
+          .argv
+      })
+      var expected = [
+        'Options:',
+        '  --info  Display info  [boolean]',
+        ''
+      ]
+      option.logs[0].split('\n').should.deep.equal(expected)
+      command.logs[0].split('\n').should.deep.equal(expected)
+    })
+
+    it('enables given string as help option and command with custom description with two string arguments and `true` argument', function () {
+      var option = checkOutput(function () {
+        return yargs('--info')
+          .help('info', 'Display info', true)
+          .wrap(null)
+          .argv
+      })
+      var command = checkOutput(function () {
+        return yargs('info')
+          .help('info', 'Display info', true)
+          .wrap(null)
+          .argv
+      })
+      var expected = [
+        'Options:',
+        '  --info  Display info  [boolean]',
+        ''
+      ]
+      option.logs[0].split('\n').should.deep.equal(expected)
+      command.logs[0].split('\n').should.deep.equal(expected)
+    })
+
+    it('enables given string as help option only and custom description with two string arguments and `false` argument', function () {
+      var option = checkOutput(function () {
+        return yargs('--info')
+          .help('info', 'Display info', false)
+          .wrap(null)
+          .argv
+      })
+      var command = checkOutput(function () {
+        return yargs('info')
+          .help('info', 'Display info', false)
+          .wrap(null)
+          .argv
+      })
+      option.logs[0].split('\n').should.deep.equal([
+        'Options:',
+        '  --info  Display info  [boolean]',
+        ''
+      ])
+      command.result.should.have.property('_').and.deep.equal(['info'])
+    })
+  })
+
+  describe('.help() with .alias()', function () {
+    it('uses multi-char (but not single-char) help alias as command', function () {
+      var info = checkOutput(function () {
+        return yargs('info')
+          .help().alias('h', 'help').alias('h', 'info')
+          .wrap(null)
+          .argv
+      })
+      var h = checkOutput(function () {
+        return yargs('h')
+          .help().alias('h', 'help').alias('h', 'info')
+          .wrap(null)
+          .argv
+      })
+      info.logs[0].split('\n').should.deep.equal([
+        'Options:',
+        '  -h, --help, --info  Show help  [boolean]',
+        ''
+      ])
+      h.result.should.have.property('_').and.deep.equal(['h'])
+    })
+
+    it('uses single-char help alias as command if there are no multi-char aliases', function () {
+      var h = checkOutput(function () {
+        return yargs('h')
+          .help('h').alias('h', '?')
+          .wrap(null)
+          .argv
+      })
+      var q = checkOutput(function () {
+        return yargs('?')
+          .help('h').alias('h', '?')
+          .wrap(null)
+          .argv
+      })
+      var expected = [
+        'Options:',
+        '  -h, -?  Show help  [boolean]',
+        ''
+      ]
+      h.logs[0].split('\n').should.deep.equal(expected)
+      q.logs[0].split('\n').should.deep.equal(expected)
+    })
+  })
 })
 
 describe('yargs context', function () {