Repository: yargs/yargs
Pull request #526 (2016-06-01 17:11:39)

Added documentation about commands not inheriting configurations
As it seems, it's still unclear how commands actually behave when it comes to configuration. This is probably because of that information missing in the documentation.

These new lines are meant to make clear that commands do not inherit the configuration of their parent context, which means that users have to re-apply configurations they want to keep across scopes.

What does everyone think?

nexdrew: I think we should list the configuration we expect to be applied to commands, something like:

- options marked `global` from any parent context
- `.help()` and `.version()` options (by default)
- `.wrap()` config

And there may be a couple others we should add to this, like [`.env()`](https://github.com/yargs/yargs/issues/486) and possibly [`.check()`](https://github.com/yargs/yargs/issues/403) (though I think we agreed the latter should not be inherited, and we should fix #403 with documentation added here).
coveralls: 
[![Coverage Status](https://coveralls.io/builds/6421106/badge)](https://coveralls.io/builds/6421106)

Coverage remained the same at 100.0% when pulling **6d774af7f7856695bf5fda50e8d9c60af140c636 on add-doc-about-config-inheritance** into **5904d5c8200cf1a92ffbc8f95897ea31142d452d on master**.


Patch:
diff --git a/README.md b/README.md
index 652e012..34c91f3 100644
--- a/README.md
+++ b/README.md
@@ -481,6 +481,14 @@ yargs.command('get', 'make a get HTTP request', {
   .argv
 ```
 
+Note that commands will not automatically inherit configuration _or_ options
+of their parent context. This means you'll have to re-apply configuration
+if necessary, and make options global manually using the [global](#global) method.
+
+Additionally, the [`help`](#help) and [`version`](#version)
+options (if used) **always** apply globally, just like the
+[`.wrap()`](#wrap) configuration.
+
 `builder` can also be a function. This function is executed
 with a `yargs` instance, and can be used to provide _advanced_ command specific help:
 
@@ -805,16 +813,16 @@ var argv = require('yargs')
   .argv
 ```
 
-You can also pass an explicit configuration `object`, it will be parsed 
+You can also pass an explicit configuration `object`, it will be parsed
 and its properties will be set as arguments.
-             
+
 ```js
 var argv = require('yargs')
   .config({foo: 1, bar: 2})
   .argv
 console.log(argv)
 ```
- 
+
 ```
 $ node test.js
 { _: [],
@@ -834,7 +842,7 @@ flag occurrences rather than `true` or `false`. Default value is thus `0`.
 .defaults(key, value, [description])
 ------------------------------------
 
-**Note:** The `.defaults()` alias is deprecated. It will be 
+**Note:** The `.defaults()` alias is deprecated. It will be
 removed in the next major version.
 
 Set `argv[key]` to `value` if no option was specified in `process.argv`.
@@ -1092,7 +1100,7 @@ var yargs = require('yargs')(['--help'])
     Options:
       --help  Show help  [boolean]
 
-.help([option, [description]])
+<a name="help"></a>.help([option, [description]])
 ------------------------------
 
 Add an option (e.g. `--help`) that displays the usage string and exits the
@@ -1520,7 +1528,7 @@ present script similar to how `$0` works in bash or perl.
 
 `opts` is optional and acts like calling `.options(opts)`.
 
-.version([option], [description], [version])
+<a name="version"></a>.version([option], [description], [version])
 ----------------------------------------
 
 Add an option (e.g. `--version`) that displays the version number (given by the
@@ -1540,7 +1548,7 @@ var argv = require('yargs')
   .argv;
 ```
 
-.wrap(columns)
+<a name="wrap"></a>.wrap(columns)
 --------------
 
 Format usage output to wrap at `columns` many columns.
diff --git a/lib/command.js b/lib/command.js
index 77555e9..b753d3a 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -1,5 +1,7 @@
 const path = require('path')
+const inspect = require('util').inspect
 const requireDirectory = require('require-directory')
+const whichModule = require('which-module')
 
 // handles parsing positional arguments,
 // and populating argv with said positional
@@ -10,11 +12,12 @@ module.exports = function (yargs, usage, validation) {
   var handlers = {}
   self.addHandler = function (cmd, description, builder, handler) {
     // allow modules that define (a) all properties or (b) only command and description
-    if (typeof cmd === 'object' && typeof cmd.command === 'string') {
+    if (typeof cmd === 'object') {
+      const commandString = typeof cmd.command === 'string' ? cmd.command : moduleName(cmd)
       if (cmd.builder && typeof cmd.handler === 'function') {
-        self.addHandler(cmd.command, extractDesc(cmd), cmd.builder, cmd.handler)
+        self.addHandler(commandString, extractDesc(cmd), cmd.builder, cmd.handler)
       } else {
-        self.addHandler(cmd.command, extractDesc(cmd))
+        self.addHandler(commandString, extractDesc(cmd))
       }
       return
     }
@@ -64,7 +67,7 @@ module.exports = function (yargs, usage, validation) {
         context.files.push(joined)
         // map "command path" to the directory path it came from
         // so that dir can be relative in the API
-        context.dirs[context.commands.concat(parseCommand(visited.command).cmd).join('|')] = dir
+        context.dirs[context.commands.concat(parseCommand(visited.command || commandFromFilename(filename)).cmd).join('|')] = dir
         self.addHandler(visited)
       }
       return visited
@@ -72,6 +75,19 @@ module.exports = function (yargs, usage, validation) {
     requireDirectory({ require: req, filename: mainFilename }, dir, opts)
   }
 
+  // lookup module object from require()d command and derive name
+  // if module was not require()d and no name given, throw error
+  function moduleName (obj) {
+    const mod = whichModule(obj)
+    if (!mod) throw new Error('No command name given for module: ' + inspect(obj))
+    return commandFromFilename(mod.filename)
+  }
+
+  // derive command name from filename
+  function commandFromFilename (filename) {
+    return path.basename(filename, path.extname(filename))
+  }
+
   function extractDesc (obj) {
     for (var keys = ['describe', 'description', 'desc'], i = 0, l = keys.length, test; i < l; i++) {
       test = obj[keys[i]]
diff --git a/package.json b/package.json
index f3a8bef..eeee27d 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,6 @@
     "LICENSE"
   ],
   "dependencies": {
-    "camelcase": "^3.0.0",
     "cliui": "^3.2.0",
     "decamelize": "^1.1.1",
     "lodash.assign": "^4.0.3",
@@ -23,6 +22,7 @@
     "require-main-filename": "^1.0.1",
     "set-blocking": "^2.0.0",
     "string-width": "^1.0.1",
+    "which-module": "^1.0.0",
     "window-size": "^0.2.0",
     "y18n": "^3.2.1",
     "yargs-parser": "^2.4.0"


Test patch:
diff --git a/test/command.js b/test/command.js
index 34e2a9f..965942a 100644
--- a/test/command.js
+++ b/test/command.js
@@ -435,5 +435,24 @@ describe('Command', function () {
         ''
       ])
     })
+
+    it('derives \'command\' string from filename when not exported', function () {
+      var r = checkOutput(function () {
+        return yargs('--help').help().wrap(null)
+          // assumes cwd is node_modules/mocha/bin
+          .commandDir('../../../test/fixtures/cmddir_noname')
+          .argv
+      })
+      r.should.have.property('exit').and.be.true
+      r.should.have.property('errors').with.length(0)
+      r.should.have.property('logs')
+      r.logs.join('\n').split(/\n+/).should.deep.equal([
+        'Commands:',
+        '  nameless  Command name derived from module filename',
+        'Options:',
+        '  --help  Show help  [boolean]',
+        ''
+      ])
+    })
   })
 })
diff --git a/test/fixtures/cmddir_noname/nameless.js b/test/fixtures/cmddir_noname/nameless.js
new file mode 100644
index 0000000..6da3ae9
--- /dev/null
+++ b/test/fixtures/cmddir_noname/nameless.js
@@ -0,0 +1,14 @@
+exports.description = 'Command name derived from module filename'
+
+exports.builder = {
+  banana: {
+    default: 'cool'
+  },
+  batman: {
+    default: 'sad'
+  }
+}
+
+exports.handler = function (argv) {
+  global.commandHandlerCalledWith = argv
+}
diff --git a/test/yargs.js b/test/yargs.js
index 347c40f..f829722 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -377,6 +377,31 @@ describe('yargs dsl tests', function () {
       global.commandHandlerCalledWith.foo.should.equal('bar')
       delete global.commandHandlerCalledWith
     })
+
+    it('derives \'command\' string from filename when missing', function () {
+      var argv = yargs('nameless --foo bar')
+        .command(require('./fixtures/cmddir_noname/nameless'))
+        .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
+    })
+
+    it('throws error for non-module command object missing \'command\' string', function () {
+      expect(function () {
+        yargs.command({
+          desc: 'A command with no name',
+          builder: function (yargs) { return yargs },
+          handler: function (argv) {}
+        })
+      }).to.throw(/No command name given for module: { desc: 'A command with no name',\n {2}builder: \[Function\],\n {2}handler: \[Function\] }/)
+    })
   })
 
   describe('terminalWidth', function () {