diff --git a/completion.zsh.hbs b/completion.zsh.hbs new file mode 100644 index 000000000..ac6180586 --- /dev/null +++ b/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[@]\}")) + IFS=$si + _describe 'values' reply +} +compdef _{{app_name}}_yargs_completions {{app_name}} +###-end-{{app_name}}-completions-### \ No newline at end of file diff --git a/docs/api.md b/docs/api.md index e7d6411e1..877b06aec 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 diff --git a/lib/completion.js b/lib/completion.js index ad6969a2d..ef0986fb4 100644 --- a/lib/completion.js +++ b/lib/completion.js @@ -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) { @@ -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}`) + } } }) } @@ -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) diff --git a/test/completion.js b/test/completion.js index fb0ba3c71..dbf2fb904 100644 --- a/test/completion.js +++ b/test/completion.js @@ -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') @@ -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') @@ -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 => { @@ -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') @@ -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: { @@ -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) @@ -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) @@ -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') @@ -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) @@ -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' @@ -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' 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/) }) @@ -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') @@ -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', '--']) @@ -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') + }) }) diff --git a/yargs.js b/yargs.js index d7a62383f..1f8912629 100644 --- a/yargs.js +++ b/yargs.js @@ -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)