dry-cli-autocomplete 0.1.3 → 0.6.0
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 +4 -4
- data/.vscode/settings.json +23 -0
- data/CHANGELOG.md +17 -7
- data/CLAUDE.md +34 -21
- data/README.md +162 -11
- data/Rakefile +2 -2
- data/{SPECIFICATION.md → docs/SPECIFICATION.md} +3 -0
- data/lib/dry/cli/autocomplete/command.rb +24 -11
- data/lib/dry/cli/autocomplete/emitters/bash.rb +4 -6
- data/lib/dry/cli/autocomplete/emitters/zsh.rb +26 -7
- data/lib/dry/cli/autocomplete/spec_builder.rb +31 -5
- data/lib/dry/cli/autocomplete/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b392efc439cb2a04ce5858ab9f8931cf3be6c987df699c4ef56e0c20782775d9
|
|
4
|
+
data.tar.gz: 749295c776797acfd2a82a059d632df3d1f02170e158c4b3e8ad260def61009a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b33692dd9535f11c1057a75c5c689dc05d6dff0850094478b7f7cf4867436272855786d6b2c7a1f406ecf029da678be33ff13738d7cbecca69fe1536fde292ef
|
|
7
|
+
data.tar.gz: 5dacc2b7a57ddc9ca177ec1f0c29a1f9d6514e08f0eeec9ce1863a3c88217583ce6f9013f02d0293671fecdf7657e19d9c8d3a57224e34d71bd9aa7be7ac6f1a
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"search.exclude": {
|
|
3
|
+
"**/node_modules": true,
|
|
4
|
+
"**/bower_components": true,
|
|
5
|
+
"**/*.code-search": true,
|
|
6
|
+
"**/.ruby-lsp": true,
|
|
7
|
+
"**/.rubymate": true
|
|
8
|
+
},
|
|
9
|
+
"files.watcherExclude": {
|
|
10
|
+
".git/objects/**": true,
|
|
11
|
+
".git/subtree-cache/**": true,
|
|
12
|
+
".hg/store/**": true,
|
|
13
|
+
"*/.git/objects/**": true,
|
|
14
|
+
"*/.git/subtree-cache/**": true,
|
|
15
|
+
"*/.hg/store/**": true,
|
|
16
|
+
"**/.trunk/*actions/": true,
|
|
17
|
+
"**/.trunk/*logs/": true,
|
|
18
|
+
"**/.trunk/*notifications/": true,
|
|
19
|
+
"**/.trunk/*out/": true,
|
|
20
|
+
"**/.trunk/*plugins/": true,
|
|
21
|
+
"**/.rubymate": true
|
|
22
|
+
}
|
|
23
|
+
}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- Options complete the way dry-cli parses them. `option :as_of` completed as `--as_of`, which dry-cli accepts but its own help never shows; it is now `--as-of`. A boolean also completes its `--no-` form, and an alias declared without dashes (`aliases: ["f"]`) gains them (`-f`). A name with capitals is downcased as dry-cli registers it: `option :dryRun` completes as `--dryrun`.
|
|
4
|
+
- In zsh, a `type: :flag` option no longer takes a value. `run --quiet x` completed `x` as `--quiet`'s argument, while dry-cli parses it as `quiet: true` and an argument `x`.
|
|
5
|
+
- In zsh, a boolean's two forms exclude each other, so once `--force` is on the line zsh stops offering `--no-force`. The `--no-` form is described as `Turn off --force` rather than repeating the description, which read backwards.
|
|
6
|
+
- `Command[MyCLI]` keeps its description and examples. dry-cli empties both on every subclass, so `mycli completion --help` printed usage alone. The examples now name the bound `program_name:`.
|
|
7
|
+
- `completion` now writes to the stream a host passes to `Dry::CLI#call(out:)`. It used to write to `$stdout` regardless, so a launcher's own STDOUT, and an in-process Aruba run, received nothing. Called directly, outside `Dry::CLI`, it still falls back to `$stdout`.
|
|
8
|
+
|
|
9
|
+
## [0.5.0] - 2026-09-16
|
|
10
|
+
|
|
11
|
+
Documentation only; no change to the gem's behaviour.
|
|
12
|
+
|
|
13
|
+
- The README now matches the generated scripts. The bash example is real output, a zsh excerpt is included, and it documents `file:` on arguments and the name-based file guess, `program_name:`, option and positional values, `eval` for bash, placing the zsh `eval` after `compinit`, and Ruby 4.0.
|
|
14
|
+
- The specification moved to `docs/SPECIFICATION.md` and is marked as describing version 0.1.0. It is no longer part of the YARD docs.
|
|
15
|
+
- The YARD docs title names this gem instead of an unrelated project.
|
|
16
|
+
- Added a warning about AI contributions and an author section to the README.
|
|
17
|
+
|
|
3
18
|
## [0.1.3] - 2026-08-23
|
|
4
19
|
|
|
5
|
-
- The zsh script now registers itself when sourced. `eval "$(mycli completion zsh)"`
|
|
6
|
-
|
|
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.
|
|
20
|
+
- The zsh script now registers itself when sourced. `eval "$(mycli completion zsh)"` previously ran the completion function outside a completion context, and zsh answered with `_tags:comptags:36: can only be called from completion function`. The script now calls itself when autoloaded from `$fpath` and calls `compdef` when sourced, so both work.
|
|
21
|
+
- The zsh function is named `_mycli` rather than `_mycli_completions`, matching the name a `#compdef` file is autoloaded under. The bash function is unchanged.
|
|
12
22
|
|
|
13
23
|
## [0.1.2] - 2026-08-22
|
|
14
24
|
|
data/CLAUDE.md
CHANGED
|
@@ -6,9 +6,9 @@ Guidance for Claude Code (claude.ai/code) working in this repository.
|
|
|
6
6
|
|
|
7
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
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.
|
|
9
|
+
**Read `docs/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
10
|
|
|
11
|
-
The gem is
|
|
11
|
+
The gem is implemented and released (tags `v0.1.0` through `v0.1.3`). `README.md` documents the host-facing behaviour; keep it in step with `lib/` when behaviour changes.
|
|
12
12
|
|
|
13
13
|
## Environment
|
|
14
14
|
|
|
@@ -23,11 +23,12 @@ bundle install
|
|
|
23
23
|
bundle exec rspec # the suite
|
|
24
24
|
bundle exec rubocop # the linter
|
|
25
25
|
bundle exec rubocop -a # autocorrect
|
|
26
|
-
bundle exec rake #
|
|
26
|
+
bundle exec rake # the suite only; it is the default task and what CI runs
|
|
27
|
+
bundle exec rake doc # YARD documentation
|
|
27
28
|
bin/console # IRB with the gem loaded
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
The gemspec sets `required_ruby_version >=
|
|
31
|
+
The gemspec sets `required_ruby_version >= 4.0`, `.rubocop.yml` sets `TargetRubyVersion: 4.0`, and CI runs Ruby 4.0.6. Keep them in step: raising one without the other produces a linter that permits syntax the gemspec claims to support, or the reverse.
|
|
31
32
|
|
|
32
33
|
## The trap that has already bitten this repository once
|
|
33
34
|
|
|
@@ -43,6 +44,8 @@ module Dry
|
|
|
43
44
|
|
|
44
45
|
`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
|
|
|
47
|
+
The same lexical scoping bites constants. Every file here sits inside `module Dry`, so a bare `Struct` or `Data` resolves to `Dry::Struct` or `Dry::Data` first when a host has loaded them. Write `::Data` and `::Struct`. `spec/dry/cli/autocomplete/dry_struct_host_spec.rb` loads dry-struct to catch this.
|
|
48
|
+
|
|
46
49
|
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
50
|
|
|
48
51
|
## Architecture
|
|
@@ -58,31 +61,41 @@ Four pieces, and the boundary between the first and the rest is load-bearing.
|
|
|
58
61
|
|
|
59
62
|
Two rules hold this shape together, both measured rather than assumed:
|
|
60
63
|
|
|
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.
|
|
64
|
+
- **The command shim loads no emitter.** A host pays nothing at boot for a command run once per shell. `docs/SPECIFICATION.md` §2.4.
|
|
65
|
+
- **The spec builder touches only the registry.** It must be safe to run at shell startup, because it runs at every shell startup. `docs/SPECIFICATION.md` §2.2.
|
|
66
|
+
|
|
67
|
+
`SpecBuilder` returns frozen `::Data` values: a `CompletionSpec` holding one `Node` per visible command or group, each with `OptionSpec` and `ArgumentSpec` entries. An argument is a file argument when it declares `file: true`, or, with no `file:` key, when its name matches `/file|path/i`.
|
|
63
68
|
|
|
64
69
|
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
70
|
|
|
66
71
|
## Conventions
|
|
67
72
|
|
|
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.
|
|
73
|
+
- **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 `docs/SPECIFICATION.md` §2.3.
|
|
69
74
|
- **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.
|
|
75
|
+
- **Test against registries this project did not write.** A generator tested against one CLI encodes that CLI's shape. `docs/SPECIFICATION.md` §5.
|
|
76
|
+
- **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. `spec/support/shell_helpers.rb` skips a missing shell locally and fails on CI.
|
|
77
|
+
- **Emitter output is pinned by golden files** in `spec/support/golden/`. A deliberate output change means updating the golden file in the same commit.
|
|
78
|
+
- **The bash emitter targets bash 3.2.** macOS ships it as `/bin/bash`, so no associative arrays.
|
|
79
|
+
- **The zsh script works both from `$fpath` and from `eval`.** Its footer checks `$funcstack[1]` against the function name, so the function must stay named `_<program>`.
|
|
72
80
|
- **Commit messages**: imperative mood, 50-character subject, no full stop. A body only where the change needs explaining, saying what and why.
|
|
73
81
|
- **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
82
|
|
|
75
83
|
## Repository layout
|
|
76
84
|
|
|
77
|
-
| Path | What it is
|
|
78
|
-
| ------------------------------------- |
|
|
79
|
-
| `SPECIFICATION.md`
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
| Path | What it is |
|
|
86
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
87
|
+
| `docs/SPECIFICATION.md` | What to build, why, and what "done" means |
|
|
88
|
+
| `README.md` | Host-facing documentation |
|
|
89
|
+
| `CHANGELOG.md` | Release notes |
|
|
90
|
+
| `lib/dry/cli/autocomplete.rb` | Entry point for `require "dry/cli/autocomplete"`. Hosts require `command.rb` instead |
|
|
91
|
+
| `lib/dry-cli-autocomplete.rb` | Bundler-style entry point, requires the one above |
|
|
92
|
+
| `lib/dry/cli/autocomplete/version.rb` | Version, loaded standalone by the gemspec |
|
|
93
|
+
| `sig/` | RBS signatures. Only `VERSION` is declared so far |
|
|
94
|
+
| `spec/support/fixtures/` | Registries the suite walks, including shapes modelled on other CLIs |
|
|
95
|
+
| `spec/support/golden/` | Expected bash and zsh output, compared byte for byte |
|
|
96
|
+
| `.github/workflows/main.yml` | CI: installs zsh, runs `bundle exec rake` |
|
|
97
|
+
| `.github/workflows/rubocop.yml` | CI: runs `bundle exec rubocop` |
|
|
98
|
+
|
|
99
|
+
## Releasing
|
|
100
|
+
|
|
101
|
+
Bump `VERSION` in `lib/dry/cli/autocomplete/version.rb`, add a `CHANGELOG.md` entry, then `bundle exec rake release`. The gemspec requires MFA for pushes. The `dry-` prefix and the `Dry::CLI::Autocomplete` namespace imply an affiliation with dry-rb that does not exist; the README says so, and that note should stay.
|
data/README.md
CHANGED
|
@@ -5,7 +5,66 @@
|
|
|
5
5
|
Shell completion for [dry-cli](https://github.com/dry-rb/dry-cli) applications, with no Ruby in the TAB path.
|
|
6
6
|
|
|
7
7
|
> [!NOTE]
|
|
8
|
-
> For the specification of this gem see [SPECIFICATION](SPECIFICATION.md)
|
|
8
|
+
> For the original specification of this gem see [SPECIFICATION](docs/SPECIFICATION.md)
|
|
9
|
+
|
|
10
|
+
______________________________________________________________________
|
|
11
|
+
|
|
12
|
+
> [!WARNING]
|
|
13
|
+
> This gem was written with a collaboration with Claude Code. Most of the ruby was written by a human (myself), reviewed and pushed to GitHub by Claude (anyone loves writing commit descriptions?). The part where Claude authored the most code is the ZSH autocompletion code as I'm less familiar with it than BASH. If you prefer not to use gems that had some AI contributions that were reviewed by a human, do not use this gem.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
For the impatient:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install dry-cli dry-cli-autocomplete -N
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Register one command:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require "dry/cli"
|
|
27
|
+
require "dry/cli/autocomplete/command"
|
|
28
|
+
|
|
29
|
+
module MyCLI
|
|
30
|
+
extend Dry::CLI::Registry
|
|
31
|
+
|
|
32
|
+
register "deploy", Deploy
|
|
33
|
+
register "completion", Dry::CLI::Autocomplete::Command[MyCLI]
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then load the script from your shell's initialization file:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
eval "$(mycli completion bash)" # ~/.bashrc
|
|
41
|
+
eval "$(mycli completion zsh)" # ~/.zshrc, after compinit
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Real Example
|
|
45
|
+
|
|
46
|
+
[`examples/`](examples/README.md) holds a working CLI with the command registered, to try it end to end.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
$ cd examples && bundle exec bin/mycli -h
|
|
50
|
+
mycli
|
|
51
|
+
|
|
52
|
+
Downloads URLs and finds hosts on the local network, several at once.
|
|
53
|
+
|
|
54
|
+
USAGE
|
|
55
|
+
mycli COMMAND [OPTIONS]
|
|
56
|
+
|
|
57
|
+
COMMANDS
|
|
58
|
+
version, v Print version
|
|
59
|
+
download-urls, download Download URLs, each to its own file
|
|
60
|
+
find-hosts, hosts Find hosts on the local network that listen on
|
|
61
|
+
common TCP ports
|
|
62
|
+
completion Print a shell completion script
|
|
63
|
+
|
|
64
|
+
OPTIONS
|
|
65
|
+
-h, --help Show help
|
|
66
|
+
-v, --version Print version
|
|
67
|
+
```
|
|
9
68
|
|
|
10
69
|
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
70
|
|
|
@@ -41,6 +100,28 @@ There is a dependency argument too. `completely` pulls `colsole`, `docopt_ng` an
|
|
|
41
100
|
Given this registry:
|
|
42
101
|
|
|
43
102
|
```ruby
|
|
103
|
+
class Version < Dry::CLI::Command
|
|
104
|
+
desc "Print the version"
|
|
105
|
+
option :format, values: %w[json plain], desc: "Output format"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class Deploy < Dry::CLI::Command
|
|
109
|
+
desc "Deploy the application"
|
|
110
|
+
option :force, type: :boolean, aliases: ["-f"], desc: "Skip confirmation"
|
|
111
|
+
argument :environment, values: %w[staging production], required: true, desc: "Target environment"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class DbStatus < Dry::CLI::Command
|
|
115
|
+
desc "Show pending migrations"
|
|
116
|
+
option :verbose, type: :boolean, desc: "Print full migration history"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class DbMigrate < Dry::CLI::Command
|
|
120
|
+
desc "Run pending migrations"
|
|
121
|
+
option :step, desc: "Migrate to a specific step"
|
|
122
|
+
argument :file, desc: "Migration file to run"
|
|
123
|
+
end
|
|
124
|
+
|
|
44
125
|
register "version", Version
|
|
45
126
|
register "deploy", Deploy
|
|
46
127
|
register "db", DbStatus do |prefix|
|
|
@@ -49,17 +130,23 @@ end
|
|
|
49
130
|
register "secret", Secret, hidden: true
|
|
50
131
|
```
|
|
51
132
|
|
|
52
|
-
`mycli completion bash` prints a `complete -F` function
|
|
133
|
+
`mycli completion bash` prints a `complete -F` function. It walks `COMP_WORDS` to find the command path under the cursor, answers option values first, then offers that path's words:
|
|
53
134
|
|
|
54
135
|
```bash
|
|
55
136
|
_mycli_completions() {
|
|
56
137
|
# ...walks COMP_WORDS to find the current command path...
|
|
138
|
+
|
|
139
|
+
case "$path:$prev" in
|
|
140
|
+
"version:--format") COMPREPLY=($(compgen -W "json plain" -- "$cur")); return ;;
|
|
141
|
+
esac
|
|
142
|
+
|
|
57
143
|
words=""
|
|
58
144
|
case "$path" in
|
|
59
145
|
"") words="version deploy db" ;;
|
|
60
146
|
"version") words="--format" ;;
|
|
61
|
-
"deploy") words="--force -f" ;;
|
|
62
|
-
"db") words="migrate --verbose" ;;
|
|
147
|
+
"deploy") words="--force --no-force -f staging production" ;;
|
|
148
|
+
"db") words="migrate --verbose --no-verbose" ;;
|
|
149
|
+
"db migrate") words="--step" ;;
|
|
63
150
|
esac
|
|
64
151
|
|
|
65
152
|
COMPREPLY=($(compgen -W "$words" -- "$cur"))
|
|
@@ -70,17 +157,62 @@ _mycli_completions() {
|
|
|
70
157
|
complete -F _mycli_completions mycli
|
|
71
158
|
```
|
|
72
159
|
|
|
73
|
-
Read what that output proves
|
|
160
|
+
Read what that output proves:
|
|
161
|
+
|
|
162
|
+
- `db` offers `migrate` alongside its own `--verbose`, so a group with both a command and children keeps both.
|
|
163
|
+
- `db migrate` gets real file completion.
|
|
164
|
+
- `secret` is absent, because hidden commands stay hidden.
|
|
165
|
+
- The `-f` alias on `deploy` is there because you declared it.
|
|
166
|
+
- `--no-force` is there because `--force` is a boolean, and dry-cli accepts both forms. Options are spelled the way dry-cli parses them, so `option :dry_run` completes as `--dry-run`, and an alias declared as `"f"` completes as `-f`.
|
|
167
|
+
- `mycli version --format <TAB>` offers `json plain`, and nothing else.
|
|
168
|
+
- `mycli deploy <TAB>` offers `staging production`, the values declared on the positional.
|
|
169
|
+
|
|
170
|
+
The script uses no associative arrays, so it runs under the bash 3.2 that macOS ships as `/bin/bash`.
|
|
171
|
+
|
|
172
|
+
`mycli completion zsh` prints a native `#compdef` script. Options go through `_arguments` with their `desc` as help text, subcommands go through `_describe` with the command's `desc`, declared values become a value list, and file arguments use `_files`:
|
|
173
|
+
|
|
174
|
+
```zsh
|
|
175
|
+
('deploy')
|
|
176
|
+
_arguments -s \
|
|
177
|
+
'--force[Skip confirmation]' \
|
|
178
|
+
'--no-force[Skip confirmation]' \
|
|
179
|
+
'-f[Skip confirmation]' \
|
|
180
|
+
'*:Target environment:(staging production)' && ret=0
|
|
181
|
+
;;
|
|
182
|
+
('db')
|
|
183
|
+
_arguments -s \
|
|
184
|
+
'--verbose[Print full migration history]' \
|
|
185
|
+
'--no-verbose[Print full migration history]' && ret=0
|
|
186
|
+
commands=(
|
|
187
|
+
'migrate:Run pending migrations'
|
|
188
|
+
)
|
|
189
|
+
_describe -t commands 'db command' commands && ret=0
|
|
190
|
+
;;
|
|
191
|
+
```
|
|
74
192
|
|
|
75
|
-
|
|
193
|
+
### Enum values
|
|
76
194
|
|
|
77
|
-
|
|
195
|
+
Values declared on an option or argument come through at no cost:
|
|
78
196
|
|
|
79
197
|
```ruby
|
|
80
|
-
option :format, values: %w[json yaml table] # completes json yaml table
|
|
198
|
+
option :format, values: %w[json yaml table] # completes json yaml table after --format
|
|
81
199
|
argument :component, values: %w[major minor] # completes major minor
|
|
82
200
|
```
|
|
83
201
|
|
|
202
|
+
### File arguments
|
|
203
|
+
|
|
204
|
+
An argument completes file paths when it declares `file: true`. Without that key, the generator treats any argument whose name contains `file` or `path` as a file argument. Declare `file: false` to opt out of the guess:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
argument :output, file: true # completes paths
|
|
208
|
+
argument :path, file: false # does not, despite the name
|
|
209
|
+
argument :config_file # completes paths, by name
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Program names
|
|
213
|
+
|
|
214
|
+
Shell function names derive from the program name. A program installed as `my-tool` gets `_my_tool_completions` in bash and `_my_tool` in zsh.
|
|
215
|
+
|
|
84
216
|
## Installation
|
|
85
217
|
|
|
86
218
|
```bash
|
|
@@ -105,19 +237,33 @@ end
|
|
|
105
237
|
|
|
106
238
|
That require pulls in the command class and nothing else. No emitter loads until someone actually runs `mycli completion`.
|
|
107
239
|
|
|
240
|
+
The command reads the program name from `$PROGRAM_NAME` when it runs. If your executable can be invoked under a different name, such as through a wrapper or a binstub, pin it:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
register "completion", Dry::CLI::Autocomplete::Command[MyCLI, program_name: "mycli"]
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The command takes one required argument, `bash` or `zsh`, and prints the script to standard output.
|
|
247
|
+
|
|
108
248
|
Then have your users write the script once and source it. For bash:
|
|
109
249
|
|
|
110
250
|
```bash
|
|
111
251
|
mycli completion bash > /usr/local/etc/bash_completion.d/mycli
|
|
112
252
|
```
|
|
113
253
|
|
|
254
|
+
Or evaluate it from `.bashrc`:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
eval "$(mycli completion bash)"
|
|
258
|
+
```
|
|
259
|
+
|
|
114
260
|
For zsh, put it anywhere on your `$fpath`:
|
|
115
261
|
|
|
116
262
|
```bash
|
|
117
263
|
mycli completion zsh > "${fpath[1]}/_mycli"
|
|
118
264
|
```
|
|
119
265
|
|
|
120
|
-
Sourcing it from `.zshrc` works too, if you would rather not manage a file:
|
|
266
|
+
Sourcing it from `.zshrc` works too, if you would rather not manage a file. Place the line after `compinit`, since the script calls `compdef` to register itself:
|
|
121
267
|
|
|
122
268
|
```bash
|
|
123
269
|
eval "$(mycli completion zsh)"
|
|
@@ -155,19 +301,24 @@ The same table explains two other decisions. The generator will not be optimised
|
|
|
155
301
|
|
|
156
302
|
## Development
|
|
157
303
|
|
|
158
|
-
Ruby
|
|
304
|
+
Ruby 4.0 or newer, matching the gemspec and CI. This repository uses rbenv, so activate it first:
|
|
159
305
|
|
|
160
306
|
```bash
|
|
161
307
|
eval "$(rbenv init -)"
|
|
162
308
|
bundle install
|
|
163
309
|
bundle exec rspec # the suite
|
|
164
310
|
bundle exec rubocop # the linter
|
|
165
|
-
bundle exec rake #
|
|
311
|
+
bundle exec rake # the suite, and the default task
|
|
312
|
+
bundle exec rake doc # YARD documentation
|
|
166
313
|
bin/console # IRB with the gem loaded
|
|
167
314
|
```
|
|
168
315
|
|
|
169
316
|
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
317
|
|
|
318
|
+
## Author
|
|
319
|
+
|
|
320
|
+
- Konstantin Gredeskoul pairing with Claude Code. Every line has been reviewed and co-written by a human. The commits were pushed by Claude to save time writing comment descriptions.
|
|
321
|
+
|
|
171
322
|
## Contributing
|
|
172
323
|
|
|
173
324
|
Bug reports and pull requests are welcome at <https://github.com/kigster/dry-cli-autocomplete>.
|
data/Rakefile
CHANGED
|
@@ -29,8 +29,8 @@ end
|
|
|
29
29
|
task build: :permissions
|
|
30
30
|
|
|
31
31
|
YARD::Rake::YardocTask.new(:doc) do |t|
|
|
32
|
-
t.files = %w[lib/**/*.rb
|
|
33
|
-
t.options.unshift("--title", '"
|
|
32
|
+
t.files = %w[lib/**/*.rb - README.md LICENSE.txt CHANGELOG.md]
|
|
33
|
+
t.options.unshift("--title", '"dry-cli-autocomplete: shell completion for dry-cli"')
|
|
34
34
|
t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# `dry-cli-autocomplete`: Specification
|
|
2
2
|
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> This is the specification for version 0.1.0. Later versions may differ; see `CHANGELOG.md` and `README.md` for current behaviour.
|
|
5
|
+
|
|
3
6
|
Generate static shell completion scripts for any `Dry::CLI` application, from the command registry alone.
|
|
4
7
|
|
|
5
8
|
The intended use is one line in a shell profile:
|
|
@@ -12,7 +12,7 @@ module Dry
|
|
|
12
12
|
# for whatever this require pulls on *every* invocation, while the
|
|
13
13
|
# command itself runs about once per shell. The generator is required
|
|
14
14
|
# inside #call, where the cost is actually incurred. See
|
|
15
|
-
# SPECIFICATION.md §1.3 and §2.4, and the spec that pins it.
|
|
15
|
+
# docs/SPECIFICATION.md §1.3 and §2.4, and the spec that pins it.
|
|
16
16
|
class Command < Dry::CLI::Command
|
|
17
17
|
SHELLS = %w[bash zsh].freeze
|
|
18
18
|
|
|
@@ -22,10 +22,17 @@ module Dry
|
|
|
22
22
|
# gem may be required long before anyone knows how it was invoked.
|
|
23
23
|
#
|
|
24
24
|
# register "completion", Dry::CLI::Autocomplete::Command[MyCLI]
|
|
25
|
+
#
|
|
26
|
+
# dry-cli's inherited hook empties a subclass's description and
|
|
27
|
+
# examples, and this returns a subclass, so both are set again here.
|
|
28
|
+
# The examples name the bound program, not whatever loaded the gem.
|
|
25
29
|
def self.[](registry, program_name: nil)
|
|
26
30
|
Class.new(self) do
|
|
27
31
|
@registry = registry
|
|
28
32
|
@program_name = program_name
|
|
33
|
+
|
|
34
|
+
desc superclass.description
|
|
35
|
+
example examples_for(program_name || File.basename($PROGRAM_NAME))
|
|
29
36
|
end
|
|
30
37
|
end
|
|
31
38
|
|
|
@@ -33,24 +40,33 @@ module Dry
|
|
|
33
40
|
attr_reader :registry, :program_name
|
|
34
41
|
end
|
|
35
42
|
|
|
43
|
+
def self.examples_for(name)
|
|
44
|
+
[
|
|
45
|
+
"bash > /usr/local/etc/bash_completion.d/#{name}",
|
|
46
|
+
"zsh > \"${fpath[1]}/_#{name}\""
|
|
47
|
+
]
|
|
48
|
+
end
|
|
49
|
+
|
|
36
50
|
desc "Print a shell completion script"
|
|
37
51
|
|
|
38
|
-
argument :shell, required: true, values: SHELLS,
|
|
39
|
-
desc: "Shell to generate completions for"
|
|
52
|
+
argument :shell, required: true, values: SHELLS, desc: "Shell to generate completions for"
|
|
40
53
|
|
|
41
|
-
example
|
|
42
|
-
"bash > /usr/local/etc/bash_completion.d/#{File.basename($PROGRAM_NAME)}",
|
|
43
|
-
"zsh > \"${fpath[1]}/_#{File.basename($PROGRAM_NAME)}\""
|
|
44
|
-
]
|
|
54
|
+
example examples_for(File.basename($PROGRAM_NAME))
|
|
45
55
|
|
|
46
56
|
def call(shell:, **)
|
|
47
57
|
require_relative "spec_builder"
|
|
48
58
|
require_relative "emitters/#{shell}"
|
|
49
59
|
|
|
50
|
-
spec = SpecBuilder.call(registry, program_name:
|
|
60
|
+
spec = SpecBuilder.call(registry, program_name:)
|
|
61
|
+
|
|
51
62
|
out.puts emitter_for(shell).call(spec)
|
|
52
63
|
end
|
|
53
64
|
|
|
65
|
+
# Dry::CLI sets @out from Dry::CLI#call(out:) before it calls a command,
|
|
66
|
+
# and only when the command has not set @out itself, so never assign it
|
|
67
|
+
# here. $stdout is for a command called directly, outside Dry::CLI.
|
|
68
|
+
def out = @out || $stdout
|
|
69
|
+
|
|
54
70
|
private
|
|
55
71
|
|
|
56
72
|
def registry
|
|
@@ -65,9 +81,6 @@ module Dry
|
|
|
65
81
|
def emitter_for(shell)
|
|
66
82
|
Emitters.const_get(shell.capitalize)
|
|
67
83
|
end
|
|
68
|
-
|
|
69
|
-
# Overridable so specs can capture output without reaching for $stdout.
|
|
70
|
-
def out = $stdout
|
|
71
84
|
end
|
|
72
85
|
end
|
|
73
86
|
end
|
|
@@ -7,7 +7,7 @@ module Dry
|
|
|
7
7
|
module Autocomplete
|
|
8
8
|
module Emitters
|
|
9
9
|
# Turns a CompletionSpec into a bash `complete -F` script.
|
|
10
|
-
# See SPECIFICATION.md §4.1: a static, case-statement walk over
|
|
10
|
+
# See docs/SPECIFICATION.md §4.1: a static, case-statement walk over
|
|
11
11
|
# COMP_WORDS resolves which node the cursor is under, then
|
|
12
12
|
# `compgen -W` fills COMPREPLY from that node's children and
|
|
13
13
|
# option flags, with `compgen -f` added where an argument is a
|
|
@@ -97,7 +97,7 @@ module Dry
|
|
|
97
97
|
def function_name = "_#{shell_identifier}_completions"
|
|
98
98
|
|
|
99
99
|
# A program installed as `my-tool` cannot name a shell function
|
|
100
|
-
# directly. See SPECIFICATION.md §4.2.
|
|
100
|
+
# directly. See docs/SPECIFICATION.md §4.2.
|
|
101
101
|
def shell_identifier
|
|
102
102
|
Dry::Inflector.new.underscore(spec.program_name.to_s).gsub(/[^A-Za-z0-9_]/, "_")
|
|
103
103
|
end
|
|
@@ -127,12 +127,10 @@ module Dry
|
|
|
127
127
|
# legitimate next words at this point in the line.
|
|
128
128
|
def node_words(node)
|
|
129
129
|
node.children +
|
|
130
|
-
node.options.flat_map
|
|
130
|
+
node.options.flat_map(&:flags) +
|
|
131
131
|
node.arguments.flat_map { |argument| Array(argument.values) }
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
def option_words(option) = ["--#{option.name}"] + Array(option.aliases)
|
|
135
|
-
|
|
136
134
|
# An option that declares values gets its own arm, keyed on the word
|
|
137
135
|
# before the cursor. Typing `--format ` then TAB should offer what
|
|
138
136
|
# --format accepts, not the command list again, so this arm answers
|
|
@@ -145,7 +143,7 @@ module Dry
|
|
|
145
143
|
next if values.empty?
|
|
146
144
|
|
|
147
145
|
key = path_key(node.path)
|
|
148
|
-
|
|
146
|
+
option.flags.map do |name|
|
|
149
147
|
" \"#{quote(key)}:#{quote(name)}\") " \
|
|
150
148
|
"COMPREPLY=($(compgen -W \"#{quote(values.join(' '))}\" -- \"$cur\")); return ;;"
|
|
151
149
|
end
|
|
@@ -13,7 +13,7 @@ module Dry
|
|
|
13
13
|
# `_describe`, declared enum values become a `(a b c)` action, and a
|
|
14
14
|
# file argument becomes `_files`. That per-option help is the thing
|
|
15
15
|
# zsh users lose with a shim, and the reason this emitter exists.
|
|
16
|
-
# See SPECIFICATION.md §4.1.
|
|
16
|
+
# See docs/SPECIFICATION.md §4.1.
|
|
17
17
|
#
|
|
18
18
|
# Shares no code with the bash emitter by design: a fourth shell
|
|
19
19
|
# should be a new class here, never another branch inside one of
|
|
@@ -132,15 +132,34 @@ module Dry
|
|
|
132
132
|
# Each alias gets its own spec rather than a {-f,--force} group:
|
|
133
133
|
# the grouped form needs its description outside the quotes, and
|
|
134
134
|
# one entry per flag is easier to read in the generated file.
|
|
135
|
+
#
|
|
136
|
+
# A boolean's two forms exclude each other, so once `--force` is on
|
|
137
|
+
# the line zsh stops offering `--no-force`, and the reverse. The
|
|
138
|
+
# `--no-` form says what it turns off rather than repeating the
|
|
139
|
+
# description, which would read backwards.
|
|
135
140
|
def option_specs(option)
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
positives = [option.long, *option.alias_flags]
|
|
142
|
+
specs = positives.map do |name|
|
|
143
|
+
single_quote("#{exclusion([option.negation])}#{name}#{bracketed(option.desc)}#{option_action(option)}")
|
|
144
|
+
end
|
|
145
|
+
return specs unless option.negation
|
|
146
|
+
|
|
147
|
+
specs.insert(1, single_quote("#{exclusion(positives)}#{option.negation}[Turn off #{option.long}]"))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# A zsh exclusion list such as `(--no-force)`, or nothing when there
|
|
151
|
+
# is nothing to exclude.
|
|
152
|
+
def exclusion(names)
|
|
153
|
+
names = names.compact
|
|
154
|
+
names.empty? ? "" : "(#{names.join(' ')})"
|
|
138
155
|
end
|
|
139
156
|
|
|
140
|
-
# A boolean flag takes no value
|
|
141
|
-
#
|
|
157
|
+
# A boolean or a `type: :flag` option takes no value: dry-cli parses
|
|
158
|
+
# `run --quiet x` as `quiet: true` with `x` as an argument. Anything
|
|
159
|
+
# else gets one field naming what it wants and one supplying the
|
|
160
|
+
# completions for it.
|
|
142
161
|
def option_action(option)
|
|
143
|
-
return "" if option.boolean
|
|
162
|
+
return "" if option.boolean || option.flag
|
|
144
163
|
|
|
145
164
|
":#{escape_spec(option.name)}:#{value_action(option.values)}"
|
|
146
165
|
end
|
|
@@ -193,7 +212,7 @@ module Dry
|
|
|
193
212
|
def function_name = "_#{shell_identifier}"
|
|
194
213
|
|
|
195
214
|
# A program installed as `my-tool` cannot name a shell function
|
|
196
|
-
# directly. See SPECIFICATION.md §4.2.
|
|
215
|
+
# directly. See docs/SPECIFICATION.md §4.2.
|
|
197
216
|
def shell_identifier
|
|
198
217
|
Dry::Inflector.new.underscore(spec.program_name.to_s).gsub(/[^A-Za-z0-9_]/, "_")
|
|
199
218
|
end
|
|
@@ -9,7 +9,7 @@ module Dry
|
|
|
9
9
|
#
|
|
10
10
|
# Touches nothing beyond the registry and the command classes it
|
|
11
11
|
# already holds, so it stays cheap enough to run on every shell start.
|
|
12
|
-
# See SPECIFICATION.md §2.2 and §2.3.
|
|
12
|
+
# See docs/SPECIFICATION.md §2.2 and §2.3.
|
|
13
13
|
class SpecBuilder
|
|
14
14
|
# `::Data`, with the leading colons, and never a bare `Data`. This file
|
|
15
15
|
# is lexically inside `module Dry`, so an unqualified constant is looked
|
|
@@ -25,13 +25,23 @@ module Dry
|
|
|
25
25
|
# method Struct defines and Data does not.
|
|
26
26
|
CompletionSpec = ::Data.define(:program_name, :nodes)
|
|
27
27
|
Node = ::Data.define(:path, :desc, :options, :arguments, :children)
|
|
28
|
+
# `long`, `negation` and `alias_flags` are spelled the way dry-cli
|
|
29
|
+
# 1.4.1 spells them in Option#parser_options and #alias_names, so
|
|
30
|
+
# both emitters offer exactly what the parser accepts. `flag` is a
|
|
31
|
+
# `type: :flag` option: like a boolean it takes no value, but dry-cli
|
|
32
|
+
# gives it no `--no-` form.
|
|
28
33
|
OptionSpec = ::Data.define(
|
|
29
|
-
:name, :type, :values, :aliases, :default, :desc, :required, :boolean, :array
|
|
30
|
-
|
|
34
|
+
:name, :type, :values, :aliases, :default, :desc, :required, :boolean, :array,
|
|
35
|
+
:flag, :long, :negation, :alias_flags
|
|
36
|
+
) do
|
|
37
|
+
# @return [Array<String>] every spelling: the long name, its `--no-`
|
|
38
|
+
# form for a boolean, then the aliases
|
|
39
|
+
def flags = [long, negation, *alias_flags].compact
|
|
40
|
+
end
|
|
31
41
|
ArgumentSpec = ::Data.define(:name, :values, :desc, :required, :file)
|
|
32
42
|
|
|
33
43
|
# A bare heuristic, used only when a host does not declare `file:`
|
|
34
|
-
# explicitly on the argument. See SPECIFICATION.md §4.3.
|
|
44
|
+
# explicitly on the argument. See docs/SPECIFICATION.md §4.3.
|
|
35
45
|
FILE_ARGUMENT_HEURISTIC = /file|path/i
|
|
36
46
|
|
|
37
47
|
def self.call(registry, program_name:)
|
|
@@ -77,13 +87,29 @@ module Dry
|
|
|
77
87
|
end
|
|
78
88
|
|
|
79
89
|
def build_option(option)
|
|
90
|
+
long = "--#{dasherize(option.name)}"
|
|
80
91
|
OptionSpec.new(
|
|
81
92
|
name: option.name.to_s, type: option.type, values: option.values,
|
|
82
93
|
aliases: option.aliases, default: option.default, desc: option.options[:desc],
|
|
83
|
-
required: option.required? || false, boolean: option.boolean?, array: option.array
|
|
94
|
+
required: option.required? || false, boolean: option.boolean?, array: option.array?,
|
|
95
|
+
flag: option.respond_to?(:flag?) && option.flag?, long: long,
|
|
96
|
+
negation: option.boolean? ? long.sub("--", "--no-") : nil,
|
|
97
|
+
alias_flags: Array(option.aliases).map { |name| alias_flag(name) }.uniq
|
|
84
98
|
)
|
|
85
99
|
end
|
|
86
100
|
|
|
101
|
+
# What Dry::CLI::Inflector.dasherize does, without depending on a
|
|
102
|
+
# module dry-cli marks private: `dry_run` becomes `--dry-run` and `dryRun`
|
|
103
|
+
# becomes `--dryrun`, as dry-cli registers them.
|
|
104
|
+
def dasherize(name) = name.to_s.downcase.gsub(/[[:space:]_]/, "-")
|
|
105
|
+
|
|
106
|
+
# One letter gets one dash and anything longer two, whatever the host
|
|
107
|
+
# wrote: `"f"`, `"-f"` and `"--f"` all register as `-f`.
|
|
108
|
+
def alias_flag(name)
|
|
109
|
+
bare = name.to_s.sub(/\A-{1,2}/, "")
|
|
110
|
+
bare.size == 1 ? "-#{bare}" : "--#{bare}"
|
|
111
|
+
end
|
|
112
|
+
|
|
87
113
|
def build_argument(argument)
|
|
88
114
|
ArgumentSpec.new(
|
|
89
115
|
name: argument.name.to_s, values: argument.values, desc: argument.options[:desc],
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dry-cli-autocomplete
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Gredeskoul
|
|
@@ -46,12 +46,13 @@ extensions: []
|
|
|
46
46
|
extra_rdoc_files: []
|
|
47
47
|
files:
|
|
48
48
|
- ".secrets.baseline"
|
|
49
|
+
- ".vscode/settings.json"
|
|
49
50
|
- CHANGELOG.md
|
|
50
51
|
- CLAUDE.md
|
|
51
52
|
- LICENSE.txt
|
|
52
53
|
- README.md
|
|
53
54
|
- Rakefile
|
|
54
|
-
- SPECIFICATION.md
|
|
55
|
+
- docs/SPECIFICATION.md
|
|
55
56
|
- docs/img/badge.svg
|
|
56
57
|
- justfile
|
|
57
58
|
- lefthook.yml
|
|
@@ -86,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
86
87
|
- !ruby/object:Gem::Version
|
|
87
88
|
version: '0'
|
|
88
89
|
requirements: []
|
|
89
|
-
rubygems_version: 4.0.
|
|
90
|
+
rubygems_version: 4.0.22
|
|
90
91
|
specification_version: 4
|
|
91
92
|
summary: A missing auto-complete addition for dry-cli powered Ruby CLI tools for BASH
|
|
92
93
|
& ZSH
|