// Each command has a completion function that takes an options object and a cb
// The callback gets called with an error and an array of possible completions.
// The options object is built up based on the environment variables set by
// zsh or bash when calling a function for completion, based on the cursor
// position and the command line thus far. These are:
// COMP_CWORD: the index of the "word" in the command line being completed
// COMP_LINE: the full command line thusfar as a string
// COMP_POINT: the cursor index at the point of triggering completion
//
// We parse the command line with nopt, like npm does, and then create an
// options object containing:
// words: array of words in the command line
// w: the index of the word being completed (ie, COMP_CWORD)
// word: the word being completed
// line: the COMP_LINE
// lineLength
// point: the COMP_POINT, usually equal to line length, but not always, eg if
// the user has pressed the left-arrow to complete an earlier word
// partialLine: the line up to the point
// partialWord: the word being completed (which might be ''), up to the point
// conf: a nopt parse of the command line
//
// When the implementation completion method returns its list of strings,
// and arrays of strings, we filter that by any that start with the
// partialWord, since only those can possibly be valid matches.
//
// Matches are wrapped with ' to escape them, if necessary, and then printed
// one per line for the shell completion method to consume in IFS=$'\n' mode
// as an array.
//
const fs = require('@npmcli/fs')
const nopt = require('nopt')
const { definitions, shorthands } = require('../utils/config/index.js')
const { aliases, commands, plumbing } = require('../utils/cmd-list.js')
const aliasNames = Object.keys(aliases)
const fullList = commands.concat(aliasNames).filter(c => !plumbing.includes(c))
const configNames = Object.keys(definitions)
const shorthandNames = Object.keys(shorthands)
const allConfs = configNames.concat(shorthandNames)
const { isWindowsShell } = require('../utils/is-windows.js')
const fileExists = async (file) => {
try {
const stat = await fs.stat(file)
return stat.isFile()
} catch {
return false
}
}
const BaseCommand = require('../base-command.js')
class Completion extends BaseCommand {
static description = 'Tab Completion for npm'
static name = 'completion'
static ignoreImplicitWorkspace = false
// completion for the completion command
async completion (opts) {
if (opts.w > 2) {
return
}
const { resolve } = require('path')
const [bashExists, zshExists] = await Promise.all([
fileExists(resolve(process.env.HOME, '.bashrc')),
fileExists(resolve(process.env.HOME, '.zshrc')),
])
const out = []
if (zshExists) {
out.push(['>>', '~/.zshrc'])
}
if (bashExists) {
out.push(['>>', '~/.bashrc'])
}
return out
}
async exec (args) {
if (isWindowsShell) {
const msg = 'npm completion supported only in MINGW / Git bash on Windows'
throw Object.assign(new Error(msg), {
code: 'ENOTSUP',
})
}
const { COMP_CWORD, COMP_LINE, COMP_POINT } = process.env
// if the COMP_* isn't in the env, then just dump the script.
if (COMP_CWORD === undefined ||
COMP_LINE === undefined ||
COMP_POINT === undefined) {
return dumpScript()
}
// ok we're actually looking at the envs and outputting the suggestions
// get the partial line and partial word,
// if the point isn't at the end.
// ie, tabbing at: npm foo b|ar
const w = +COMP_CWORD
const words = args.map(unescape)
const word = words[w]
const line = COMP_LINE
const point = +COMP_POINT
const partialLine = line.slice(0, point)
const partialWords = words.slice(0, w)
// figure out where in that last word the point is.
const partialWordRaw = args[w]
let i = partialWordRaw.length
while (partialWordRaw.slice(0, i) !== partialLine.slice(-1 * i) && i > 0) {
i--
}
const partialWord = unescape(partialWordRaw.slice(0, i))
partialWords.push(partialWord)
const opts = {
words,
w,
word,
line,
lineLength: line.length,
point,
partialLine,
partialWords,
partialWord,
raw: args,
}
if (partialWords.slice(0, -1).indexOf('--') === -1) {
if (word.charAt(0) === '-') {
return this.wrap(opts, configCompl(opts))
}
if (words[w - 1] &&
words[w - 1].charAt(0) === '-' &&
!isFlag(words[w - 1])) {
// awaiting a value for a non-bool config.
// don't even try to do this for now
return this.wrap(opts, configValueCompl(opts))
}
}
// try to find the npm command.
// it's the first thing after all the configs.
// take a little shortcut and use npm's arg parsing logic.
// don't have to worry about the last arg being implicitly
// boolean'ed, since the last block will catch that.
const types = Object.entries(definitions).reduce((types, [key, def]) => {
types[key] = def.type
return types
}, {})
const parsed = opts.conf =
nopt(types, shorthands, partialWords.slice(0, -1), 0)
// check if there's a command already.
const cmd = parsed.argv.remain[1]
if (!cmd) {
return this.wrap(opts, cmdCompl(opts, this.npm))
}
Object.keys(parsed).forEach(k => this.npm.config.set(k, parsed[k]))
// at this point, if words[1] is some kind of npm command,
// then complete on it.
// otherwise, do nothing
const impl = await this.npm.cmd(cmd)
if (impl.completion) {
const comps = await impl.completion(opts)
return this.wrap(opts, comps)
}
}
// The command should respond with an array. Loop over that,
// wrapping quotes around any that have spaces, and writing
// them to stdout.
// If any of the items are arrays, then join them with a space.
// Ie, returning ['a', 'b c', ['d', 'e']] would allow it to expand
// to: 'a', 'b c', or 'd' 'e'
wrap (opts, compls) {
// TODO this was dead code, leaving it in case we find some command we
// forgot that requires this. if so *that command should fix its
// completions*
// compls = compls.map(w => !/\s+/.test(w) ? w : '\'' + w + '\'')
if (opts.partialWord) {
compls = compls.filter(c => c.startsWith(opts.partialWord))
}
if (compls.length > 0) {
this.npm.output(compls.join('\n'))
}
}
}
const dumpScript = async () => {
const { resolve } = require('path')
const p = resolve(__dirname, '..', 'utils', 'completion.sh')
const d = (await fs.readFile(p, 'utf8')).replace(/^#!.*?\n/, '')
await new Promise((res, rej) => {
let done = false
process.stdout.on('error', er => {
if (done) {
return
}
done = true
// Darwin is a pain 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, `. <(npm completion)`
// can never ever work on OS X.
// TODO Ignoring coverage, see 'non EPIPE errors cause failures' test.
/* istanbul ignore next */
if (er.errno === 'EPIPE') {
res()
} else {
rej(er)
}
})
process.stdout.write(d, () => {
if (done) {
return
}
done = true
res()
})
})
}
const unescape = w => w.charAt(0) === '\'' ? w.replace(/^'|'$/g, '')
: w.replace(/\\ /g, ' ')
// the current word has a dash. Return the config names,
// with the same number of dashes as the current word has.
const configCompl = opts => {
const word = opts.word
const split = word.match(/^(-+)((?:no-)*)(.*)$/)
const dashes = split[1]
const no = split[2]
const flags = configNames.filter(isFlag)
return allConfs.map(c => dashes + c)
.concat(flags.map(f => dashes + (no || 'no-') + f))
}
// expand with the valid values of various config values.
// not yet implemented.
const configValueCompl = opts => []
// check if the thing is a flag or not.
const isFlag = word => {
// shorthands never take args.
const split = word.match(/^(-*)((?:no-)+)?(.*)$/)
const no = split[2]
const conf = split[3]
const { type } = definitions[conf]
return no ||
type === Boolean ||
(Array.isArray(type) && type.includes(Boolean)) ||
shorthands[conf]
}
// complete against the npm commands
// if they all resolve to the same thing, just return the thing it already is
const cmdCompl = (opts, npm) => {
const matches = fullList.filter(c => c.startsWith(opts.partialWord))
if (!matches.length) {
return matches
}
const derefs = new Set([...matches.map(c => npm.deref(c))])
if (derefs.size === 1) {
return [...derefs]
}
return fullList
}
module.exports = Completion
| Name | Type | Size | Permission | Actions |
|---|---|---|---|---|
| access.js | File | 5.45 KB | 0644 |
|
| adduser.js | File | 2.2 KB | 0644 |
|
| audit.js | File | 11.95 KB | 0644 |
|
| bin.js | File | 729 B | 0644 |
|
| birthday.js | File | 508 B | 0644 |
|
| bugs.js | File | 815 B | 0644 |
|
| cache.js | File | 7.08 KB | 0644 |
|
| ci.js | File | 3.63 KB | 0644 |
|
| completion.js | File | 8.91 KB | 0644 |
|
| config.js | File | 8.11 KB | 0644 |
|
| dedupe.js | File | 1.37 KB | 0644 |
|
| deprecate.js | File | 2.06 KB | 0644 |
|
| diff.js | File | 8.1 KB | 0644 |
|
| dist-tag.js | File | 5.47 KB | 0644 |
|
| docs.js | File | 447 B | 0644 |
|
| doctor.js | File | 9.22 KB | 0644 |
|
| edit.js | File | 2 KB | 0644 |
|
| exec.js | File | 2.44 KB | 0644 |
|
| explain.js | File | 3.55 KB | 0644 |
|
| explore.js | File | 2.33 KB | 0644 |
|
| find-dupes.js | File | 602 B | 0644 |
|
| fund.js | File | 6.37 KB | 0644 |
|
| get.js | File | 524 B | 0644 |
|
| help-search.js | File | 5.62 KB | 0644 |
|
| help.js | File | 4.53 KB | 0644 |
|
| hook.js | File | 3.93 KB | 0644 |
|
| init.js | File | 6.81 KB | 0644 |
|
| install-ci-test.js | File | 377 B | 0644 |
|
| install-test.js | File | 374 B | 0644 |
|
| install.js | File | 5.11 KB | 0644 |
|
| link.js | File | 5.02 KB | 0644 |
|
| ll.js | File | 234 B | 0644 |
|
| logout.js | File | 1.34 KB | 0644 |
|
| ls.js | File | 16.94 KB | 0644 |
|
| org.js | File | 4.2 KB | 0644 |
|
| outdated.js | File | 8.84 KB | 0644 |
|
| owner.js | File | 5.88 KB | 0644 |
|
| pack.js | File | 2.36 KB | 0644 |
|
| ping.js | File | 874 B | 0644 |
|
| pkg.js | File | 3.47 KB | 0644 |
|
| prefix.js | File | 343 B | 0644 |
|
| profile.js | File | 11.25 KB | 0644 |
|
| prune.js | File | 779 B | 0644 |
|
| publish.js | File | 6.33 KB | 0644 |
|
| query.js | File | 2.81 KB | 0644 |
|
| rebuild.js | File | 2.16 KB | 0644 |
|
| repo.js | File | 1.24 KB | 0644 |
|
| restart.js | File | 351 B | 0644 |
|
| root.js | File | 298 B | 0644 |
|
| run-script.js | File | 6.9 KB | 0644 |
|
| search.js | File | 2.72 KB | 0644 |
|
| set-script.js | File | 2.63 KB | 0644 |
|
| set.js | File | 572 B | 0644 |
|
| shrinkwrap.js | File | 2.64 KB | 0644 |
|
| star.js | File | 1.87 KB | 0644 |
|
| stars.js | File | 1.03 KB | 0644 |
|
| start.js | File | 341 B | 0644 |
|
| stop.js | File | 336 B | 0644 |
|
| team.js | File | 4.44 KB | 0644 |
|
| test.js | File | 336 B | 0644 |
|
| token.js | File | 6.79 KB | 0644 |
|
| uninstall.js | File | 1.52 KB | 0644 |
|
| unpublish.js | File | 4.51 KB | 0644 |
|
| unstar.js | File | 182 B | 0644 |
|
| update.js | File | 1.7 KB | 0644 |
|
| version.js | File | 3.6 KB | 0644 |
|
| view.js | File | 14.38 KB | 0644 |
|
| whoami.js | File | 514 B | 0644 |
|