├── .bindown.yaml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── completion_install.go ├── completion_install_test.go ├── doc.go ├── example_test.go ├── go.mod ├── go.sum ├── internal └── positionalpredictor │ ├── positional.go │ └── positional_test.go ├── kongplete.go ├── kongplete_test.go └── script ├── bindown ├── fmt ├── generate ├── lint └── test /.bindown.yaml: -------------------------------------------------------------------------------- 1 | systems: 2 | - darwin/amd64 3 | - darwin/arm64 4 | - linux/amd64 5 | - windows/amd64 6 | dependencies: 7 | gh: 8 | template: origin#gh 9 | vars: 10 | version: 2.29.0 11 | gofumpt: 12 | template: origin#gofumpt 13 | vars: 14 | version: 0.5.0 15 | golangci-lint: 16 | template: origin#golangci-lint 17 | vars: 18 | version: 1.54.2 19 | goreleaser: 20 | template: origin#goreleaser 21 | vars: 22 | version: 1.18.2 23 | handcrafted: 24 | template: origin#handcrafted 25 | vars: 26 | version: 0.0.0 27 | jq: 28 | template: origin#jq 29 | vars: 30 | version: "1.6" 31 | semver-next: 32 | template: origin#semver-next 33 | vars: 34 | version: 2.0.0 35 | semver-prev: 36 | template: origin#semver-prev 37 | vars: 38 | version: 0.0.1 39 | shellcheck: 40 | template: origin#shellcheck 41 | vars: 42 | version: 0.9.0 43 | shfmt: 44 | template: origin#shfmt 45 | vars: 46 | version: 3.6.0 47 | templates: 48 | origin#gh: 49 | homepage: https://github.com/cli/cli 50 | description: GitHub’s official command line tool 51 | url: https://github.com/cli/cli/releases/download/v{{.version}}/gh_{{.version}}_{{.os}}_{{.arch}}{{.urlSuffix}} 52 | archive_path: gh_{{.version}}_{{.os}}_{{.arch}}/bin/gh{{.archivePathSuffix}} 53 | bin: gh 54 | vars: 55 | archivePathSuffix: "" 56 | urlSuffix: .zip 57 | overrides: 58 | - matcher: 59 | os: 60 | - windows 61 | dependency: 62 | archive_path: bin/gh{{.archivePathSuffix}} 63 | vars: 64 | archivePathSuffix: .exe 65 | - matcher: 66 | os: 67 | - linux 68 | dependency: 69 | vars: 70 | urlSuffix: .tar.gz 71 | - matcher: 72 | os: 73 | - darwin 74 | version: 75 | - < 2.28.0 76 | dependency: 77 | vars: 78 | urlSuffix: .tar.gz 79 | substitutions: 80 | os: 81 | darwin: macOS 82 | systems: 83 | - darwin/amd64 84 | - darwin/arm64 85 | - linux/386 86 | - linux/amd64 87 | - linux/arm64 88 | - windows/386 89 | - windows/amd64 90 | - windows/arm64 91 | required_vars: 92 | - version 93 | origin#gofumpt: 94 | url: https://github.com/mvdan/gofumpt/releases/download/v{{.version}}/gofumpt_v{{.version}}_{{.os}}_{{.arch}}{{.suffix}} 95 | archive_path: gofumpt_v{{.version}}_{{.os}}_{{.arch}}{{.suffix}} 96 | bin: gofumpt{{.suffix}} 97 | vars: 98 | suffix: "" 99 | overrides: 100 | - matcher: 101 | os: 102 | - windows 103 | dependency: 104 | vars: 105 | suffix: .exe 106 | systems: 107 | - darwin/amd64 108 | - darwin/arm64 109 | - linux/386 110 | - linux/amd64 111 | - linux/arm 112 | - linux/arm64 113 | - windows/386 114 | - windows/amd64 115 | required_vars: 116 | - version 117 | origin#golangci-lint: 118 | url: https://github.com/golangci/golangci-lint/releases/download/v{{.version}}/golangci-lint-{{.version}}-{{.os}}-{{.arch}}{{.urlsuffix}} 119 | archive_path: golangci-lint-{{.version}}-{{.os}}-{{.arch}}/golangci-lint{{.archivepathsuffix}} 120 | bin: golangci-lint 121 | link: true 122 | vars: 123 | archivepathsuffix: "" 124 | urlsuffix: .tar.gz 125 | overrides: 126 | - matcher: 127 | os: 128 | - windows 129 | dependency: 130 | vars: 131 | archivepathsuffix: .exe 132 | urlsuffix: .zip 133 | systems: 134 | - darwin/amd64 135 | - darwin/arm64 136 | - linux/386 137 | - linux/amd64 138 | - linux/arm64 139 | - windows/386 140 | - windows/amd64 141 | - freebsd/386 142 | - freebsd/amd64 143 | - linux/mips64 144 | - linux/mips64le 145 | - linux/s390x 146 | - linux/ppc64le 147 | required_vars: 148 | - version 149 | origin#goreleaser: 150 | homepage: https://github.com/goreleaser/goreleaser 151 | description: Deliver Go binaries as fast and easily as possible 152 | url: https://github.com/goreleaser/goreleaser/releases/download/v{{.version}}/goreleaser_{{.os}}_{{.arch}}{{.urlSuffix}} 153 | archive_path: goreleaser{{.archivePathSuffix}} 154 | bin: goreleaser 155 | vars: 156 | archivePathSuffix: "" 157 | urlSuffix: .tar.gz 158 | overrides: 159 | - matcher: 160 | os: 161 | - windows 162 | dependency: 163 | vars: 164 | archivePathSuffix: .exe 165 | urlSuffix: .zip 166 | substitutions: 167 | arch: 168 | "386": i386 169 | amd64: x86_64 170 | os: 171 | windows: Windows 172 | substitutions: 173 | arch: 174 | "386": i386 175 | amd64: x86_64 176 | os: 177 | darwin: Darwin 178 | linux: Linux 179 | systems: 180 | - darwin/amd64 181 | - darwin/arm64 182 | - linux/386 183 | - linux/amd64 184 | - linux/arm64 185 | - linux/ppc64 186 | - windows/386 187 | - windows/amd64 188 | - windows/arm64 189 | required_vars: 190 | - version 191 | origin#handcrafted: 192 | homepage: https://github.com/willabides/handcrafted 193 | description: lists non-generated go files in a package 194 | url: https://github.com/WillAbides/handcrafted/releases/download/v{{.version}}/handcrafted_{{.version}}_{{.os}}_{{.arch}}{{.urlSuffix}} 195 | archive_path: handcrafted{{.archivePathSuffix}} 196 | bin: handcrafted 197 | vars: 198 | archivePathSuffix: "" 199 | urlSuffix: .tar.gz 200 | overrides: 201 | - matcher: 202 | os: 203 | - windows 204 | dependency: 205 | vars: 206 | archivePathSuffix: .exe 207 | systems: 208 | - darwin/amd64 209 | - darwin/arm64 210 | - linux/386 211 | - linux/amd64 212 | - linux/arm64 213 | - windows/386 214 | - windows/amd64 215 | - windows/arm64 216 | required_vars: 217 | - version 218 | origin#jq: 219 | homepage: https://github.com/stedolan/jq 220 | description: Command-line JSON processor 221 | url: https://github.com/stedolan/jq/releases/download/jq-{{.version}}/jq-{{.os}}{{.arch}}{{.extension}} 222 | archive_path: jq-{{.os}}{{.arch}}{{.extension}} 223 | bin: jq 224 | vars: 225 | extension: "" 226 | overrides: 227 | - matcher: 228 | arch: 229 | - amd64 230 | - arm64 231 | os: 232 | - darwin 233 | dependency: 234 | url: https://github.com/stedolan/jq/releases/download/jq-1.6/jq-osx-amd64 235 | archive_path: jq-osx-amd64 236 | - matcher: 237 | os: 238 | - windows 239 | dependency: 240 | vars: 241 | extension: .exe 242 | substitutions: 243 | arch: 244 | "386": "32" 245 | amd64: "64" 246 | os: 247 | windows: win 248 | systems: 249 | - linux/386 250 | - linux/amd64 251 | - darwin/amd64 252 | - darwin/arm64 253 | - windows/386 254 | - windows/amd64 255 | required_vars: 256 | - version 257 | origin#semver-next: 258 | homepage: https://github.com/WillAbides/semver-next 259 | url: https://github.com/WillAbides/semver-next/releases/download/v{{.version}}/semver-next_{{.version}}_{{.os}}_{{.arch}}{{.urlSuffix}} 260 | archive_path: semver-next{{.archivePathSuffix}} 261 | bin: semver-next 262 | vars: 263 | archivePathSuffix: "" 264 | urlSuffix: .tar.gz 265 | overrides: 266 | - matcher: 267 | os: 268 | - windows 269 | dependency: 270 | vars: 271 | archivePathSuffix: .exe 272 | systems: 273 | - darwin/amd64 274 | - darwin/arm64 275 | - linux/386 276 | - linux/amd64 277 | - linux/arm64 278 | - windows/386 279 | - windows/amd64 280 | - windows/arm64 281 | required_vars: 282 | - version 283 | origin#semver-prev: 284 | homepage: https://github.com/willabides/semver-prev 285 | url: https://github.com/WillAbides/semver-prev/releases/download/v{{.version}}/semver-prev_{{.version}}_{{.os}}_{{.arch}}{{.urlSuffix}} 286 | archive_path: semver-prev{{.archivePathSuffix}} 287 | bin: semver-prev 288 | vars: 289 | archivePathSuffix: "" 290 | urlSuffix: .tar.gz 291 | overrides: 292 | - matcher: 293 | os: 294 | - windows 295 | dependency: 296 | vars: 297 | archivePathSuffix: .exe 298 | systems: 299 | - darwin/amd64 300 | - darwin/arm64 301 | - linux/386 302 | - linux/amd64 303 | - linux/arm64 304 | - windows/386 305 | - windows/amd64 306 | - windows/arm64 307 | required_vars: 308 | - version 309 | origin#shellcheck: 310 | url: https://github.com/koalaman/shellcheck/releases/download/v{{.version}}/shellcheck-v{{.version}}.{{.os}}.{{.arch}}.tar.xz 311 | archive_path: shellcheck-v{{.version}}/shellcheck 312 | bin: shellcheck 313 | overrides: 314 | - matcher: 315 | os: 316 | - windows 317 | dependency: 318 | url: https://github.com/koalaman/shellcheck/releases/download/v{{.version}}/shellcheck-v{{.version}}.zip 319 | archive_path: shellcheck.exe 320 | - matcher: 321 | arch: 322 | - arm64 323 | os: 324 | - darwin 325 | dependency: 326 | vars: 327 | arch: amd64 328 | substitutions: 329 | arch: 330 | amd64: x86_64 331 | systems: 332 | - darwin/amd64 333 | - darwin/arm64 334 | - linux/amd64 335 | - windows/amd64 336 | required_vars: 337 | - version 338 | origin#shfmt: 339 | homepage: https://github.com/mvdan/sh 340 | description: A shell parser, formatter, and interpreter with bash support; includes shfmt 341 | url: https://github.com/mvdan/sh/releases/download/v{{.version}}/shfmt_v{{.version}}_{{.os}}_{{.arch}}{{.urlSuffix}} 342 | archive_path: shfmt_v{{.version}}_{{.os}}_{{.arch}}{{.urlSuffix}} 343 | bin: shfmt 344 | vars: 345 | archivePathSuffix: "" 346 | urlSuffix: "" 347 | overrides: 348 | - matcher: 349 | os: 350 | - windows 351 | dependency: 352 | vars: 353 | urlSuffix: .exe 354 | systems: 355 | - darwin/amd64 356 | - darwin/arm64 357 | - linux/386 358 | - linux/amd64 359 | - linux/arm64 360 | - windows/386 361 | - windows/amd64 362 | required_vars: 363 | - version 364 | template_sources: 365 | origin: https://raw.githubusercontent.com/WillAbides/bindown-templates/master/bindown.yml 366 | url_checksums: 367 | https://github.com/WillAbides/handcrafted/releases/download/v0.0.0/handcrafted_0.0.0_darwin_amd64.tar.gz: df5dbf9c8b282d8209a8baddfe3410c5b3ace87bdce808fce0a0d49356c9ff4d 368 | https://github.com/WillAbides/handcrafted/releases/download/v0.0.0/handcrafted_0.0.0_darwin_arm64.tar.gz: c03133084f87e064f9801d4b2a9739be755fcee5875382f4da0fc10cd8306dfb 369 | https://github.com/WillAbides/handcrafted/releases/download/v0.0.0/handcrafted_0.0.0_linux_amd64.tar.gz: 1a7885a9854d2455dce1be3bc19f2d61a61ebdc99e2a98e4969ab1965c2a64ad 370 | https://github.com/WillAbides/handcrafted/releases/download/v0.0.0/handcrafted_0.0.0_windows_amd64.tar.gz: 5ce8cddc9bdbd19adde3104397d698ecca7eb8ad2ac540cc709a15821f9b2609 371 | https://github.com/WillAbides/semver-next/releases/download/v2.0.0/semver-next_2.0.0_darwin_amd64.tar.gz: 2bb0e3a6fda58689c7c983c33f131b44298484ea42c95455f711b16e3a93d033 372 | https://github.com/WillAbides/semver-next/releases/download/v2.0.0/semver-next_2.0.0_darwin_arm64.tar.gz: d86b2a7e0cb21cd56b9168913994b4470be2c1a0d724d91c85d91aa3e8c63755 373 | https://github.com/WillAbides/semver-next/releases/download/v2.0.0/semver-next_2.0.0_linux_amd64.tar.gz: 336beb1eb6ce340a6dfbb61b7f3ffd8af821414112479a38f9b10db03dc7a6ea 374 | https://github.com/WillAbides/semver-next/releases/download/v2.0.0/semver-next_2.0.0_windows_amd64.tar.gz: 2991941f5ab181a426789a8c3a33d999086ed21dba8a8ae7690f59b3c2dbb274 375 | https://github.com/WillAbides/semver-prev/releases/download/v0.0.1/semver-prev_0.0.1_darwin_amd64.tar.gz: de2902df5f99e6db91b143199446ccf9c7bdacd37980fca55f71e2da35211d0f 376 | https://github.com/WillAbides/semver-prev/releases/download/v0.0.1/semver-prev_0.0.1_darwin_arm64.tar.gz: 367eaccc368bec6119d47cc78d0ad743cc8709dc1900e8f170c5314418c3a14e 377 | https://github.com/WillAbides/semver-prev/releases/download/v0.0.1/semver-prev_0.0.1_linux_amd64.tar.gz: 1c1ab5926e9548659bdc1786cf1da9f62f80ae5314c5ccdf71123767e4f4da1d 378 | https://github.com/WillAbides/semver-prev/releases/download/v0.0.1/semver-prev_0.0.1_windows_amd64.tar.gz: 31bc3527ad74cdfda9d5868287141f44e72e8fbd1709c4f6af05205f9eb8bb8b 379 | https://github.com/cli/cli/releases/download/v2.29.0/gh_2.29.0_linux_amd64.tar.gz: 9fe05f43a11a7bf8eacf731422452d1997e6708d4160ef0efcb13c103320390e 380 | https://github.com/cli/cli/releases/download/v2.29.0/gh_2.29.0_macOS_amd64.zip: e116d0f9c310450482cdcd7f4d2d1c7c4cab8d4f025a340260ce3f15329c5145 381 | https://github.com/cli/cli/releases/download/v2.29.0/gh_2.29.0_macOS_arm64.zip: 38ca9a355376abd1475362cf8b3cacf2a757198fe5fe70349cb1767666abacc6 382 | https://github.com/cli/cli/releases/download/v2.29.0/gh_2.29.0_windows_amd64.zip: 031eb343ebff6f8cc712d58d79267ee00b0c61b37d6698927161daae895044c6 383 | https://github.com/golangci/golangci-lint/releases/download/v1.54.2/golangci-lint-1.54.2-darwin-amd64.tar.gz: 925c4097eae9e035b0b052a66d0a149f861e2ab611a4e677c7ffd2d4e05b9b89 384 | https://github.com/golangci/golangci-lint/releases/download/v1.54.2/golangci-lint-1.54.2-darwin-arm64.tar.gz: 7b33fb1be2f26b7e3d1f3c10ce9b2b5ce6d13bb1d8468a4b2ba794f05b4445e1 385 | https://github.com/golangci/golangci-lint/releases/download/v1.54.2/golangci-lint-1.54.2-linux-amd64.tar.gz: 17c9ca05253efe833d47f38caf670aad2202b5e6515879a99873fabd4c7452b3 386 | https://github.com/golangci/golangci-lint/releases/download/v1.54.2/golangci-lint-1.54.2-windows-amd64.zip: ce17d122f3f93e0a9e52009d2c03cc1c1a1ae28338c2702a1f53eccd10a1afa3 387 | https://github.com/goreleaser/goreleaser/releases/download/v1.18.2/goreleaser_Darwin_arm64.tar.gz: 7eec9f4d0b86b2c9c9f6af1770a11315998bd4d4617633b0a73eeb036e97393e 388 | https://github.com/goreleaser/goreleaser/releases/download/v1.18.2/goreleaser_Darwin_x86_64.tar.gz: 95338eed333347152e23837b68a8c6ce0c62b9f5abb68bd5b4b08178766400b9 389 | https://github.com/goreleaser/goreleaser/releases/download/v1.18.2/goreleaser_Linux_x86_64.tar.gz: 811e0c63e347f78f3c8612a19ca8eeb564eb45f0265ce3f38aec39c8fdbcfa10 390 | https://github.com/goreleaser/goreleaser/releases/download/v1.18.2/goreleaser_Windows_x86_64.zip: 4b67f9a0159dc4f6a19fdea46eda506d58efe9e9d01aebc6ee39c9e9c14f9715 391 | https://github.com/koalaman/shellcheck/releases/download/v0.9.0/shellcheck-v0.9.0.darwin.x86_64.tar.xz: 7d3730694707605d6e60cec4efcb79a0632d61babc035aa16cda1b897536acf5 392 | https://github.com/koalaman/shellcheck/releases/download/v0.9.0/shellcheck-v0.9.0.linux.x86_64.tar.xz: 700324c6dd0ebea0117591c6cc9d7350d9c7c5c287acbad7630fa17b1d4d9e2f 393 | https://github.com/koalaman/shellcheck/releases/download/v0.9.0/shellcheck-v0.9.0.zip: ae58191b1ea4ffd9e5b15da9134146e636440302ce3e2f46863e8d71c8be1bbb 394 | https://github.com/mvdan/gofumpt/releases/download/v0.5.0/gofumpt_v0.5.0_darwin_amd64: 870f05a23541aad3d20d208a3ea17606169a240f608ac1cf987426198c14b2ed 395 | https://github.com/mvdan/gofumpt/releases/download/v0.5.0/gofumpt_v0.5.0_darwin_arm64: f2df95d5fad8498ad8eeb0be8abdb8bb8d05e8130b332cb69751dfd090fabac4 396 | https://github.com/mvdan/gofumpt/releases/download/v0.5.0/gofumpt_v0.5.0_linux_amd64: 759c6ab56bfbf62cafb35944aef1e0104a117e0aebfe44816fd79ef4b28521e4 397 | https://github.com/mvdan/gofumpt/releases/download/v0.5.0/gofumpt_v0.5.0_windows_amd64.exe: c9ca0a8a95c2ead0a009a349d5a326e385f5f15a96b084e11c4a7c1cb86b694b 398 | https://github.com/mvdan/sh/releases/download/v3.6.0/shfmt_v3.6.0_darwin_amd64: b8c9c025b498e2816b62f0b717f6032e9ab49e725a45b8205f52f66318f17185 399 | https://github.com/mvdan/sh/releases/download/v3.6.0/shfmt_v3.6.0_darwin_arm64: 633f242246ee0a866c5f5df25cbf61b6af0d5e143555aca32950059cf13d91e0 400 | https://github.com/mvdan/sh/releases/download/v3.6.0/shfmt_v3.6.0_linux_amd64: 5741a02a641de7e56b8da170e71a97e58050d66a3cf485fb268d6a5a8bb74afb 401 | https://github.com/mvdan/sh/releases/download/v3.6.0/shfmt_v3.6.0_windows_amd64.exe: 18122d910ba434be366588f37c302c309cde4ca5403f93285254a3cf96839d01 402 | https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64: af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 403 | https://github.com/stedolan/jq/releases/download/jq-1.6/jq-osx-amd64: 5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef 404 | https://github.com/stedolan/jq/releases/download/jq-1.6/jq-win64.exe: a51d36968dcbdeabb3142c6f5cf9b401a65dc3a095f3144bd0c118d5bb192753 405 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [ push, workflow_dispatch ] 3 | jobs: 4 | cibuild: 5 | name: cibuild 6 | runs-on: ubuntu-22.04 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: WillAbides/setup-go-faster@v1 10 | id: setup-go 11 | with: 12 | go-version: '1.21.x' 13 | - uses: actions/cache@v2 14 | with: 15 | path: | 16 | ${{ steps.setup-go.outputs.GOCACHE }} 17 | ${{ steps.setup-go.outputs.GOMODCACHE }} 18 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 19 | restore-keys: ${{ runner.os }}-go- 20 | - run: script/generate --check 21 | - run: script/test 22 | - run: script/lint 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /tmp/ 3 | /.bindown/ 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # configure golangci-lint 2 | # see https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml 3 | issues: 4 | exclude-use-default: false 5 | exclude-rules: 6 | - path: _test\.go 7 | linters: 8 | - dupl 9 | - gosec 10 | - goconst 11 | - text: "hugeParam: a is heavy" 12 | linters: 13 | - gocritic 14 | linters: 15 | enable: 16 | - gosec 17 | - unconvert 18 | - gocyclo 19 | - goconst 20 | - goimports 21 | - gocritic 22 | - gofumpt 23 | - revive 24 | linters-settings: 25 | gocritic: 26 | enabled-tags: 27 | - style 28 | - diagnostic 29 | - performance 30 | errcheck: 31 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 32 | # default is false: such cases aren't reported by default. 33 | check-blank: true 34 | govet: 35 | # report about shadowed variables 36 | check-shadowing: true 37 | maligned: 38 | # print struct with more effective memory layout or not, false by default 39 | suggest-new: true 40 | revive: 41 | rules: 42 | - name: package-comments 43 | disabled: true 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 WillAbides 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 | # kongplete 2 | 3 | You kongplete me. 4 | 5 | kongplete lets you generate shell completions for your command-line programs using 6 | [github.com/alecthomas/kong](https://github.com/alecthomas/kong) and [github.com/posener/complete](https://github.com/posener/complete). 7 | 8 | ## Examples 9 | 10 | ```golang 11 | // This example is adapted from the shell example in github.com/alecthomas/kong 12 | 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | "os" 18 | 19 | "github.com/alecthomas/kong" 20 | "github.com/posener/complete" 21 | "github.com/willabides/kongplete" 22 | ) 23 | 24 | var shellCli struct { 25 | Rm struct { 26 | User string `help:"Run as user." short:"u" default:"default"` 27 | Force bool `help:"Force removal." short:"f"` 28 | Recursive bool `help:"Recursively remove files." short:"r"` 29 | Hidden string `help:"A hidden flag" hidden:""` 30 | 31 | Paths []string `arg:"" help:"Paths to remove." type:"path" name:"path" predictor:"file"` 32 | } `cmd:"" help:"Remove files."` 33 | 34 | Ls struct { 35 | Paths []string `arg:"" optional:"" help:"Paths to list." type:"path" predictor:"file"` 36 | } `cmd:"" help:"List paths."` 37 | 38 | Hidden struct{} `cmd:"" help:"A hidden command" hidden:""` 39 | 40 | Debug bool `help:"Debug mode."` 41 | 42 | InstallCompletions kongplete.InstallCompletions `cmd:"" help:"install shell completions"` 43 | } 44 | 45 | func main() { 46 | // Create a kong parser as usual, but don't run Parse quite yet. 47 | parser := kong.Must(&shellCli, 48 | kong.Name("shell"), 49 | kong.Description("A shell-like example app."), 50 | kong.UsageOnError(), 51 | ) 52 | 53 | // Run kongplete.Complete to handle completion requests 54 | kongplete.Complete(parser, 55 | kongplete.WithPredictor("file", complete.PredictFiles("*")), 56 | ) 57 | 58 | // Proceed as normal after kongplete.Complete. 59 | ctx, err := parser.Parse(os.Args[1:]) 60 | parser.FatalIfErrorf(err) 61 | 62 | switch ctx.Command() { 63 | case "rm ": 64 | fmt.Println(shellCli.Rm.Paths, shellCli.Rm.Force, shellCli.Rm.Recursive) 65 | 66 | case "ls", "hidden": 67 | } 68 | } 69 | 70 | ``` 71 | -------------------------------------------------------------------------------- /completion_install.go: -------------------------------------------------------------------------------- 1 | package kongplete 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/alecthomas/kong" 10 | "github.com/riywo/loginshell" 11 | ) 12 | 13 | // InstallCompletions is a kong command for installing or uninstalling shell completions 14 | type InstallCompletions struct { 15 | Uninstall bool 16 | } 17 | 18 | // BeforeApply installs completion into the users shell. 19 | func (c *InstallCompletions) BeforeApply(ctx *kong.Context) error { 20 | err := installCompletionFromContext(ctx) 21 | if err != nil { 22 | return err 23 | } 24 | ctx.Exit(0) 25 | return nil 26 | } 27 | 28 | var shellInstall = map[string]string{ 29 | "bash": "complete -C ${bin} ${cmd}\n", 30 | "zsh": `autoload -U +X bashcompinit && bashcompinit 31 | complete -C ${bin} ${cmd} 32 | `, 33 | "fish": `function __complete_${cmd} 34 | set -lx COMP_LINE (commandline -cp) 35 | test -z (commandline -ct) 36 | and set COMP_LINE "$COMP_LINE " 37 | ${bin} 38 | end 39 | complete -f -c ${cmd} -a "(__complete_${cmd})" 40 | `, 41 | } 42 | 43 | // installCompletionFromContext writes shell completion for the given command. 44 | func installCompletionFromContext(ctx *kong.Context) error { 45 | shell, err := loginshell.Shell() 46 | if err != nil { 47 | return fmt.Errorf("couldn't determine user's shell: %w", err) 48 | } 49 | bin, err := os.Executable() 50 | if err != nil { 51 | return fmt.Errorf("couldn't find absolute path to ourselves: %w", err) 52 | } 53 | bin, err = filepath.Abs(bin) 54 | if err != nil { 55 | return fmt.Errorf("couldn't find absolute path to ourselves: %w", err) 56 | } 57 | w := ctx.Stdout 58 | cmd := ctx.Model.Name 59 | return installCompletion(w, shell, cmd, bin) 60 | } 61 | 62 | // installCompletion writes shell completion for a command. 63 | func installCompletion(w io.Writer, shell, cmd, bin string) error { 64 | script, ok := shellInstall[filepath.Base(shell)] 65 | if !ok { 66 | return fmt.Errorf("unsupported shell %s", shell) 67 | } 68 | vars := map[string]string{"cmd": cmd, "bin": bin} 69 | fragment := os.Expand(script, func(s string) string { 70 | v, ok := vars[s] 71 | if !ok { 72 | return "$" + s 73 | } 74 | return v 75 | }) 76 | _, err := fmt.Fprint(w, fragment) 77 | return err 78 | } 79 | -------------------------------------------------------------------------------- /completion_install_test.go: -------------------------------------------------------------------------------- 1 | package kongplete 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestInstallCompletion(t *testing.T) { 11 | tests := map[string]string{ 12 | "zsh": "autoload -U +X bashcompinit && bashcompinit\ncomplete -C /usr/bin/docker docker\n", 13 | "bash": "complete -C /usr/bin/docker docker\n", 14 | "fish": `function __complete_docker 15 | set -lx COMP_LINE (commandline -cp) 16 | test -z (commandline -ct) 17 | and set COMP_LINE "$COMP_LINE " 18 | /usr/bin/docker 19 | end 20 | complete -f -c docker -a "(__complete_docker)" 21 | `, 22 | } 23 | for shell, fragment := range tests { 24 | t.Run(shell, func(t *testing.T) { 25 | w := &strings.Builder{} 26 | err := installCompletion(w, shell, "docker", "/usr/bin/docker") 27 | require.NoError(t, err) 28 | require.Equal(t, fragment, w.String()) 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package kongplete lets you generate shell completions for your command-line programs using 3 | github.com/alecthomas/kong and github.com/posener/complete. 4 | */ 5 | package kongplete 6 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // This example is adapted from the shell example in github.com/alecthomas/kong 2 | 3 | package kongplete_test 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | 9 | "github.com/alecthomas/kong" 10 | "github.com/posener/complete" 11 | "github.com/willabides/kongplete" 12 | ) 13 | 14 | var shellCli struct { 15 | Rm struct { 16 | User string `help:"Run as user." short:"u" default:"default"` 17 | Force bool `help:"Force removal." short:"f"` 18 | Recursive bool `help:"Recursively remove files." short:"r"` 19 | Hidden string `help:"A hidden flag" hidden:""` 20 | 21 | Paths []string `arg:"" help:"Paths to remove." type:"path" name:"path" predictor:"file"` 22 | } `cmd:"" help:"Remove files."` 23 | 24 | Ls struct { 25 | Paths []string `arg:"" optional:"" help:"Paths to list." type:"path" predictor:"file"` 26 | } `cmd:"" help:"List paths."` 27 | 28 | Hidden struct{} `cmd:"" help:"A hidden command" hidden:""` 29 | 30 | Debug bool `help:"Debug mode."` 31 | 32 | InstallCompletions kongplete.InstallCompletions `cmd:"" help:"install shell completions"` 33 | } 34 | 35 | func Example() { 36 | // Create a kong parser as usual, but don't run Parse quite yet. 37 | parser := kong.Must(&shellCli, 38 | kong.Name("shell"), 39 | kong.Description("A shell-like example app."), 40 | kong.UsageOnError(), 41 | ) 42 | 43 | // Run kongplete.Complete to handle completion requests 44 | kongplete.Complete(parser, 45 | kongplete.WithPredictor("file", complete.PredictFiles("*")), 46 | ) 47 | 48 | // Proceed as normal after kongplete.Complete. 49 | ctx, err := parser.Parse(os.Args[1:]) 50 | parser.FatalIfErrorf(err) 51 | 52 | switch ctx.Command() { 53 | case "rm ": 54 | fmt.Println(shellCli.Rm.Paths, shellCli.Rm.Force, shellCli.Rm.Recursive) 55 | 56 | case "ls", "hidden": 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/willabides/kongplete 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/alecthomas/kong v0.8.1 7 | github.com/posener/complete v1.2.3 8 | github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab 9 | github.com/stretchr/testify v1.8.4 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/hashicorp/errwrap v1.1.0 // indirect 15 | github.com/hashicorp/go-multierror v1.1.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= 2 | github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= 3 | github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= 4 | github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= 5 | github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= 6 | github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 11 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 12 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 13 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 14 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 15 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 16 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 17 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= 21 | github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= 22 | github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab h1:ZjX6I48eZSFetPb41dHudEyVr5v953N15TsNZXlkcWY= 23 | github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab/go.mod h1:/PfPXh0EntGc3QAAyUaviy4S9tzy4Zp0e2ilq4voC6E= 24 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 25 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 26 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 27 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 28 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 29 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 30 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 31 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 35 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 37 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 38 | -------------------------------------------------------------------------------- /internal/positionalpredictor/positional.go: -------------------------------------------------------------------------------- 1 | package positionalpredictor 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/posener/complete" 7 | ) 8 | 9 | // PositionalPredictor is a predictor for positional arguments 10 | type PositionalPredictor struct { 11 | Predictors []complete.Predictor 12 | ArgFlags []string 13 | BoolFlags []string 14 | IsCumulative bool 15 | } 16 | 17 | // Predict implements complete.Predict 18 | func (p *PositionalPredictor) Predict(a complete.Args) []string { 19 | predictor := p.predictor(a) 20 | if predictor == nil { 21 | return []string{} 22 | } 23 | return predictor.Predict(a) 24 | } 25 | 26 | func (p *PositionalPredictor) predictor(a complete.Args) complete.Predictor { 27 | position := p.predictorIndex(a) 28 | complete.Log("predicting positional argument(%d)", position) 29 | if p.IsCumulative && position >= len(p.Predictors) { 30 | return p.Predictors[len(p.Predictors)-1] 31 | } 32 | if position < 0 || position > len(p.Predictors)-1 { 33 | return nil 34 | } 35 | return p.Predictors[position] 36 | } 37 | 38 | // predictorIndex returns the index in predictors to use. Returns -1 if no predictor should be used. 39 | func (p *PositionalPredictor) predictorIndex(a complete.Args) int { 40 | idx := 0 41 | for i := 0; i < len(a.Completed); i++ { 42 | if !p.nonPredictorPos(a, i) { 43 | idx++ 44 | } 45 | } 46 | return idx 47 | } 48 | 49 | // nonPredictorPos returns true if the value at this position is either a flag or a flag's argument 50 | func (p *PositionalPredictor) nonPredictorPos(a complete.Args, pos int) bool { 51 | if pos < 0 || pos > len(a.All)-1 { 52 | return false 53 | } 54 | val := a.All[pos] 55 | if p.valIsFlag(val) { 56 | return true 57 | } 58 | if pos == 0 { 59 | return false 60 | } 61 | prev := a.All[pos-1] 62 | return p.nextValueIsFlagArg(prev) 63 | } 64 | 65 | // valIsFlag returns true if the value matches a flag from the configuration 66 | func (p *PositionalPredictor) valIsFlag(val string) bool { 67 | val = strings.Split(val, "=")[0] 68 | for _, flag := range p.BoolFlags { 69 | if flag == val { 70 | return true 71 | } 72 | } 73 | for _, flag := range p.ArgFlags { 74 | if flag == val { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | 81 | // nextValueIsFlagArg returns true if the value matches an ArgFlag and doesn't contain an equal sign. 82 | func (p *PositionalPredictor) nextValueIsFlagArg(val string) bool { 83 | if strings.Contains(val, "=") { 84 | return false 85 | } 86 | for _, flag := range p.ArgFlags { 87 | if flag == val { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | -------------------------------------------------------------------------------- /internal/positionalpredictor/positional_test.go: -------------------------------------------------------------------------------- 1 | package positionalpredictor 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "unicode" 7 | 8 | "github.com/posener/complete" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestPositionalPredictor_position(t *testing.T) { 13 | posPredictor := &PositionalPredictor{ 14 | BoolFlags: []string{"--mybool", "-b"}, 15 | ArgFlags: []string{"--myarg", "-a"}, 16 | } 17 | 18 | for args, want := range map[string]int{ 19 | ``: 0, 20 | `foo`: 0, 21 | `foo `: 1, 22 | `-b foo `: 1, 23 | `-a foo `: 0, 24 | `-a=omg foo `: 1, 25 | `--myarg omg foo `: 1, 26 | `--myarg=omg foo `: 1, 27 | `foo bar`: 1, 28 | `foo bar `: 2, 29 | } { 30 | t.Run(args, func(t *testing.T) { 31 | got := posPredictor.predictorIndex(newArgs("foo " + args)) 32 | assert.Equal(t, want, got) 33 | }) 34 | } 35 | } 36 | 37 | func TestPositionalPredictor_predictor(t *testing.T) { 38 | predictor1 := complete.PredictSet("1") 39 | predictor2 := complete.PredictSet("2") 40 | posPredictor := &PositionalPredictor{ 41 | Predictors: []complete.Predictor{predictor1, predictor2}, 42 | } 43 | 44 | for args, want := range map[string]complete.Predictor{ 45 | ``: predictor1, 46 | `foo`: predictor1, 47 | `foo `: predictor2, 48 | `foo bar`: predictor2, 49 | `foo bar `: nil, 50 | } { 51 | t.Run(args, func(t *testing.T) { 52 | got := posPredictor.predictor(newArgs("app " + args)) 53 | assert.Equal(t, want, got) 54 | }) 55 | } 56 | } 57 | 58 | func TestPositionalPredictor_cumulativepredictor(t *testing.T) { 59 | predictor1 := complete.PredictSet("1") 60 | predictor2 := complete.PredictSet("2") 61 | posPredictor := &PositionalPredictor{ 62 | Predictors: []complete.Predictor{predictor1, predictor2}, 63 | IsCumulative: true, 64 | } 65 | 66 | for args, want := range map[string]complete.Predictor{ 67 | ``: predictor1, 68 | `foo`: predictor1, 69 | `foo `: predictor2, 70 | `foo bar`: predictor2, 71 | `foo bar `: predictor2, 72 | `foo bar baz `: predictor2, 73 | } { 74 | t.Run(args, func(t *testing.T) { 75 | got := posPredictor.predictor(newArgs("app " + args)) 76 | assert.Equal(t, want, got) 77 | }) 78 | } 79 | } 80 | 81 | // The code below is taken from https://github.com/posener/complete/blob/f6dd29e97e24f8cb51a8d4050781ce2b238776a4/args.go 82 | // to assist in tests. 83 | 84 | func newArgs(line string) complete.Args { 85 | var ( 86 | all []string 87 | completed []string 88 | ) 89 | parts := splitFields(line) 90 | if len(parts) > 0 { 91 | all = parts[1:] 92 | completed = removeLast(parts[1:]) 93 | } 94 | return complete.Args{ 95 | All: all, 96 | Completed: completed, 97 | Last: last(parts), 98 | LastCompleted: last(completed), 99 | } 100 | } 101 | 102 | // splitFields returns a list of fields from the given command line. 103 | // If the last character is space, it appends an empty field in the end 104 | // indicating that the field before it was completed. 105 | // If the last field is of the form "a=b", it splits it to two fields: "a", "b", 106 | // So it can be completed. 107 | func splitFields(line string) []string { 108 | parts := strings.Fields(line) 109 | 110 | // Add empty field if the last field was completed. 111 | if len(line) > 0 && unicode.IsSpace(rune(line[len(line)-1])) { 112 | parts = append(parts, "") 113 | } 114 | 115 | // Treat the last field if it is of the form "a=b" 116 | parts = splitLastEqual(parts) 117 | return parts 118 | } 119 | 120 | func splitLastEqual(line []string) []string { 121 | if len(line) == 0 { 122 | return line 123 | } 124 | parts := strings.Split(line[len(line)-1], "=") 125 | return append(line[:len(line)-1], parts...) 126 | } 127 | 128 | func removeLast(a []string) []string { 129 | if len(a) > 0 { 130 | return a[:len(a)-1] 131 | } 132 | return a 133 | } 134 | 135 | func last(args []string) string { 136 | if len(args) == 0 { 137 | return "" 138 | } 139 | return args[len(args)-1] 140 | } 141 | -------------------------------------------------------------------------------- /kongplete.go: -------------------------------------------------------------------------------- 1 | package kongplete 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/alecthomas/kong" 7 | "github.com/posener/complete" 8 | "github.com/willabides/kongplete/internal/positionalpredictor" 9 | ) 10 | 11 | const predictorTag = "predictor" 12 | 13 | type options struct { 14 | predictors map[string]complete.Predictor 15 | exitFunc func(code int) 16 | errorHandler func(error) 17 | } 18 | 19 | // Option is a configuration option for running Complete 20 | type Option func(*options) 21 | 22 | // WithPredictor use the named predictor 23 | func WithPredictor(name string, predictor complete.Predictor) Option { 24 | return func(o *options) { 25 | if o.predictors == nil { 26 | o.predictors = map[string]complete.Predictor{} 27 | } 28 | o.predictors[name] = predictor 29 | } 30 | } 31 | 32 | // WithPredictors use these predictors 33 | func WithPredictors(predictors map[string]complete.Predictor) Option { 34 | return func(o *options) { 35 | for k, v := range predictors { 36 | WithPredictor(k, v)(o) 37 | } 38 | } 39 | } 40 | 41 | // WithExitFunc the exit command that is run after completions 42 | func WithExitFunc(exitFunc func(code int)) Option { 43 | return func(o *options) { 44 | o.exitFunc = exitFunc 45 | } 46 | } 47 | 48 | // WithErrorHandler handle errors with completions 49 | func WithErrorHandler(handler func(error)) Option { 50 | return func(o *options) { 51 | o.errorHandler = handler 52 | } 53 | } 54 | 55 | func buildOptions(opt ...Option) *options { 56 | opts := &options{ 57 | predictors: map[string]complete.Predictor{}, 58 | } 59 | for _, o := range opt { 60 | o(opts) 61 | } 62 | return opts 63 | } 64 | 65 | // Command returns a completion Command for a kong parser 66 | func Command(parser *kong.Kong, opt ...Option) (complete.Command, error) { 67 | opts := buildOptions(opt...) 68 | if parser == nil || parser.Model == nil { 69 | return complete.Command{}, nil 70 | } 71 | command, err := nodeCommand(parser.Model.Node, opts.predictors) 72 | if err != nil { 73 | return complete.Command{}, err 74 | } 75 | return *command, err 76 | } 77 | 78 | // Complete runs completion for a kong parser 79 | func Complete(parser *kong.Kong, opt ...Option) { 80 | if parser == nil { 81 | return 82 | } 83 | opts := buildOptions(opt...) 84 | errHandler := opts.errorHandler 85 | if errHandler == nil { 86 | errHandler = func(err error) { 87 | parser.Errorf("error running command completion: %v", err) 88 | } 89 | } 90 | exitFunc := opts.exitFunc 91 | if exitFunc == nil { 92 | exitFunc = parser.Exit 93 | } 94 | cmd, err := Command(parser, opt...) 95 | if err != nil { 96 | errHandler(err) 97 | exitFunc(1) 98 | } 99 | cmp := complete.New(parser.Model.Name, cmd) 100 | cmp.Out = parser.Stdout 101 | done := cmp.Complete() 102 | if done { 103 | exitFunc(0) 104 | } 105 | } 106 | 107 | func nodeCommand(node *kong.Node, predictors map[string]complete.Predictor) (*complete.Command, error) { 108 | if node == nil { 109 | return nil, nil 110 | } 111 | 112 | cmd := complete.Command{ 113 | Sub: complete.Commands{}, 114 | GlobalFlags: complete.Flags{}, 115 | } 116 | 117 | for _, child := range node.Children { 118 | if child == nil || child.Hidden { 119 | continue 120 | } 121 | childCmd, err := nodeCommand(child, predictors) 122 | if err != nil { 123 | return nil, err 124 | } 125 | if childCmd != nil { 126 | cmd.Sub[child.Name] = *childCmd 127 | } 128 | } 129 | 130 | for _, flag := range node.Flags { 131 | if flag == nil || flag.Hidden { 132 | continue 133 | } 134 | predictor, err := flagPredictor(flag, predictors) 135 | if err != nil { 136 | return nil, err 137 | } 138 | for _, f := range flagNamesWithHyphens(flag) { 139 | cmd.GlobalFlags[f] = predictor 140 | } 141 | } 142 | 143 | boolFlags, nonBoolFlags := boolAndNonBoolFlags(node.Flags) 144 | isCumulative := false 145 | if len(node.Positional) > 0 && node.Positional[len(node.Positional)-1].IsCumulative() { 146 | isCumulative = true 147 | } 148 | 149 | pps, err := positionalPredictors(node.Positional, predictors) 150 | if err != nil { 151 | return nil, err 152 | } 153 | cmd.Args = &positionalpredictor.PositionalPredictor{ 154 | Predictors: pps, 155 | ArgFlags: flagNamesWithHyphens(nonBoolFlags...), 156 | BoolFlags: flagNamesWithHyphens(boolFlags...), 157 | IsCumulative: isCumulative, 158 | } 159 | 160 | return &cmd, nil 161 | } 162 | 163 | func flagNamesWithHyphens(flags ...*kong.Flag) []string { 164 | names := make([]string, 0, len(flags)*2) 165 | if flags == nil { 166 | return names 167 | } 168 | for _, flag := range flags { 169 | names = append(names, "--"+flag.Name) 170 | if flag.Short != 0 { 171 | names = append(names, "-"+string(flag.Short)) 172 | } 173 | } 174 | return names 175 | } 176 | 177 | // boolAndNonBoolFlags divides a list of flags into boolean and non-boolean flags 178 | func boolAndNonBoolFlags(flags []*kong.Flag) (boolFlags, nonBoolFlags []*kong.Flag) { 179 | boolFlags = make([]*kong.Flag, 0, len(flags)) 180 | nonBoolFlags = make([]*kong.Flag, 0, len(flags)) 181 | for _, flag := range flags { 182 | switch flag.Value.IsBool() { 183 | case true: 184 | boolFlags = append(boolFlags, flag) 185 | case false: 186 | nonBoolFlags = append(nonBoolFlags, flag) 187 | } 188 | } 189 | return boolFlags, nonBoolFlags 190 | } 191 | 192 | // kongTag interface for *kong.kongTag 193 | type kongTag interface { 194 | Has(string) bool 195 | Get(string) string 196 | } 197 | 198 | func tagPredictor(tag kongTag, predictors map[string]complete.Predictor) (complete.Predictor, error) { 199 | if tag == nil { 200 | return nil, nil 201 | } 202 | if !tag.Has(predictorTag) { 203 | return nil, nil 204 | } 205 | if predictors == nil { 206 | predictors = map[string]complete.Predictor{} 207 | } 208 | predictorName := tag.Get(predictorTag) 209 | predictor, ok := predictors[predictorName] 210 | if !ok { 211 | return nil, fmt.Errorf("no predictor with name %q", predictorName) 212 | } 213 | return predictor, nil 214 | } 215 | 216 | func valuePredictor(value *kong.Value, predictors map[string]complete.Predictor) (complete.Predictor, error) { 217 | if value == nil { 218 | return nil, nil 219 | } 220 | predictor, err := tagPredictor(value.Tag, predictors) 221 | if err != nil { 222 | return nil, err 223 | } 224 | if predictor != nil { 225 | return predictor, nil 226 | } 227 | switch { 228 | case value.IsBool(): 229 | return complete.PredictNothing, nil 230 | case value.Enum != "": 231 | enumVals := make([]string, 0, len(value.EnumMap())) 232 | for enumVal := range value.EnumMap() { 233 | enumVals = append(enumVals, enumVal) 234 | } 235 | return complete.PredictSet(enumVals...), nil 236 | default: 237 | return complete.PredictAnything, nil 238 | } 239 | } 240 | 241 | func positionalPredictors(args []*kong.Positional, predictors map[string]complete.Predictor) ([]complete.Predictor, error) { 242 | res := make([]complete.Predictor, len(args)) 243 | var err error 244 | for i, arg := range args { 245 | res[i], err = valuePredictor(arg, predictors) 246 | if err != nil { 247 | return nil, err 248 | } 249 | } 250 | return res, nil 251 | } 252 | 253 | func flagPredictor(flag *kong.Flag, predictors map[string]complete.Predictor) (complete.Predictor, error) { 254 | return valuePredictor(flag.Value, predictors) 255 | } 256 | -------------------------------------------------------------------------------- /kongplete_test.go: -------------------------------------------------------------------------------- 1 | package kongplete 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/alecthomas/kong" 11 | "github.com/posener/complete" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | const ( 17 | envLine = "COMP_LINE" 18 | envPoint = "COMP_POINT" 19 | ) 20 | 21 | func TestComplete(t *testing.T) { 22 | type embed struct { 23 | Lion string 24 | } 25 | 26 | predictors := map[string]complete.Predictor{ 27 | "things": complete.PredictSet("thing1", "thing2"), 28 | "otherthings": complete.PredictSet("otherthing1", "otherthing2"), 29 | } 30 | 31 | var cli struct { 32 | Foo struct { 33 | Embedded embed `kong:"embed"` 34 | Bar string `kong:"predictor=things"` 35 | Baz bool 36 | Qux bool `kong:"hidden"` 37 | Rabbit struct{} `kong:"cmd"` 38 | Duck struct{} `kong:"cmd"` 39 | } `kong:"cmd"` 40 | Bar struct { 41 | Tiger string `kong:"arg,predictor=things"` 42 | Bear string `kong:"arg,predictor=otherthings"` 43 | OMG string `kong:"required,enum='oh,my,gizzles'"` 44 | Number int `kong:"required,short=n,enum='1,2,3'"` 45 | BooFlag bool `kong:"name=boofl,short=b"` 46 | } `kong:"cmd"` 47 | Baz struct{} `kong:"cmd,hidden"` 48 | Pos struct { 49 | Cumulative []string `kong:"arg,predictor=things"` 50 | } `kong:"cmd"` 51 | } 52 | 53 | for _, td := range []completeTest{ 54 | { 55 | parser: kong.Must(&cli), 56 | want: []string{"thing1", "thing2"}, 57 | line: "myApp pos thing1 ", 58 | }, 59 | { 60 | parser: kong.Must(&cli), 61 | want: []string{"foo", "bar", "pos"}, 62 | line: "myApp ", 63 | }, 64 | { 65 | parser: kong.Must(&cli), 66 | want: []string{"foo"}, 67 | line: "myApp foo", 68 | }, 69 | { 70 | parser: kong.Must(&cli), 71 | want: []string{"rabbit", "duck"}, 72 | line: "myApp foo ", 73 | }, 74 | { 75 | parser: kong.Must(&cli), 76 | want: []string{"rabbit"}, 77 | line: "myApp foo r", 78 | }, 79 | { 80 | parser: kong.Must(&cli), 81 | want: []string{"--bar", "--baz", "--lion", "--help", "-h"}, 82 | line: "myApp foo -", 83 | }, 84 | { 85 | parser: kong.Must(&cli), 86 | want: []string{}, 87 | line: "myApp foo --lion ", 88 | }, 89 | { 90 | parser: kong.Must(&cli), 91 | want: []string{"rabbit", "duck"}, 92 | line: "myApp foo --baz ", 93 | }, 94 | { 95 | parser: kong.Must(&cli), 96 | want: []string{"--bar", "--baz", "--lion", "--help", "-h"}, 97 | line: "myApp foo --baz -", 98 | }, 99 | { 100 | parser: kong.Must(&cli), 101 | 102 | want: []string{"thing1", "thing2"}, 103 | line: "myApp foo --bar ", 104 | }, 105 | { 106 | parser: kong.Must(&cli), 107 | want: []string{"thing1", "thing2"}, 108 | line: "myApp bar ", 109 | }, 110 | { 111 | parser: kong.Must(&cli), 112 | want: []string{"thing1", "thing2"}, 113 | line: "myApp bar thing", 114 | }, 115 | { 116 | parser: kong.Must(&cli), 117 | want: []string{"otherthing1", "otherthing2"}, 118 | line: "myApp bar thing1 ", 119 | }, 120 | { 121 | parser: kong.Must(&cli), 122 | want: []string{"oh", "my", "gizzles"}, 123 | line: "myApp bar --omg ", 124 | }, 125 | { 126 | parser: kong.Must(&cli), 127 | want: []string{"-n", "--number", "--omg", "--help", "-h", "--boofl", "-b"}, 128 | line: "myApp bar -", 129 | }, 130 | { 131 | parser: kong.Must(&cli), 132 | want: []string{"thing1", "thing2"}, 133 | line: "myApp bar -b ", 134 | }, 135 | { 136 | parser: kong.Must(&cli), 137 | want: []string{"-n", "--number", "--omg", "--help", "-h", "--boofl", "-b"}, 138 | line: "myApp bar -b thing1 -", 139 | }, 140 | { 141 | parser: kong.Must(&cli), 142 | want: []string{"oh", "my", "gizzles"}, 143 | line: "myApp bar -b thing1 --omg ", 144 | }, 145 | { 146 | parser: kong.Must(&cli), 147 | want: []string{"otherthing1", "otherthing2"}, 148 | line: "myApp bar -b thing1 --omg gizzles ", 149 | }, 150 | } { 151 | name := td.name 152 | if name == "" { 153 | name = td.line 154 | } 155 | t.Run(name, func(t *testing.T) { 156 | options := []Option{WithPredictors(predictors)} 157 | got := runComplete(t, td.parser, td.line, options) 158 | assert.ElementsMatch(t, td.want, got) 159 | }) 160 | } 161 | } 162 | 163 | func Test_tagPredictor(t *testing.T) { 164 | t.Run("nil", func(t *testing.T) { 165 | got, err := tagPredictor(nil, nil) 166 | assert.NoError(t, err) 167 | assert.Nil(t, got) 168 | }) 169 | 170 | t.Run("no predictor tag", func(t *testing.T) { 171 | got, err := tagPredictor(testTag{}, nil) 172 | assert.NoError(t, err) 173 | assert.Nil(t, got) 174 | }) 175 | 176 | t.Run("missing predictor", func(t *testing.T) { 177 | got, err := tagPredictor(testTag{predictorTag: "foo"}, nil) 178 | assert.Error(t, err) 179 | assert.Equal(t, `no predictor with name "foo"`, err.Error()) 180 | assert.Nil(t, got) 181 | }) 182 | 183 | t.Run("existing predictor", func(t *testing.T) { 184 | got, err := tagPredictor(testTag{predictorTag: "foo"}, map[string]complete.Predictor{"foo": complete.PredictAnything}) 185 | assert.NoError(t, err) 186 | assert.NotNil(t, got) 187 | }) 188 | } 189 | 190 | type testTag map[string]string 191 | 192 | func (t testTag) Has(k string) bool { 193 | _, ok := t[k] 194 | return ok 195 | } 196 | 197 | func (t testTag) Get(k string) string { 198 | return t[k] 199 | } 200 | 201 | type completeTest struct { 202 | name string 203 | parser *kong.Kong 204 | want []string 205 | line string 206 | } 207 | 208 | func setLineAndPoint(t *testing.T, line string) func() { 209 | t.Helper() 210 | origLine, hasOrigLine := os.LookupEnv(envLine) 211 | origPoint, hasOrigPoint := os.LookupEnv(envPoint) 212 | require.NoError(t, os.Setenv(envLine, line)) 213 | require.NoError(t, os.Setenv(envPoint, strconv.Itoa(len(line)))) 214 | return func() { 215 | t.Helper() 216 | require.NoError(t, os.Unsetenv(envLine)) 217 | require.NoError(t, os.Unsetenv(envPoint)) 218 | if hasOrigLine { 219 | require.NoError(t, os.Setenv(envLine, origLine)) 220 | } 221 | if hasOrigPoint { 222 | require.NoError(t, os.Setenv(envPoint, origPoint)) 223 | } 224 | } 225 | } 226 | 227 | func runComplete(t *testing.T, parser *kong.Kong, line string, options []Option) []string { 228 | t.Helper() 229 | options = append(options, 230 | WithErrorHandler(func(err error) { 231 | t.Helper() 232 | assert.NoError(t, err) 233 | }), 234 | WithExitFunc(func(code int) { 235 | t.Helper() 236 | assert.Equal(t, 0, code) 237 | }), 238 | ) 239 | cleanup := setLineAndPoint(t, line) 240 | defer cleanup() 241 | var buf bytes.Buffer 242 | if parser != nil { 243 | parser.Stdout = &buf 244 | } 245 | Complete(parser, options...) 246 | return parseOutput(buf.String()) 247 | } 248 | 249 | func parseOutput(output string) []string { 250 | lines := strings.Split(output, "\n") 251 | options := []string{} 252 | for _, l := range lines { 253 | if l != "" { 254 | options = append(options, l) 255 | } 256 | } 257 | return options 258 | } 259 | -------------------------------------------------------------------------------- /script/bindown: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | CDPATH="" cd -- "$(dirname -- "$0")/.." 6 | 7 | mkdir -p bin 8 | 9 | [ -f bin/bindown ] || sh -c "$( 10 | curl -sfL https://github.com/WillAbides/bindown/releases/download/v4.6.2/bootstrap-bindown.sh 11 | )" 12 | 13 | exec bin/bindown "$@" 14 | -------------------------------------------------------------------------------- /script/fmt: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #/ script/fmt formats go code and shell scripts. 3 | 4 | set -e 5 | 6 | CDPATH="" cd -- "$(dirname -- "$0")/.." 7 | 8 | script/bindown -q install gofumpt shfmt handcrafted 9 | 10 | git ls-files -o -c --exclude-standard '*.go' | 11 | bin/handcrafted | 12 | xargs bin/gofumpt -w -extra 13 | 14 | bin/shfmt -ci -i 2 -ci -sr -w -s ./script 15 | -------------------------------------------------------------------------------- /script/generate: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | CDPATH="" cd -- "$(dirname -- "$0")/.." 6 | 7 | if [ "$1" = "--check" ]; then 8 | [ -z "$(git status --porcelain)" ] || { 9 | git status 10 | echo 1>&2 "Running 'script/generate --check' requires a clean git working tree. Please commit or stash changes and try again." 11 | exit 1 12 | } 13 | script/generate 14 | [ -z "$(git status --porcelain)" ] || { 15 | git status 16 | echo 1>&2 "script/generate resulted in changes. Please commit changes (or 'git reset --hard HEAD' if you aren't ready to commit changes)." 17 | git diff 18 | exit 1 19 | } 20 | exit 0 21 | fi 22 | 23 | go generate ./... 24 | -------------------------------------------------------------------------------- /script/lint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | CDPATH="" cd -- "$(dirname -- "$0")/.." 6 | 7 | script/bindown -q install golangci-lint shellcheck 8 | bin/golangci-lint run ./... 9 | bin/shellcheck script/* 10 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | CDPATH="" cd -- "$(dirname -- "$0")/.." 6 | 7 | go test -race -covermode=atomic ./... 8 | --------------------------------------------------------------------------------