dry-cli-autocomplete 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.secrets.baseline +127 -0
- data/CHANGELOG.md +25 -0
- data/CLAUDE.md +88 -0
- data/LICENSE.txt +21 -0
- data/README.md +180 -0
- data/Rakefile +39 -0
- data/SPECIFICATION.md +246 -0
- data/docs/img/badge.svg +21 -0
- data/justfile +110 -0
- data/lefthook.yml +34 -0
- data/lib/dry/cli/autocomplete/command.rb +74 -0
- data/lib/dry/cli/autocomplete/emitters/bash.rb +177 -0
- data/lib/dry/cli/autocomplete/emitters/zsh.rb +213 -0
- data/lib/dry/cli/autocomplete/spec_builder.rb +103 -0
- data/lib/dry/cli/autocomplete/version.rb +15 -0
- data/lib/dry/cli/autocomplete.rb +19 -0
- data/lib/dry-cli-autocomplete.rb +3 -0
- data/sig/dry/cli/autocomplete.rbs +8 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5de81918d8ea085c400d5578481fd513b595d21b4e6c46fa2cce70ffbbe051f1
|
|
4
|
+
data.tar.gz: b57a07d39c36db90a843474694e61c1fcf970f58a2267bc44449ed60017f3369
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fd15d74c1ebece2e83817330d91f09f664b301753cbe09f2c5413ee042db260ea04acff53c6b090b1ea275b748f38949fd348f2b4bc847680446e546f57c6c68
|
|
7
|
+
data.tar.gz: c7bdc08cdeefa48c8ea9f67c9667fb76053b28a75ff96f7d1239ab2f1135d6f22a296a402a64c364fbedb7e8b7ede2b25e2ae8ebaa14087233d51cd6a3f40ca3
|
data/.secrets.baseline
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.5.0",
|
|
3
|
+
"plugins_used": [
|
|
4
|
+
{
|
|
5
|
+
"name": "ArtifactoryDetector"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"name": "AWSKeyDetector"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "AzureStorageKeyDetector"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "Base64HighEntropyString",
|
|
15
|
+
"limit": 4.5
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "BasicAuthDetector"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "CloudantDetector"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "DiscordBotTokenDetector"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "GitHubTokenDetector"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "GitLabTokenDetector"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "HexHighEntropyString",
|
|
34
|
+
"limit": 3.0
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "IbmCloudIamDetector"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "IbmCosHmacDetector"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "IPPublicDetector"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "JwtTokenDetector"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "KeywordDetector",
|
|
50
|
+
"keyword_exclude": ""
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "MailchimpDetector"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "NpmDetector"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "OpenAIDetector"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "PrivateKeyDetector"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "PypiTokenDetector"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "SendGridDetector"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "SlackDetector"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "SoftlayerDetector"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "SquareOAuthDetector"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "StripeDetector"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "TelegramBotTokenDetector"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "TwilioKeyDetector"
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"filters_used": [
|
|
90
|
+
{
|
|
91
|
+
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
|
|
95
|
+
"min_level": 2
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"path": "detect_secrets.filters.heuristic.is_indirect_reference"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"path": "detect_secrets.filters.heuristic.is_likely_id_string"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"path": "detect_secrets.filters.heuristic.is_lock_file"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"path": "detect_secrets.filters.heuristic.is_potential_uuid"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"path": "detect_secrets.filters.heuristic.is_sequential_string"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"path": "detect_secrets.filters.heuristic.is_swagger_file"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"path": "detect_secrets.filters.heuristic.is_templated_secret"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"results": {},
|
|
126
|
+
"generated_at": "2026-04-13T21:29:03Z"
|
|
127
|
+
}
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.1.3] - 2026-08-23
|
|
4
|
+
|
|
5
|
+
- The zsh script now registers itself when sourced. `eval "$(mycli completion zsh)"`
|
|
6
|
+
previously ran the completion function outside a completion context, and zsh
|
|
7
|
+
answered with `_tags:comptags:36: can only be called from completion function`.
|
|
8
|
+
The script now calls itself when autoloaded from `$fpath` and calls `compdef`
|
|
9
|
+
when sourced, so both work.
|
|
10
|
+
- The zsh function is named `_mycli` rather than `_mycli_completions`, matching
|
|
11
|
+
the name a `#compdef` file is autoloaded under. The bash function is unchanged.
|
|
12
|
+
|
|
13
|
+
## [0.1.2] - 2026-08-22
|
|
14
|
+
|
|
15
|
+
- The bash script now completes values declared on an option. `mycli fmt --format` then TAB offered the command list again instead of the values `--format` accepts; zsh had always emitted them, so only the bash side was dropping them.
|
|
16
|
+
- The bash script now offers values declared on a positional argument at that position, alongside the node's flags and subcommands.
|
|
17
|
+
|
|
18
|
+
## [0.1.1] - 2026-08-22
|
|
19
|
+
|
|
20
|
+
- Fixed loading inside any host that has dry-struct loaded. The value objects were built from a bare `Struct`, which resolves to `Dry::Struct` from inside `module Dry`, so `mycli completion bash` raised `wrong number of arguments` on the host's machine while this gem's own suite stayed green.
|
|
21
|
+
- Value objects are `Data` rather than `Struct`: frozen, and a missing field raises instead of arriving as a silent nil.
|
|
22
|
+
|
|
23
|
+
## [0.1.0] - 2026-08-22
|
|
24
|
+
|
|
25
|
+
- Initial release
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Guidance for Claude Code (claude.ai/code) working in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
A Ruby gem that generates static shell completion scripts for any `Dry::CLI` application. A host registers one command; `mycli completion bash` prints a script; the user evaluates it from a shell profile.
|
|
8
|
+
|
|
9
|
+
**Read `SPECIFICATION.md` first.** It carries the design decisions, the measurements behind them, and the acceptance criteria. This file covers how to work in the repository; that one covers what to build and why.
|
|
10
|
+
|
|
11
|
+
The gem is a skeleton from `bundle gem` with no implementation yet. Nothing in `lib/` does anything.
|
|
12
|
+
|
|
13
|
+
## Environment
|
|
14
|
+
|
|
15
|
+
Ruby is managed by rbenv. Prefix every Ruby command:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
eval "$(rbenv init -)" && bundle exec rspec
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bundle install
|
|
23
|
+
bundle exec rspec # the suite
|
|
24
|
+
bundle exec rubocop # the linter
|
|
25
|
+
bundle exec rubocop -a # autocorrect
|
|
26
|
+
bundle exec rake # both, and the default task
|
|
27
|
+
bin/console # IRB with the gem loaded
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The gemspec sets `required_ruby_version >= 3.2.0` and `.rubocop.yml` sets `TargetRubyVersion: 3.2`. Keep the two in step: raising one without the other produces a linter that permits syntax the gemspec claims to support, or the reverse.
|
|
31
|
+
|
|
32
|
+
## The trap that has already bitten this repository once
|
|
33
|
+
|
|
34
|
+
`bundle gem dry-cli-autocomplete` generates `module Dry; module Cli`. **dry-cli declares `Dry::CLI`, and it is a class, not a module.** Reopening a class as a module raises `TypeError` the moment both are loaded, and the error names neither file usefully.
|
|
35
|
+
|
|
36
|
+
Six files were generated wrong and have been fixed. If you add a file under `lib/dry/cli/`, nest it as:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
module Dry
|
|
40
|
+
class CLI # class, and CLI is an acronym
|
|
41
|
+
module Autocomplete
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`lib/dry/cli/autocomplete/version.rb` deliberately does **not** `require "dry/cli"`, because the gemspec loads it at build time when the dependency may not be installed. It reopens `class CLI` on its own. dry-cli's `CLI` inherits from `Object`, so an empty reopening is compatible whichever loads first.
|
|
45
|
+
|
|
46
|
+
For deriving names at runtime, use `dry-inflector`. `Dry::Inflector.new { |i| i.acronym("CLI") }` handles both the casing above and the `underscore` needed for shell function identifiers. `Dry::CLI::Inflector` ships with dry-cli but only has `dasherize` and is marked `@api private`; do not depend on it.
|
|
47
|
+
|
|
48
|
+
## Architecture
|
|
49
|
+
|
|
50
|
+
Four pieces, and the boundary between the first and the rest is load-bearing.
|
|
51
|
+
|
|
52
|
+
| File | Role |
|
|
53
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
54
|
+
| `lib/dry/cli/autocomplete/command.rb` | The shim a host registers. Defines the command class and nothing else. `require`s the generator **inside `#call`**, never at the top. |
|
|
55
|
+
| `lib/dry/cli/autocomplete/spec_builder.rb` | Walks a registry through public API and returns a shell-agnostic description of every completion. |
|
|
56
|
+
| `lib/dry/cli/autocomplete/emitters/bash.rb` | Turns that description into a `complete -F` script. |
|
|
57
|
+
| `lib/dry/cli/autocomplete/emitters/zsh.rb` | Turns it into a native `#compdef` script with per-option descriptions. |
|
|
58
|
+
|
|
59
|
+
Two rules hold this shape together, both measured rather than assumed:
|
|
60
|
+
|
|
61
|
+
- **The command shim loads no emitter.** A host pays nothing at boot for a command run once per shell. `SPECIFICATION.md` §2.4.
|
|
62
|
+
- **The spec builder touches only the registry.** It must be safe to run at shell startup, because it runs at every shell startup. `SPECIFICATION.md` §2.2.
|
|
63
|
+
|
|
64
|
+
The emitters take the same description and share no code. A fourth shell should be a new emitter class, never a branch inside an existing one.
|
|
65
|
+
|
|
66
|
+
## Conventions
|
|
67
|
+
|
|
68
|
+
- **The generator is not a hot path.** It runs in 0.067ms against a 27-command registry. Do not optimise it, do not add native extensions, and do not cache anything. The reasoning is in `SPECIFICATION.md` §2.3.
|
|
69
|
+
- **Read a registry through its methods, not its ivars.** `registry.get(path)` returns a result exposing `command`, `children` and `names`. `instance_variable_get(:@node)` is what the gem this one replaces does, and it will break on a dry-cli release. Do not mistake this for a public API: in 1.4.1 `Registry#get`, all of `CommandRegistry`, every `LookupResult` reader and every `Node` reader carry `@api private`. There is no public way to enumerate a registry, so an upgrade can break the walk and the fixture suite is what catches it.
|
|
70
|
+
- **Test against registries this project did not write.** A generator tested against one CLI encodes that CLI's shape. `SPECIFICATION.md` §5.
|
|
71
|
+
- **Validate generated shell with the shell.** `bash -n` and `zsh -n` parse without executing. A regex over generated output proves nothing about whether it runs.
|
|
72
|
+
- **Commit messages**: imperative mood, 50-character subject, no full stop. A body only where the change needs explaining, saying what and why.
|
|
73
|
+
- **Writing prose here**: no em dashes, active voice, plain words. Say what a thing does, not how it feels. If a sentence could appear unchanged in another project's README, it says nothing about this one and should go.
|
|
74
|
+
|
|
75
|
+
## Repository layout
|
|
76
|
+
|
|
77
|
+
| Path | What it is |
|
|
78
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| `SPECIFICATION.md` | What to build, why, and what "done" means |
|
|
80
|
+
| `lib/dry/cli/autocomplete.rb` | Entry point |
|
|
81
|
+
| `lib/dry/cli/autocomplete/version.rb` | Version, loaded standalone by the gemspec |
|
|
82
|
+
| `sig/` | RBS signatures, generated by `bundle gem` and not yet real |
|
|
83
|
+
| `exe/dry-cli-autocomplete` | Generated by `bundle gem`. **Probably should be deleted**: this is a library, and a host provides the executable. |
|
|
84
|
+
| `.github/workflows/main.yml` | CI |
|
|
85
|
+
|
|
86
|
+
## Before the first release
|
|
87
|
+
|
|
88
|
+
The gemspec still carries `bundle gem` TODOs that will refuse to build: `spec.summary`, `spec.description`, and `spec.metadata["allowed_push_host"]`. The `dry-` prefix and the `Dry::CLI::Autocomplete` namespace imply an affiliation with dry-rb that does not exist, so say so in the README, or ask them first.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Konstantin Gredeskoul
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# dry-cli-autocomplete
|
|
2
|
+
|
|
3
|
+
[](https://github.com/kigster/dry-cli-autocomplete/actions/workflows/main.yml) 
|
|
4
|
+
|
|
5
|
+
Shell completion for [dry-cli](https://github.com/dry-rb/dry-cli) applications, with no Ruby in the TAB path.
|
|
6
|
+
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> For the specification of this gem see [SPECIFICATION](SPECIFICATION.md)
|
|
9
|
+
|
|
10
|
+
Your CLI knows its own commands, options, aliases and enum values. The shell does not. This gem walks your registry once, prints a bash or zsh script, and you source it from your profile. Pressing TAB then spawns nothing and costs nothing, because every completion the script will ever offer is already inside it.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
mycli completion bash > /usr/local/etc/bash_completion.d/mycli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## The problem
|
|
17
|
+
|
|
18
|
+
A dry-cli app with nested subcommands gives the shell nothing to work with. `mycli db <TAB>` completes filenames from the current directory, which is never what you wanted.
|
|
19
|
+
|
|
20
|
+
[`rngtng/dry-cli-completion`](https://github.com/rngtng/dry-cli-completion) already solves part of this, and it is worth reading before you reach for this gem. It falls short in four ways, and each one is an acceptance criterion here.
|
|
21
|
+
|
|
22
|
+
**A group that has both a command and children loses the children.** Register an overview command at a group's bare name so `mycli db --help` can explain the group, and its subcommands stop completing:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
register "db", DbStatus # the node now has a command
|
|
26
|
+
register "db migrate", Migrate # ...and children, which never get walked
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`mycli db <TAB>` then offers `--help` and nothing else. This is what any app does when it wants group-level help.
|
|
30
|
+
|
|
31
|
+
**File arguments vanish.** `Input#input_line` returns early on `<file>`, so a command with a path argument produces no `compgen -f`, no `-o default`, no `_filedir`. Completing a path is the single most common thing a user wants from a CLI, and it is the one thing that does not work.
|
|
32
|
+
|
|
33
|
+
**The entry point is not free.** `command.rb` opens with a require that pulls the generator, which pulls `completely`. Every host pays for that at boot. Measured: `require "dry/cli"` costs 160ms, and adding the completion gem takes it to 190ms. Thirty milliseconds on every single invocation, for a command that runs once per shell.
|
|
34
|
+
|
|
35
|
+
**zsh is a bashcompinit shim.** It emits `autoload -Uz +X bashcompinit && bashcompinit` followed by bash. That works, but zsh users get no per-option descriptions and none of the behaviour they expect from a native completion.
|
|
36
|
+
|
|
37
|
+
There is a dependency argument too. `completely` pulls `colsole`, `docopt_ng` and `mister_bin`, and `mister_bin` is itself a CLI framework. That is four gems, one of them a second CLI framework, to print a shell script. This gem depends on `dry-cli` and `dry-inflector`, and nothing else.
|
|
38
|
+
|
|
39
|
+
## What it generates
|
|
40
|
+
|
|
41
|
+
Given this registry:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
register "version", Version
|
|
45
|
+
register "deploy", Deploy
|
|
46
|
+
register "db", DbStatus do |prefix|
|
|
47
|
+
prefix.register "migrate", DbMigrate
|
|
48
|
+
end
|
|
49
|
+
register "secret", Secret, hidden: true
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`mycli completion bash` prints a `complete -F` function that dispatches on the command path:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
_mycli_completions() {
|
|
56
|
+
# ...walks COMP_WORDS to find the current command path...
|
|
57
|
+
words=""
|
|
58
|
+
case "$path" in
|
|
59
|
+
"") words="version deploy db" ;;
|
|
60
|
+
"version") words="--format" ;;
|
|
61
|
+
"deploy") words="--force -f" ;;
|
|
62
|
+
"db") words="migrate --verbose" ;;
|
|
63
|
+
esac
|
|
64
|
+
|
|
65
|
+
COMPREPLY=($(compgen -W "$words" -- "$cur"))
|
|
66
|
+
case "$path" in
|
|
67
|
+
"db migrate") COMPREPLY+=($(compgen -f -- "$cur")) ;;
|
|
68
|
+
esac
|
|
69
|
+
}
|
|
70
|
+
complete -F _mycli_completions mycli
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Read what that output proves. `db` offers `migrate` alongside its own `--verbose`, so a group with both a command and children keeps both. `db migrate` gets real file completion. `secret` is absent, because hidden commands stay hidden. The `-f` alias on `deploy` is there because you declared it.
|
|
74
|
+
|
|
75
|
+
`mycli completion zsh` prints a native `#compdef` script built on `_arguments` and `_describe`, carrying each option's `desc` as help text next to it.
|
|
76
|
+
|
|
77
|
+
Enum values declared on an option or argument come through at no cost:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
option :format, values: %w[json yaml table] # completes json yaml table
|
|
81
|
+
argument :component, values: %w[major minor] # completes major minor
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Installation
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
gem install dry-cli-autocomplete
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Or add it to your `Gemfile`.
|
|
91
|
+
|
|
92
|
+
Then register the command in your CLI. Require the command file, not the gem: it pulls in no emitter and no generator, so a host pays nothing at boot for a command that runs once per shell.
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
require "dry/cli/autocomplete/command"
|
|
96
|
+
|
|
97
|
+
module MyCLI
|
|
98
|
+
extend Dry::CLI::Registry
|
|
99
|
+
|
|
100
|
+
register "version", Version
|
|
101
|
+
register "deploy", Deploy
|
|
102
|
+
register "completion", Dry::CLI::Autocomplete::Command[MyCLI]
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
That require pulls in the command class and nothing else. No emitter loads until someone actually runs `mycli completion`.
|
|
107
|
+
|
|
108
|
+
Then have your users write the script once and source it. For bash:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
mycli completion bash > /usr/local/etc/bash_completion.d/mycli
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For zsh, put it anywhere on your `$fpath`:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
mycli completion zsh > "${fpath[1]}/_mycli"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Sourcing it from `.zshrc` works too, if you would rather not manage a file:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
eval "$(mycli completion zsh)"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The script tells the two apart and registers itself either way.
|
|
127
|
+
|
|
128
|
+
Regenerate it when you add or rename commands. Nothing watches for changes, by design.
|
|
129
|
+
|
|
130
|
+
## Why the script is static
|
|
131
|
+
|
|
132
|
+
Cobra and clap route every TAB press to a hidden `__complete` subcommand. That is the right call for a Go or Rust binary that starts in 10ms. It is the wrong call here.
|
|
133
|
+
|
|
134
|
+
| Measurement | Time |
|
|
135
|
+
| --------------------------------------------------------- | -------------: |
|
|
136
|
+
| Bare `ruby -e ''` | 100ms |
|
|
137
|
+
| `require "dry/cli"` | 160ms |
|
|
138
|
+
| `require "dry/cli"` + `dry-cli-completion` + `completely` | 190ms |
|
|
139
|
+
| `require "tax_engine"` (a heavy host) | 520ms |
|
|
140
|
+
| First touch of that host's data store | +239ms |
|
|
141
|
+
| **Registry walk and full completion spec build** | **0.067ms** |
|
|
142
|
+
| Generated bash script for 27 commands | 257 lines, 9KB |
|
|
143
|
+
|
|
144
|
+
Half a second of dead air per keystroke is unusable, and no amount of lazy loading gets under the host's own require cost. So there is no `__complete` command. It was considered, costed at roughly 90 lines, and rejected on that table.
|
|
145
|
+
|
|
146
|
+
The same table explains two other decisions. The generator will not be optimised, because at 0.067ms it is 0.01% of the cheapest possible invocation and all the time goes to interpreter startup. Native extensions were rejected for the same reason, plus they would put a compiled artifact in every consumer's dependency chain.
|
|
147
|
+
|
|
148
|
+
## What it will not do
|
|
149
|
+
|
|
150
|
+
**Values your host has to compute.** The walk touches only objects dry-cli already holds. The moment an option's `values:` calls into your data layer, that cost lands at class-definition time on *every* invocation, not just completion. In the profiled host that meant 239ms of YAML parsing added to shell startup. Declare the values on the option, where dry-cli validates against them anyway and the generator sees them free.
|
|
151
|
+
|
|
152
|
+
**fish, PowerShell, nushell.** Worth adding later. The emitter interface is built so a fourth shell is a new class rather than a new branch in an existing one.
|
|
153
|
+
|
|
154
|
+
**Watch your registry.** Regenerating is your call, in your release process.
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
Ruby 3.2 or newer. This repository uses rbenv, so activate it first:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
eval "$(rbenv init -)"
|
|
162
|
+
bundle install
|
|
163
|
+
bundle exec rspec # the suite
|
|
164
|
+
bundle exec rubocop # the linter
|
|
165
|
+
bundle exec rake # both
|
|
166
|
+
bin/console # IRB with the gem loaded
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Two conventions in the suite are worth knowing before you add to it. Fixtures include registries this project did not write, because a generator tested against one CLI quietly encodes that CLI's shape. And generated scripts are validated by the shells themselves, with `bash -n` and `zsh -n` parsing without executing, since a regex over the output proves nothing about whether it runs.
|
|
170
|
+
|
|
171
|
+
## Contributing
|
|
172
|
+
|
|
173
|
+
Bug reports and pull requests are welcome at <https://github.com/kigster/dry-cli-autocomplete>.
|
|
174
|
+
|
|
175
|
+
> [!WARNING]
|
|
176
|
+
> A quick note on the name. The `dry-` prefix and the `Dry::CLI::Autocomplete` namespace do not imply endorsement by `dry-rb`. This is an independent gem that extends theirs. I hope this functionality will make it into `dry-cli` one day, however.
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "yard"
|
|
7
|
+
|
|
8
|
+
def shell(*args)
|
|
9
|
+
puts "running: #{args.join(' ')}"
|
|
10
|
+
system(args.join(" "))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task :clean do
|
|
14
|
+
shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
task gem: [:build] do
|
|
18
|
+
shell("gem install pkg/*")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
task permissions: [:clean] do
|
|
22
|
+
# One traversal replaces a six-level glob chain that printed "No such file
|
|
23
|
+
# or directory" for every level this project does not have, skipped dotfiles
|
|
24
|
+
# entirely, and silently stopped at depth six. .git is pruned — its objects
|
|
25
|
+
# have no business being group-readable.
|
|
26
|
+
shell("find . -path ./.git -prune -o -type d -exec chmod o+rx,g+rx {} + -o -type f -exec chmod o+r,g+r {} +")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
task build: :permissions
|
|
30
|
+
|
|
31
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
|
32
|
+
t.files = %w[lib/**/*.rb exe/*.rb - README.md LICENSE.txt CHANGELOG.md SPECIFICATION.md]
|
|
33
|
+
t.options.unshift("--title", '"FlowEngine — DSL + AST for buildiong complex flows in Ruby."')
|
|
34
|
+
t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
38
|
+
|
|
39
|
+
task default: :spec
|