dry-cli-autocomplete 0.5.0 → 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 +6 -0
- data/README.md +60 -3
- data/lib/dry/cli/autocomplete/command.rb +23 -10
- data/lib/dry/cli/autocomplete/emitters/bash.rb +2 -4
- data/lib/dry/cli/autocomplete/emitters/zsh.rb +24 -5
- data/lib/dry/cli/autocomplete/spec_builder.rb +29 -3
- data/lib/dry/cli/autocomplete/version.rb +1 -1
- metadata +3 -2
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,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## [0.5.0] - 2026-09-16
|
|
4
10
|
|
|
5
11
|
Documentation only; no change to the gem's behaviour.
|
data/README.md
CHANGED
|
@@ -12,6 +12,60 @@ ______________________________________________________________________
|
|
|
12
12
|
> [!WARNING]
|
|
13
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
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
|
+
```
|
|
68
|
+
|
|
15
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.
|
|
16
70
|
|
|
17
71
|
```bash
|
|
@@ -90,8 +144,8 @@ _mycli_completions() {
|
|
|
90
144
|
case "$path" in
|
|
91
145
|
"") words="version deploy db" ;;
|
|
92
146
|
"version") words="--format" ;;
|
|
93
|
-
"deploy") words="--force -f staging production" ;;
|
|
94
|
-
"db") words="migrate --verbose" ;;
|
|
147
|
+
"deploy") words="--force --no-force -f staging production" ;;
|
|
148
|
+
"db") words="migrate --verbose --no-verbose" ;;
|
|
95
149
|
"db migrate") words="--step" ;;
|
|
96
150
|
esac
|
|
97
151
|
|
|
@@ -109,6 +163,7 @@ Read what that output proves:
|
|
|
109
163
|
- `db migrate` gets real file completion.
|
|
110
164
|
- `secret` is absent, because hidden commands stay hidden.
|
|
111
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`.
|
|
112
167
|
- `mycli version --format <TAB>` offers `json plain`, and nothing else.
|
|
113
168
|
- `mycli deploy <TAB>` offers `staging production`, the values declared on the positional.
|
|
114
169
|
|
|
@@ -120,12 +175,14 @@ The script uses no associative arrays, so it runs under the bash 3.2 that macOS
|
|
|
120
175
|
('deploy')
|
|
121
176
|
_arguments -s \
|
|
122
177
|
'--force[Skip confirmation]' \
|
|
178
|
+
'--no-force[Skip confirmation]' \
|
|
123
179
|
'-f[Skip confirmation]' \
|
|
124
180
|
'*:Target environment:(staging production)' && ret=0
|
|
125
181
|
;;
|
|
126
182
|
('db')
|
|
127
183
|
_arguments -s \
|
|
128
|
-
'--verbose[Print full migration history]'
|
|
184
|
+
'--verbose[Print full migration history]' \
|
|
185
|
+
'--no-verbose[Print full migration history]' && ret=0
|
|
129
186
|
commands=(
|
|
130
187
|
'migrate:Run pending migrations'
|
|
131
188
|
)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -25,9 +25,19 @@ 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:`
|
|
@@ -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,6 +46,7 @@ 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
|
|
@@ -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
|