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