From f058d06e901f6c48fc27efc2f6bf4ae2eb88f78b Mon Sep 17 00:00:00 2001 From: Trevor Linton Date: Sun, 3 Feb 2019 13:59:13 -0700 Subject: [PATCH 1/4] WIP zsh --- completion.zsh.hbs | 17 +++++++++++++++++ lib/completion.js | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 completion.zsh.hbs diff --git a/completion.zsh.hbs b/completion.zsh.hbs new file mode 100644 index 000000000..5cabf52b6 --- /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_name}} --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/lib/completion.js b/lib/completion.js index ad6969a2d..f2706a7d9 100644 --- a/lib/completion.js +++ b/lib/completion.js @@ -79,8 +79,10 @@ module.exports = function completion (yargs, usage, command) { // generate the completion script to add to your .bashrc. self.generateCompletionScript = function generateCompletionScript ($0, cmd) { + let zshShell = process.env.SHELL && process.env.SHELL.indexOf('zsh') !== -1 + 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) From 40be95dd34bd43dfe11169ed8db8044ad07688db Mon Sep 17 00:00:00 2001 From: Trevor Linton Date: Tue, 12 Feb 2019 11:10:40 -0700 Subject: [PATCH 2/4] Added initial skeleton test for completion script --- completion.zsh.hbs | 2 +- test/completion.js | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/completion.zsh.hbs b/completion.zsh.hbs index 5cabf52b6..ac6180586 100644 --- a/completion.zsh.hbs +++ b/completion.zsh.hbs @@ -9,7 +9,7 @@ _{{app_name}}_yargs_completions() { local reply local si=$IFS - IFS=$'\n' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" {{app_name}} --get-yargs-completions "$\{words[@]\}")) + IFS=$'\n' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" {{app_path}} --get-yargs-completions "$\{words[@]\}")) IFS=$si _describe 'values' reply } diff --git a/test/completion.js b/test/completion.js index fb0ba3c71..caa8543a8 100644 --- a/test/completion.js +++ b/test/completion.js @@ -196,10 +196,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/) }) From 48f62fa87e9d3fc28325cb83506503a8e5c52b2d Mon Sep 17 00:00:00 2001 From: Trevor Linton Date: Tue, 12 Feb 2019 13:52:19 -0700 Subject: [PATCH 3/4] Adding tests for zsh, adds description to zsh auto-complete --- lib/completion.js | 18 +++++-- test/completion.js | 126 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 134 insertions(+), 10 deletions(-) diff --git a/lib/completion.js b/lib/completion.js index f2706a7d9..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}`) + } } }) } @@ -79,8 +91,6 @@ module.exports = function completion (yargs, usage, command) { // generate the completion script to add to your .bashrc. self.generateCompletionScript = function generateCompletionScript ($0, cmd) { - let zshShell = process.env.SHELL && process.env.SHELL.indexOf('zsh') !== -1 - let script = fs.readFileSync( path.resolve(__dirname, zshShell ? '../completion.zsh.hbs' : '../completion.sh.hbs'), 'utf-8' diff --git a/test/completion.js b/test/completion.js index caa8543a8..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' @@ -310,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') @@ -329,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', '--']) @@ -346,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') + }) }) From aab038738ff6c59934a090acd7fbc7d3ac6dc138 Mon Sep 17 00:00:00 2001 From: Trevor Linton Date: Tue, 12 Feb 2019 14:05:16 -0700 Subject: [PATCH 4/4] Fixed docs and command name to include zsh as well as bash --- docs/api.md | 10 +++++----- yargs.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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/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)