You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							176 lines
						
					
					
						
							5.9 KiB
						
					
					
				
			
		
		
	
	
							176 lines
						
					
					
						
							5.9 KiB
						
					
					
				'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); | 
						|
    }); | 
						|
}
 | 
						|
 |