completion.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. module.exports = completion
  2. completion.usage = 'source <(npm completion)'
  3. var npm = require('./npm.js')
  4. var npmconf = require('./config/core.js')
  5. var configDefs = npmconf.defs
  6. var configTypes = configDefs.types
  7. var shorthands = configDefs.shorthands
  8. var nopt = require('nopt')
  9. var configNames = Object.keys(configTypes)
  10. .filter(function (e) { return e.charAt(0) !== '_' })
  11. var shorthandNames = Object.keys(shorthands)
  12. var allConfs = configNames.concat(shorthandNames)
  13. var once = require('once')
  14. var isWindowsShell = require('./utils/is-windows-shell.js')
  15. var output = require('./utils/output.js')
  16. completion.completion = function (opts, cb) {
  17. if (opts.w > 3) return cb()
  18. var fs = require('graceful-fs')
  19. var path = require('path')
  20. var bashExists = null
  21. var zshExists = null
  22. fs.stat(path.resolve(process.env.HOME, '.bashrc'), function (er) {
  23. bashExists = !er
  24. next()
  25. })
  26. fs.stat(path.resolve(process.env.HOME, '.zshrc'), function (er) {
  27. zshExists = !er
  28. next()
  29. })
  30. function next () {
  31. if (zshExists === null || bashExists === null) return
  32. var out = []
  33. if (zshExists) out.push('~/.zshrc')
  34. if (bashExists) out.push('~/.bashrc')
  35. if (opts.w === 2) {
  36. out = out.map(function (m) {
  37. return ['>>', m]
  38. })
  39. }
  40. cb(null, out)
  41. }
  42. }
  43. function completion (args, cb) {
  44. if (isWindowsShell) {
  45. var e = new Error('npm completion supported only in MINGW / Git bash on Windows')
  46. e.code = 'ENOTSUP'
  47. e.errno = require('constants').ENOTSUP // eslint-disable-line node/no-deprecated-api
  48. return cb(e)
  49. }
  50. // if the COMP_* isn't in the env, then just dump the script.
  51. if (process.env.COMP_CWORD === undefined ||
  52. process.env.COMP_LINE === undefined ||
  53. process.env.COMP_POINT === undefined) {
  54. return dumpScript(cb)
  55. }
  56. console.error(process.env.COMP_CWORD)
  57. console.error(process.env.COMP_LINE)
  58. console.error(process.env.COMP_POINT)
  59. // get the partial line and partial word,
  60. // if the point isn't at the end.
  61. // ie, tabbing at: npm foo b|ar
  62. var w = +process.env.COMP_CWORD
  63. var words = args.map(unescape)
  64. var word = words[w]
  65. var line = process.env.COMP_LINE
  66. var point = +process.env.COMP_POINT
  67. var partialLine = line.substr(0, point)
  68. var partialWords = words.slice(0, w)
  69. // figure out where in that last word the point is.
  70. var partialWord = args[w]
  71. var i = partialWord.length
  72. while (partialWord.substr(0, i) !== partialLine.substr(-1 * i) && i > 0) {
  73. i--
  74. }
  75. partialWord = unescape(partialWord.substr(0, i))
  76. partialWords.push(partialWord)
  77. var opts = {
  78. words: words,
  79. w: w,
  80. word: word,
  81. line: line,
  82. lineLength: line.length,
  83. point: point,
  84. partialLine: partialLine,
  85. partialWords: partialWords,
  86. partialWord: partialWord,
  87. raw: args
  88. }
  89. cb = wrapCb(cb, opts)
  90. console.error(opts)
  91. if (partialWords.slice(0, -1).indexOf('--') === -1) {
  92. if (word.charAt(0) === '-') return configCompl(opts, cb)
  93. if (words[w - 1] &&
  94. words[w - 1].charAt(0) === '-' &&
  95. !isFlag(words[w - 1])) {
  96. // awaiting a value for a non-bool config.
  97. // don't even try to do this for now
  98. console.error('configValueCompl')
  99. return configValueCompl(opts, cb)
  100. }
  101. }
  102. // try to find the npm command.
  103. // it's the first thing after all the configs.
  104. // take a little shortcut and use npm's arg parsing logic.
  105. // don't have to worry about the last arg being implicitly
  106. // boolean'ed, since the last block will catch that.
  107. var parsed = opts.conf =
  108. nopt(configTypes, shorthands, partialWords.slice(0, -1), 0)
  109. // check if there's a command already.
  110. console.error(parsed)
  111. var cmd = parsed.argv.remain[1]
  112. if (!cmd) return cmdCompl(opts, cb)
  113. Object.keys(parsed).forEach(function (k) {
  114. npm.config.set(k, parsed[k])
  115. })
  116. // at this point, if words[1] is some kind of npm command,
  117. // then complete on it.
  118. // otherwise, do nothing
  119. cmd = npm.commands[cmd]
  120. if (cmd && cmd.completion) return cmd.completion(opts, cb)
  121. // nothing to do.
  122. cb()
  123. }
  124. function dumpScript (cb) {
  125. var fs = require('graceful-fs')
  126. var path = require('path')
  127. var p = path.resolve(__dirname, 'utils/completion.sh')
  128. // The Darwin patch below results in callbacks first for the write and then
  129. // for the error handler, so make sure we only call our callback once.
  130. cb = once(cb)
  131. fs.readFile(p, 'utf8', function (er, d) {
  132. if (er) return cb(er)
  133. d = d.replace(/^#!.*?\n/, '')
  134. process.stdout.write(d, function () { cb() })
  135. process.stdout.on('error', function (er) {
  136. // Darwin is a pain sometimes.
  137. //
  138. // This is necessary because the "source" or "." program in
  139. // bash on OS X closes its file argument before reading
  140. // from it, meaning that you get exactly 1 write, which will
  141. // work most of the time, and will always raise an EPIPE.
  142. //
  143. // Really, one should not be tossing away EPIPE errors, or any
  144. // errors, so casually. But, without this, `. <(npm completion)`
  145. // can never ever work on OS X.
  146. if (er.errno === 'EPIPE') er = null
  147. cb(er)
  148. })
  149. })
  150. }
  151. function unescape (w) {
  152. if (w.charAt(0) === '\'') return w.replace(/^'|'$/g, '')
  153. else return w.replace(/\\ /g, ' ')
  154. }
  155. function escape (w) {
  156. if (!w.match(/\s+/)) return w
  157. return '\'' + w + '\''
  158. }
  159. // The command should respond with an array. Loop over that,
  160. // wrapping quotes around any that have spaces, and writing
  161. // them to stdout. Use console.log, not the outfd config.
  162. // If any of the items are arrays, then join them with a space.
  163. // Ie, returning ['a', 'b c', ['d', 'e']] would allow it to expand
  164. // to: 'a', 'b c', or 'd' 'e'
  165. function wrapCb (cb, opts) {
  166. return function (er, compls) {
  167. if (!Array.isArray(compls)) compls = compls ? [compls] : []
  168. compls = compls.map(function (c) {
  169. if (Array.isArray(c)) c = c.map(escape).join(' ')
  170. else c = escape(c)
  171. return c
  172. })
  173. if (opts.partialWord) {
  174. compls = compls.filter(function (c) {
  175. return c.indexOf(opts.partialWord) === 0
  176. })
  177. }
  178. console.error([er && er.stack, compls, opts.partialWord])
  179. if (er || compls.length === 0) return cb(er)
  180. output(compls.join('\n'))
  181. cb()
  182. }
  183. }
  184. // the current word has a dash. Return the config names,
  185. // with the same number of dashes as the current word has.
  186. function configCompl (opts, cb) {
  187. var word = opts.word
  188. var split = word.match(/^(-+)((?:no-)*)(.*)$/)
  189. var dashes = split[1]
  190. var no = split[2]
  191. var flags = configNames.filter(isFlag)
  192. console.error(flags)
  193. return cb(null, allConfs.map(function (c) {
  194. return dashes + c
  195. }).concat(flags.map(function (f) {
  196. return dashes + (no || 'no-') + f
  197. })))
  198. }
  199. // expand with the valid values of various config values.
  200. // not yet implemented.
  201. function configValueCompl (opts, cb) {
  202. console.error('configValue', opts)
  203. return cb(null, [])
  204. }
  205. // check if the thing is a flag or not.
  206. function isFlag (word) {
  207. // shorthands never take args.
  208. var split = word.match(/^(-*)((?:no-)+)?(.*)$/)
  209. var no = split[2]
  210. var conf = split[3]
  211. return no || configTypes[conf] === Boolean || shorthands[conf]
  212. }
  213. // complete against the npm commands
  214. function cmdCompl (opts, cb) {
  215. return cb(null, npm.fullList)
  216. }