Repository: mattallty/Caporal.js (ts)

Issue:
Error setting up completion in bash
I get the following error when I try to setup auto completion in bash (macOS 10.14):

```
$ source <([app name] completion bash)
$ Error: write EPIPE
    at WriteWrap.afterWrite [as oncomplete] (net.js:788:14)
Emitted 'error' event at:
    at onwriteError (_stream_writable.js:431:12)
    at onwrite (_stream_writable.js:456:5)
    at _destroy (internal/streams/destroy.js:40:7)
    at Socket.dummyDestroy [as _destroy] (internal/process/stdio.js:11:34)
    at Socket.destroy (internal/streams/destroy.js:32:8)
    at WriteWrap.afterWrite [as oncomplete] (net.js:790:10)
```

This happens with my own cli and with the example cli. 

For recreating with the example app;
1. `cd` to `examples/pizza`
2. run `npm install`
3. run `npm link`
4. run `fly` to confirm the command is installed
5. run `source <(fly completion bash)`. This is when the error is thrown



Pull request:
Various fixes
- Fixes autocompletion of arguments
- Fix validation error for LIST 

Should fix #136 

Patch:
diff --git a/examples/pizza/package.json b/examples/pizza/package.json
index c787c1b..511739f 100644
--- a/examples/pizza/package.json
+++ b/examples/pizza/package.json
@@ -12,7 +12,7 @@
     "tsc": "^1.20150623.0"
   },
   "bin": {
-    "fly": "./pizza.js"
+    "pizza": "./pizza.js"
   },
   "author": "",
   "license": "ISC"
diff --git a/lib/autocomplete.js b/lib/autocomplete.js
index 865391d..06a7b2d 100644
--- a/lib/autocomplete.js
+++ b/lib/autocomplete.js
@@ -42,10 +42,10 @@ class Autocomplete {
 
     let cmd;
     const commandArr = [];
-    const args = cmdStr.split(' ').filter(s => s!= '');
+    const args = cmdStr.split(' ').filter(s => s != '');
     const commands = this._program.getCommands();
 
-    while(!cmd && args.length) {
+    while (!cmd && args.length) {
       commandArr.push(args.shift());
       const cmdStr = commandArr.join(' ');
       cmd = commands.filter(c => {
@@ -82,6 +82,15 @@ class Autocomplete {
       currentCommand.options().some(o => (lastPartial === o.getShortName() || lastPartial === o.getLongName()));
   }
 
+  _lastPartialIsKnownArg(currentCommand, lastPartialIsArg, lastPartial) {
+    return lastPartialIsArg &&
+      currentCommand &&
+      currentCommand.args().some(
+        a => (a.getChoices().length && a.getChoices().includes(lastPartial))
+      );
+  }
+
+
   _getCurrentOption(currentCommand, lastPartialIsKnownOption, lastPartial) {
     return lastPartialIsKnownOption ?
       currentCommand.options()
@@ -89,18 +98,24 @@ class Autocomplete {
       : null;
   }
 
-  _getPossibleArgumentValues(currentCommand, lastPartialIsOption, argsCount) {
+  _getPossibleArgumentValues(currentCommand, lastPartialIsOption, argsCount, lastPartial) {
     if (!currentCommand ||
-      lastPartialIsOption ||
-      !currentCommand.args(argsCount)) {
+      lastPartialIsOption) {
       return Promise.resolve([]);
     }
 
-    const arg = currentCommand.args(argsCount);
+    const arg = currentCommand.args(lastPartial ? argsCount - 1 : argsCount);
+    if (!arg) {
+      return Promise.resolve([]);
+    }
 
     // Choices
     if (arg.getChoices().length) {
-      return Promise.resolve(arg.getChoices().map(choice => choice + ':Value for argument ' + arg.synopsis()));
+      return Promise.resolve(
+        arg.getChoices()
+          .map(choice => choice + ':Value for argument ' + arg.synopsis())
+          .filter(choice => !lastPartial || choice.startsWith(lastPartial))
+      );
     }
 
     // Promise completion
@@ -128,9 +143,9 @@ class Autocomplete {
     const optNames = currentCommand ?
       currentCommand.options().map(o => {
         if ((o.getShortName() && o.getShortName() != lastPartial && o.getShortName().startsWith(lastPartial)) ||
-          (o.getLongName() && o.getLongName() != lastPartial && o.getLongName().startsWith(lastPartial)) ) {
+          (o.getLongName() && o.getLongName() != lastPartial && o.getLongName().startsWith(lastPartial))) {
           return o.getLongOrShortName() + ':' + o.description();
-        } else if(!lastPartialIsOption && optionsAlreadyUsed.indexOf(o.getShortName()) === -1 &&
+        } else if (!lastPartialIsOption && optionsAlreadyUsed.indexOf(o.getShortName()) === -1 &&
           optionsAlreadyUsed.indexOf(o.getLongName()) === -1) {
           return o.getLongOrShortName() + ':' + o.description();
         }
@@ -165,8 +180,8 @@ class Autocomplete {
           return true;
         }
         return (c.name().startsWith(cmdStr) ||
-        (c.getAlias() && c.getAlias().startsWith(cmdStr))) &&
-        (!currentCommand || !this._isSameCommand(c, currentCommand))
+          (c.getAlias() && c.getAlias().startsWith(cmdStr))) &&
+          (!currentCommand || !this._isSameCommand(c, currentCommand))
       })
       .filter(c => c.name() !== '') // do not take the default command
       .map(c => {
@@ -181,21 +196,21 @@ class Autocomplete {
   }
 
   _complete(data, done) {
+
     const currCommand = this._findCommand(data.args.slice(3).join(' '));
     const args = parseArgs(data.args.slice(1), currCommand ? currCommand.parseArgsOpts : {});
     const cmd = args._.join(' ');
 
-
-
     const realArgsCount = this._countArgs(currCommand, args);
     const optionsAlreadyUsed = this._getOptionsAlreadyUsed(data.args);
     const lastPartial = data.lastPartial;
 
-    const lastPartIsOption = data.lastPartial.startsWith('-');
+    const lastPartIsOption = data.lastPartial.startsWith('-')
     const lastPartIsKnownOption = this._lastPartialIsKnownOption(currCommand, lastPartIsOption, lastPartial);
     const currOption = this._getCurrentOption(currCommand, lastPartIsKnownOption, lastPartial);
 
-    const possArgValues = this._getPossibleArgumentValues(currCommand, lastPartIsOption, realArgsCount);
+    const possArgValues = this._getPossibleArgumentValues(currCommand,
+      lastPartIsOption, realArgsCount, data.lastPartial);
     const possOptNames = this._getPossibleOptionNames(currCommand, optionsAlreadyUsed, lastPartial, lastPartIsOption);
     const possOptValues = this._getPossibleOptionValues(currOption);
     const possCommands = this._getPossibleCommands(currCommand, cmd);
diff --git a/lib/validator.js b/lib/validator.js
index 9d686a8..33fcf5e 100644
--- a/lib/validator.js
+++ b/lib/validator.js
@@ -95,6 +95,8 @@ class Validator {
 
       if (typeof value === 'string') {
         value = value.split(',');
+      } else if (!Array.isArray(value)) {
+        value = []
       }
 
       return value.map(function (el) {


Test patch:
diff --git a/tests/autocomplete.js b/tests/autocomplete.js
index 995ed22..ab56843 100644
--- a/tests/autocomplete.js
+++ b/tests/autocomplete.js
@@ -21,13 +21,13 @@ describe('Autocomplete', () => {
       .argument('<kind>', 'Kind of pizza', ["margherita", "hawaiian", "fredo"])
       .argument('<from-store>', 'Which store to order from')
       // enable auto-completion for <from-store> argument using a sync function returning an array
-      .complete(function() {
+      .complete(function () {
         return ['store-1', 'store-2', 'store-3', 'store-4', 'store-5'];
       })
 
       .argument('<account>', 'Which account id to use')
       // enable auto-completion for <account> argument using a Promise
-      .complete(function() {
+      .complete(function () {
         return Promise.resolve(['account-1', 'account-2']);
       })
 
@@ -35,13 +35,13 @@ describe('Autocomplete', () => {
       .option('-d, --discount <amount>', 'Discount offer', this.program.FLOAT)
       .option('-p, --pay-by <mean>', 'Pay by option')
       // enable auto-completion for -p | --pay-by argument using a Promise
-      .complete(function() {
+      .complete(function () {
         return Promise.resolve(['cash', 'credit-card']);
       })
 
       // --extra will be auto-magicaly autocompleted by providing the user with 3 choices
       .option('-e <ingredients>', 'Add extra ingredients', ['pepperoni', 'onion', 'cheese'])
-      .action(function(args, options, logger) {
+      .action(function (args, options, logger) {
 
       })
 
@@ -50,16 +50,16 @@ describe('Autocomplete', () => {
       // <kind> will be auto-magicaly autocompleted by providing the user with 3 choices
       .argument('<order-id>', 'Order id')
       // enable auto-completion for <from-store> argument using the `done` callback
-      .complete(function() {
+      .complete(function () {
         return Promise.resolve(['#82792', '#71727', '#526Z52']);
       })
       .argument('<to-store>', 'Store id')
       .option('--ask-change <other-kind-pizza>', 'Ask for other kind of pizza')
-      .complete(function() {
+      .complete(function () {
         return Promise.resolve(["margherita", "hawaiian", "fredo"]);
       })
       .option('--say-something <something>', 'Say something to the manager')
-      .action(function(args, options, logger) {
+      .action(function (args, options, logger) {
 
       });
 
@@ -71,7 +71,7 @@ describe('Autocomplete', () => {
     this._complete.restore();
   });
 
-  it(`should complete commands when none provided`, function(done) {
+  it(`should complete commands when none provided`, function (done) {
 
     process.env.COMP_LINE = "_mocha";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
@@ -91,7 +91,7 @@ describe('Autocomplete', () => {
     });
   });
 
-  it(`should complete commands when substring provided`, function(done) {
+  it(`should complete commands when substring provided`, function (done) {
 
     process.env.COMP_LINE = "_mocha or";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
@@ -113,7 +113,7 @@ describe('Autocomplete', () => {
   });
 
 
-  it(`should complete commands when substring (alias) provided`, function(done) {
+  it(`should complete commands when substring (alias) provided`, function (done) {
 
     process.env.COMP_LINE = "_mocha gi";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
@@ -132,9 +132,34 @@ describe('Autocomplete', () => {
     });
   });
 
-  it(`should not complete commands if none available after string`, function(done) {
+  it(`should complete argument when substring provided`, function (done) {
 
-    process.env.COMP_LINE = "_mocha order";
+    process.env.COMP_LINE = "_mocha order mar";
+    process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
+    process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString();
+
+    this.program.parse(['node', '_mocha', 'completion', '--'].concat(process.env.COMP_LINE.split(' ')));
+
+    setImmediate(() => {
+      should(this._complete.called).be.true();
+      const response = this._complete.returnValues[0];
+      should(response).be.Promise();
+      should(response)
+        .be.fulfilledWith([
+          'margherita:Value for argument <kind>',
+          '--number:Number of pizza',
+          '--discount:Discount offer',
+          '--pay-by:Pay by option',
+          '-e:Add extra ingredients'
+        ])
+        .then(_ => done()).catch(err => done(err));
+
+    });
+  });
+
+  it(`should not complete commands if none available after string`, function (done) {
+
+    process.env.COMP_LINE = "_mocha order ";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
     process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString();
 
@@ -159,9 +184,9 @@ describe('Autocomplete', () => {
 
   });
 
-  it(`should not suggest argument(s) already provided`, function(done) {
+  it(`should not suggest argument(s) already provided`, function (done) {
 
-    process.env.COMP_LINE = "_mocha order margherita";
+    process.env.COMP_LINE = "_mocha order margherita ";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
     process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString();
 
@@ -189,9 +214,9 @@ describe('Autocomplete', () => {
     });
   });
 
-  it(`should not complete current command`, function(done) {
+  it(`should not complete current command`, function (done) {
 
-    process.env.COMP_LINE = "_mocha order";
+    process.env.COMP_LINE = "_mocha order ";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
     process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString();
 
@@ -218,9 +243,9 @@ describe('Autocomplete', () => {
 
   });
 
-  it(`should handle command alias`, function(done) {
+  it(`should handle command alias`, function (done) {
 
-    process.env.COMP_LINE = "_mocha give-it-to-me";
+    process.env.COMP_LINE = "_mocha give-it-to-me ";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
     process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString();
 
@@ -246,7 +271,7 @@ describe('Autocomplete', () => {
     });
   });
 
-  it(`should suggest long option names`, function(done) {
+  it(`should suggest long option names`, function (done) {
 
     process.env.COMP_LINE = "_mocha order --n";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
@@ -269,7 +294,7 @@ describe('Autocomplete', () => {
 
   });
 
-  it(`should suggest long option value`, function(done) {
+  it(`should suggest long option value`, function (done) {
 
     process.env.COMP_LINE = "_mocha order -e";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
@@ -289,7 +314,7 @@ describe('Autocomplete', () => {
     });
   });
 
-  it(`should work with _default command`, function(done) {
+  it(`should work with _default command`, function (done) {
 
     this.program = new Program();
 
@@ -301,13 +326,13 @@ describe('Autocomplete', () => {
       .argument('<kind>', 'Kind of pizza', ["margherita", "hawaiian", "fredo"])
       .argument('<from-store>', 'Which store to order from')
       // enable auto-completion for <from-store> argument using the `done` callback
-      .complete(function() {
+      .complete(function () {
         return ['store-1', 'store-2', 'store-3', 'store-4', 'store-5'];
       })
 
       .argument('<account>', 'Which account id to use')
       // enable auto-completion for <account> argument using a Promise
-      .complete(function() {
+      .complete(function () {
         return Promise.resolve(['account-1', 'account-2']);
       })
 
@@ -315,13 +340,13 @@ describe('Autocomplete', () => {
       .option('-d, --discount <amount>', 'Discount offer', this.program.FLOAT)
       .option('-p, --pay-by <mean>', 'Pay by option')
       // enable auto-completion for -p | --pay-by argument using a Promise
-      .complete(function() {
+      .complete(function () {
         return Promise.resolve(['cash', 'credit-card']);
       })
 
       // --extra will be auto-magicaly autocompleted by providing the user with 3 choices
       .option('-e <ingredients>', 'Add extra ingredients', ['pepperoni', 'onion', 'cheese'])
-      .action(function(args, options, logger) {
+      .action(function (args, options, logger) {
 
       });
 
@@ -345,7 +370,7 @@ describe('Autocomplete', () => {
     });
   });
 
-  it(`should not suggest option names if last partial does not match`, function(done) {
+  it(`should not suggest option names if last partial does not match`, function (done) {
 
     process.env.COMP_LINE = "_mocha order --foo";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
@@ -365,9 +390,9 @@ describe('Autocomplete', () => {
     });
   });
 
-  it(`should work with simple completers returning an array`, function(done) {
+  it(`should work with simple completers returning an array`, function (done) {
 
-    process.env.COMP_LINE = "_mocha return";
+    process.env.COMP_LINE = "_mocha return ";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
     process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString();
 
@@ -391,9 +416,9 @@ describe('Autocomplete', () => {
     });
   });
 
-  it(`should handle arguments without completers`, function(done) {
+  it(`should handle arguments without completers`, function (done) {
 
-    process.env.COMP_LINE = "_mocha return #82792";
+    process.env.COMP_LINE = "_mocha return #82792 ";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
     process.env.COMP_CWORD = process.env.COMP_LINE.trim().split(' ').length.toString();
 
@@ -415,7 +440,7 @@ describe('Autocomplete', () => {
   });
 
 
-  it(`should handle options completers`, function(done) {
+  it(`should handle options completers`, function (done) {
 
     process.env.COMP_LINE = "_mocha return --ask-change";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
@@ -439,7 +464,7 @@ describe('Autocomplete', () => {
     });
   });
 
-  it(`should handle options without completers`, function(done) {
+  it(`should handle options without completers`, function (done) {
 
     process.env.COMP_LINE = "_mocha return --say-something";
     process.env.COMP_POINT = process.env.COMP_LINE.length.toString();
@@ -459,7 +484,7 @@ describe('Autocomplete', () => {
     });
   });
 
-  it(`should handle a failling completion promise`, function(done) {
+  it(`should handle a failling completion promise`, function (done) {
 
     this.program = new Program();
 
@@ -467,10 +492,10 @@ describe('Autocomplete', () => {
       .logger(logger)
       .version('1.0.0')
       .argument('<from-store>', 'Which store to order from')
-      .complete(function() {
+      .complete(function () {
         return Promise.reject(new Error("foo"));
       })
-      .action(function(args, options, logger) {
+      .action(function (args, options, logger) {
 
       });