diff --git a/README.md b/README.md index 1e19f985..b992616b 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ are supported. Alternative shells include: On **macOS** and **Linux,** the standard terminal window is supported. Optionally, `bash` command auto completion may be enabled by copying the -[balena-completion.bash](https://github.com/balena-io/balena-cli/blob/master/balena-completion.bash) +[balena_comp](https://github.com/balena-io/balena-cli/blob/master/completion/balena-completion.bash) file to your system's `bash_completion` directory: check [Docker's command completion guide](https://docs.docker.com/compose/completion/) for system setup instructions. diff --git a/completion/_balena b/completion/_balena new file mode 100644 index 00000000..eb0fab36 --- /dev/null +++ b/completion/_balena @@ -0,0 +1,79 @@ +#compdef balena +#autoload + +#GENERATED FILE DON'T MODIFY# + +_balena() { + typeset -A opt_args + local context state line curcontext="$curcontext" + + # Valid top-level completions + main_commands=( apps build deploy envs join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env internal key key local os tag util ) + # Sub-completions + api_key_cmds=( generate ) + app_cmds=( create purge rename restart rm ) + config_cmds=( generate inject read reconfigure write ) + device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown ) + devices_cmds=( supported ) + env_cmds=( add rename rm ) + internal_cmds=( osinit ) + key_cmds=( add rm ) + local_cmds=( configure flash ) + os_cmds=( build-config configure download initialize versions ) + tag_cmds=( rm set ) + + + _arguments -C \ + '(- 1 *)--version[show version and exit]' \ + '(- 1 *)'{-h,--help}'[show help options and exit]' \ + '1:first command:_balena_main_cmds' \ + '2:second command:_balena_sec_cmds' \ + && ret=0 +} + +(( $+functions[_balena_main_cmds] )) || +_balena_main_cmds() { + _describe -t main_commands 'command' main_commands "$@" && ret=0 +} + +(( $+functions[_balena_sec_cmds] )) || +_balena_sec_cmds() { + case $line[1] in + "api-key") + _describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0 + ;; + "app") + _describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0 + ;; + "config") + _describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0 + ;; + "device") + _describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0 + ;; + "devices") + _describe -t devices_cmds 'devices_cmd' devices_cmds "$@" && ret=0 + ;; + "env") + _describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0 + ;; + "internal") + _describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0 + ;; + "key") + _describe -t key_cmds 'key_cmd' key_cmds "$@" && ret=0 + ;; + "local") + _describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0 + ;; + "os") + _describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0 + ;; + "tag") + _describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0 + ;; + + esac +} + +_balena "$@" diff --git a/balena-completion.bash b/completion/balena-completion.bash similarity index 51% rename from balena-completion.bash rename to completion/balena-completion.bash index a1500b6c..1e9c7513 100644 --- a/balena-completion.bash +++ b/completion/balena-completion.bash @@ -1,24 +1,26 @@ #!/bin/bash +#GENERATED FILE DON'T MODIFY# + _balena_complete() { local cur prev # Valid top-level completions - commands="app apps build config deploy device devices env envs help key \ - keys local login logout logs note os preload quickstart settings \ - scan ssh util version whoami" + main_commands="apps build deploy envs join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env internal key key local os tag util" # Sub-completions - app_cmds="create restart rm" + api_key_cmds="generate" + app_cmds="create purge rename restart rm" config_cmds="generate inject read reconfigure write" - device_cmds="identify init move public-url reboot register rename rm \ - shutdown" - device_public_url_cmds="disable enable status" + device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown" + devices_cmds="supported" env_cmds="add rename rm" + internal_cmds="osinit" key_cmds="add rm" local_cmds="configure flash" os_cmds="build-config configure download initialize versions" - util_cmds="available-drives" + tag_cmds="rm set" + COMPREPLY=() @@ -27,43 +29,44 @@ _balena_complete() if [ $COMP_CWORD -eq 1 ] then - COMPREPLY=( $(compgen -W "${commands}" -- $cur) ) + COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) ) elif [ $COMP_CWORD -eq 2 ] then case "$prev" in - "app") + api-key) + COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) ) + ;; + app) COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) ) ;; - "config") + config) COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) ) ;; - "device") + device) COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) ) ;; - "env") + devices) + COMPREPLY=( $(compgen -W "$devices_cmds" -- $cur) ) + ;; + env) COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) ) ;; - "key") + internal) + COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) ) + ;; + key) COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) ) ;; - "local") + local) COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) ) ;; - "os") + os) COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) ) ;; - "util") - COMPREPLY=( $(compgen -W "$util_cmds" -- $cur) ) - ;; - "*") - ;; - esac - elif [ $COMP_CWORD -eq 3 ] - then - case "$prev" in - "public-url") - COMPREPLY=( $(compgen -W "$device_public_url_cmds" -- $cur) ) + tag) + COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) ) ;; + "*") ;; esac diff --git a/completion/generate-completion.js b/completion/generate-completion.js new file mode 100644 index 00000000..2d0e93a5 --- /dev/null +++ b/completion/generate-completion.js @@ -0,0 +1,175 @@ +/** + * @license + * Copyright 2021 Balena Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const path = require('path'); +const rootDir = path.join(__dirname, '..'); +const fs = require('fs'); +const manifestFile = 'oclif.manifest.json'; + +commandsFilePath = path.join(rootDir, manifestFile); +if (fs.existsSync(commandsFilePath)) { + console.log('Generating shell auto completion files...'); +} else { + console.error(`generate-completion.js: Could not find "${manifestFile}"`); + process.exitCode = 1; + return; +} + +const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8')); + +var mainCommands = []; +var additionalCommands = []; +for (const key of Object.keys(commandsJson.commands)) { + const cmd = key.split(':'); + if (cmd.length > 1) { + additionalCommands.push(cmd); + if (!mainCommands.includes(cmd[0])) { + mainCommands.push(cmd[0]); + } + } else { + mainCommands.push(cmd[0]); + } +} +const mainCommandsStr = mainCommands.join(' '); + +// GENERATE BASH COMPLETION FILE +bashFilePathIn = path.join(__dirname, '/templates/bash.template'); +bashFilePathOut = path.join(__dirname, 'balena-completion.bash'); + +try { + fs.unlinkSync(bashFilePathOut); +} catch (error) { + process.exitCode = 1; + return console.error(error); +} + +fs.readFile(bashFilePathIn, 'utf8', function (err, data) { + if (err) { + process.exitCode = 1; + return console.error(err); + } + + data = data.replace( + '#TEMPLATE FILE FOR BASH COMPLETION#', + "#GENERATED FILE DON'T MODIFY#", + ); + + data = data.replace( + /\$main_commands\$/g, + 'main_commands="' + mainCommandsStr + '"', + ); + var subCommands = []; + var prevElement = additionalCommands[0][0]; + additionalCommands.forEach(function (element) { + if (element[0] === prevElement) { + subCommands.push(element[1]); + } else { + const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds'; + data = data.replace( + /\$sub_cmds\$/g, + ' ' + prevElement2 + '="' + subCommands.join(' ') + '"\n$sub_cmds$', + ); + data = data.replace( + /\$sub_cmds_prev\$/g, + ' ' + + prevElement + + ')\n COMPREPLY=( $(compgen -W "$' + + prevElement2 + + '" -- $cur) )\n ;;\n$sub_cmds_prev$', + ); + prevElement = element[0]; + subCommands = []; + subCommands.push(element[1]); + } + }); + // cleanup placeholders + data = data.replace(/\$sub_cmds\$/g, ''); + data = data.replace(/\$sub_cmds_prev\$/g, ''); + + fs.writeFile(bashFilePathOut, data, 'utf8', function (error) { + if (error) { + process.exitCode = 1; + return console.error(error); + } + }); +}); + +// GENERATE ZSH COMPLETION FILE +zshFilePathIn = path.join(__dirname, '/templates/zsh.template'); +zshFilePathOut = path.join(__dirname, '_balena'); + +try { + fs.unlinkSync(zshFilePathOut); +} catch (error) { + process.exitCode = 1; + return console.error(error); +} + +fs.readFile(zshFilePathIn, 'utf8', function (err, data) { + if (err) { + process.exitCode = 1; + return console.error(err); + } + + data = data.replace( + '#TEMPLATE FILE FOR ZSH COMPLETION#', + "#GENERATED FILE DON'T MODIFY#", + ); + + data = data.replace( + /\$main_commands\$/g, + 'main_commands=( ' + mainCommandsStr + ' )', + ); + var subCommands = []; + var prevElement = additionalCommands[0][0]; + additionalCommands.forEach(function (element) { + if (element[0] === prevElement) { + subCommands.push(element[1]); + } else { + const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds'; + data = data.replace( + /\$sub_cmds\$/g, + ' ' + prevElement2 + '=( ' + subCommands.join(' ') + ' )\n$sub_cmds$', + ); + data = data.replace( + /\$sub_cmds_prev\$/g, + ' "' + + prevElement + + '")\n _describe -t ' + + prevElement2 + + " '" + + prevElement + + "_cmd' " + + prevElement2 + + ' "$@" && ret=0\n ;;\n$sub_cmds_prev$', + ); + prevElement = element[0]; + subCommands = []; + subCommands.push(element[1]); + } + }); + // cleanup placeholders + data = data.replace(/\$sub_cmds\$/g, ''); + data = data.replace(/\$sub_cmds_prev\$/g, ''); + + fs.writeFile(zshFilePathOut, data, 'utf8', function (error) { + if (error) { + process.exitCode = 1; + return console.error(error); + } + }); +}); diff --git a/completion/templates/bash.template b/completion/templates/bash.template new file mode 100644 index 00000000..0ca665c9 --- /dev/null +++ b/completion/templates/bash.template @@ -0,0 +1,32 @@ +#!/bin/bash + +#TEMPLATE FILE FOR BASH COMPLETION# + +_balena_complete() +{ + local cur prev + + # Valid top-level completions + $main_commands$ + # Sub-completions +$sub_cmds$ + + + COMPREPLY=() + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + if [ $COMP_CWORD -eq 1 ] + then + COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) ) + elif [ $COMP_CWORD -eq 2 ] + then + case "$prev" in +$sub_cmds_prev$ + "*") + ;; + esac + fi + +} +complete -F _balena_complete balena diff --git a/completion/templates/zsh.template b/completion/templates/zsh.template new file mode 100644 index 00000000..4676869f --- /dev/null +++ b/completion/templates/zsh.template @@ -0,0 +1,35 @@ +#compdef balena +#autoload + +#TEMPLATE FILE FOR ZSH COMPLETION# + +_balena() { + typeset -A opt_args + local context state line curcontext="$curcontext" + + # Valid top-level completions + $main_commands$ + # Sub-completions +$sub_cmds$ + + _arguments -C \ + '(- 1 *)--version[show version and exit]' \ + '(- 1 *)'{-h,--help}'[show help options and exit]' \ + '1:first command:_balena_main_cmds' \ + '2:second command:_balena_sec_cmds' \ + && ret=0 +} + +(( $+functions[_balena_main_cmds] )) || +_balena_main_cmds() { + _describe -t main_commands 'command' main_commands "$@" && ret=0 +} + +(( $+functions[_balena_sec_cmds] )) || +_balena_sec_cmds() { + case $line[1] in +$sub_cmds_prev$ + esac +} + +_balena "$@" diff --git a/doc/cli.markdown b/doc/cli.markdown index 0296718b..55a0ff6d 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -43,7 +43,7 @@ are supported. Alternative shells include: On **macOS** and **Linux,** the standard terminal window is supported. Optionally, `bash` command auto completion may be enabled by copying the -[balena-completion.bash](https://github.com/balena-io/balena-cli/blob/master/balena-completion.bash) +[balena_comp](https://github.com/balena-io/balena-cli/blob/master/completion/balena-completion.bash) file to your system's `bash_completion` directory: check [Docker's command completion guide](https://docs.docker.com/compose/completion/) for system setup instructions. diff --git a/package.json b/package.json index c1ae824a..d372695e 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,11 @@ "postinstall": "node patches/apply-patches.js", "prebuild": "rimraf build/ build-bin/", "build": "npm run build:src && npm run catch-uncommitted", - "build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc", + "build:src": "npm run lint && npm run build:fast && npm run build:test && npm run build:doc && npm run build:completion", "build:fast": "gulp pages && tsc && npx oclif-dev manifest", "build:test": "tsc -P ./tsconfig.dev.json --noEmit && tsc -P ./tsconfig.js.json --noEmit", "build:doc": "mkdirp doc/ && ts-node --transpile-only automation/capitanodoc/index.ts > doc/cli.markdown", + "build:completion": "node completion/generate-completion.js", "build:standalone": "ts-node --transpile-only automation/run.ts build:standalone", "build:installer": "ts-node --transpile-only automation/run.ts build:installer", "package": "npm run build:fast && npm run build:standalone && npm run build:installer", @@ -67,7 +68,7 @@ "catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted", "ci": "npm run test && npm run catch-uncommitted", "watch": "gulp watch", - "lint": "balena-lint -e ts -e js --typescript --fix automation/ lib/ typings/ tests/ bin/balena bin/balena-dev gulpfile.js .mocharc.js .mocharc-standalone.js", + "lint": "balena-lint -e ts -e js --typescript --fix automation/ completion/ lib/ typings/ tests/ bin/balena bin/balena-dev gulpfile.js .mocharc.js .mocharc-standalone.js", "update": "ts-node --transpile-only ./automation/update-module.ts", "prepare": "echo {} > bin/.fast-boot.json", "prepublishOnly": "npm run build"