clamp 1.4.0 → 1.5.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/CHANGES.md +4 -0
- data/README.md +44 -0
- data/examples/gitdown +1 -0
- data/lib/clamp/completion/bash_generator.rb +141 -0
- data/lib/clamp/completion/fish_generator.rb +75 -0
- data/lib/clamp/completion/zsh_generator.rb +126 -0
- data/lib/clamp/completion.rb +163 -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: 8f2a9c7d404e87023cc8694e18498df5719974cd11e58663df640c08a0f87c3e
|
|
4
|
+
data.tar.gz: da2a65496e24eb9abc13eaddc370f31d3e5764a832c3d0a18015dd636e43dbfa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7318c3498b2e425ecd18395e4ad3d24109e130bac38f9f9406fba97b02b8b78d859374f5e1439a064dff1e4037b2cab2df8210a13a9ea54cac030df8b35e86c8
|
|
7
|
+
data.tar.gz: 5f46c6724d06d2fa30425369a1867f0e2199e05997d796142af125dad5369a08c444f5a06fb15c2ad206b8e62afcb582fe1442c36fbe7ce994fa7d8400f71993
|
data/CHANGES.md
CHANGED
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,141 @@
|
|
|
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
|
+
completion_function,
|
|
23
|
+
"",
|
|
24
|
+
"complete -F _#{function_name} #{@executable_name}"
|
|
25
|
+
].push("").join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def function_name
|
|
31
|
+
@executable_name.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def completion_function
|
|
35
|
+
fn = function_name
|
|
36
|
+
[
|
|
37
|
+
"_#{fn}() {",
|
|
38
|
+
" local cur prev",
|
|
39
|
+
" if type _init_completion &>/dev/null; then",
|
|
40
|
+
" _init_completion",
|
|
41
|
+
" else",
|
|
42
|
+
' cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
43
|
+
' prev="${COMP_WORDS[COMP_CWORD-1]}"',
|
|
44
|
+
" fi",
|
|
45
|
+
"",
|
|
46
|
+
" local subcmd",
|
|
47
|
+
" subcmd=$(__#{fn}_find_subcmd)",
|
|
48
|
+
" if __#{fn}_takes_value \"$prev\" \"$subcmd\"; then",
|
|
49
|
+
" return",
|
|
50
|
+
" fi",
|
|
51
|
+
"",
|
|
52
|
+
completions_case("$subcmd"),
|
|
53
|
+
"}",
|
|
54
|
+
"",
|
|
55
|
+
find_subcmd_function
|
|
56
|
+
].join("\n")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def find_subcmd_function
|
|
60
|
+
fn = function_name
|
|
61
|
+
subcmds = Completion.collect_subcommand_names(@command_class).join("|")
|
|
62
|
+
[
|
|
63
|
+
"__#{fn}_find_subcmd() {",
|
|
64
|
+
" local i=1 word subcmd",
|
|
65
|
+
' while [ "$i" -lt "$COMP_CWORD" ]; do',
|
|
66
|
+
' word="${COMP_WORDS[$i]}"',
|
|
67
|
+
' case "$word" in',
|
|
68
|
+
" -*)",
|
|
69
|
+
" if __#{fn}_takes_value \"$word\" \"$subcmd\"; then",
|
|
70
|
+
" ((i++))",
|
|
71
|
+
" fi",
|
|
72
|
+
" ;;",
|
|
73
|
+
" *)",
|
|
74
|
+
' case "$word" in',
|
|
75
|
+
" #{subcmds})",
|
|
76
|
+
' if [ -z "$subcmd" ]; then',
|
|
77
|
+
' subcmd="$word"',
|
|
78
|
+
" else",
|
|
79
|
+
' subcmd="${subcmd}::${word}"',
|
|
80
|
+
" fi",
|
|
81
|
+
" ;;",
|
|
82
|
+
" esac",
|
|
83
|
+
" ;;",
|
|
84
|
+
" esac",
|
|
85
|
+
" ((i++))",
|
|
86
|
+
" done",
|
|
87
|
+
' echo "$subcmd"',
|
|
88
|
+
"}"
|
|
89
|
+
].join("\n")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def completions_case(var)
|
|
93
|
+
entries = {}
|
|
94
|
+
Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
|
|
95
|
+
path_str = path.map { |sub| sub.names.first }.join("::")
|
|
96
|
+
words = Completion.visible_options(cmd).flat_map { |o| Completion.expanded_switches(o) }
|
|
97
|
+
cmd.recognised_subcommands.each { |sub| words.concat(sub.names) } if has_children
|
|
98
|
+
entries[path_str] = words.join(" ")
|
|
99
|
+
end
|
|
100
|
+
lines = [" case \"#{var}\" in"]
|
|
101
|
+
entries.each do |path, words|
|
|
102
|
+
pattern = path.empty? ? '""' : "\"#{path}\""
|
|
103
|
+
lines << " #{pattern})"
|
|
104
|
+
lines << " COMPREPLY=($(compgen -W \"#{words}\" -- \"$cur\"))"
|
|
105
|
+
lines << " ;;"
|
|
106
|
+
end
|
|
107
|
+
lines << " esac"
|
|
108
|
+
lines.join("\n")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def takes_value_function
|
|
112
|
+
entries = {}
|
|
113
|
+
Completion.walk_command_tree(@command_class) do |cmd, path, _has_children|
|
|
114
|
+
path_str = path.map { |sub| sub.names.first }.join("::")
|
|
115
|
+
entries[path_str] = Completion.visible_options(cmd).reject(&:flag?)
|
|
116
|
+
.flat_map { |o| Completion.expanded_switches(o) }
|
|
117
|
+
end
|
|
118
|
+
lines = [
|
|
119
|
+
"__#{function_name}_takes_value() {",
|
|
120
|
+
' local option="$1"',
|
|
121
|
+
' local subcmd="$2"',
|
|
122
|
+
' case "$subcmd" in'
|
|
123
|
+
]
|
|
124
|
+
entries.each do |path, switches|
|
|
125
|
+
next if switches.empty?
|
|
126
|
+
|
|
127
|
+
pattern = path.empty? ? '""' : "\"#{path}\""
|
|
128
|
+
lines << " #{pattern})"
|
|
129
|
+
lines << ' case "$option" in'
|
|
130
|
+
lines << " #{switches.join('|')}) return 0 ;;"
|
|
131
|
+
lines << " esac"
|
|
132
|
+
lines << " ;;"
|
|
133
|
+
end
|
|
134
|
+
lines.push(" esac", " return 1", "}")
|
|
135
|
+
lines.join("\n")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
Completion.walk_command_tree(@command_class) do |cmd, path, has_children|
|
|
20
|
+
child_names = has_children ? cmd.recognised_subcommands.flat_map(&:names) : []
|
|
21
|
+
condition = condition_for(path, child_names)
|
|
22
|
+
Completion.visible_options(cmd).each do |option|
|
|
23
|
+
lines << option_completion(option, condition)
|
|
24
|
+
end
|
|
25
|
+
next unless has_children
|
|
26
|
+
|
|
27
|
+
cmd.recognised_subcommands.each do |sub|
|
|
28
|
+
sub.names.each do |name|
|
|
29
|
+
lines << "complete -c #{@executable_name} -f -n '#{condition}' -a #{name} " \
|
|
30
|
+
"-d '#{escape(sub.description)}'"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
lines << ""
|
|
34
|
+
end
|
|
35
|
+
"#{lines.join("\n")}\n"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def condition_for(path, child_names)
|
|
41
|
+
if path.empty?
|
|
42
|
+
"__fish_use_subcommand"
|
|
43
|
+
else
|
|
44
|
+
parts = path.map { |sub| "__fish_seen_subcommand_from #{sub.names.join(' ')}" }
|
|
45
|
+
parts << "not __fish_seen_subcommand_from #{child_names.join(' ')}" if child_names.any?
|
|
46
|
+
parts.join("; and ")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def option_completion(option, condition)
|
|
51
|
+
parts = ["complete -c #{@executable_name} -f"]
|
|
52
|
+
parts << "-n '#{condition}'"
|
|
53
|
+
|
|
54
|
+
Completion.expanded_switches(option).each do |switch|
|
|
55
|
+
parts << if switch.start_with?("--")
|
|
56
|
+
"-l #{switch.sub(/^--/, '')}"
|
|
57
|
+
else
|
|
58
|
+
"-s #{switch.sub(/^-/, '')}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
parts << "-r" unless option.flag?
|
|
63
|
+
parts << "-d '#{escape(option.description)}'"
|
|
64
|
+
|
|
65
|
+
parts.join(" ")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def escape(str)
|
|
69
|
+
str.gsub("'", "\\\\'")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
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, [function_name], Set.new)
|
|
18
|
+
lines << "_#{function_name}"
|
|
19
|
+
lines.push("").join("\n")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def function_name
|
|
25
|
+
@executable_name.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
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).map { |o| option_spec(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).map { |o| option_spec(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 + [sanitize(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 + [sanitize(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_spec(option)
|
|
101
|
+
expanded = Completion.expanded_switches(option)
|
|
102
|
+
desc = "[#{escape(option.description)}]"
|
|
103
|
+
arg_spec = option.flag? ? "" : ":#{option.type.to_s.downcase}:"
|
|
104
|
+
exclusion = expanded.length > 1 ? "(#{expanded.join(' ')})" : ""
|
|
105
|
+
|
|
106
|
+
"'#{exclusion}#{switch_pattern(option.switches)}#{desc}#{arg_spec}'"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def switch_pattern(switches)
|
|
110
|
+
short = switches.find { |s| s =~ /^-[^-]/ }
|
|
111
|
+
long = switches.find { |s| s =~ /^--/ }
|
|
112
|
+
short && long ? "{#{short},#{long}}" : (long || short).to_s
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def sanitize(name)
|
|
116
|
+
name.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def escape(str)
|
|
120
|
+
str.gsub("'", "'\\''")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "clamp/command"
|
|
4
|
+
require "clamp/completion/bash_generator"
|
|
5
|
+
require "clamp/completion/fish_generator"
|
|
6
|
+
require "clamp/completion/zsh_generator"
|
|
7
|
+
|
|
8
|
+
module Clamp
|
|
9
|
+
|
|
10
|
+
# Shell completion script generation.
|
|
11
|
+
#
|
|
12
|
+
module Completion
|
|
13
|
+
|
|
14
|
+
GENERATORS = {
|
|
15
|
+
bash: Clamp::Completion::BashGenerator,
|
|
16
|
+
fish: Clamp::Completion::FishGenerator,
|
|
17
|
+
zsh: Clamp::Completion::ZshGenerator
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# Raised when --shell-completions is used; caught by Command.run.
|
|
21
|
+
#
|
|
22
|
+
class Wanted < StandardError
|
|
23
|
+
|
|
24
|
+
def initialize(command, shell)
|
|
25
|
+
super("completion requested")
|
|
26
|
+
@command = command
|
|
27
|
+
@shell = shell
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :command, :shell
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def generate(command_class, shell, executable_name)
|
|
37
|
+
generator_class = GENERATORS.fetch(shell) do
|
|
38
|
+
raise ArgumentError, "unsupported shell: #{shell.inspect}"
|
|
39
|
+
end
|
|
40
|
+
generator_class.new(command_class, executable_name).generate
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Return switches with --[no-]foo expanded to --foo and --no-foo.
|
|
44
|
+
def expanded_switches(option)
|
|
45
|
+
option.switches.flat_map do |switch|
|
|
46
|
+
if switch =~ /^--\[no-\](.*)/
|
|
47
|
+
["--#{Regexp.last_match(1)}", "--no-#{Regexp.last_match(1)}"]
|
|
48
|
+
else
|
|
49
|
+
switch
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Options visible in completion (excludes hidden).
|
|
55
|
+
def visible_options(command_class)
|
|
56
|
+
command_class.recognised_options.reject(&:hidden?)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Walk the command tree depth-first, yielding (command_class, path, has_children).
|
|
60
|
+
# Path is an array of Subcommand::Definition objects.
|
|
61
|
+
# Always yields, even for revisited classes (with has_children=false).
|
|
62
|
+
def walk_command_tree(command_class, path = [], visited = Set.new, &block)
|
|
63
|
+
fresh = !visited.include?(command_class)
|
|
64
|
+
visited |= [command_class]
|
|
65
|
+
has_children = command_class.has_subcommands? && fresh
|
|
66
|
+
yield command_class, path, has_children
|
|
67
|
+
return unless has_children
|
|
68
|
+
|
|
69
|
+
command_class.recognised_subcommands.each do |sub|
|
|
70
|
+
walk_command_tree(sub.subcommand_class, path + [sub], visited, &block)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Collect all subcommand names across the command tree.
|
|
75
|
+
def collect_subcommand_names(command_class)
|
|
76
|
+
names = []
|
|
77
|
+
walk_command_tree(command_class) do |cmd, _path, has_children|
|
|
78
|
+
cmd.recognised_subcommands.each { |sub| names.concat(sub.names) } if has_children
|
|
79
|
+
end
|
|
80
|
+
names.uniq
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
module Clamp
|
|
87
|
+
|
|
88
|
+
# Reopened to add completion support.
|
|
89
|
+
#
|
|
90
|
+
class Command
|
|
91
|
+
|
|
92
|
+
def self.generate_completion(shell, executable_name)
|
|
93
|
+
Clamp::Completion.generate(self, shell, executable_name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Adds --shell-completions option and handles the Wanted exception.
|
|
97
|
+
#
|
|
98
|
+
module RunWithCompletion
|
|
99
|
+
|
|
100
|
+
def run(invocation_path = File.basename($PROGRAM_NAME), arguments = ARGV, context = {})
|
|
101
|
+
context[:root_command_class] ||= self
|
|
102
|
+
super
|
|
103
|
+
rescue Clamp::Completion::Wanted => e
|
|
104
|
+
shell_name = File.basename(e.shell).to_sym
|
|
105
|
+
begin
|
|
106
|
+
puts generate_completion(shell_name, invocation_path)
|
|
107
|
+
rescue ArgumentError => ex
|
|
108
|
+
$stderr.puts "ERROR: #{ex.message}"
|
|
109
|
+
exit(1)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class << self
|
|
116
|
+
|
|
117
|
+
prepend RunWithCompletion
|
|
118
|
+
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
module Clamp
|
|
126
|
+
module Option
|
|
127
|
+
|
|
128
|
+
# Adds implicit --shell-completions option to all commands.
|
|
129
|
+
#
|
|
130
|
+
module Declaration
|
|
131
|
+
|
|
132
|
+
# Declares --shell-completions alongside other implicit options.
|
|
133
|
+
#
|
|
134
|
+
module WithCompletionOption
|
|
135
|
+
|
|
136
|
+
def recognised_options
|
|
137
|
+
unless @implicit_completion_option_declared
|
|
138
|
+
@implicit_completion_option_declared = true
|
|
139
|
+
declare_implicit_completion_option
|
|
140
|
+
end
|
|
141
|
+
super
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def declare_implicit_completion_option
|
|
147
|
+
return if effective_options.find { |o| o.handles?("--shell-completions") }
|
|
148
|
+
|
|
149
|
+
option "--shell-completions", "SHELL",
|
|
150
|
+
"generate shell completion script",
|
|
151
|
+
hidden: true do |shell|
|
|
152
|
+
raise Clamp::Completion::Wanted.new(self, shell)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
prepend WithCompletionOption
|
|
159
|
+
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
end
|
|
163
|
+
end
|
data/lib/clamp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clamp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Williams
|
|
@@ -17,20 +17,9 @@ executables: []
|
|
|
17
17
|
extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
|
19
19
|
files:
|
|
20
|
-
- ".autotest"
|
|
21
|
-
- ".editorconfig"
|
|
22
|
-
- ".github/workflows/ci.yml"
|
|
23
|
-
- ".gitignore"
|
|
24
|
-
- ".rspec"
|
|
25
|
-
- ".rubocop.yml"
|
|
26
20
|
- CHANGES.md
|
|
27
|
-
- CODEOWNERS
|
|
28
|
-
- Gemfile
|
|
29
|
-
- Guardfile
|
|
30
21
|
- LICENSE
|
|
31
22
|
- README.md
|
|
32
|
-
- Rakefile
|
|
33
|
-
- clamp.gemspec
|
|
34
23
|
- examples/admin
|
|
35
24
|
- examples/defaulted
|
|
36
25
|
- examples/flipflop
|
|
@@ -45,6 +34,10 @@ files:
|
|
|
45
34
|
- lib/clamp/attribute/definition.rb
|
|
46
35
|
- lib/clamp/attribute/instance.rb
|
|
47
36
|
- lib/clamp/command.rb
|
|
37
|
+
- lib/clamp/completion.rb
|
|
38
|
+
- lib/clamp/completion/bash_generator.rb
|
|
39
|
+
- lib/clamp/completion/fish_generator.rb
|
|
40
|
+
- lib/clamp/completion/zsh_generator.rb
|
|
48
41
|
- lib/clamp/errors.rb
|
|
49
42
|
- lib/clamp/help.rb
|
|
50
43
|
- lib/clamp/messages.rb
|
|
@@ -60,15 +53,6 @@ files:
|
|
|
60
53
|
- lib/clamp/subcommand/parsing.rb
|
|
61
54
|
- lib/clamp/truthy.rb
|
|
62
55
|
- lib/clamp/version.rb
|
|
63
|
-
- spec/clamp/command_group_spec.rb
|
|
64
|
-
- spec/clamp/command_option_module_spec.rb
|
|
65
|
-
- spec/clamp/command_option_reordering_spec.rb
|
|
66
|
-
- spec/clamp/command_spec.rb
|
|
67
|
-
- spec/clamp/help/builder_spec.rb
|
|
68
|
-
- spec/clamp/messages_spec.rb
|
|
69
|
-
- spec/clamp/option/definition_spec.rb
|
|
70
|
-
- spec/clamp/parameter/definition_spec.rb
|
|
71
|
-
- spec/spec_helper.rb
|
|
72
56
|
homepage: https://github.com/mdub/clamp
|
|
73
57
|
licenses:
|
|
74
58
|
- MIT
|
data/.autotest
DELETED