├── .changelogrc.js ├── .commitlintrc.js ├── .cz.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .huskyrc.js ├── .lintstagedrc.js ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── run └── run.cmd ├── package.json ├── src ├── base.ts ├── commands │ └── completion │ │ ├── generate │ │ ├── alias.ts │ │ └── index.ts │ │ └── index.ts ├── index.ts └── utils │ ├── bash.ts │ ├── fish.ts │ ├── get-bin-aliases.ts │ ├── template.ts │ └── zsh.ts ├── tsconfig.json └── yarn.lock /.changelogrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | preset: { 4 | name: 'conventionalcommits', 5 | types: [ 6 | { type: 'feat', section: 'Features' }, 7 | { type: 'fix', section: 'Bug Fixes' }, 8 | { type: 'perf', section: 'Performance Improvements' }, 9 | { type: 'revert', section: 'Reverts' }, 10 | { type: 'docs', section: 'Documentation', hidden: true }, 11 | { type: 'style', section: 'Styles', hidden: true }, 12 | { 13 | type: 'chore', 14 | section: 'Miscellaneous Chores', 15 | hidden: true, 16 | }, 17 | { type: 'refactor', section: 'Code Refactoring', hidden: true }, 18 | { type: 'test', section: 'Tests', hidden: true }, 19 | { type: 'build', section: 'Build System', hidden: true }, 20 | { type: 'ci', section: 'Continuous Integration', hidden: true }, 21 | ], 22 | }, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /.cz.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "@commitlint/prompt" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Build directories 2 | lib 3 | 4 | # Dependency directories 5 | node_modules 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'oclif', 4 | 'oclif-typescript', 5 | 'plugin:prettier/recommended', 6 | 'prettier/@typescript-eslint', 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Build directories 9 | lib 10 | 11 | # Dependency directories 12 | node_modules 13 | 14 | # oclif 15 | oclif.manifest.json 16 | 17 | # Others 18 | coverage 19 | .nyc_output 20 | package-lock.json 21 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | const tasks = (taskList) => taskList.join(' && ') 2 | 3 | module.exports = { 4 | hooks: { 5 | 'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS', 6 | 'pre-commit': tasks(['lint-staged', 'pretty-quick --staged']), 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{ts}': () => 'tsc -p tsconfig.json --noEmit', 3 | '*.{ts}': ['eslint'], 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Build directories 2 | lib 3 | 4 | # Dependency directories 5 | node_modules 6 | 7 | # Autogenerated files 8 | CHANGELOG.md 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | endOfLine: 'lf', 3 | semi: false, 4 | singleQuote: true, 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 (2020-05-27) 2 | 3 | 4 | ### Features 5 | 6 | * add completion for zsh ([0dd134f](https://github.com/MunifTanjim/oclif-plugin-completion/commit/0dd134f7e823dab2723a0e81c78858a30c32e355)) 7 | 8 | ## [0.2.0](https://github.com/MunifTanjim/oclif-plugin-completion/compare/0.1.0...0.2.0) (2020-06-21) 9 | 10 | 11 | ### Features 12 | 13 | * add basic template util ([1d36e9f](https://github.com/MunifTanjim/oclif-plugin-completion/commit/1d36e9f671acfa822287e5708bca2ec8d4b5cc65)) 14 | * add completion for bash ([833797c](https://github.com/MunifTanjim/oclif-plugin-completion/commit/833797cd691b563b2c921f7f78492f07196cc778)) 15 | 16 | ## [0.3.0](https://github.com/MunifTanjim/oclif-plugin-completion/compare/0.2.0...0.3.0) (2020-06-21) 17 | 18 | 19 | ### Features 20 | 21 | * add completion for fish ([575dffe](https://github.com/MunifTanjim/oclif-plugin-completion/commit/575dffe70ca8a8bd49d74ebf51ab495a8355d65b)) 22 | * make all flags multiple for zsh ([56c7436](https://github.com/MunifTanjim/oclif-plugin-completion/commit/56c74360a5c5410c4f9f0e242134de6bfe1551b9)) 23 | 24 | ### [0.3.1](https://github.com/MunifTanjim/oclif-plugin-completion/compare/0.3.0...0.3.1) (2020-06-30) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * zsh completion ([c51386e](https://github.com/MunifTanjim/oclif-plugin-completion/commit/c51386e9bb5c8182cad3c1df8413d2f4b02143a4)) 30 | 31 | ### [0.3.2](https://github.com/MunifTanjim/oclif-plugin-completion/compare/0.3.1...0.3.2) (2020-07-07) 32 | 33 | ## [0.4.0](https://github.com/MunifTanjim/oclif-plugin-completion/compare/0.3.2...0.4.0) (2021-03-20) 34 | 35 | 36 | ### Features 37 | 38 | * integrate usage guide ([2af585c](https://github.com/MunifTanjim/oclif-plugin-completion/commit/2af585ccd00b569f9d4fa1cd26665c6c670c42fa)) 39 | 40 | ### [0.4.1](https://github.com/MunifTanjim/oclif-plugin-completion/compare/0.4.0...0.4.1) (2021-03-20) 41 | 42 | 43 | ### Features 44 | 45 | * update usage guide ([16d4a98](https://github.com/MunifTanjim/oclif-plugin-completion/commit/16d4a98e5f0bd4ea5585320912ceb7412073b84b)) 46 | 47 | ## [0.5.0](https://github.com/MunifTanjim/oclif-plugin-completion/compare/0.4.1...0.5.0) (2021-03-20) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * escape description string for fish properly ([52aff4a](https://github.com/MunifTanjim/oclif-plugin-completion/commit/52aff4af19e49ff0e4fbd6fb16904afab448413c)) 53 | 54 | ### [0.5.1](https://github.com/MunifTanjim/oclif-plugin-completion/compare/0.5.0...0.5.1) (2021-03-30) 55 | 56 | 57 | ### Features 58 | 59 | * update description ([1a39c43](https://github.com/MunifTanjim/oclif-plugin-completion/commit/1a39c4329c163b6a6cfc1ec261a30f21470b5ef9)) 60 | 61 | ## [0.6.0](https://github.com/MunifTanjim/oclif-plugin-completion/compare/0.5.1...0.6.0) (2021-03-30) 62 | 63 | 64 | ### Features 65 | 66 | * add support for command aliases ([62db201](https://github.com/MunifTanjim/oclif-plugin-completion/commit/62db20150c65b4d24a3bdec099e20b459072d7a1)) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * typo in description ([708e310](https://github.com/MunifTanjim/oclif-plugin-completion/commit/708e31053062e15979d5d340e5a6c9a1f686acf4)) 72 | 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Munif Tanjim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![oclif Plugin](https://img.shields.io/badge/oclif-plugin-brightgreen?style=for-the-badge)](https://oclif.io) 2 | [![Version](https://img.shields.io/npm/v/oclif-plugin-completion?style=for-the-badge)](https://npmjs.org/package/oclif-plugin-completion) 3 | [![License](https://img.shields.io/npm/l/oclif-plugin-completion?style=for-the-badge)](https://github.com/MunifTanjim/oclif-plugin-completion/blob/master/LICENSE) 4 | 5 | # oclif Plugin: completion 6 | 7 | oclif plugin for generating shell completions 8 | 9 | 10 | 11 | - [oclif Plugin: completion](#oclif-plugin-completion) 12 | - [Completion Features](#completion-features) 13 | - [Supported Shells](#supported-shells) 14 | - [Commands](#commands) 15 | 16 | 17 | # Completion Features 18 | 19 | Consider the following `dummy` CLI: 20 | 21 | ```sh 22 | Usage: dummy [OPTION...] 23 | 24 | Commands: 25 | 26 | open open a dummy (as read-only or read-write) 27 | save save a dummy 28 | search search a dummy 29 | 30 | Options (open): 31 | 32 | -i, --id dummy id 33 | -f, --file path to dummy file 34 | -d, --dir path to dummy directory (default: ./dummies) 35 | -v, --verbose boolean flag 36 | 37 | Options (save): 38 | 39 | -n, --name dummy name 40 | -a, --age dummy age 41 | -t, --tag dummy tag, can be multiple (a/b/c/d) 42 | -f, --file path to dummy file 43 | -d, --dir path to dummy directory (default: ./dummies) 44 | -o, --overwrite boolean flag 45 | -v, --verbose boolean flag 46 | 47 | Options (search): 48 | 49 | -n, --name dummy name 50 | -a, --age dummy age 51 | -t, --tag dummy tag, can be multiple (a/b/c/d) 52 | -v, --verbose boolean flag 53 | ``` 54 | 55 | Running on the current directory with tree: 56 | 57 | ```sh 58 | |- dir-one/ 59 | | |- 042.dummy-with-id-042.json 60 | |- dir-two/ 61 | |- dummies/ 62 | | |- 109.dummy-with-id-109.json 63 | | |- 110.dummy-with-id-110.json 64 | | |- 111.dummy-with-id-111.json 65 | |- file-one.txt 66 | |- file-two.txt 67 | ``` 68 | 69 | ## Features Description 70 | 71 | **File Path completion**: 72 | 73 | Completion will suggest the files on disk matching glob pattern. 74 | 75 | **Directory Path completion**: 76 | 77 | Completion will suggest the directories on disk matching glob pattern. 78 | 79 | **Dynamic Runtime completion**: 80 | 81 | Completion will generate the suggestion based on state of runtime environment and/or configuration. 82 | 83 | ## Features Examples 84 | 85 | | Feature | Input | Output | 86 | | -------------------------- | ------------------------------------ | ---------------------------- | 87 | | File Path completion | `dummy open --file=./dir/one/` | `042.dummy-with-id-042.json` | 88 | | Directory Path completion | `dummy open --dir ./di` | `dir-one dir-two` | 89 | | Dynamic Runtime completion | `dummy open --id ` | `109 110 111` | 90 | | Dynamic Runtime completion | `dummy open -d ./dir-one --id ` | `042` | 91 | 92 | ## Feature Support Matrix 93 | 94 | | :+1: | :-1: | :grey_exclamation: | :bug: | :heavy_check_mark: | :heavy_minus_sign: | :x: | 95 | | --------- | ----------- | ------------------ | ----- | ------------------ | --------------------- | --------------- | 96 | | Supported | Unsupported | Unknown | Bug | Implemented | Partially Implemented | Not Implemented | 97 | 98 | | oclif | Feature | Example | Bash | Zsh | Fish | 99 | | ---------- | ---------------------------- | ---------------------------------- | ----------------------- | ----------------------- | ----------------------- | 100 | | :+1: | Positional argument | `ro` | :grey_exclamation: :x: | :+1: :heavy_check_mark: | :grey_exclamation: :x: | 101 | | :+1: | Basic Long | `--name john --age 42 --overwrite` | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | 102 | | :+1: | Alternate Long | `--name=john --age=42` | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | 103 | | :+1: | Basic Short | `-n john -a 42 -o` | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | 104 | | :+1: | Alternative Short | `-njohn -a42` | :+1: :x: | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | 105 | | :+1: | Stacking Short | `-ov` | :grey_exclamation: :x: | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | 106 | | :+1: | Stacking Short with argument | `-ova 42` | :grey_exclamation: :x: | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | 107 | | :+1: | Options / Enum | `--tag a` | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | :+1: :heavy_check_mark: | 108 | | :+1: :bug: | Multiple | `-t c --tag d` | :+1: :heavy_minus_sign: | :+1: :heavy_minus_sign: | :+1: :heavy_minus_sign: | 109 | | :-1: | File Path completion | `--file ...` | :+1: :heavy_minus_sign: | :+1: :x: | :+1: :heavy_minus_sign: | 110 | | :-1: | Directory Path completion | `--dir ...` | :+1: :heavy_minus_sign: | :+1: :x: | :+1: :heavy_minus_sign: | 111 | | :-1: | Dynamic Runtime completion | `--dir ./dummies --id 111` | :+1: :x: | :+1: :x: | :+1: :x: | 112 | 113 | # Supported Shells 114 | 115 | ## Bash 116 | 117 | Reference: [Bash Completion](https://github.com/scop/bash-completion) 118 | 119 | You need to have `bash-completion` package installed on your system. 120 | 121 | ### Bash Usage 122 | 123 | You can enable completion for Bash using various methods. A few of them are mentioned below: 124 | 125 | **vanilla (.bashrc)**: 126 | 127 | Add the following line in your `.bashrc` file: 128 | 129 | ```sh 130 | eval "$(dummy completion:generate --shell bash);" 131 | ``` 132 | 133 | **vanilla (completions directory)**: 134 | 135 | Run the following command: 136 | 137 | ```sh 138 | dummy completion:generate --shell bash | tee ~/.local/share/bash-completion/completions/dummy 139 | ``` 140 | 141 | Depending on you system, the completion script can also be put into one of these directories: 142 | 143 | - `${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions` (linux/macos) 144 | - `/usr/local/share/bash-completion/completions` (macos) 145 | - `/usr/share/bash-completion/completions` (linux) 146 | 147 | ## Zsh 148 | 149 | Reference: [Zsh Completion System](http://zsh.sourceforge.net/Doc/Release/Completion-System.html) 150 | 151 | ### Zsh Usage 152 | 153 | You can enable completion for Zsh using various methods. A few of them are mentioned below: 154 | 155 | **vanilla (.zshrc)**: 156 | 157 | Add the following line in your `.zshrc` file: 158 | 159 | ```sh 160 | eval "$(dummy completion:generate --shell zsh); compdef _dummy dummy;" 161 | ``` 162 | 163 | **vanilla (site-functions directory)**: 164 | 165 | Run the following command: 166 | 167 | ```sh 168 | dummy completion:generate --shell zsh | tee "$(echo ${FPATH} | tr ':' '\n' | grep site-functions | head -n1)/_dummy" 169 | ``` 170 | 171 | The completion script can also be put into one of the directories present in `$FPATH` variable: 172 | 173 | ```sh 174 | echo $FPATH 175 | ``` 176 | 177 | [**zinit**](https://github.com/zdharma/zinit): 178 | 179 | Run the following commands: 180 | 181 | ```sh 182 | dummy completion:generate --shell zsh > ~/.local/share/zsh/completions/_dummy 183 | zinit creinstall ~/.local/share/zsh/completions 184 | ``` 185 | 186 | ## Fish 187 | 188 | Reference: [Fish Completion](https://fishshell.com/docs/current/cmds/complete.html) 189 | 190 | ### Fish Usage 191 | 192 | Reference: [Where to put completions](https://fishshell.com/docs/current/index.html#where-to-put-completions) 193 | 194 | You can enable completion for Fish using various methods. A few of them are mentioned below: 195 | 196 | **vanilla (completions directory)**: 197 | 198 | Run the following command: 199 | 200 | ```sh 201 | dummy completion:generate --shell fish | tee ~/.config/fish/completions/dummy.fish 202 | ``` 203 | 204 | # Commands 205 | 206 | 207 | 208 | - [`dummy completion`](#dummy-completion) 209 | - [`dummy completion:generate`](#dummy-completiongenerate) 210 | - [`dummy completion:generate:alias ALIAS`](#dummy-completiongeneratealias-alias) 211 | 212 | ## `dummy completion` 213 | 214 | Generate shell completion script 215 | 216 | ``` 217 | USAGE 218 | $ dummy completion 219 | 220 | OPTIONS 221 | -s, --shell=bash|fish|zsh (required) Name of shell 222 | 223 | DESCRIPTION 224 | Run this command to see instructions for your shell. 225 | 226 | EXAMPLE 227 | $ dummy completion --shell zsh 228 | ``` 229 | 230 | _See code: [src/commands/completion/index.ts](https://github.com/MunifTanjim/oclif-plugin-completion/blob/0.6.0/src/commands/completion/index.ts)_ 231 | 232 | ## `dummy completion:generate` 233 | 234 | Generates completion script 235 | 236 | ``` 237 | USAGE 238 | $ dummy completion:generate 239 | 240 | OPTIONS 241 | -s, --shell=bash|fish|zsh (required) Name of shell 242 | 243 | DESCRIPTION 244 | Run the "completion" command to see instructions about how to use the script generated by this command. 245 | 246 | EXAMPLE 247 | $ dummy completion:generate --shell zsh 248 | ``` 249 | 250 | _See code: [src/commands/completion/generate/index.ts](https://github.com/MunifTanjim/oclif-plugin-completion/blob/0.6.0/src/commands/completion/generate/index.ts)_ 251 | 252 | ## `dummy completion:generate:alias ALIAS` 253 | 254 | Generates completion script for alias 255 | 256 | ``` 257 | USAGE 258 | $ dummy completion:generate:alias ALIAS 259 | 260 | ARGUMENTS 261 | ALIAS name of the alias 262 | 263 | OPTIONS 264 | -s, --shell=bash|fish (required) Name of shell 265 | 266 | DESCRIPTION 267 | This needs the completion script for the main command to be present. 268 | 269 | Check the "completion:generate" command. 270 | ``` 271 | 272 | _See code: [src/commands/completion/generate/alias.ts](https://github.com/MunifTanjim/oclif-plugin-completion/blob/0.6.0/src/commands/completion/generate/alias.ts)_ 273 | 274 | 275 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command').run() 4 | .catch(require('@oclif/errors/handle')) 5 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oclif-plugin-completion", 3 | "version": "0.6.0", 4 | "description": "oclif plugin for generating shell completions", 5 | "keywords": [ 6 | "oclif", 7 | "oclif-plugin", 8 | "shell", 9 | "zsh", 10 | "completion", 11 | "autocomplete" 12 | ], 13 | "homepage": "https://github.com/MunifTanjim/oclif-plugin-completion#readme", 14 | "bugs": "https://github.com/MunifTanjim/oclif-plugin-completion/issues", 15 | "license": "MIT", 16 | "author": "Munif Tanjim (https://muniftanjim.dev)", 17 | "files": [ 18 | "/lib", 19 | "/oclif.manifest.json", 20 | "/yarn.lock" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/MunifTanjim/oclif-plugin-completion" 25 | }, 26 | "scripts": { 27 | "prebuild": "rm -rf lib", 28 | "build": "tsc -b", 29 | "changelog": "conventional-changelog --config .changelogrc.js -i CHANGELOG.md -s -a", 30 | "postchangelog": "git add CHANGELOG.md", 31 | "cz": "npx git-cz", 32 | "prepack": "npm run build && oclif-dev manifest", 33 | "postpack": "rm -f oclif.manifest.json", 34 | "readme": "oclif-dev readme", 35 | "postreadme": "prettier --write README.md && git add README.md", 36 | "prerelease": "git commit -m \"chore: release $npm_package_version\" && git tag $npm_package_version -am $npm_package_version", 37 | "release": "yarn publish", 38 | "test": "echo NO TESTS", 39 | "posttest": "eslint . --ext .ts --config .eslintrc.js", 40 | "preversion": "yarn config set version-git-tag false", 41 | "version": "npm run changelog && npm run readme", 42 | "postversion": "git add package.json" 43 | }, 44 | "dependencies": { 45 | "@oclif/command": "^1", 46 | "@oclif/config": "^1", 47 | "tslib": "^2" 48 | }, 49 | "devDependencies": { 50 | "@commitlint/cli": "^8.3.5", 51 | "@commitlint/config-conventional": "^8.3.4", 52 | "@commitlint/prompt-cli": "^8.3.5", 53 | "@oclif/dev-cli": "^1", 54 | "@oclif/plugin-help": "^3", 55 | "@types/node": "^14.0", 56 | "commitizen": "^4.1.2", 57 | "conventional-changelog-cli": "^2.0.34", 58 | "conventional-changelog-conventionalcommits": "^4.3.1", 59 | "eslint": "^6.8", 60 | "eslint-config-oclif": "^3.1", 61 | "eslint-config-oclif-typescript": "^0.1", 62 | "eslint-config-prettier": "^6.10", 63 | "eslint-plugin-prettier": "^3.1", 64 | "husky": "^4.2", 65 | "lint-staged": "^10.2", 66 | "prettier": "^2.0", 67 | "pretty-quick": "^2.0", 68 | "ts-node": "^8.6", 69 | "typescript": "^3.8" 70 | }, 71 | "peerDependencies": { 72 | "@oclif/errors": "^1.2" 73 | }, 74 | "engines": { 75 | "node": ">=8.0.0" 76 | }, 77 | "oclif": { 78 | "commands": "./lib/commands", 79 | "bin": "dummy", 80 | "devPlugins": [ 81 | "@oclif/plugin-help" 82 | ], 83 | "repositoryPrefix": "<%- repo %>/blob/<%- version %>/<%- commandPath %>" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/base.ts: -------------------------------------------------------------------------------- 1 | import Command, { flags } from '@oclif/command' 2 | import { basename } from 'path' 3 | import { getBinAliases } from './utils/get-bin-aliases' 4 | 5 | export abstract class CompletionBase extends Command { 6 | aliases: string[] = [] 7 | 8 | static flags = { 9 | shell: flags.string({ 10 | description: 'Name of shell', 11 | char: 's', 12 | env: 'SHELL', 13 | parse: (shell) => basename(shell), 14 | options: ['bash', 'fish', 'zsh'], 15 | required: true, 16 | }), 17 | } 18 | 19 | async init() { 20 | this.aliases.push( 21 | ...getBinAliases({ 22 | bin: this.config.bin, 23 | // this is okay, `@oclif/config` package's `PJSON.CLI` type is too restrictive 24 | // @ts-expect-error 25 | pjson: this.config.pjson, 26 | }) 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/completion/generate/alias.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { CompletionBase } from '../../../base' 3 | import { generateCompletionAliasScriptForBash } from '../../../utils/bash' 4 | import { generateCompletionAliasScriptForFish } from '../../../utils/fish' 5 | 6 | export default class CompletionGenerateAlias extends CompletionBase { 7 | static description = [ 8 | `Generates completion script for alias`, 9 | ``, 10 | `This needs the completion script for the main command to be present.`, 11 | ``, 12 | `Check the "completion:generate" command.`, 13 | ].join('\n') 14 | 15 | static args = [ 16 | { 17 | name: 'ALIAS', 18 | required: true, 19 | description: 'name of the alias', 20 | }, 21 | ] 22 | 23 | static flags = { 24 | ...CompletionBase.flags, 25 | shell: flags.string({ 26 | ...CompletionBase.flags.shell, 27 | options: CompletionBase.flags.shell.options?.filter( 28 | (shell) => shell !== 'zsh' 29 | ), 30 | required: true, 31 | }), 32 | } 33 | 34 | async run() { 35 | const { args, flags } = this.parse(CompletionGenerateAlias) 36 | const shell = flags.shell 37 | 38 | const { bin } = this.config 39 | const alias = args.ALIAS 40 | 41 | if (bin === alias) { 42 | return this.error(`ALIAS can not be ${bin}`, { exit: 1 }) 43 | } 44 | 45 | let scriptContent = '' 46 | 47 | if (shell === 'bash') { 48 | scriptContent = generateCompletionAliasScriptForBash({ bin, alias }) 49 | } 50 | 51 | if (shell === 'fish') { 52 | scriptContent = generateCompletionAliasScriptForFish({ bin, alias }) 53 | } 54 | 55 | if (shell === 'zsh') { 56 | this.error(`not needed for ${shell}`, { exit: 1 }) 57 | } 58 | 59 | this.log(scriptContent) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/commands/completion/generate/index.ts: -------------------------------------------------------------------------------- 1 | import { CompletionBase } from '../../../base' 2 | import { generateCompletionScriptForBash } from '../../../utils/bash' 3 | import { generateCompletionScriptForFish } from '../../../utils/fish' 4 | import { generateCompletionScriptForZsh } from '../../../utils/zsh' 5 | 6 | export default class CompletionGenerate extends CompletionBase { 7 | static description = [ 8 | `Generates completion script`, 9 | ``, 10 | `Run the "completion" command to see instructions about how to use the script generated by this command.`, 11 | ].join('\n') 12 | 13 | static args = [] 14 | 15 | static flags = { 16 | ...CompletionBase.flags, 17 | } 18 | 19 | static examples = ['$ <%= config.bin %> completion:generate --shell zsh'] 20 | 21 | async run() { 22 | const { flags } = this.parse(CompletionGenerate) 23 | const shell = flags.shell 24 | 25 | const aliases = this.aliases 26 | const { bin, commands } = this.config 27 | 28 | let scriptContent = '' 29 | 30 | if (shell === 'bash') { 31 | scriptContent = generateCompletionScriptForBash({ 32 | bin, 33 | aliases, 34 | commands, 35 | }) 36 | } 37 | 38 | if (shell === 'fish') { 39 | scriptContent = generateCompletionScriptForFish({ 40 | bin, 41 | aliases, 42 | commands, 43 | }) 44 | } 45 | 46 | if (shell === 'zsh') { 47 | scriptContent = generateCompletionScriptForZsh({ 48 | bin, 49 | aliases, 50 | commands, 51 | }) 52 | } 53 | 54 | this.log(scriptContent) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/completion/index.ts: -------------------------------------------------------------------------------- 1 | import { CompletionBase } from '../../base' 2 | import { getInstructionsForBash } from '../../utils/bash' 3 | import { getInstructionsForFish } from '../../utils/fish' 4 | import { getInstructionsForZsh } from '../../utils/zsh' 5 | 6 | export default class Completion extends CompletionBase { 7 | static description = [ 8 | `Generate shell completion script`, 9 | ``, 10 | `Run this command to see instructions for your shell.`, 11 | ].join('\n') 12 | 13 | static args = [] 14 | 15 | static flags = { 16 | ...CompletionBase.flags, 17 | } 18 | 19 | static examples = ['$ <%= config.bin %> completion --shell zsh'] 20 | 21 | async run() { 22 | const { flags } = this.parse(Completion) 23 | const shell = flags.shell 24 | 25 | const aliases = this.aliases 26 | const { bin } = this.config 27 | 28 | let instructions: string[] = [] 29 | 30 | if (shell === 'bash') { 31 | instructions = getInstructionsForBash({ bin, shell, aliases }) 32 | } 33 | 34 | if (shell === 'fish') { 35 | instructions = getInstructionsForFish({ bin, shell, aliases }) 36 | } 37 | 38 | if (shell === 'zsh') { 39 | instructions = getInstructionsForZsh({ bin, shell }) 40 | } 41 | 42 | this.log(instructions.join('\n')) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /src/utils/bash.ts: -------------------------------------------------------------------------------- 1 | import type { Command } from '@oclif/command' 2 | import { template } from './template' 3 | 4 | const getBootstrap = (rootCommandName: string) => { 5 | const bootstrapTemplate = template` 6 | __${'bin'}_debug() 7 | { 8 | if [[ -n \${BASH_COMP_DEBUG_FILE} ]]; then 9 | echo "$*" >> "\${BASH_COMP_DEBUG_FILE}" 10 | fi 11 | } 12 | 13 | # Homebrew on Macs have version 1.3 of bash-completion which doesn't include 14 | # _init_completion. This is a very minimal version of that function. 15 | __${'bin'}_init_completion() 16 | { 17 | COMPREPLY=() 18 | _get_comp_words_by_ref "$@" cur prev words cword 19 | } 20 | 21 | __${'bin'}_index_of_word() 22 | { 23 | local w word=$1 24 | shift 25 | index=0 26 | for w in "$@"; do 27 | [[ $w = "$word" ]] && return 28 | index=$((index+1)) 29 | done 30 | index=-1 31 | } 32 | 33 | __${'bin'}_contains_word() 34 | { 35 | local w word=$1; shift 36 | for w in "$@"; do 37 | [[ $w = "$word" ]] && return 38 | done 39 | return 1 40 | } 41 | 42 | __${'bin'}_filter_flag() 43 | { 44 | local flag 45 | 46 | for inserted_flag in "\${inserted_flags[@]}"; do 47 | for i in "\${!COMPREPLY[@]}"; do 48 | flag="\${COMPREPLY[i]%%=*}" 49 | if [[ $flag = $inserted_flag ]]; then 50 | if ! __${'bin'}_contains_word $flag "\${multi_flags[@]}"; then 51 | __${'bin'}_debug "\${FUNCNAME[0]}: \${COMPREPLY[i]}" 52 | unset 'COMPREPLY[i]' 53 | fi 54 | fi 55 | done 56 | done 57 | 58 | COMPREPLY=("\${COMPREPLY[@]}") 59 | } 60 | 61 | __${'bin'}_handle_reply() 62 | { 63 | local comp 64 | 65 | __${'bin'}_debug "\${FUNCNAME[0]}: c is $c words[c] is \${words[c]} cur is $cur" 66 | 67 | case $cur in 68 | -*) 69 | if [[ $(type -t compopt) = "builtin" ]]; then 70 | compopt -o nospace 71 | fi 72 | 73 | local allflags=("\${flags[*]}") 74 | 75 | while IFS='' read -r comp; do 76 | COMPREPLY+=("$comp") 77 | done < <(compgen -W "\${allflags[*]}" -- "$cur") 78 | 79 | if [[ $(type -t compopt) = "builtin" ]]; then 80 | [[ "\${COMPREPLY[0]}" == *= ]] || compopt +o nospace 81 | fi 82 | 83 | __${'bin'}_filter_flag 84 | 85 | # complete after --flag=abc 86 | if [[ $cur == *=* ]]; then 87 | COMPREPLY=() 88 | 89 | if [[ $(type -t compopt) = "builtin" ]]; then 90 | compopt +o nospace 91 | fi 92 | 93 | local index flag 94 | flag="\${cur%%=*}" 95 | __${'bin'}_index_of_word "$flag" "\${option_flags[@]}" 96 | 97 | __${'bin'}_debug "\${FUNCNAME[0]}: flag is $flag index is $index" 98 | 99 | if [[ \${index} -ge 0 ]]; then 100 | cur="\${cur#*=}" 101 | 102 | local option_flag_handler="\${option_flag_handlers[\${index}]}" 103 | $option_flag_handler 104 | fi 105 | fi 106 | 107 | return 108 | ;; 109 | esac 110 | 111 | local index 112 | __${'bin'}_index_of_word "$prev" "\${option_flags[@]}" 113 | 114 | __${'bin'}_debug "\${FUNCNAME[0]}: flag is $flag index is $index" 115 | 116 | if [[ \${index} -ge 0 ]]; then 117 | local option_flag_handler="\${option_flag_handlers[\${index}]}" 118 | $option_flag_handler 119 | return 120 | fi 121 | 122 | local completions 123 | 124 | completions=("\${commands[@]}") 125 | 126 | while IFS='' read -r comp; do 127 | COMPREPLY+=("$comp") 128 | done < <(compgen -W "\${completions[*]}" -- "$cur") 129 | 130 | __${'bin'}_filter_flag 131 | 132 | # available in bash-completion >= 2, not always present on macOS 133 | if declare -F __ltrim_colon_completions >/dev/null; then 134 | __ltrim_colon_completions "$cur" 135 | fi 136 | 137 | # If there is only 1 completion and it is a flag with an = it will be completed 138 | # but we don't want a space after the = 139 | if [[ "\${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "\${COMPREPLY[0]}" == --*= ]]; then 140 | compopt -o nospace 141 | fi 142 | } 143 | 144 | __${'bin'}_handle_flag() 145 | { 146 | 147 | __${'bin'}_debug "\${FUNCNAME[0]}: c is $c words[c] is \${words[c]}" 148 | 149 | # skip the argument to option flag without = 150 | if [[ \${words[c]} != *"="* ]] && __${'bin'}_contains_word "\${words[c]}" "\${option_flags[@]}"; then 151 | __${'bin'}_debug "\${FUNCNAME[0]}: found a flag \${words[c]}, skip the next argument" 152 | 153 | c=$((c+1)) 154 | 155 | # if we are looking for a flags value, don't show commands 156 | if [[ $c -eq $cword ]]; then 157 | commands=() 158 | fi 159 | fi 160 | 161 | c=$((c+1)) 162 | } 163 | 164 | __${'bin'}_handle_command() 165 | { 166 | __${'bin'}_debug "\${FUNCNAME[0]}: c is $c words[c] is \${words[c]}" 167 | 168 | local next_command 169 | if [[ -n \${last_command} ]]; then 170 | next_command="_\${last_command}_\${words[c]//:/__}" 171 | else 172 | if [[ $c -eq 0 ]]; then 173 | next_command="_${'bin'}" 174 | else 175 | next_command="_\${words[c]//:/__}" 176 | fi 177 | fi 178 | 179 | c=$((c+1)) 180 | 181 | __${'bin'}_debug "\${FUNCNAME[0]}: looking for \${next_command}" 182 | 183 | declare -F "$next_command" >/dev/null && $next_command 184 | } 185 | 186 | __${'bin'}_handle_word() 187 | { 188 | __${'bin'}_debug "\${FUNCNAME[0]}: c is $c words[c] is \${words[c]}" 189 | 190 | if [[ -z "\${BASH_VERSION}" || "\${BASH_VERSINFO[0]}" -gt 3 ]]; then 191 | if __${'bin'}_contains_word "\${words[c]}" "\${command_aliases[@]}"; then 192 | __${'bin'}_debug "\${FUNCNAME[0]}: words[c] is \${words[c]} -> \${command_by_alias[$\{words[c]}]}" 193 | 194 | words[c]=\${command_by_alias[$\{words[c]}]} 195 | fi 196 | fi 197 | 198 | if [[ $c -ge $cword ]]; then 199 | __${'bin'}_handle_reply 200 | 201 | __${'bin'}_debug "\${FUNCNAME[0]}: COMPREPLY is \${COMPREPLY[@]}" 202 | return 203 | fi 204 | 205 | if [[ "\${words[c]}" == -* ]]; then 206 | __${'bin'}_handle_flag 207 | elif __${'bin'}_contains_word "\${words[c]}" "\${commands[@]}"; then 208 | __${'bin'}_handle_command 209 | elif __${'bin'}_contains_word "\${words[c]}" "\${command_aliases[@]}"; then 210 | __${'bin'}_handle_command 211 | elif [[ $c -eq 0 ]]; then 212 | __${'bin'}_handle_command 213 | else 214 | c=$((c+1)) 215 | fi 216 | 217 | __${'bin'}_handle_word 218 | } 219 | ` 220 | 221 | return bootstrapTemplate({ bin: rootCommandName }) 222 | } 223 | 224 | const getInit = (bin: string, aliases: string[]) => { 225 | const initFunctionTemplate = template` 226 | __${'bin'}_init() 227 | { 228 | __${'bin'}_debug "" 229 | 230 | local cur prev words cword 231 | 232 | if declare -F _init_completion >/dev/null 2>&1; then 233 | _init_completion -n ":" -n "=" || return 234 | else 235 | __${'bin'}_init_completion -n ":" -n "=" || return 236 | fi 237 | 238 | __${'bin'}_debug "\${FUNCNAME[0]}: words is \${words[@]}" 239 | 240 | local c=0 241 | local last_command 242 | 243 | local commands=("${'bin'}") 244 | local command_aliases=() 245 | declare -A command_by_alias 2>/dev/null || : 246 | 247 | local args=() 248 | 249 | local flags=() 250 | 251 | local multi_flags=() 252 | local option_flags=() 253 | local option_flag_handlers=() 254 | 255 | local inserted_flags=() 256 | 257 | __${'bin'}_handle_word 258 | } 259 | ` 260 | 261 | const initTemplate = template` 262 | if [[ $(type -t compopt) = "builtin" ]]; then 263 | ${'initForBuiltin'} 264 | else 265 | ${'init'} 266 | fi 267 | ` 268 | 269 | const commandNames = [bin, ...aliases] 270 | 271 | const parts = [ 272 | initFunctionTemplate({ bin }).trim(), 273 | initTemplate({ 274 | init: commandNames 275 | .map((name) => { 276 | return template`complete -o default -o nospace -F __${'bin'}_init ${'name'}`( 277 | { bin, name } 278 | ) 279 | }) 280 | .join('\n '), 281 | initForBuiltin: commandNames 282 | .map((name) => { 283 | return template`complete -o default -F __${'bin'}_init ${'name'}`({ 284 | bin, 285 | name, 286 | }) 287 | }) 288 | .join('\n '), 289 | }).trim(), 290 | ] 291 | 292 | return parts.join('\n'.repeat(2)) 293 | } 294 | 295 | export function generateCompletionScriptForBash({ 296 | bin, 297 | aliases, 298 | commands, 299 | }: Pick & { aliases: string[] }) { 300 | const scriptParts = [] 301 | 302 | scriptParts.push(getBootstrap(bin)) 303 | 304 | commands.forEach((command) => { 305 | let commandName = [bin, command.id].join(' ') 306 | commandName = commandName?.replace(/ /g, '_') 307 | commandName = commandName?.replace(/:/g, '__') 308 | 309 | const parts: string[] = [] 310 | 311 | for (const [name, flag] of Object.entries(command.flags)) { 312 | if (flag.type === 'option' && flag.options) { 313 | parts.push(`_${commandName}___flag_options--${name}()`) 314 | parts.push(`{`) 315 | parts.push(` local options=()`) 316 | for (const option of flag.options) { 317 | parts.push(` options+=("${option}")`) 318 | } 319 | parts.push(` COMPREPLY=( $( compgen -W "\${options[*]}" -- "$cur" ) )`) 320 | parts.push(`}`) 321 | } 322 | } 323 | 324 | parts.push(`_${commandName}()`) 325 | parts.push(`{`) 326 | 327 | parts.push(` last_command=${commandName}`) 328 | 329 | parts.push(` commands=()`) 330 | parts.push(` command_aliases=()`) 331 | 332 | parts.push(` args=()`) 333 | 334 | parts.push(` flags=()`) 335 | parts.push(` flag_aliases=()`) 336 | 337 | parts.push(` multi_flags=()`) 338 | parts.push(` option_flags=()`) 339 | parts.push(` option_flag_handlers=()`) 340 | parts.push(` required_flags=()`) 341 | 342 | parts.push(` inserted_flags=()`) 343 | 344 | for (const arg of command.args) { 345 | if (arg.hidden) { 346 | continue 347 | } 348 | } 349 | 350 | for (const [name, flag] of Object.entries(command.flags)) { 351 | if (flag.hidden) { 352 | continue 353 | } 354 | 355 | parts.push(` flags+=("--${name}")`) 356 | 357 | if (flag.char) { 358 | parts.push(` flags+=("-${flag.char}")`) 359 | } 360 | 361 | if (flag.type === 'option') { 362 | // TODO: need upstream fix. `flag.multiple` property does not exist 363 | // Upstream PR: https://github.com/oclif/config/pull/113 364 | // @ts-expect-error 365 | if (flag.multiple || !flag.multiple) { 366 | // Until the Upstream PR is merged, everything is multiple 🤷‍♂️ 367 | parts.push(` multi_flags+=("--${name}")`) 368 | 369 | if (flag.char) { 370 | parts.push(` multi_flags+=("-${flag.char}")`) 371 | } 372 | } 373 | 374 | if (flag.options) { 375 | parts.push(` option_flags+=("--${name}")`) 376 | parts.push( 377 | ` option_flag_handlers+=("_${commandName}___flag_options--${name}")` 378 | ) 379 | 380 | if (flag.char) { 381 | parts.push(` option_flags+=("-${flag.char}")`) 382 | parts.push( 383 | ` option_flag_handlers+=("_${commandName}___flag_options--${name}")` 384 | ) 385 | } 386 | } 387 | } 388 | 389 | if (flag.required) { 390 | parts.push(` required_flags+=("--${name}")`) 391 | 392 | if (flag.char) { 393 | parts.push(` required_flags+=("-${flag.char}")`) 394 | } 395 | } 396 | } 397 | 398 | parts.push(`}`) 399 | 400 | scriptParts.push(parts.join(`\n`)) 401 | }) 402 | 403 | const rootCommandParts: string[] = [] 404 | 405 | rootCommandParts.push(`_${bin}()`) 406 | rootCommandParts.push(`{`) 407 | 408 | rootCommandParts.push(` commands=()`) 409 | 410 | for (const command of commands) { 411 | if (command.hidden) { 412 | continue 413 | } 414 | 415 | rootCommandParts.push(` commands+=("${command.id}")`) 416 | 417 | if (command.aliases.length > 0) { 418 | for (const alias of command.aliases) { 419 | rootCommandParts.push(` command_aliases+=("${alias}")`) 420 | } 421 | 422 | rootCommandParts.push( 423 | ` if [[ -z "\${BASH_VERSION}" || "\${BASH_VERSINFO[0]}" -gt 3 ]]; then` 424 | ) 425 | 426 | for (const alias of command.aliases) { 427 | rootCommandParts.push(` command_by_alias[${alias}]=${command.id}`) 428 | } 429 | 430 | rootCommandParts.push(` else`) 431 | 432 | for (const alias of command.aliases) { 433 | rootCommandParts.push(` command+=("${alias}")`) 434 | } 435 | 436 | rootCommandParts.push(` fi`) 437 | } 438 | } 439 | 440 | rootCommandParts.push(` last_command=${bin}`) 441 | rootCommandParts.push(`}`) 442 | 443 | scriptParts.push(rootCommandParts.join(`\n`)) 444 | 445 | scriptParts.push(getInit(bin, aliases)) 446 | 447 | return scriptParts.join('\n'.repeat(2)) 448 | } 449 | 450 | export function generateCompletionAliasScriptForBash({ 451 | bin, 452 | }: { 453 | bin: string 454 | alias: string 455 | }) { 456 | const scriptParts: string[] = [] 457 | 458 | scriptParts.push(`_xfunc ${bin} __${bin}_init`) 459 | 460 | return scriptParts.join('\n'.repeat(2)) 461 | } 462 | 463 | export function getInstructionsForBash({ 464 | bin, 465 | shell, 466 | aliases, 467 | }: { 468 | bin: string 469 | shell: string 470 | aliases: string[] 471 | }): string[] { 472 | const scriptName = (name: string) => name 473 | 474 | const lines = [ 475 | `Make sure you have the "bash-completion" package installed on your system.`, 476 | ``, 477 | `Running the following command will generate the completion script for ${shell} shell:`, 478 | ``, 479 | ` $ ${bin} completion:generate --shell=${shell} > ${scriptName(bin)}`, 480 | ] 481 | 482 | lines.push( 483 | ``, 484 | `You need to put that "${scriptName( 485 | bin 486 | )}" file in one of the following directories (depending on your system):`, 487 | ``, 488 | ` - $XDG_DATA_HOME/bash-completion/completions`, 489 | ` - ~/.local/share/bash-completion/completions`, 490 | ` - /usr/local/share/bash-completion/completions`, 491 | ` - /usr/share/bash-completion/completions`, 492 | ``, 493 | `Usually this should work:`, 494 | ``, 495 | ` $ ${bin} completion:generate --shell=${shell} | tee ~/.local/share/bash-completion/completions/${scriptName( 496 | bin 497 | )}` 498 | ) 499 | 500 | if (aliases.length > 0) { 501 | const plural = aliases.length > 1 502 | lines.push( 503 | ``, 504 | `Also, '${bin}' provides ${plural ? 'these' : 'the'} ${ 505 | plural ? 'aliases' : 'alias' 506 | }: '${aliases.join("', '")}'. You can generate completion ${ 507 | plural ? 'scripts' : 'script' 508 | } for ${ 509 | plural ? 'those' : 'that' 510 | } using the "completion:generate:alias" command. For example:`, 511 | ``, 512 | ` $ ${bin} completion:generate:alias --shell=${shell} ${ 513 | aliases[0] 514 | } | tee ~/.local/share/bash-completion/completions/${scriptName( 515 | aliases[0] 516 | )}` 517 | ) 518 | } 519 | 520 | lines.push( 521 | ``, 522 | `For more info, visit: https://www.npmjs.com/package/oclif-plugin-completion#${shell}`, 523 | ``, 524 | `Enjoy!` 525 | ) 526 | 527 | return lines 528 | } 529 | -------------------------------------------------------------------------------- /src/utils/fish.ts: -------------------------------------------------------------------------------- 1 | import type { Command } from '@oclif/command' 2 | import { escapeString, getFirstLine } from './template' 3 | 4 | export function generateCompletionScriptForFish({ 5 | bin, 6 | aliases, 7 | commands, 8 | }: Pick & { aliases: string[] }) { 9 | const scriptParts: string[] = [] 10 | 11 | scriptParts.push(`set -l seen __fish_seen_subcommand_from`) 12 | 13 | scriptParts.push( 14 | `set -l commands ${commands.map((command) => command.id).join(' ')}` 15 | ) 16 | 17 | for (const command of commands) { 18 | if (command.hidden) { 19 | continue 20 | } 21 | 22 | const commandParts: string[] = [] 23 | 24 | commandParts.push( 25 | `complete -c ${bin} -n "not $seen $commands" -f -a ${ 26 | command.id 27 | } -d '${escapeString(getFirstLine(command.description), "'")}'` 28 | ) 29 | 30 | for (const [name, flag] of Object.entries(command.flags)) { 31 | if (flag.hidden) { 32 | continue 33 | } 34 | 35 | let flagPart = `complete -c ${bin} -n "$seen ${command.id}" -l ${name}` 36 | 37 | if (flag.char) { 38 | flagPart += ` -s ${flag.char}` 39 | } 40 | 41 | if (flag.type === 'option') { 42 | if (flag.options) { 43 | flagPart += ` -x -a "${flag.options.join(' ')}"` 44 | } else { 45 | flagPart += ` -r` 46 | } 47 | } 48 | 49 | if (flag.description) { 50 | flagPart += ` -d '${escapeString(getFirstLine(flag.description), "'")}'` 51 | } 52 | 53 | commandParts.push(flagPart) 54 | } 55 | 56 | scriptParts.push(commandParts.join('\n')) 57 | } 58 | 59 | scriptParts.push( 60 | aliases.map((alias) => `complete -c ${alias} -w ${bin}`).join('\n') 61 | ) 62 | 63 | return scriptParts.join('\n'.repeat(2)) 64 | } 65 | 66 | export function generateCompletionAliasScriptForFish({ 67 | bin, 68 | alias, 69 | }: { 70 | bin: string 71 | alias: string 72 | }) { 73 | const scriptParts: string[] = [] 74 | 75 | scriptParts.push(`complete -c ${alias} -w ${bin}`) 76 | 77 | return scriptParts.join('\n'.repeat(2)) 78 | } 79 | 80 | export function getInstructionsForFish({ 81 | bin, 82 | shell, 83 | aliases, 84 | }: { 85 | bin: string 86 | shell: string 87 | aliases: string[] 88 | }): string[] { 89 | const scriptName = (name: string) => `${name}.fish` 90 | 91 | const lines = [ 92 | `Running the following command will generate the completion script for ${shell} shell:`, 93 | ``, 94 | ` $ ${bin} completion:generate --shell=${shell} > ${scriptName(bin)}`, 95 | ] 96 | 97 | lines.push( 98 | ``, 99 | `You need to put that "${scriptName( 100 | bin 101 | )}" file in one of the directories present in "$fish_complete_path" environment variable:`, 102 | ``, 103 | ` $ echo $fish_complete_path`, 104 | ``, 105 | `Usually this should work:`, 106 | ``, 107 | ` $ ${bin} completion:generate --shell=${shell} | tee ~/.config/fish/completions/${scriptName( 108 | bin 109 | )}` 110 | ) 111 | 112 | if (aliases.length > 0) { 113 | const plural = aliases.length > 1 114 | lines.push( 115 | ``, 116 | `Also, '${bin}' provides ${plural ? 'these' : 'the'} ${ 117 | plural ? 'aliases' : 'alias' 118 | }: '${aliases.join("', '")}'. You can generate completion ${ 119 | plural ? 'scripts' : 'script' 120 | } for ${ 121 | plural ? 'those' : 'that' 122 | } using the "completion:generate:alias" command. For example:`, 123 | ``, 124 | ` $ ${bin} completion:generate:alias --shell=${shell} ${ 125 | aliases[0] 126 | } | tee ~/.config/fish/completions/${scriptName(aliases[0])}` 127 | ) 128 | } 129 | 130 | lines.push( 131 | ``, 132 | `For more info, visit: https://www.npmjs.com/package/oclif-plugin-completion#${shell}`, 133 | ``, 134 | `Enjoy!` 135 | ) 136 | 137 | return lines 138 | } 139 | -------------------------------------------------------------------------------- /src/utils/get-bin-aliases.ts: -------------------------------------------------------------------------------- 1 | export function getBinAliases({ 2 | bin, 3 | pjson, 4 | }: { 5 | bin: string 6 | pjson: { bin?: { [name: string]: string } } 7 | }): string[] { 8 | const aliases: string[] = [] 9 | 10 | if (!pjson.bin) { 11 | return aliases 12 | } 13 | 14 | const binPath = pjson.bin[bin] 15 | 16 | for (const [name, path] of Object.entries(pjson.bin)) { 17 | if (name !== bin && path === binPath) { 18 | aliases.push(name) 19 | } 20 | } 21 | 22 | return aliases 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/template.ts: -------------------------------------------------------------------------------- 1 | export function template( 2 | literals: TemplateStringsArray, 3 | ...keys: Array 4 | ) { 5 | return ( 6 | values: Array | { [key: string]: number | string } 7 | ): string => { 8 | const dict: Record = Array.isArray(values) 9 | ? values.reduce((dict: Record, value, index) => { 10 | dict[index] = value 11 | return dict 12 | }, {}) 13 | : values 14 | 15 | const parts = keys.reduce((parts, key, index) => { 16 | const value = `${dict[key]}` 17 | parts.push(literals[index], value) 18 | return parts 19 | }, []) 20 | 21 | parts.push(literals[literals.length - 1]) 22 | 23 | return parts.join('') 24 | } 25 | } 26 | 27 | export const escapeString = (string: string, chars: string) => { 28 | const pattern = new RegExp(`([${chars}])`, 'g') 29 | return string.replace(pattern, '\\$1') 30 | } 31 | 32 | export const getFirstLine = (string = ''): string => { 33 | return string.split('\n')[0] 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/zsh.ts: -------------------------------------------------------------------------------- 1 | import type { Command } from '@oclif/command' 2 | import { escapeString, getFirstLine } from './template' 3 | 4 | const getArgs = (command: Command['config']['commands'][number]): string[] => { 5 | const args: string[] = [] 6 | 7 | command.args.forEach((arg, index) => { 8 | const MESSAGE = ` ` 9 | let ACTION = `` 10 | 11 | if (arg.options && arg.options.length > 0) { 12 | ACTION = `(${arg.options.join(' ')})` 13 | } else if (arg.required) { 14 | ACTION = `( )` 15 | } 16 | 17 | args.push(`"${index + 1}:${MESSAGE}:${ACTION}"`) 18 | }) 19 | 20 | Object.entries(command.flags).forEach(([name, flag]) => { 21 | let GROUP: string | null = null 22 | 23 | const OPTNAME: { 24 | long: string 25 | neg?: string 26 | short?: string 27 | } = { 28 | long: `--${name}`, 29 | } 30 | 31 | if (flag.char) { 32 | OPTNAME.short = `-${flag.char}` 33 | } 34 | 35 | if (flag.type === 'boolean') { 36 | if (flag.allowNo) { 37 | OPTNAME.neg = `--no-${name}` 38 | } 39 | 40 | if (OPTNAME.neg || OPTNAME.short) { 41 | GROUP = `(${Object.values(OPTNAME).filter(Boolean).join(' ')})` 42 | } 43 | } 44 | 45 | if (flag.type === 'option') { 46 | OPTNAME.long = `${OPTNAME.long}=` 47 | 48 | if (OPTNAME.short) { 49 | OPTNAME.short = `${OPTNAME.short}+` 50 | } 51 | 52 | // TODO: need upstream fix. `flag.multiple` property does not exist 53 | // Upstream PR: https://github.com/oclif/config/pull/113 54 | // @ts-expect-error 55 | if (flag.multiple || !flag.multiple) { 56 | // Until the Upstream PR is merged, everything is multiple 🤷‍♂️ 57 | OPTNAME.long = `*${OPTNAME.long}` 58 | 59 | if (OPTNAME.short) { 60 | OPTNAME.short = `*${OPTNAME.short}` 61 | } 62 | } else if (OPTNAME.short) { 63 | GROUP = `(${Object.values(OPTNAME).filter(Boolean).join(' ')})` 64 | } 65 | } 66 | 67 | const OPTSPEC: string[] = [] 68 | 69 | if (GROUP) { 70 | OPTSPEC.push(`{${Object.values(OPTNAME).filter(Boolean).join(',')}}`) 71 | } else { 72 | OPTSPEC.push(`${OPTNAME.long}`) 73 | 74 | if (OPTNAME.short) { 75 | OPTSPEC.push(`${OPTNAME.short}`) 76 | } 77 | } 78 | 79 | const EXPLANATION = `[${escapeString( 80 | getFirstLine(flag.description), 81 | ':"' 82 | )}]` 83 | 84 | const MESSAGE = `${name}` 85 | 86 | let ACTION = `` 87 | if (flag.type === 'option' && flag.options && flag.options.length > 0) { 88 | ACTION = `(${flag.options.join(' ')})` 89 | } else if (flag.required) { 90 | ACTION = `( )` 91 | } 92 | 93 | let OPTARG = `` 94 | 95 | if (flag.type === 'option') { 96 | OPTARG = `:${MESSAGE}:${ACTION}` 97 | } 98 | 99 | if (GROUP) { 100 | args.push(`"${GROUP}"${OPTSPEC}"${EXPLANATION}${OPTARG}"`) 101 | } else { 102 | args.push( 103 | ...OPTSPEC.map((optspec) => `"${optspec}${EXPLANATION}${OPTARG}"`) 104 | ) 105 | } 106 | }) 107 | 108 | return args 109 | } 110 | 111 | export function generateCompletionScriptForZsh({ 112 | bin, 113 | aliases, 114 | commands, 115 | }: Pick & { aliases: string[] }) { 116 | const scriptParts = [`#compdef ${[bin, ...aliases].join(' ')}`] 117 | 118 | const rootCommandPart = ` 119 | function _${bin} { 120 | local state 121 | local -a commands 122 | 123 | _arguments -s -w -S -C \\ 124 | "1: :->command" \\ 125 | "*::arg:->args" 126 | 127 | case $state in 128 | command) 129 | commands=( 130 | ${commands 131 | .map((command) => { 132 | const NAME = escapeString(command.id, ':') 133 | const DESCRIPTION = escapeString( 134 | getFirstLine(command.description), 135 | ':' 136 | ) 137 | 138 | return `"${NAME}:${DESCRIPTION}"` 139 | }) 140 | .join(`\n${' '.repeat(8)}`)} 141 | ) 142 | _describe "command" commands 143 | ;; 144 | esac 145 | 146 | case "$words[1]" in 147 | ${commands 148 | .map((command) => { 149 | return `${command.id})\n${' '.repeat(6)}_${bin}_${ 150 | command.id 151 | }\n${' '.repeat(6)};;` 152 | }) 153 | .join(`\n${' '.repeat(4)}`)} 154 | esac 155 | } 156 | `.trim() 157 | 158 | scriptParts.push(rootCommandPart) 159 | 160 | commands.forEach((command) => { 161 | const functionParts: string[] = [] 162 | 163 | const args = getArgs(command) 164 | 165 | if (args.length > 0) { 166 | functionParts.push( 167 | `_arguments -s -w -S -C ${args.join(` \\\n${' '.repeat(4)}`)}` 168 | ) 169 | } 170 | 171 | const commandPart = ` 172 | function _${bin}_${command.id} { 173 | ${functionParts.join(`\n${' '.repeat(2)}`)} 174 | } 175 | `.trim() 176 | 177 | scriptParts.push(commandPart) 178 | }) 179 | 180 | return scriptParts.join('\n'.repeat(2)) 181 | } 182 | 183 | export function getInstructionsForZsh({ 184 | bin, 185 | shell, 186 | }: { 187 | bin: string 188 | shell: string 189 | }): string[] { 190 | const scriptName = `_${bin}` 191 | 192 | const lines = [ 193 | `Running the following command will generate the completion script for ${shell} shell:`, 194 | ``, 195 | ` $ ${bin} completion:generate --shell=${shell} > ${scriptName}`, 196 | ``, 197 | `You need to put that "${scriptName}" file in one of the directories present in "$FPATH" variable:`, 198 | ``, 199 | ` $ echo $FPATH`, 200 | ``, 201 | `Usually this should work by automatically find an appropriate directory for you:`, 202 | ``, 203 | ` $ ${bin} completion:generate --shell=${shell} | tee "$(echo \${FPATH} | tr ':' '\\n' | grep site-functions | head -n1)/${scriptName}"`, 204 | ``, 205 | `For more info, visit: https://www.npmjs.com/package/oclif-plugin-completion#${shell}`, 206 | ``, 207 | `Enjoy!`, 208 | ] 209 | 210 | return lines 211 | } 212 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "importHelpers": true, 7 | "outDir": "lib", 8 | "rootDir": "src", 9 | "strict": true 10 | }, 11 | "include": ["src/**/*"] 12 | } 13 | --------------------------------------------------------------------------------