Repository: yargs/yargs
Pull request #1272 (2019-01-09 08:10:13)

Consistent bash-completion if a command is not found
Hey,
it seems that if a command is not found, the fallback file autocompletion does not work the same as the standard bash autocompletion:

**CURRENT**
`some-cmd folder-na(tab)` -> `some-cmd folder-name `
currently the folder name is inserted, including a space at the end

**PROPOSED**
`some-cmd folder-na(tab)` -> `some-cmd folder-name/`
In the current bash versions the folder name is inserted with a slash at the end, allowing you to chain folder paths

With this change bash will decide how to handle the autocompletion.

### tested on
MacOS 10.14

### references
https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html
https://stackoverflow.com/a/22827323


antonstefanov: nvm, this can result in `No such file or directory` errors if a folder is not found, will reopen in the future

Patch:
diff --git a/completion.sh.hbs b/completion.sh.hbs
index 819c8ae..272540b 100644
--- a/completion.sh.hbs
+++ b/completion.sh.hbs
@@ -19,10 +19,10 @@ _yargs_completions()
 
     # if no match was found, fall back to filename completion
     if [ ${#COMPREPLY[@]} -eq 0 ]; then
-      COMPREPLY=( $(compgen -f -- "${cur_word}" ) )
+      COMPREPLY=()
     fi
 
     return 0
 }
-complete -F _yargs_completions {{app_name}}
+complete -o default -F _yargs_completions {{app_name}}
 ###-end-{{app_name}}-completions-###
diff --git a/docs/advanced.md b/docs/advanced.md
index c9cd9da..5a9b98d 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -204,6 +204,29 @@ yargs.command('get <source> [proxy]', 'make a get HTTP request', require('my-mod
   .argv
 ```
 
+#### Testing a Command Module
+
+If you want to test a command in it's entirety you can test it like this:
+
+```js
+it("returns help output", async () => {
+  // Initialize parser using the command module
+  const parser = yargs.command(require('./my-command-module')).help();
+
+  // Run the command module with --help as argument
+  const output = await new Promise((resolve) => {
+    parser.parse("--help", (err, argv, output) => {
+      resolve(output);
+    })
+  });
+
+  // Verify the output is correct
+  expect(output).toBe(expect.stringContaining("helpful message"));
+});
+```
+
+This example uses [jest](https://github.com/facebook/jest) as a test runner, but the concept is independent of framework.
+
 .commandDir(directory, [opts])
 ------------------------------
 
@@ -455,6 +478,30 @@ const normalizeCredentials = (argv) => {
   }
   return {}
 }
+
+// Add normalizeCredentials to yargs
+yargs.middleware(normalizeCredentials)
+```
+
+### Example Async Credentials Middleware
+
+This example is exactly the same however it loads the `username` and `password` asynchronously.
+
+#### Middleware function
+
+```
+const { promisify } = require('util') // since node 8.0.0
+const readFile = promisify(require('fs').readFile)
+
+const normalizeCredentials = (argv) => {
+  if (!argv.username || !argv.password) {
+    return readFile('~/.credentials').then(data => JSON.parse(data))
+  }
+  return {}
+}
+
+// Add normalizeCredentials to yargs
+yargs.middleware(normalizeCredentials)
 ```
 
 #### yargs parsing configuration
@@ -467,7 +514,7 @@ var argv = require('yargs')
                     .option('password')
       } ,(argv) => {
         authenticateUser(argv.username, argv.password)
-      }, 
+      },
       [normalizeCredentials]
      )
   .argv;
diff --git a/docs/examples.md b/docs/examples.md
index b17d338..b50cf9f 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -285,7 +285,7 @@ var argv = require('yargs')
     .demandOption(['f'])
     .help('h')
     .alias('h', 'help')
-    .epilog('copyright 2015')
+    .epilog('copyright 2019')
     .argv;
 
 var fs = require('fs');
@@ -302,25 +302,38 @@ s.on('end', function () {
 ````
 
 ***
-    $ node line_count.js count
+    $ node line_count.js 
     Usage: line_count.js <command> [options]
 
     Commands:
-      count    Count the lines in a file
+      line_count.js count  Count the lines in a file
 
     Options:
-      -f, --file  Load a file        [required]
-      -h, --help  Show help           [boolean]
+      --version   Show version number      [boolean]
+      -f, --file  Load a file             [required]
+      -h, --help  Show help                [boolean]
 
     Examples:
       line_count.js count -f foo.js  count the lines in the given file
 
-    copyright 2015
+    copyright 2019
+
+    Missing required argument: f
+    
+    $ node line_count.js count
+    line_count.js count
+
+    Count the lines in a file
+
+    Options:
+      --version   Show version number      [boolean]
+      -f, --file  Load a file             [required]
+      -h, --help  Show help                [boolean]
 
-    Missing required arguments: f
+    Missing required argument: f
 
     $ node line_count.js count --file line_count.js
-    26
+    25
 
     $ node line_count.js count -f line_count.js
-    26
+    25
diff --git a/lib/command.js b/lib/command.js
index 65ffc4e..fdf9e37 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -1,6 +1,8 @@
 'use strict'
 
 const inspect = require('util').inspect
+const isPromise = require('./is-promise')
+const {applyMiddleware} = require('./middleware')
 const path = require('path')
 const Parser = require('yargs-parser')
 
@@ -229,17 +231,18 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) {
 
     if (commandHandler.handler && !yargs._hasOutput()) {
       yargs._setHasOutput()
-      if (commandHandler.middlewares.length > 0) {
-        const middlewareArgs = commandHandler.middlewares.reduce(function (initialObj, middleware) {
-          return Object.assign(initialObj, middleware(innerArgv))
-        }, {})
-        Object.assign(innerArgv, middlewareArgs)
-      }
-      const handlerResult = commandHandler.handler(innerArgv)
-      if (handlerResult && typeof handlerResult.then === 'function') {
-        handlerResult.then(
-          null,
-          (error) => yargs.getUsageInstance().fail(null, error)
+
+      const middlewares = commandHandler.middlewares || []
+
+      innerArgv = applyMiddleware(innerArgv, middlewares)
+
+      const handlerResult = isPromise(innerArgv)
+        ? innerArgv.then(argv => commandHandler.handler(argv))
+        : commandHandler.handler(innerArgv)
+
+      if (isPromise(handlerResult)) {
+        handlerResult.catch(error =>
+          yargs.getUsageInstance().fail(null, error)
         )
       }
     }
diff --git a/lib/is-promise.js b/lib/is-promise.js
new file mode 100644
index 0000000..0516def
--- /dev/null
+++ b/lib/is-promise.js
@@ -0,0 +1,3 @@
+module.exports = function isPromise (maybePromise) {
+  return maybePromise instanceof Promise
+}
diff --git a/lib/middleware.js b/lib/middleware.js
index 567ba57..f58593f 100644
--- a/lib/middleware.js
+++ b/lib/middleware.js
@@ -1,3 +1,5 @@
+const isPromise = require('./is-promise')
+
 module.exports = function (globalMiddleware, context) {
   return function (callback) {
     if (Array.isArray(callback)) {
@@ -8,3 +10,24 @@ module.exports = function (globalMiddleware, context) {
     return context
   }
 }
+
+module.exports.applyMiddleware = function (argv, middlewares) {
+  return middlewares
+    .reduce((accumulation, middleware) => {
+      if (isPromise(accumulation)) {
+        return accumulation
+          .then(initialObj =>
+            Promise.all([initialObj, middleware(initialObj)])
+          )
+          .then(([initialObj, middlewareObj]) =>
+            Object.assign(initialObj, middlewareObj)
+          )
+      } else {
+        const result = middleware(argv)
+
+        return isPromise(result)
+          ? result.then(middlewareObj => Object.assign(accumulation, middlewareObj))
+          : Object.assign(accumulation, result)
+      }
+    }, argv)
+}
diff --git a/package.json b/package.json
index 826df9e..31b549f 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
     "decamelize": "^1.2.0",
     "find-up": "^3.0.0",
     "get-caller-file": "^1.0.1",
-    "os-locale": "^3.0.0",
+    "os-locale": "^3.1.0",
     "require-directory": "^2.1.1",
     "require-main-filename": "^1.0.1",
     "set-blocking": "^2.0.0",
diff --git a/yargs.js b/yargs.js
index 24c8e99..24eee91 100644
--- a/yargs.js
+++ b/yargs.js
@@ -1152,7 +1152,7 @@ function Yargs (processArgs, cwd, parentRequire) {
   }
 
   self._runValidation = function runValidation (argv, aliases, positionalMap, parseErrors) {
-    if (parseErrors) throw new YError(parseErrors.message)
+    if (parseErrors) throw new YError(parseErrors.message || parseErrors)
     validation.nonOptionCount(argv)
     validation.requiredArguments(argv)
     if (strict) validation.unknownArguments(argv, aliases, positionalMap)


Test patch:
diff --git a/test/middleware.js b/test/middleware.js
index 295ec95..0dfae92 100644
--- a/test/middleware.js
+++ b/test/middleware.js
@@ -1,6 +1,7 @@
 'use strict'
 /* global describe, it, beforeEach, afterEach */
 
+const {expect} = require('chai')
 const middlewareFactory = require('../lib/middleware')
 let yargs
 require('chai').should()
@@ -105,4 +106,38 @@ describe('middleware', () => {
       .exitProcess(false) // defaults to true.
       .parse()
   })
+
+  // addresses https://github.com/yargs/yargs/issues/1237
+  describe('async', () => {
+    it('fails when the promise returned by the middleware rejects', (done) => {
+      const error = new Error()
+      const handlerErr = new Error('should not have been called')
+      yargs('foo')
+        .command('foo', 'foo command', () => {}, (argv) => done(handlerErr), [ (argv) => Promise.reject(error) ])
+        .fail((msg, err) => {
+          expect(msg).to.equal(null)
+          expect(err).to.equal(error)
+          done()
+        })
+        .parse()
+    })
+
+    it('calls the command handler when all middleware promises resolve', (done) => {
+      const middleware = (key, value) => () => new Promise((resolve, reject) => {
+        setTimeout(() => {
+          return resolve({ [key]: value })
+        }, 5)
+      })
+      yargs('foo hello')
+        .command('foo <pos>', 'foo command', () => {}, (argv) => {
+          argv.hello.should.equal('world')
+          argv.foo.should.equal('bar')
+          done()
+        }, [ middleware('hello', 'world'), middleware('foo', 'bar') ])
+        .fail((msg, err) => {
+          return done(Error('should not have been called'))
+        })
+        .parse()
+    })
+  })
 })