completion.coffee 5.4 KB

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