Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zsh auto completion #1292

Merged
merged 4 commits into from Feb 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions completion.zsh.hbs
@@ -0,0 +1,17 @@
###-begin-{{app_name}}-completions-###
#
# yargs command completion script
#
# Installation: {{app_path}} {{completion_command}} >> ~/.zshrc
# or {{app_path}} {{completion_command}} >> ~/.zsh_profile on OSX.
#
_{{app_name}}_yargs_completions()
{
local reply
local si=$IFS
IFS=$'\n' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" {{app_path}} --get-yargs-completions "$\{words[@]\}"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting, at the end of the day this isn't too far off bash completion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, not to much of a difference really.

IFS=$si
_describe 'values' reply
}
compdef _{{app_name}}_yargs_completions {{app_name}}
###-end-{{app_name}}-completions-###
10 changes: 5 additions & 5 deletions docs/api.md
Expand Up @@ -271,14 +271,14 @@ discussion of the advanced features exposed in the Command API.
.completion([cmd], [description], [fn])
---------------------------------------

Enable bash-completion shortcuts for commands and options.
Enable bash/zsh-completion shortcuts for commands and options.

`cmd`: When present in `argv._`, will result in the `.bashrc` completion script
being outputted. To enable bash completions, concat the generated script to your
`.bashrc` or `.bash_profile`.
`cmd`: When present in `argv._`, will result in the `.bashrc` or `.zshrc` completion script
being outputted. To enable bash/zsh completions, concat the generated script to your
`.bashrc` or `.bash_profile` (or `.zshrc` for zsh).

`description`: Provide a description in your usage instructions for the command
that generates bash completion scripts.
that generates the completion scripts.

`fn`: Rather than relying on yargs' default completion functionality, which
shiver me timbers is pretty awesome, you can provide your own completion
Expand Down
18 changes: 15 additions & 3 deletions lib/completion.js
Expand Up @@ -9,6 +9,7 @@ module.exports = function completion (yargs, usage, command) {
completionKey: 'get-yargs-completions'
}

const zshShell = process.env.SHELL && process.env.SHELL.indexOf('zsh') !== -1
// get a list of completion commands.
// 'args' is the array of strings from the line to be completed
self.getCompletion = function getCompletion (args, done) {
Expand Down Expand Up @@ -58,18 +59,29 @@ module.exports = function completion (yargs, usage, command) {
usage.getCommands().forEach((usageCommand) => {
const commandName = command.parseCommand(usageCommand[0]).cmd
if (args.indexOf(commandName) === -1) {
completions.push(commandName)
if (!zshShell) {
completions.push(commandName)
} else {
const desc = usageCommand[1] || ''
completions.push(commandName.replace(/:/g, '\\:') + ':' + desc)
}
}
})
}

if (current.match(/^-/)) {
const descs = usage.getDescriptions()
Object.keys(yargs.getOptions().key).forEach((key) => {
// If the key and its aliases aren't in 'args', add the key to 'completions'
const keyAndAliases = [key].concat(aliases[key] || [])
const notInArgs = keyAndAliases.every(val => args.indexOf(`--${val}`) === -1)
if (notInArgs) {
completions.push(`--${key}`)
if (!zshShell) {
completions.push(`--${key}`)
} else {
const desc = descs[key] || ''
completions.push(`--${key.replace(/:/g, '\\:')}:${desc}`)
}
}
})
}
Expand All @@ -80,7 +92,7 @@ module.exports = function completion (yargs, usage, command) {
// generate the completion script to add to your .bashrc.
self.generateCompletionScript = function generateCompletionScript ($0, cmd) {
let script = fs.readFileSync(
path.resolve(__dirname, '../completion.sh.hbs'),
path.resolve(__dirname, zshShell ? '../completion.zsh.hbs' : '../completion.sh.hbs'),
'utf-8'
)
const name = path.basename($0)
Expand Down
139 changes: 132 additions & 7 deletions test/completion.js
Expand Up @@ -14,7 +14,8 @@ describe('Completion', () => {
})

describe('default completion behavior', () => {
it('it returns a list of commands as completion suggestions', () => {
it('it returns a list of commands as completion suggestions (bash)', () => {
process.env.SHELL = '/bin/bash'
const r = checkUsage(() => yargs(['./completion', '--get-yargs-completions', ''])
.command('foo', 'bar')
.command('apple', 'banana')
Expand All @@ -26,7 +27,21 @@ describe('Completion', () => {
r.logs.should.include('foo')
})

it('avoids interruption from command recommendations', () => {
it('it returns a list of commands as completion suggestions (zsh)', () => {
process.env.SHELL = '/bin/zsh'
const r = checkUsage(() => yargs(['./completion', '--get-yargs-completions', ''])
.command('foo', 'bar')
.command('apple', 'banana')
.completion()
.argv
)

r.logs.should.include('apple:banana')
r.logs.should.include('foo:bar')
})

it('avoids interruption from command recommendations (bash)', () => {
process.env.SHELL = '/bin/bash'
const r = checkUsage(() =>
yargs(['./completion', '--get-yargs-completions', './completion', 'a'])
.command('apple', 'fruit')
Expand All @@ -41,7 +56,24 @@ describe('Completion', () => {
r.logs.should.include('aardvark')
})

it('avoids interruption from default command', () => {
it('avoids interruption from command recommendations (zsh)', () => {
process.env.SHELL = '/bin/zsh'
const r = checkUsage(() =>
yargs(['./completion', '--get-yargs-completions', './completion', 'a'])
.command('apple', 'fruit')
.command('aardvark', 'animal')
.recommendCommands()
.completion()
.argv
)

r.errors.should.deep.equal([])
r.logs.should.include('apple:fruit')
r.logs.should.include('aardvark:animal')
})

it('avoids interruption from default command (bash)', () => {
process.env.SHELL = '/bin/bash'
const r = checkUsage(() =>
yargs(['./usage', '--get-yargs-completions', './usage', ''])
.usage('$0 [thing]', 'skipped', subYargs => {
Expand All @@ -57,7 +89,25 @@ describe('Completion', () => {
r.logs.should.include('aardvark')
})

it('avoids interruption from default command (zsh)', () => {
process.env.SHELL = '/bin/zsh'
const r = checkUsage(() =>
yargs(['./usage', '--get-yargs-completions', './usage', ''])
.usage('$0 [thing]', 'skipped', subYargs => {
subYargs.command('aardwolf', 'is a thing according to google')
})
.command('aardvark', 'animal')
.completion()
.argv
)

r.errors.should.deep.equal([])
r.logs.should.not.include('aardwolf')
r.logs.should.include('aardvark:animal')
})

it('avoids repeating already included commands', () => {
process.env.SHELL = '/bin/bash'
const r = checkUsage(() => yargs(['./completion', '--get-yargs-completions', 'apple'])
.command('foo', 'bar')
.command('apple', 'banana')
Expand Down Expand Up @@ -96,7 +146,8 @@ describe('Completion', () => {
r.logs.should.not.include('--foo')
})

it('completes options for a command', () => {
it('completes options for a command (bash)', () => {
process.env.SHELL = '/bin/bash'
const r = checkUsage(() => yargs(['./completion', '--get-yargs-completions', 'foo', '--b'])
.command('foo', 'foo command', subYargs => subYargs.options({
bar: {
Expand All @@ -114,7 +165,27 @@ describe('Completion', () => {
r.logs.should.include('--help')
})

it('completes options for a command (zsh)', () => {
process.env.SHELL = '/bin/zsh'
const r = checkUsage(() => yargs(['./completion', '--get-yargs-completions', 'foo', '--b'])
.command('foo', 'foo command', subYargs => subYargs.options({
bar: {
describe: 'bar option'
}
})
.help(true)
.version(false))
.completion()
.argv
)

r.logs.should.have.length(2)
r.logs.should.include('--bar:bar option')
r.logs.should.include('--help:__yargsString__:Show help')
})

it('completes options for the correct command', () => {
process.env.SHELL = '/bin/bash'
const r = checkUsage(() => yargs(['./completion', '--get-yargs-completions', 'cmd2', '--o'])
.help(false)
.version(false)
Expand All @@ -141,6 +212,7 @@ describe('Completion', () => {
})

it('does not complete hidden commands', () => {
process.env.SHELL = '/bin/bash'
const r = checkUsage(() => yargs(['./completion', '--get-yargs-completions', 'cmd'])
.command('cmd1', 'first command')
.command('cmd2', false)
Expand All @@ -153,6 +225,7 @@ describe('Completion', () => {
})

it('does not include possitional arguments', function () {
process.env.SHELL = '/bin/bash'
var r = checkUsage(function () {
return yargs(['./completion', '--get-yargs-completions', 'cmd'])
.command('cmd1 [arg]', 'first command')
Expand All @@ -167,6 +240,7 @@ describe('Completion', () => {
})

it('works if command has no options', () => {
process.env.SHELL = '/bin/bash'
const r = checkUsage(() => yargs(['./completion', '--get-yargs-completions', 'foo', '--b'])
.help(false)
.version(false)
Expand All @@ -181,6 +255,7 @@ describe('Completion', () => {
})

it("returns arguments as completion suggestion, if next contains '-'", () => {
process.env.SHELL = '/bin/basg'
const r = checkUsage(() => yargs(['./usage', '--get-yargs-completions', '-f'])
.option('foo', {
describe: 'foo option'
Expand All @@ -196,10 +271,21 @@ describe('Completion', () => {
})

describe('generateCompletionScript()', () => {
it('replaces application variable with $0 in script', () => {
it('replaces application variable with $0 in script (bash)', () => {
process.env.SHELL = '/bin/bash'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this approach 👍

const r = checkUsage(() => yargs([])
.showCompletionScript(), ['ndm'])

r.logs[0].should.match(/bashrc/)
r.logs[0].should.match(/ndm --get-yargs-completions/)
})

it('replaces application variable with $0 in script (zsh)', () => {
process.env.SHELL = '/bin/zsh'
const r = checkUsage(() => yargs([])
.showCompletionScript(), ['ndm'])

r.logs[0].should.match(/zshrc/)
r.logs[0].should.match(/ndm --get-yargs-completions/)
})

Expand Down Expand Up @@ -299,8 +385,9 @@ describe('Completion', () => {
})
})

describe('getCompletion()', () => {
describe('getCompletion() - bash', () => {
it('returns default completion to callback', () => {
process.env.SHELL = '/bin/bash'
const r = checkUsage(() => {
yargs()
.command('foo', 'bar')
Expand All @@ -318,8 +405,29 @@ describe('Completion', () => {
})
})

describe('getCompletion() - zsh', () => {
it('returns default completion to callback', () => {
process.env.SHELL = '/bin/zsh'
const r = checkUsage(() => {
yargs()
.command('foo', 'bar')
.command('apple', 'banana')
.completion()
.getCompletion([''], (completions) => {
;(completions || []).forEach((completion) => {
console.log(completion)
})
})
})

r.logs.should.include('apple:banana')
r.logs.should.include('foo:bar')
})
})

// fixes for #177.
it('does not apply validation when --get-yargs-completions is passed in', () => {
it('does not apply validation when --get-yargs-completions is passed in (bash)', () => {
process.env.SHELL = '/bin/bash'
const r = checkUsage(() => {
try {
return yargs(['./completion', '--get-yargs-completions', '--'])
Expand All @@ -335,4 +443,21 @@ describe('Completion', () => {
r.errors.length.should.equal(0)
r.logs.should.include('--foo')
})
it('does not apply validation when --get-yargs-completions is passed in (zsh)', () => {
process.env.SHELL = '/bin/zsh'
const r = checkUsage(() => {
try {
return yargs(['./completion', '--get-yargs-completions', '--'])
.option('foo', {'describe': 'bar'})
.completion()
.strict()
.argv
} catch (e) {
console.log(e.message)
}
})

r.errors.length.should.equal(0)
r.logs.should.include('--foo:bar')
})
})
2 changes: 1 addition & 1 deletion yargs.js
Expand Up @@ -903,7 +903,7 @@ function Yargs (processArgs, cwd, parentRequire) {
// register the completion command.
completionCommand = cmd || 'completion'
if (!desc && desc !== false) {
desc = 'generate bash completion script'
desc = 'generate completion script'
}
self.command(completionCommand, desc)

Expand Down