| 1 | 'use strict';
|
|---|
| 2 |
|
|---|
| 3 | const constants = require('constants');
|
|---|
| 4 | const fs = require('fs');
|
|---|
| 5 | const path = require('path');
|
|---|
| 6 |
|
|---|
| 7 | const Q = require('q');
|
|---|
| 8 |
|
|---|
| 9 | const shell = require('./shell');
|
|---|
| 10 | const escape = shell.escape;
|
|---|
| 11 | const unescape = shell.unescape;
|
|---|
| 12 |
|
|---|
| 13 | /**
|
|---|
| 14 | * Most of the code adopted from the npm package shell completion code.
|
|---|
| 15 | * See https://github.com/isaacs/npm/blob/master/lib/completion.js
|
|---|
| 16 | *
|
|---|
| 17 | * @returns {COA.CoaObject}
|
|---|
| 18 | */
|
|---|
| 19 | module.exports = function completion() {
|
|---|
| 20 | return this
|
|---|
| 21 | .title('Shell completion')
|
|---|
| 22 | .helpful()
|
|---|
| 23 | .arg()
|
|---|
| 24 | .name('raw')
|
|---|
| 25 | .title('Completion words')
|
|---|
| 26 | .arr()
|
|---|
| 27 | .end()
|
|---|
| 28 | .act((opts, args) => {
|
|---|
| 29 | if(process.platform === 'win32') {
|
|---|
| 30 | const e = new Error('shell completion not supported on windows');
|
|---|
| 31 | e.code = 'ENOTSUP';
|
|---|
| 32 | e.errno = constants.ENOTSUP;
|
|---|
| 33 | return this.reject(e);
|
|---|
| 34 | }
|
|---|
| 35 |
|
|---|
| 36 | // if the COMP_* isn't in the env, then just dump the script
|
|---|
| 37 | if((process.env.COMP_CWORD == null)
|
|---|
| 38 | || (process.env.COMP_LINE == null)
|
|---|
| 39 | || (process.env.COMP_POINT == null)) {
|
|---|
| 40 | return dumpScript(this._cmd._name);
|
|---|
| 41 | }
|
|---|
| 42 |
|
|---|
| 43 | console.error('COMP_LINE: %s', process.env.COMP_LINE);
|
|---|
| 44 | console.error('COMP_CWORD: %s', process.env.COMP_CWORD);
|
|---|
| 45 | console.error('COMP_POINT: %s', process.env.COMP_POINT);
|
|---|
| 46 | console.error('args: %j', args.raw);
|
|---|
| 47 |
|
|---|
| 48 | // completion opts
|
|---|
| 49 | opts = getOpts(args.raw);
|
|---|
| 50 |
|
|---|
| 51 | // cmd
|
|---|
| 52 | const parsed = this._cmd._parseCmd(opts.partialWords);
|
|---|
| 53 | return Q.when(complete(parsed.cmd, parsed.opts), compls => {
|
|---|
| 54 | console.error('filtered: %j', compls);
|
|---|
| 55 | return console.log(compls.map(escape).join('\n'));
|
|---|
| 56 | });
|
|---|
| 57 | });
|
|---|
| 58 | };
|
|---|
| 59 |
|
|---|
| 60 | function dumpScript(name) {
|
|---|
| 61 | const defer = Q.defer();
|
|---|
| 62 |
|
|---|
| 63 | fs.readFile(path.resolve(__dirname, 'completion.sh'), 'utf8', function(err, d) {
|
|---|
| 64 | if(err) return defer.reject(err);
|
|---|
| 65 | d = d.replace(/{{cmd}}/g, path.basename(name)).replace(/^#!.*?\n/, '');
|
|---|
| 66 |
|
|---|
| 67 | process.stdout.on('error', onError);
|
|---|
| 68 | process.stdout.write(d, () => defer.resolve());
|
|---|
| 69 | });
|
|---|
| 70 |
|
|---|
| 71 | return defer.promise;
|
|---|
| 72 |
|
|---|
| 73 | function onError(err) {
|
|---|
| 74 | // Darwin is a real dick sometimes.
|
|---|
| 75 | //
|
|---|
| 76 | // This is necessary because the "source" or "." program in
|
|---|
| 77 | // bash on OS X closes its file argument before reading
|
|---|
| 78 | // from it, meaning that you get exactly 1 write, which will
|
|---|
| 79 | // work most of the time, and will always raise an EPIPE.
|
|---|
| 80 | //
|
|---|
| 81 | // Really, one should not be tossing away EPIPE errors, or any
|
|---|
| 82 | // errors, so casually. But, without this, `. <(cmd completion)`
|
|---|
| 83 | // can never ever work on OS X.
|
|---|
| 84 | if(err.errno !== constants.EPIPE) return defer.reject(err);
|
|---|
| 85 | process.stdout.removeListener('error', onError);
|
|---|
| 86 | return defer.resolve();
|
|---|
| 87 | }
|
|---|
| 88 | }
|
|---|
| 89 |
|
|---|
| 90 | function getOpts(argv) {
|
|---|
| 91 | // get the partial line and partial word, if the point isn't at the end
|
|---|
| 92 | // ie, tabbing at: cmd foo b|ar
|
|---|
| 93 | const line = process.env.COMP_LINE;
|
|---|
| 94 | const w = +process.env.COMP_CWORD;
|
|---|
| 95 | const point = +process.env.COMP_POINT;
|
|---|
| 96 | const words = argv.map(unescape);
|
|---|
| 97 | const word = words[w];
|
|---|
| 98 | const partialLine = line.substr(0, point);
|
|---|
| 99 | const partialWords = words.slice(0, w);
|
|---|
| 100 |
|
|---|
| 101 | // figure out where in that last word the point is
|
|---|
| 102 | let partialWord = argv[w] || '';
|
|---|
| 103 | let i = partialWord.length;
|
|---|
| 104 | while(partialWord.substr(0, i) !== partialLine.substr(-1 * i) && i > 0) i--;
|
|---|
| 105 |
|
|---|
| 106 | partialWord = unescape(partialWord.substr(0, i));
|
|---|
| 107 | partialWord && partialWords.push(partialWord);
|
|---|
| 108 |
|
|---|
| 109 | return {
|
|---|
| 110 | line,
|
|---|
| 111 | w,
|
|---|
| 112 | point,
|
|---|
| 113 | words,
|
|---|
| 114 | word,
|
|---|
| 115 | partialLine,
|
|---|
| 116 | partialWords,
|
|---|
| 117 | partialWord
|
|---|
| 118 | };
|
|---|
| 119 | }
|
|---|
| 120 |
|
|---|
| 121 | function complete(cmd, opts) {
|
|---|
| 122 | let optWord, optPrefix,
|
|---|
| 123 | compls = [];
|
|---|
| 124 |
|
|---|
| 125 | // Complete on cmds
|
|---|
| 126 | if(opts.partialWord.indexOf('-'))
|
|---|
| 127 | compls = Object.keys(cmd._cmdsByName);
|
|---|
| 128 | // Complete on required opts without '-' in last partial word
|
|---|
| 129 | // (if required not already specified)
|
|---|
| 130 | //
|
|---|
| 131 | // Commented out because of uselessness:
|
|---|
| 132 | // -b, --block suggest results in '-' on cmd line;
|
|---|
| 133 | // next completion suggest all options, because of '-'
|
|---|
| 134 | //.concat Object.keys(cmd._optsByKey).filter (v) -> cmd._optsByKey[v]._req
|
|---|
| 135 | else {
|
|---|
| 136 | // complete on opt values: --opt=| case
|
|---|
| 137 | const m = opts.partialWord.match(/^(--\w[\w-_]*)=(.*)$/);
|
|---|
| 138 | if(m) {
|
|---|
| 139 | optWord = m[1];
|
|---|
| 140 | optPrefix = optWord + '=';
|
|---|
| 141 | } else
|
|---|
| 142 | // complete on opts
|
|---|
| 143 | // don't complete on opts in case of --opt=val completion
|
|---|
| 144 | // TODO: don't complete on opts in case of unknown arg after commands
|
|---|
| 145 | // TODO: complete only on opts with arr() or not already used
|
|---|
| 146 | // TODO: complete only on full opts?
|
|---|
| 147 | compls = Object.keys(cmd._optsByKey);
|
|---|
| 148 | }
|
|---|
| 149 |
|
|---|
| 150 | // complete on opt values: next arg case
|
|---|
| 151 | opts.partialWords[opts.w - 1].indexOf('-') || (optWord = opts.partialWords[opts.w - 1]);
|
|---|
| 152 |
|
|---|
| 153 | // complete on opt values: completion
|
|---|
| 154 | let opt;
|
|---|
| 155 | optWord
|
|---|
| 156 | && (opt = cmd._optsByKey[optWord])
|
|---|
| 157 | && !opt._flag
|
|---|
| 158 | && opt._comp
|
|---|
| 159 | && (compls = Q.join(compls,
|
|---|
| 160 | Q.when(opt._comp(opts),
|
|---|
| 161 | (c, o) => c.concat(o.map(v => (optPrefix || '') + v)))));
|
|---|
| 162 |
|
|---|
| 163 | // TODO: complete on args values (context aware, custom completion?)
|
|---|
| 164 |
|
|---|
| 165 | // custom completion on cmds
|
|---|
| 166 | cmd._comp && (compls = Q.join(compls, Q.when(cmd._comp(opts)), (c, o) => c.concat(o)));
|
|---|
| 167 |
|
|---|
| 168 | // TODO: context aware custom completion on cmds, opts and args
|
|---|
| 169 | // (can depend on already entered values, especially options)
|
|---|
| 170 |
|
|---|
| 171 | return Q.when(compls, complitions => {
|
|---|
| 172 | console.error('partialWord: %s', opts.partialWord);
|
|---|
| 173 | console.error('compls: %j', complitions);
|
|---|
| 174 | return compls.filter(c => c.indexOf(opts.partialWord) === 0);
|
|---|
| 175 | });
|
|---|
| 176 | }
|
|---|