From 723ff201f62a9c232c2afdf2b8e27ce61c7ce164 Mon Sep 17 00:00:00 2001 From: Munif Tanjim Date: Tue, 26 May 2020 19:17:41 +0600 Subject: [PATCH] add completion generation for zsh (#137) --- src/commands/completion.command.ts | 135 +++++++++++++++++++++++++++++ src/program.ts | 22 +++++ 2 files changed, 157 insertions(+) create mode 100644 src/commands/completion.command.ts diff --git a/src/commands/completion.command.ts b/src/commands/completion.command.ts new file mode 100644 index 0000000000..289377c608 --- /dev/null +++ b/src/commands/completion.command.ts @@ -0,0 +1,135 @@ +import * as program from "commander"; +import { Response } from "jslib/cli/models/response"; +import { MessageResponse } from "jslib/cli/models/response/messageResponse"; + +type Option = { + long: string; + short: string; + description: string; +}; + +type Command = { + commands?: Command[]; + options?: Option[]; + _name: string; + _description: string; +}; + +const zshCompletion = (rootName: string, rootCommand: Command) => { + const renderCommandBlock = (name: string, command: Command): string => { + const { commands = [], options = [] } = command; + const hasOptions = options.length > 0; + const hasCommands = commands.length > 0; + + const _arguments = options + .map(({ long, short, description }) => { + const aliases = [short, long].filter(Boolean); + + const OPTS = aliases.join(","); + + const DESCRIPTION = `[${description.replace("'", `'"'"'`)}]`; + + return aliases.length > 1 + ? `'(${aliases.join(" ")})'{${OPTS}}'${DESCRIPTION}'` + : `'${OPTS}${DESCRIPTION}'`; + }) + .concat( + `'(-h --help)'{-h,--help}'[output usage information]'`, + hasCommands ? '"1: :->cmnds"' : null, + '"*::arg:->args"' + ) + .filter(Boolean); + + const commandBlockFunctionParts = []; + + if (hasCommands) { + commandBlockFunctionParts.push("local -a commands"); + } + + if (hasOptions) { + commandBlockFunctionParts.push( + `_arguments -C \\\n ${_arguments.join(` \\\n `)}` + ); + } + + if (hasCommands) { + commandBlockFunctionParts.push( + `case $state in + cmnds) + commands=( + ${commands + .map(({ _name, _description }) => `"${_name}:${_description}"`) + .join("\n ")} + ) + _describe "command" commands + ;; + esac + + case "$words[1]" in + ${commands + .map(({ _name }) => + [`${_name})`, `_${name}_${_name}`, ";;"].join("\n ") + ) + .join("\n ")} + esac` + ); + } + + const commandBlocParts = [ + `function _${name} {\n ${commandBlockFunctionParts.join( + "\n\n " + )}\n}`, + ]; + + if (hasCommands) { + commandBlocParts.push( + commands + .map((command) => + renderCommandBlock(`${name}_${command._name}`, command) + ) + .join("\n\n") + ); + } + + return commandBlocParts.join("\n\n"); + }; + + const render = () => { + return [ + `#compdef _${rootName} ${rootName}`, + "", + renderCommandBlock(rootName, rootCommand), + ].join("\n"); + }; + + return { + render, + }; +}; + +const validShells = ["zsh"]; + +export class CompletionCommand { + constructor() {} + + async run(cmd: program.Command) { + const shell: typeof validShells[number] = cmd.shell; + + if (!shell) { + return Response.badRequest("`shell` was not provided!"); + } + + if (!validShells.includes(shell)) { + return Response.badRequest(`Unsupported shell!`); + } + + let content = ""; + + if (shell === "zsh") { + content = zshCompletion("bw", cmd.parent).render(); + } + + const res = new MessageResponse(content, null); + return Response.success(res); + } +} diff --git a/src/program.ts b/src/program.ts index 6f8a40c557..beb1d7791b 100644 --- a/src/program.ts +++ b/src/program.ts @@ -21,6 +21,8 @@ import { ShareCommand } from './commands/share.command'; import { SyncCommand } from './commands/sync.command'; import { UnlockCommand } from './commands/unlock.command'; +import { CompletionCommand } from './commands/completion.command'; + import { LogoutCommand } from 'jslib/cli/commands/logout.command'; import { UpdateCommand } from 'jslib/cli/commands/update.command'; @@ -660,6 +662,26 @@ export class Program extends BaseProgram { this.processResponse(response); }); + program + .command('completion') + .description('Generate shell completions.') + .option('--shell ', 'Shell to generate completions for.') + .on('--help', () => { + writeLn('\n Notes:'); + writeLn(''); + writeLn(' Valid shells are `zsh`.') + writeLn(''); + writeLn(' Examples:'); + writeLn(''); + writeLn(' bw completion --shell zsh'); + writeLn('', true); + }) + .action(async (cmd: program.Command) => { + const command = new CompletionCommand(); + const response = await command.run(cmd); + this.processResponse(response); + }) + program .parse(process.argv);