123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- ###*
- Most of the code adopted from the npm package shell completion code.
- See https://github.com/isaacs/npm/blob/master/lib/completion.js
- ###
- Q = require 'q'
- escape = require('./shell').escape
- unescape = require('./shell').unescape
- module.exports = ->
- @title('Shell completion')
- .helpful()
- .arg()
- .name('raw')
- .title('Completion words')
- .arr()
- .end()
- .act (opts, args) ->
- if process.platform == 'win32'
- e = new Error 'shell completion not supported on windows'
- e.code = 'ENOTSUP'
- e.errno = require('constants').ENOTSUP
- return @reject(e)
- # if the COMP_* isn't in the env, then just dump the script
- if !process.env.COMP_CWORD? or !process.env.COMP_LINE? or !process.env.COMP_POINT?
- return dumpScript(@_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
- { cmd, argv } = @_cmd._parseCmd opts.partialWords
- Q.when complete(cmd, opts), (compls) ->
- console.error 'filtered: %j', compls
- console.log compls.map(escape).join('\n')
- dumpScript = (name) ->
- fs = require 'fs'
- path = require 'path'
- defer = Q.defer()
- fs.readFile path.resolve(__dirname, 'completion.sh'), 'utf8', (err, d) ->
- if err then return defer.reject err
- d = d.replace(/{{cmd}}/g, path.basename name).replace(/^\#\!.*?\n/, '')
- 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 == require('constants').EPIPE
- process.stdout.removeListener 'error', onError
- defer.resolve()
- else
- defer.reject(err)
- process.stdout.on 'error', onError
- process.stdout.write d, -> defer.resolve()
- defer.promise
- getOpts = (argv) ->
- # get the partial line and partial word, if the point isn't at the end
- # ie, tabbing at: cmd foo b|ar
- line = process.env.COMP_LINE
- w = +process.env.COMP_CWORD
- point = +process.env.COMP_POINT
- words = argv.map unescape
- word = words[w]
- partialLine = line.substr 0, point
- partialWords = words.slice 0, w
- # figure out where in that last word the point is
- partialWord = argv[w] or ''
- i = partialWord.length
- while partialWord.substr(0, i) isnt partialLine.substr(-1 * i) and i > 0
- i--
- partialWord = unescape partialWord.substr 0, i
- if partialWord then partialWords.push partialWord
- {
- line: line
- w: w
- point: point
- words: words
- word: word
- partialLine: partialLine
- partialWords: partialWords
- partialWord: partialWord
- }
- complete = (cmd, opts) ->
- 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
- if m = opts.partialWord.match /^(--\w[\w-_]*)=(.*)$/
- 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
- if not (o = opts.partialWords[opts.w - 1]).indexOf '-'
- optWord = o
- # complete on opt values: completion
- if optWord and opt = cmd._optsByKey[optWord]
- if not opt._flag and opt._comp
- compls = Q.join compls, Q.when opt._comp(opts), (c, o) ->
- c.concat o.map (v) -> (optPrefix or '') + v
- # TODO: complete on args values (context aware, custom completion?)
- # custom completion on cmds
- if 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)
- Q.when compls, (compls) ->
- console.error 'partialWord: %s', opts.partialWord
- console.error 'compls: %j', compls
- compls.filter (c) -> c.indexOf(opts.partialWord) is 0
|