Skip to content

Commit

Permalink
feat(completion): allow to get completions for any string, not just p…
Browse files Browse the repository at this point in the history
…rocess.argv (#470)

* feat(completion): allow to get completions for any string, not just `process.argv`

Add new method `.getCompletion(args, done)` which receives an array of strings
representing a line to complete, and calls the `done` callback with the
resulting completions.

Update the completion tests to pass the arguments explicitly to yargs. We can't
pass them by mocking `process.argv` now because `process.argv` is only checked
on `index.js`, which is called before the tests can apply the mock.

* Fix indentation

* Fix indentation

* docs(completion): document .getCompletion() method
  • Loading branch information
elas7 authored and bcoe committed May 1, 2016
1 parent b900502 commit 74fcfbc
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 44 deletions.
23 changes: 23 additions & 0 deletions README.md
Expand Up @@ -887,6 +887,29 @@ var argv = require('yargs')
.argv
```

.getCompletion(args, done);
---------------------------

Allows to programmatically get completion choices for any line.

`args`: An array of the words in the command line to complete.

`done`: The callback to be called with the resulting completions.

For example:

```js
require('yargs')
.option('foobar', {})
.option('foobaz', {})
.completion()
.getCompletion(['./test.js', '--foo'], function (completions) {
console.log(completions)
})
```

Outputs the same completion choices as `./test.js --foo`<kbd>TAB</kbd>: `--foobar` and `--foobaz`

<a name="global"></a>.global(globals)
------------

Expand Down
24 changes: 12 additions & 12 deletions lib/completion.js
Expand Up @@ -9,11 +9,11 @@ module.exports = function (yargs, usage, command) {
}

// get a list of completion commands.
self.getCompletion = function (done) {
// 'args' is the array of strings from the line to be completed
self.getCompletion = function (args, done) {
var completions = []
var current = process.argv[process.argv.length - 1]
var previous = process.argv.slice(process.argv.indexOf('--' + self.completionKey) + 1)
var argv = yargs.parse(previous)
var current = args.length ? args[args.length - 1] : ''
var argv = yargs.parse(args, true)
var aliases = yargs.parsed.aliases

// a custom completion function can be provided
Expand Down Expand Up @@ -42,28 +42,28 @@ module.exports = function (yargs, usage, command) {
}

var handlers = command.getCommandHandlers()
for (var i = 0, ii = previous.length; i < ii; ++i) {
if (handlers[previous[i]] && handlers[previous[i]].builder) {
return handlers[previous[i]].builder(yargs.reset()).argv
for (var i = 0, ii = args.length; i < ii; ++i) {
if (handlers[args[i]] && handlers[args[i]].builder) {
return handlers[args[i]].builder(yargs.reset()).argv
}
}

if (!current.match(/^-/)) {
usage.getCommands().forEach(function (command) {
if (previous.indexOf(command[0]) === -1) {
if (args.indexOf(command[0]) === -1) {
completions.push(command[0])
}
})
}

if (current.match(/^-/)) {
Object.keys(yargs.getOptions().key).forEach(function (key) {
// If the key and its aliases aren't in 'previous', add the key to 'completions'
// If the key and its aliases aren't in 'args', add the key to 'completions'
var keyAndAliases = [key].concat(aliases[key] || [])
var notInPrevious = keyAndAliases.every(function (val) {
return previous.indexOf('--' + val) === -1
var notInArgs = keyAndAliases.every(function (val) {
return args.indexOf('--' + val) === -1
})
if (notInPrevious) {
if (notInArgs) {
completions.push('--' + key)
}
})
Expand Down
69 changes: 44 additions & 25 deletions test/completion.js
Expand Up @@ -15,62 +15,62 @@ describe('Completion', function () {
describe('default completion behavior', function () {
it('it returns a list of commands as completion suggestions', function () {
var r = checkUsage(function () {
return yargs(['--get-yargs-completions'])
return yargs(['./completion', '--get-yargs-completions', ''])
.command('foo', 'bar')
.command('apple', 'banana')
.completion()
.argv
}, ['./completion', '--get-yargs-completions', ''])
})

r.logs.should.include('apple')
r.logs.should.include('foo')
})

it('avoids repeating already included commands', function () {
var r = checkUsage(function () {
return yargs(['--get-yargs-completions'])
return yargs(['./completion', '--get-yargs-completions', 'apple'])
.command('foo', 'bar')
.command('apple', 'banana')
.argv
}, ['./completion', '--get-yargs-completions', 'apple'])
})

r.logs.should.include('foo')
r.logs.should.not.include('apple')
})

it('avoids repeating already included options', function () {
var r = checkUsage(function () {
return yargs(['--get-yargs-completions'])
return yargs(['./completion', '--get-yargs-completions', './completion', '--foo', '--'])
.options({
foo: {describe: 'foo option'},
bar: {describe: 'bar option'}
})
.completion()
.argv
}, ['./completion', '--get-yargs-completions', './completion', '--foo', '--'])
})

r.logs.should.include('--bar')
r.logs.should.not.include('--foo')
})

it('avoids repeating options whose aliases are already included', function () {
var r = checkUsage(function () {
return yargs(['--get-yargs-completions'])
return yargs(['./completion', '--get-yargs-completions', './completion', '--f', '--'])
.options({
foo: {describe: 'foo option', alias: 'f'},
bar: {describe: 'bar option'}
})
.completion()
.argv
}, ['./completion', '--get-yargs-completions', './completion', '--f', '--'])
})

r.logs.should.include('--bar')
r.logs.should.not.include('--foo')
})

it('completes options for a command', function () {
var r = checkUsage(function () {
return yargs(['--get-yargs-completions'])
return yargs(['./completion', '--get-yargs-completions', 'foo', '--b'])
.command('foo', 'foo command', function (subYargs) {
return subYargs.options({
bar: {
Expand All @@ -81,7 +81,7 @@ describe('Completion', function () {
})
.completion()
.argv
}, ['./completion', '--get-yargs-completions', 'foo', '--b'])
})

r.logs.should.have.length(2)
r.logs.should.include('--bar')
Expand All @@ -90,7 +90,7 @@ describe('Completion', function () {

it('completes options for the correct command', function () {
var r = checkUsage(function () {
return yargs(['--get-yargs-completions'])
return yargs(['./completion', '--get-yargs-completions', 'cmd2', '--o'])
.command('cmd1', 'first command', function (subYargs) {
subYargs.options({
opt1: {
Expand All @@ -109,48 +109,48 @@ describe('Completion', function () {
})
.completion()
.argv
}, ['./completion', '--get-yargs-completions', 'cmd2', '--o'])
})

r.logs.should.have.length(1)
r.logs.should.include('--opt2')
})

it('does not complete hidden commands', function () {
var r = checkUsage(function () {
return yargs(['--get-yargs-completions'])
return yargs(['./completion', '--get-yargs-completions', 'cmd'])
.command('cmd1', 'first command')
.command('cmd2', false)
.completion('completion', false)
.argv
}, ['./completion', '--get-yargs-completions', 'cmd'])
})

r.logs.should.have.length(1)
r.logs.should.include('cmd1')
})

it('works if command has no options', function () {
var r = checkUsage(function () {
return yargs(['--get-yargs-completions'])
return yargs(['./completion', '--get-yargs-completions', 'foo', '--b'])
.command('foo', 'foo command', function (subYargs) {
subYargs.completion().argv
})
.completion()
.argv
}, ['./completion', '--get-yargs-completions', 'foo', '--b'])
})

r.logs.should.have.length(0)
})

it("returns arguments as completion suggestion, if next contains '-'", function () {
var r = checkUsage(function () {
return yargs(['--get-yargs-completions'])
return yargs(['./usage', '--get-yargs-completions', '-f'])
.option('foo', {
describe: 'foo option'
})
.command('bar', 'bar command')
.completion()
.argv
}, ['./usage', '--get-yargs-completions', '-f'])
})

r.logs.should.include('--foo')
r.logs.should.not.include('bar')
Expand All @@ -170,7 +170,7 @@ describe('Completion', function () {
it('if $0 has a .js extension, a ./ prefix is added', function () {
var r = checkUsage(function () {
return yargs([])
.showCompletionScript()
.showCompletionScript()
}, ['test.js'])

r.logs[0].should.match(/\.\/test.js --get-yargs-completions/)
Expand All @@ -181,8 +181,8 @@ describe('Completion', function () {
it('shows completion script if command registered with completion(cmd) is called', function () {
var r = checkUsage(function () {
return yargs(['completion'])
.completion('completion')
.argv
.completion('completion')
.argv
}, ['ndm'])

r.logs[0].should.match(/ndm --get-yargs-completions/)
Expand All @@ -204,12 +204,12 @@ describe('Completion', function () {

it('passes current arg for completion and the parsed arguments thus far to custom function', function () {
var r = checkUsage(function () {
return yargs(['--get-yargs-completions'])
return yargs(['ndm', '--get-yargs-completions', '--cool', 'ma'])
.completion('completion', function (current, argv) {
if (current === 'ma' && argv.cool) return ['success!']
})
.argv
}, ['ndm', '--get-yargs-completions', '--cool', 'ma'])
})

r.logs.should.include('success!')
})
Expand Down Expand Up @@ -268,19 +268,38 @@ describe('Completion', function () {
})
})

describe('getCompletion()', function () {
it('returns default completion to callback', function () {
var r = checkUsage(function () {
yargs()
.command('foo', 'bar')
.command('apple', 'banana')
.completion()
.getCompletion([''], function (completions) {
;(completions || []).forEach(function (completion) {
console.log(completion)
})
})
})

r.logs.should.include('apple')
r.logs.should.include('foo')
})
})

// fixes for #177.
it('does not apply validation when --get-yargs-completions is passed in', function () {
var r = checkUsage(function () {
try {
return yargs(['--get-yargs-completions'])
return yargs(['./completion', '--get-yargs-completions', '--'])
.option('foo', {})
.completion()
.strict()
.argv
} catch (e) {
console.log(e.message)
}
}, ['./completion', '--get-yargs-completions', '--'])
})

r.errors.length.should.equal(0)
r.logs.should.include('--foo')
Expand Down
17 changes: 10 additions & 7 deletions yargs.js
Expand Up @@ -351,8 +351,8 @@ function Yargs (processArgs, cwd, parentRequire) {
return self
}

self.parse = function (args) {
return parseArgs(args)
self.parse = function (args, shortCircuit) {
return parseArgs(args, shortCircuit)
}

self.option = self.options = function (key, opt) {
Expand Down Expand Up @@ -555,6 +555,10 @@ function Yargs (processArgs, cwd, parentRequire) {
return self
}

self.getCompletion = function (args, done) {
completion.getCompletion(args, done)
}

self.locale = function (locale) {
if (arguments.length === 0) {
guessLocale()
Expand Down Expand Up @@ -611,7 +615,7 @@ function Yargs (processArgs, cwd, parentRequire) {
enumerable: true
})

function parseArgs (args) {
function parseArgs (args, shortCircuit) {
options.__ = y18n.__
options.configuration = pkgConf.sync('yargs', {
defaults: {},
Expand All @@ -629,9 +633,7 @@ function Yargs (processArgs, cwd, parentRequire) {
// while building up the argv object, there
// are two passes through the parser. If completion
// is being performed short-circuit on the first pass.
if (completionCommand &&
(process.argv.join(' ')).indexOf(completion.completionKey) !== -1 &&
!argv[completion.completionKey]) {
if (shortCircuit) {
return argv
}

Expand All @@ -658,7 +660,8 @@ function Yargs (processArgs, cwd, parentRequire) {
if (completion.completionKey in argv) {
// we allow for asynchronous completions,
// e.g., loading in a list of commands from an API.
completion.getCompletion(function (completions) {
var completionArgs = args.slice(args.indexOf('--' + completion.completionKey) + 1)
completion.getCompletion(completionArgs, function (completions) {
;(completions || []).forEach(function (completion) {
console.log(completion)
})
Expand Down

0 comments on commit 74fcfbc

Please sign in to comment.