clamp 1.4.0 → 1.5.1
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/CHANGES.md +8 -0
- data/README.md +44 -0
- data/examples/gitdown +1 -0
- data/lib/clamp/completion/bash_generator.rb +207 -0
- data/lib/clamp/completion/fish_generator.rb +176 -0
- data/lib/clamp/completion/zsh_generator.rb +123 -0
- data/lib/clamp/completion.rb +187 -0
- data/lib/clamp/version.rb +1 -1
- metadata +5 -21
- data/.autotest +0 -11
- data/.editorconfig +0 -10
- data/.github/workflows/ci.yml +0 -31
- data/.gitignore +0 -9
- data/.rspec +0 -2
- data/.rubocop.yml +0 -74
- data/CODEOWNERS +0 -1
- data/Gemfile +0 -20
- data/Guardfile +0 -45
- data/Rakefile +0 -18
- data/clamp.gemspec +0 -28
- data/spec/clamp/command_group_spec.rb +0 -438
- data/spec/clamp/command_option_module_spec.rb +0 -40
- data/spec/clamp/command_option_reordering_spec.rb +0 -58
- data/spec/clamp/command_spec.rb +0 -1280
- data/spec/clamp/help/builder_spec.rb +0 -81
- data/spec/clamp/messages_spec.rb +0 -50
- data/spec/clamp/option/definition_spec.rb +0 -343
- data/spec/clamp/parameter/definition_spec.rb +0 -314
- data/spec/spec_helper.rb +0 -65
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0987845ae98be0a62e6501a780e42f4e73826df831d5d3809e17c8f8d335db2
|
|
4
|
+
data.tar.gz: 07665f169706158283c5e3d5fa9e231d668fa7105b8f4726e46f801679173071
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a2b310ce3f8a1e2cd0bb100abf9a1fec7b8dc36ad639f4b2007824136dd77409d460a2305d36fba0c11999c5e8620406fef5326ab749e31505809631671ee0e
|
|
7
|
+
data.tar.gz: 3fbc558438ae5dd809e60562a9b799c034a3bbbf620eeb77a7cec2ca0c8eb4dd35a86330214a94bde6c22a449974527dc480c9d4278774af0dacb0212a6556fe
|
data/CHANGES.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.1 (2026-03-11)
|
|
4
|
+
|
|
5
|
+
* Fix shell completion scripts: required parameters, subcommand aliases, and option value handling.
|
|
6
|
+
|
|
7
|
+
## 1.5.0 (2026-03-03)
|
|
8
|
+
|
|
9
|
+
* Add `--shell-completions` support.
|
|
10
|
+
|
|
3
11
|
## 1.4.0 (2026-02-09)
|
|
4
12
|
|
|
5
13
|
* Add support for Ruby 4.0.
|
data/README.md
CHANGED
|
@@ -413,6 +413,50 @@ Options:
|
|
|
413
413
|
-h, --help print help
|
|
414
414
|
```
|
|
415
415
|
|
|
416
|
+
## Shell completion
|
|
417
|
+
|
|
418
|
+
Clamp can generate shell completion scripts for bash, zsh, and fish. This is an opt-in feature:
|
|
419
|
+
|
|
420
|
+
```ruby
|
|
421
|
+
require 'clamp/completion'
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
This adds a hidden `--shell-completions` option to all commands. Use it to generate a completion script:
|
|
425
|
+
|
|
426
|
+
```sh
|
|
427
|
+
$ myapp --shell-completions bash # or: zsh, fish
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Activating completions
|
|
431
|
+
|
|
432
|
+
For **bash**, add to your `~/.bashrc`:
|
|
433
|
+
|
|
434
|
+
```sh
|
|
435
|
+
eval "$(myapp --shell-completions bash)"
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
For **zsh**, add to your `~/.zshrc`:
|
|
439
|
+
|
|
440
|
+
```sh
|
|
441
|
+
eval "$(myapp --shell-completions zsh)"
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
For **fish**, add to your `~/.config/fish/config.fish`:
|
|
445
|
+
|
|
446
|
+
```sh
|
|
447
|
+
myapp --shell-completions fish | source
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Programmatic API
|
|
451
|
+
|
|
452
|
+
You can also generate completion scripts programmatically:
|
|
453
|
+
|
|
454
|
+
```ruby
|
|
455
|
+
script = MyCommand.generate_completion(:fish, "myapp")
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
This returns the completion script as a string, which you can write to a file or use however you like.
|
|
459
|
+
|
|
416
460
|
## Localization
|
|
417
461
|
|
|
418
462
|
Clamp comes with support for overriding strings with custom translations. You can use localization library of your choice and override the strings at startup.
|
data/examples/gitdown
CHANGED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clamp
|
|
4
|
+
module Completion
|
|
5
|
+
|
|
6
|
+
# Generates bash shell completion scripts.
|
|
7
|
+
#
|
|
8
|
+
class BashGenerator
|
|
9
|
+
|
|
10
|
+
def initialize(command_class, executable_name)
|
|
11
|
+
@command_class = command_class
|
|
12
|
+
@executable_name = executable_name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generate
|
|
16
|
+
[
|
|
17
|
+
"# Bash completions for #{@executable_name}",
|
|
18
|
+
"# Generated by Clamp",
|
|
19
|
+
"",
|
|
20
|
+
takes_value_function,
|
|
21
|
+
"",
|
|
22
|
+
param_count_function,
|
|
23
|
+
"",
|
|
24
|
+
canonical_function,
|
|
25
|
+
"",
|
|
26
|
+
main_function,
|
|
27
|
+
"",
|
|
28
|
+
"complete -F #{completion_function} #{@executable_name}"
|
|
29
|
+
].push("").join("\n")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def completion_function
|
|
35
|
+
"_clamp_complete_#{Completion.encode_name(@executable_name)}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def main_function
|
|
39
|
+
[
|
|
40
|
+
"#{completion_function}() {",
|
|
41
|
+
" local cur prev",
|
|
42
|
+
" if type _init_completion &>/dev/null; then",
|
|
43
|
+
" _init_completion",
|
|
44
|
+
" else",
|
|
45
|
+
' cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
46
|
+
' prev="${COMP_WORDS[COMP_CWORD-1]}"',
|
|
47
|
+
" fi",
|
|
48
|
+
"",
|
|
49
|
+
" local subcmd_info subcmd params_remaining",
|
|
50
|
+
" subcmd_info=$(#{completion_function}_find_subcmd)",
|
|
51
|
+
' subcmd="${subcmd_info%% *}"',
|
|
52
|
+
' params_remaining="${subcmd_info##* }"',
|
|
53
|
+
" if #{completion_function}_takes_value \"$prev\" \"$subcmd\"; then",
|
|
54
|
+
" return",
|
|
55
|
+
" fi",
|
|
56
|
+
"",
|
|
57
|
+
options_case("$subcmd"),
|
|
58
|
+
"",
|
|
59
|
+
' if [ "$params_remaining" -eq 0 ]; then',
|
|
60
|
+
subcommands_case("$subcmd"),
|
|
61
|
+
" fi",
|
|
62
|
+
"}",
|
|
63
|
+
"",
|
|
64
|
+
find_subcmd_function
|
|
65
|
+
].join("\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def find_subcmd_function
|
|
69
|
+
[
|
|
70
|
+
"#{completion_function}_find_subcmd() {",
|
|
71
|
+
" local i=1 word subcmd skip",
|
|
72
|
+
" skip=$(#{completion_function}_param_count \"\")",
|
|
73
|
+
' while [ "$i" -lt "$COMP_CWORD" ]; do',
|
|
74
|
+
' word="${COMP_WORDS[$i]}"',
|
|
75
|
+
' case "$word" in',
|
|
76
|
+
" -*)",
|
|
77
|
+
" if #{completion_function}_takes_value \"$word\" \"$subcmd\"; then",
|
|
78
|
+
" ((i++))",
|
|
79
|
+
" fi",
|
|
80
|
+
" ;;",
|
|
81
|
+
" *)",
|
|
82
|
+
find_subcmd_match_word,
|
|
83
|
+
" ;;",
|
|
84
|
+
" esac",
|
|
85
|
+
" ((i++))",
|
|
86
|
+
" done",
|
|
87
|
+
' echo "$subcmd $skip"',
|
|
88
|
+
"}"
|
|
89
|
+
].join("\n")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def find_subcmd_match_word
|
|
93
|
+
subcmds = Completion.collect_subcommand_names(@command_class).join("|")
|
|
94
|
+
[
|
|
95
|
+
' if [ "$skip" -gt 0 ]; then',
|
|
96
|
+
" ((skip--))",
|
|
97
|
+
" else",
|
|
98
|
+
' case "$word" in',
|
|
99
|
+
" #{subcmds})",
|
|
100
|
+
" local canonical=$(#{completion_function}_canonical \"$word\")",
|
|
101
|
+
' if [ -z "$subcmd" ]; then',
|
|
102
|
+
' subcmd="$canonical"',
|
|
103
|
+
" else",
|
|
104
|
+
' subcmd="${subcmd}::${canonical}"',
|
|
105
|
+
" fi",
|
|
106
|
+
" skip=$(#{completion_function}_param_count \"$subcmd\")",
|
|
107
|
+
" ;;",
|
|
108
|
+
" esac",
|
|
109
|
+
" fi"
|
|
110
|
+
].join("\n")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def options_case(var)
|
|
114
|
+
build_case(var, " ", "COMPREPLY=") do |cmd, _has_children|
|
|
115
|
+
Completion.visible_options(cmd).flat_map { |o| Completion.expanded_switches(o) }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def subcommands_case(var)
|
|
120
|
+
build_case(var, " ", "COMPREPLY+=") do |cmd, has_children|
|
|
121
|
+
cmd.recognised_subcommands.flat_map(&:names) if has_children
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_case(var, indent, assign)
|
|
126
|
+
entries = {}
|
|
127
|
+
Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
|
|
128
|
+
words = yield(cmd, has_children)
|
|
129
|
+
next if words.nil? || words.empty?
|
|
130
|
+
|
|
131
|
+
path_str = path.map { |sub| sub.names.first }.join("::")
|
|
132
|
+
entries[path_str] = words.join(" ")
|
|
133
|
+
end
|
|
134
|
+
lines = ["#{indent}case \"#{var}\" in"]
|
|
135
|
+
entries.each do |path, words|
|
|
136
|
+
pattern = path.empty? ? '""' : "\"#{path}\""
|
|
137
|
+
lines << "#{indent} #{pattern})"
|
|
138
|
+
lines << "#{indent} #{assign}($(compgen -W \"#{words}\" -- \"$cur\"))"
|
|
139
|
+
lines << "#{indent} ;;"
|
|
140
|
+
end
|
|
141
|
+
lines << "#{indent}esac"
|
|
142
|
+
lines.join("\n")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def param_count_function
|
|
146
|
+
entries = {}
|
|
147
|
+
Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
|
|
148
|
+
next unless has_children
|
|
149
|
+
|
|
150
|
+
entries[path.map { |s| s.names.first }.join("::")] = Completion.required_parameter_count(cmd)
|
|
151
|
+
end
|
|
152
|
+
build_lookup_function("param_count", entries, "echo", default: "echo 0")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def canonical_function
|
|
156
|
+
aliases = {}
|
|
157
|
+
Completion.walk_command_tree(@command_class) do |cmd, _path, has_children|
|
|
158
|
+
next unless has_children
|
|
159
|
+
|
|
160
|
+
cmd.recognised_subcommands.each do |sub|
|
|
161
|
+
sub.names.drop(1).each { |name| aliases[name] = sub.names.first }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
build_lookup_function("canonical", aliases, "echo", default: 'echo "$1"')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def build_lookup_function(suffix, entries, verb, default:)
|
|
168
|
+
lines = ["#{completion_function}_#{suffix}() {", ' case "$1" in']
|
|
169
|
+
entries.each do |key, value|
|
|
170
|
+
pattern = key.empty? ? '""' : key
|
|
171
|
+
lines << " #{pattern}) #{verb} #{value.is_a?(String) ? "\"#{value}\"" : value} ;;"
|
|
172
|
+
end
|
|
173
|
+
lines.push(" *) #{default} ;;", " esac", "}")
|
|
174
|
+
lines.join("\n")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def takes_value_function
|
|
178
|
+
entries = {}
|
|
179
|
+
Completion.walk_command_tree(@command_class) do |cmd, path, _has_children|
|
|
180
|
+
path_str = path.map { |sub| sub.names.first }.join("::")
|
|
181
|
+
entries[path_str] = Completion.visible_options(cmd).reject(&:flag?)
|
|
182
|
+
.flat_map { |o| Completion.expanded_switches(o) }
|
|
183
|
+
end
|
|
184
|
+
lines = [
|
|
185
|
+
"#{completion_function}_takes_value() {",
|
|
186
|
+
' local option="$1"',
|
|
187
|
+
' local subcmd="$2"',
|
|
188
|
+
' case "$subcmd" in'
|
|
189
|
+
]
|
|
190
|
+
entries.each do |path, switches|
|
|
191
|
+
next if switches.empty?
|
|
192
|
+
|
|
193
|
+
pattern = path.empty? ? '""' : "\"#{path}\""
|
|
194
|
+
lines << " #{pattern})"
|
|
195
|
+
lines << ' case "$option" in'
|
|
196
|
+
lines << " #{switches.join('|')}) return 0 ;;"
|
|
197
|
+
lines << " esac"
|
|
198
|
+
lines << " ;;"
|
|
199
|
+
end
|
|
200
|
+
lines.push(" esac", " return 1", "}")
|
|
201
|
+
lines.join("\n")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clamp
|
|
4
|
+
module Completion
|
|
5
|
+
|
|
6
|
+
# Generates fish shell completion scripts.
|
|
7
|
+
class FishGenerator
|
|
8
|
+
|
|
9
|
+
def initialize(command_class, executable_name)
|
|
10
|
+
@command_class = command_class
|
|
11
|
+
@executable_name = executable_name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate
|
|
15
|
+
lines = []
|
|
16
|
+
lines << "# Fish completions for #{@executable_name}"
|
|
17
|
+
lines << "# Generated by Clamp"
|
|
18
|
+
lines << ""
|
|
19
|
+
helpers = [subcmd_args_function]
|
|
20
|
+
Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
|
|
21
|
+
child_names = has_children ? cmd.recognised_subcommands.flat_map(&:names) : []
|
|
22
|
+
condition = condition_for(path, child_names)
|
|
23
|
+
Completion.visible_options(cmd).each do |option|
|
|
24
|
+
lines << option_completion(option, condition)
|
|
25
|
+
end
|
|
26
|
+
next unless has_children
|
|
27
|
+
|
|
28
|
+
subcmd_condition = subcommand_condition(cmd, path, condition, helpers)
|
|
29
|
+
generate_subcommand_completions(lines, cmd, subcmd_condition)
|
|
30
|
+
lines << ""
|
|
31
|
+
end
|
|
32
|
+
"#{helpers.join("\n\n")}\n\n#{lines.join("\n")}\n"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def subcommand_condition(cmd, path, condition, helpers)
|
|
38
|
+
param_count = Completion.required_parameter_count(cmd)
|
|
39
|
+
return condition unless param_count.positive?
|
|
40
|
+
|
|
41
|
+
helper = ParamsSatisfiedHelper.new(@executable_name, path, param_count)
|
|
42
|
+
helpers << helper.to_s
|
|
43
|
+
"#{condition}; and #{helper.function_name}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def generate_subcommand_completions(lines, cmd, condition)
|
|
47
|
+
cmd.recognised_subcommands.each do |sub|
|
|
48
|
+
sub.names.each do |name|
|
|
49
|
+
lines << "complete -c #{@executable_name} -f -n '#{condition}' -a #{name} " \
|
|
50
|
+
"-d '#{escape(sub.description)}'"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def condition_for(path, child_names)
|
|
56
|
+
if path.empty?
|
|
57
|
+
"not #{completion_function}_subcmd_args >/dev/null"
|
|
58
|
+
else
|
|
59
|
+
parts = path.map { |sub| "#{completion_function}_seen_subcommand_from #{sub.names.join(' ')}" }
|
|
60
|
+
parts << "not #{completion_function}_seen_subcommand_from #{child_names.join(' ')}" if child_names.any?
|
|
61
|
+
parts.join("; and ")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def completion_function
|
|
66
|
+
"_clamp_complete_#{@executable_name}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def subcmd_args_function
|
|
70
|
+
optspecs = argparse_optspecs(@command_class)
|
|
71
|
+
lines = [
|
|
72
|
+
"function #{completion_function}_subcmd_args",
|
|
73
|
+
" set -l tokens (commandline -opc)",
|
|
74
|
+
" set -e tokens[1]",
|
|
75
|
+
" argparse -si #{optspecs.map { |s| "'#{s}'" }.join(' ')} -- $tokens 2>/dev/null",
|
|
76
|
+
" or return 1",
|
|
77
|
+
" test (count $argv) -gt 0",
|
|
78
|
+
" and printf '%s\\n' $argv",
|
|
79
|
+
"end",
|
|
80
|
+
"",
|
|
81
|
+
"function #{completion_function}_seen_subcommand_from",
|
|
82
|
+
" for p in (#{completion_function}_subcmd_args)",
|
|
83
|
+
" if contains -- $p $argv",
|
|
84
|
+
" return 0",
|
|
85
|
+
" end",
|
|
86
|
+
" end",
|
|
87
|
+
" return 1",
|
|
88
|
+
"end"
|
|
89
|
+
]
|
|
90
|
+
lines.join("\n")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def argparse_optspecs(command_class)
|
|
94
|
+
command_class.recognised_options.flat_map do |option|
|
|
95
|
+
Completion.argparse_specs_for(option)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def option_completion(option, condition)
|
|
100
|
+
parts = ["complete -c #{@executable_name} -f"]
|
|
101
|
+
parts << "-n '#{condition}'"
|
|
102
|
+
|
|
103
|
+
Completion.expanded_switches(option).each do |switch|
|
|
104
|
+
parts << if switch.start_with?("--")
|
|
105
|
+
"-l #{switch.sub(/^--/, '')}"
|
|
106
|
+
else
|
|
107
|
+
"-s #{switch.sub(/^-/, '')}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
parts << "-r" unless option.flag?
|
|
112
|
+
parts << "-d '#{escape(option.description)}'"
|
|
113
|
+
|
|
114
|
+
parts.join(" ")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def escape(str)
|
|
118
|
+
str.gsub("'", "\\\\'")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Generates a fish function that checks whether required parameters
|
|
124
|
+
# have been provided before offering subcommand completions.
|
|
125
|
+
class ParamsSatisfiedHelper
|
|
126
|
+
|
|
127
|
+
def initialize(executable_name, path, required_count)
|
|
128
|
+
@executable_name = executable_name
|
|
129
|
+
@path = path
|
|
130
|
+
@required_count = required_count
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def function_name
|
|
134
|
+
parts = [@executable_name]
|
|
135
|
+
@path.each { |sub| parts << sub.names.first }
|
|
136
|
+
"_clamp_complete_#{parts.join('_')}_params_satisfied"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def to_s
|
|
140
|
+
lines = ["function #{function_name}", " set -l tokens (commandline -opc)", " set -l positional 0"]
|
|
141
|
+
if @path.empty?
|
|
142
|
+
count_positional_root(lines)
|
|
143
|
+
else
|
|
144
|
+
count_positional_nested(lines)
|
|
145
|
+
end
|
|
146
|
+
lines.push(" test $positional -ge #{@required_count}", "end")
|
|
147
|
+
lines.join("\n")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def count_positional_root(lines)
|
|
153
|
+
lines.push(" for token in $tokens[2..]",
|
|
154
|
+
" if not string match -q -- '-*' $token",
|
|
155
|
+
" set positional (math $positional + 1)",
|
|
156
|
+
" end", " end")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def count_positional_nested(lines)
|
|
160
|
+
lines.push(" set -l depth 0", " for token in $tokens[2..]", " switch $depth")
|
|
161
|
+
@path.each_with_index do |sub, i|
|
|
162
|
+
all_names = sub.names.join(" ")
|
|
163
|
+
lines.push(" case #{i}",
|
|
164
|
+
" if contains -- $token #{all_names}",
|
|
165
|
+
" set depth #{i + 1}", " end")
|
|
166
|
+
end
|
|
167
|
+
lines.push(" case #{@path.length}",
|
|
168
|
+
" if not string match -q -- '-*' $token",
|
|
169
|
+
" set positional (math $positional + 1)",
|
|
170
|
+
" end", " end", " end")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clamp
|
|
4
|
+
module Completion
|
|
5
|
+
|
|
6
|
+
# Generates zsh shell completion scripts.
|
|
7
|
+
#
|
|
8
|
+
class ZshGenerator
|
|
9
|
+
|
|
10
|
+
def initialize(command_class, executable_name)
|
|
11
|
+
@command_class = command_class
|
|
12
|
+
@executable_name = executable_name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generate
|
|
16
|
+
lines = ["#compdef #{@executable_name}", ""]
|
|
17
|
+
generate_functions(lines, @command_class, [completion_function], Set.new)
|
|
18
|
+
lines << completion_function
|
|
19
|
+
lines.push("").join("\n")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def completion_function
|
|
25
|
+
"_clamp_complete_#{Completion.encode_name(@executable_name)}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def generate_functions(lines, command_class, path, visited)
|
|
29
|
+
has_children = command_class.has_subcommands? && !visited.include?(command_class)
|
|
30
|
+
visited |= [command_class]
|
|
31
|
+
func_name = path.join("_")
|
|
32
|
+
|
|
33
|
+
if has_children
|
|
34
|
+
generate_subcommand_node(lines, command_class, path, func_name, visited)
|
|
35
|
+
else
|
|
36
|
+
lines << "#{func_name}() {"
|
|
37
|
+
specs = Completion.visible_options(command_class).flat_map { |o| option_specs(o) }
|
|
38
|
+
generate_arguments_call(lines, specs) if specs.any?
|
|
39
|
+
lines << "}"
|
|
40
|
+
lines << ""
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def generate_subcommand_node(lines, command_class, path, func_name, visited)
|
|
45
|
+
lines << "#{func_name}() {"
|
|
46
|
+
lines << " local context state state_descr line"
|
|
47
|
+
lines << " typeset -A opt_args"
|
|
48
|
+
lines << ""
|
|
49
|
+
specs = Completion.visible_options(command_class).flat_map { |o| option_specs(o) }
|
|
50
|
+
specs << "'1:command:->commands'"
|
|
51
|
+
specs << "'*::args:->args'"
|
|
52
|
+
lines << " _arguments -C \\"
|
|
53
|
+
generate_spec_lines(lines, specs)
|
|
54
|
+
lines << ""
|
|
55
|
+
generate_state_dispatch(lines, command_class, path)
|
|
56
|
+
lines << "}"
|
|
57
|
+
lines << ""
|
|
58
|
+
command_class.recognised_subcommands.each do |sub|
|
|
59
|
+
generate_functions(lines, sub.subcommand_class, path + [Completion.encode_name(sub.names.first)], visited)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def generate_arguments_call(lines, specs)
|
|
64
|
+
lines << " _arguments \\"
|
|
65
|
+
generate_spec_lines(lines, specs)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def generate_spec_lines(lines, specs)
|
|
69
|
+
specs.each_with_index do |spec, i|
|
|
70
|
+
suffix = i < specs.length - 1 ? " \\" : ""
|
|
71
|
+
lines << " #{spec}#{suffix}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def generate_state_dispatch(lines, command_class, path)
|
|
76
|
+
lines << " case $state in"
|
|
77
|
+
lines << " commands)"
|
|
78
|
+
lines << " local -a cmds"
|
|
79
|
+
lines << " cmds=("
|
|
80
|
+
command_class.recognised_subcommands.each do |sub|
|
|
81
|
+
sub.names.each do |name|
|
|
82
|
+
lines << " '#{escape(name)}:#{escape(sub.description)}'"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
lines << " )"
|
|
86
|
+
lines << " _describe 'command' cmds"
|
|
87
|
+
lines << " ;;"
|
|
88
|
+
lines << " args)"
|
|
89
|
+
lines << " case $line[1] in"
|
|
90
|
+
command_class.recognised_subcommands.each do |sub|
|
|
91
|
+
sub_fn = (path + [Completion.encode_name(sub.names.first)]).join("_")
|
|
92
|
+
pattern = sub.names.join("|")
|
|
93
|
+
lines << " #{pattern}) #{sub_fn} ;;"
|
|
94
|
+
end
|
|
95
|
+
lines << " esac"
|
|
96
|
+
lines << " ;;"
|
|
97
|
+
lines << " esac"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def option_specs(option)
|
|
101
|
+
expanded = Completion.expanded_switches(option)
|
|
102
|
+
suffix = "[#{escape(option.description)}]"
|
|
103
|
+
suffix += ":#{option.type.to_s.downcase}:" unless option.flag?
|
|
104
|
+
exclusion = expanded.length > 1 ? "'(#{expanded.join(' ')})'" : ""
|
|
105
|
+
short = expanded.find { |s| s.match?(/^-[^-]/) }
|
|
106
|
+
longs = expanded.grep(/^--/)
|
|
107
|
+
|
|
108
|
+
if short && longs.length == 1
|
|
109
|
+
# Braces outside quotes for zsh brace expansion
|
|
110
|
+
["#{exclusion}{#{short},#{longs.first}}'#{suffix}'"]
|
|
111
|
+
else
|
|
112
|
+
expanded.map { |sw| "#{exclusion}'#{sw}#{suffix}'" }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def escape(str)
|
|
117
|
+
str.gsub("'", "'\\''")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
end
|
|
123
|
+
end
|