Code coverage report for coa/lib/completion.js

# All files / coa/lib completion.js

11.27% Statements 8/71
0% Branches 0/32
0% Functions 0/14
12.5% Lines 8/64

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177

    1x
    1x
    1x

    1x

    1x
    1x
    1x

    1x

     'use strict';

    const constants = require('constants');
    const fs = require('fs');
    const path = require('path');

    const Q = require('q');

    const shell = require('./shell');
    const escape = shell.escape;
    const unescape = shell.unescape;

    /**
     * Most of the code adopted from the npm package shell completion code.
     * See https://github.com/isaacs/npm/blob/master/lib/completion.js
     *
     * @returns {COA.CoaObject}
     */
    module.exports = function completion() {
        return this
            .title('Shell completion')
            .helpful()
            .arg()
                .name('raw')
                .title('Completion words')
                .arr()
                .end()
            .act((opts, args) => {
                if(process.platform === 'win32') {
                    const e = new Error('shell completion not supported on windows');
                    e.code = 'ENOTSUP';
                    e.errno = constants.ENOTSUP;
                    return this.reject(e);
                }

                // if the COMP_* isn't in the env, then just dump the script
                if((process.env.COMP_CWORD == null)
                    || (process.env.COMP_LINE == null)
                    || (process.env.COMP_POINT == null)) {
                    return dumpScript(this._cmd._name);
                }

                console.error('COMP_LINE:  %s', process.env.COMP_LINE);
                console.error('COMP_CWORD: %s', process.env.COMP_CWORD);
                console.error('COMP_POINT: %s', process.env.COMP_POINT);
                console.error('args: %j', args.raw);

                // completion opts
                opts = getOpts(args.raw);

                // cmd
                const parsed = this._cmd._parseCmd(opts.partialWords);
                return Q.when(complete(parsed.cmd, parsed.opts), (compls) => {
                    console.error('filtered: %j', compls);
                    return console.log(compls.map(escape).join('\n'));
                });
            });
    };

    function dumpScript(name) {
        const defer = Q.defer();

        fs.readFile(path.resolve(__dirname, 'completion.sh'), 'utf8', function(err, d) {
            if(err) return defer.reject(err);
            d = d.replace(/{{cmd}}/g, path.basename(name)).replace(/^\#\!.*?\n/, '');

            process.stdout.on('error', onError);
            process.stdout.write(d, () => defer.resolve());
        });

        return defer.promise;

        function onError(err) {
            // Darwin is a real dick sometimes.
            //
            // This is necessary because the "source" or "." program in
            // bash on OS X closes its file argument before reading
            // from it, meaning that you get exactly 1 write, which will
            // work most of the time, and will always raise an EPIPE.
            //
            // Really, one should not be tossing away EPIPE errors, or any
            // errors, so casually. But, without this, `. <(cmd completion)`
            // can never ever work on OS X.
            if(err.errno !== constants.EPIPE) return defer.reject(err);
            process.stdout.removeListener('error', onError);
            return defer.resolve();
        }
    }

    function getOpts(argv) {
        // get the partial line and partial word, if the point isn't at the end
        // ie, tabbing at: cmd foo b|ar
        const line = process.env.COMP_LINE;
        const w = +process.env.COMP_CWORD;
        const point = +process.env.COMP_POINT;
        const words = argv.map(unescape);
        const word = words[w];
        const partialLine = line.substr(0, point);
        const partialWords = words.slice(0, w);

        // figure out where in that last word the point is
        let partialWord = argv[w] || '';
        let i = partialWord.length;
        while(partialWord.substr(0, i) !== partialLine.substr(-1 * i) && i > 0) i--;

        partialWord = unescape(partialWord.substr(0, i));
        partialWord && partialWords.push(partialWord);

        return {
            line,
            w,
            point,
            words,
            word,
            partialLine,
            partialWords,
            partialWord
        };
    }

    function complete(cmd, opts) {
        let optWord, optPrefix,
            compls = [];

        // Complete on cmds
        if(opts.partialWord.indexOf('-'))
            compls = Object.keys(cmd._cmdsByName);
            // Complete on required opts without '-' in last partial word
            // (if required not already specified)
            //
            // Commented out because of uselessness:
            // -b, --block suggest results in '-' on cmd line;
            // next completion suggest all options, because of '-'
            //.concat Object.keys(cmd._optsByKey).filter (v) -> cmd._optsByKey[v]._req
        else {
            // complete on opt values: --opt=| case
            const m = opts.partialWord.match(/^(--\w[\w-_]*)=(.*)$/);
            if(m) {
                optWord = m[1];
                optPrefix = optWord + '=';
            } else
                // complete on opts
                // don't complete on opts in case of --opt=val completion
                // TODO: don't complete on opts in case of unknown arg after commands
                // TODO: complete only on opts with arr() or not already used
                // TODO: complete only on full opts?
                compls = Object.keys(cmd._optsByKey);
        }

        // complete on opt values: next arg case
        opts.partialWords[opts.w - 1].indexOf('-') || (optWord = opts.partialWords[opts.w - 1]);

        // complete on opt values: completion
        let opt;
        optWord
            && (opt = cmd._optsByKey[optWord])
            && !opt._flag
            && opt._comp
            && (compls = Q.join(compls,
                Q.when(opt._comp(opts),
                    (c, o) => c.concat(o.map(v => (optPrefix || '') + v)))));

        // TODO: complete on args values (context aware, custom completion?)

        // custom completion on cmds
        cmd._comp && (compls = Q.join(compls, Q.when(cmd._comp(opts)), (c, o) => c.concat(o)));

        // TODO: context aware custom completion on cmds, opts and args
        // (can depend on already entered values, especially options)

        return Q.when(compls, complitions => {
            console.error('partialWord: %s', opts.partialWord);
            console.error('compls: %j', complitions);
            return compls.filter((c) => c.indexOf(opts.partialWord) === 0);
        });
    }