Repository: yargs/yargs
Pull request #414 (2016-03-04 06:36:25)

Allow command modules to define all command properties
This just expands support for command modules such that the command string and description can also be defined by the module, like so:

```js
// foo.js
module.exports = {
  command: 'foo <bar> [baz]',
  describe: 'foo all the things',
  builder: function (yargs) { return yargs },
  handler: function (argv) {}
}
```

```js
// cli.js
var argv = require('yargs')
  .command(require('./foo.js'))
  .argv
```

A command string must be provided by the `command` property (and it must be a string).

A description is optional. If not provided, the command will be "hidden". If provided, it can be a string or a boolean, and for consistency with the options API it can be provided by a `describe`, a `description`, or a `desc` property (in that order).

I see this as an incremental step toward allowing command modules to be more self-sufficient and auto-discovered.

Note that I added a bunch of "API" tests to `test/command.js` to account for what the `.command()` method should accept as args, but I'm not sure they provide much value. But I figure, why not.

nexdrew: CI failures seem to be red herrings.

`npm install` failed on Travis, but only for Linux Node 0.12.

AppVeyor failed due to npm 1.x with Node 0.10, which will be fixed by #413.
bcoe: @nexdrew mind rebasing with:

https://github.com/yargs/yargs/pull/413

should help some of the CI weirdness.

Patch:
diff --git a/lib/command.js b/lib/command.js
index ac49188..b046a66 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -6,7 +6,13 @@ module.exports = function (yargs, usage, validation) {
 
   var handlers = {}
   self.addHandler = function (cmd, description, builder, handler) {
-    // allow a module to be provided for a command.
+    // allow a module to define all properties
+    if (typeof cmd === 'object' && typeof cmd.command === 'string' && cmd.builder && typeof cmd.handler === 'function') {
+      self.addHandler(cmd.command, extractDesc(cmd), cmd.builder, cmd.handler)
+      return
+    }
+
+    // allow a module to be provided instead of separate builder and handler
     if (typeof builder === 'object' && builder.builder && typeof builder.handler === 'function') {
       self.addHandler(cmd, description, builder.builder, builder.handler)
       return
@@ -29,6 +35,14 @@ module.exports = function (yargs, usage, validation) {
     }
   }
 
+  function extractDesc (obj) {
+    for (var keys = ['describe', 'description', 'desc'], i = 0, l = keys.length, test; i < l; i++) {
+      test = obj[keys[i]]
+      if (typeof test === 'string' || typeof test === 'boolean') return test
+    }
+    return false
+  }
+
   function parseCommand (cmd) {
     var splitCommand = cmd.split(/\s/)
     var bregex = /[\][<>]/g


Test patch:
diff --git a/test/command.js b/test/command.js
index 3e5a017..4e895d5 100644
--- a/test/command.js
+++ b/test/command.js
@@ -65,4 +65,185 @@ describe('Command', function () {
       argv.bar.should.equal('hello')
     })
   })
+
+  describe('API', function () {
+    it('accepts string, string as first 2 arguments', function () {
+      var cmd = 'foo'
+      var desc = 'i\'m not feeling very creative at the moment'
+
+      var y = yargs([]).command(cmd, desc)
+      var commands = y.getUsageInstance().getCommands()
+      commands[0].should.deep.equal([cmd, desc])
+    })
+
+    it('accepts string, boolean as first 2 arguments', function () {
+      var cmd = 'foo'
+      var desc = false
+
+      var y = yargs([]).command(cmd, desc)
+      var commands = y.getUsageInstance().getCommands()
+      commands.should.deep.equal([])
+    })
+
+    it('accepts function as 3rd argument', function () {
+      var cmd = 'foo'
+      var desc = 'i\'m not feeling very creative at the moment'
+      var builder = function (yargs) { return yargs }
+
+      var y = yargs([]).command(cmd, desc, builder)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(cmd)
+      handlers.foo.builder.should.equal(builder)
+    })
+
+    it('accepts options object as 3rd argument', function () {
+      var cmd = 'foo'
+      var desc = 'i\'m not feeling very creative at the moment'
+      var builder = {
+        hello: { default: 'world' }
+      }
+
+      var y = yargs([]).command(cmd, desc, builder)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(cmd)
+      handlers.foo.builder.should.equal(builder)
+    })
+
+    it('accepts module (with builder function and handler function) as 3rd argument', function () {
+      var cmd = 'foo'
+      var desc = 'i\'m not feeling very creative at the moment'
+      var module = {
+        builder: function (yargs) { return yargs },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(cmd, desc, module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(cmd)
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+    })
+
+    it('accepts module (with builder object and handler function) as 3rd argument', function () {
+      var cmd = 'foo'
+      var desc = 'i\'m not feeling very creative at the moment'
+      var module = {
+        builder: {
+          hello: { default: 'world' }
+        },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(cmd, desc, module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(cmd)
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+    })
+
+    it('accepts module (describe key, builder function) as 1st argument', function () {
+      var module = {
+        command: 'foo',
+        describe: 'i\'m not feeling very creative at the moment',
+        builder: function (yargs) { return yargs },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(module.command)
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+      var commands = y.getUsageInstance().getCommands()
+      commands[0].should.deep.equal([module.command, module.describe])
+    })
+
+    it('accepts module (description key, builder function) as 1st argument', function () {
+      var module = {
+        command: 'foo',
+        description: 'i\'m not feeling very creative at the moment',
+        builder: function (yargs) { return yargs },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(module.command)
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+      var commands = y.getUsageInstance().getCommands()
+      commands[0].should.deep.equal([module.command, module.description])
+    })
+
+    it('accepts module (desc key, builder function) as 1st argument', function () {
+      var module = {
+        command: 'foo',
+        desc: 'i\'m not feeling very creative at the moment',
+        builder: function (yargs) { return yargs },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(module.command)
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+      var commands = y.getUsageInstance().getCommands()
+      commands[0].should.deep.equal([module.command, module.desc])
+    })
+
+    it('accepts module (false describe, builder function) as 1st argument', function () {
+      var module = {
+        command: 'foo',
+        describe: false,
+        builder: function (yargs) { return yargs },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(module.command)
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+      var commands = y.getUsageInstance().getCommands()
+      commands.should.deep.equal([])
+    })
+
+    it('accepts module (missing describe, builder function) as 1st argument', function () {
+      var module = {
+        command: 'foo',
+        builder: function (yargs) { return yargs },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(module.command)
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+      var commands = y.getUsageInstance().getCommands()
+      commands.should.deep.equal([])
+    })
+
+    it('accepts module (describe key, builder object) as 1st argument', function () {
+      var module = {
+        command: 'foo',
+        describe: 'i\'m not feeling very creative at the moment',
+        builder: {
+          hello: {
+            default: 'world'
+          }
+        },
+        handler: function (argv) {}
+      }
+
+      var y = yargs([]).command(module)
+      var handlers = y.getCommandInstance().getCommandHandlers()
+      handlers.foo.original.should.equal(module.command)
+      handlers.foo.builder.should.equal(module.builder)
+      handlers.foo.handler.should.equal(module.handler)
+      var commands = y.getUsageInstance().getCommands()
+      commands[0].should.deep.equal([module.command, module.describe])
+    })
+  })
 })
diff --git a/test/fixtures/command-module.js b/test/fixtures/command-module.js
new file mode 100644
index 0000000..b43c84c
--- /dev/null
+++ b/test/fixtures/command-module.js
@@ -0,0 +1,17 @@
+exports.command = 'blerg <foo>'
+
+exports.describe = 'handle blerg things'
+
+exports.builder = function (yargs) {
+  return yargs
+    .option('banana', {
+      default: 'cool'
+    })
+    .option('batman', {
+      default: 'sad'
+    })
+}
+
+exports.handler = function (argv) {
+  global.commandHandlerCalledWith = argv
+}
diff --git a/test/yargs.js b/test/yargs.js
index f5af10c..93c8b0f 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -361,6 +361,21 @@ describe('yargs dsl tests', function () {
       global.commandHandlerCalledWith.foo.should.equal('bar')
       delete global.commandHandlerCalledWith
     })
+
+    it("accepts a module with a keys 'command', 'describe', 'builder', and 'handler'", function () {
+      var argv = yargs(['blerg', 'bar'])
+        .command(require('./fixtures/command-module'))
+        .argv
+
+      argv.banana.should.equal('cool')
+      argv.batman.should.equal('sad')
+      argv.foo.should.equal('bar')
+
+      global.commandHandlerCalledWith.banana.should.equal('cool')
+      global.commandHandlerCalledWith.batman.should.equal('sad')
+      global.commandHandlerCalledWith.foo.should.equal('bar')
+      delete global.commandHandlerCalledWith
+    })
   })
 
   describe('terminalWidth', function () {