Repository: yargs/yargs
Pull request #1292 (2019-02-12 18:12:03)

zsh auto completion


Patch:
diff --git a/completion.zsh.hbs b/completion.zsh.hbs
new file mode 100644
index 0000000..ac61805
--- /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 e7d6411..877b06a 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 ad6969a..ef0986f 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/yargs.js b/yargs.js
index d7a6238..1f89126 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)
 


Test patch:
diff --git a/test/completion.js b/test/completion.js
index fb0ba3c..dbf2fb9 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')
+  })
 })