hammer_cli 0.19.1 → 2.2.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/bin/hammer-complete +28 -0
- data/config/cli_config.template.yml +2 -0
- data/config/hammer.completion +5 -0
- data/doc/commands_extension.md +12 -0
- data/doc/creating_commands.md +100 -0
- data/doc/installation.md +47 -4
- data/doc/installation_rpm.md +2 -2
- data/doc/release_notes.md +31 -6
- data/lib/hammer_cli.rb +1 -0
- data/lib/hammer_cli/abstract.rb +61 -4
- data/lib/hammer_cli/apipie/api_connection.rb +5 -1
- data/lib/hammer_cli/apipie/command.rb +3 -2
- data/lib/hammer_cli/apipie/option_builder.rb +15 -13
- data/lib/hammer_cli/apipie/option_definition.rb +9 -7
- data/lib/hammer_cli/bash.rb +2 -0
- data/lib/hammer_cli/bash/completion.rb +159 -0
- data/lib/hammer_cli/bash/prebuild_command.rb +21 -0
- data/lib/hammer_cli/command_extensions.rb +21 -1
- data/lib/hammer_cli/connection.rb +4 -0
- data/lib/hammer_cli/exception_handler.rb +11 -2
- data/lib/hammer_cli/full_help.rb +8 -1
- data/lib/hammer_cli/help/builder.rb +29 -3
- data/lib/hammer_cli/logger_watch.rb +1 -1
- data/lib/hammer_cli/main.rb +5 -3
- data/lib/hammer_cli/options/normalizers.rb +7 -3
- data/lib/hammer_cli/options/option_definition.rb +26 -6
- data/lib/hammer_cli/options/option_family.rb +114 -0
- data/lib/hammer_cli/options/predefined.rb +1 -1
- data/lib/hammer_cli/output/adapter/abstract.rb +1 -5
- data/lib/hammer_cli/output/adapter/base.rb +1 -1
- data/lib/hammer_cli/output/adapter/csv.rb +3 -2
- data/lib/hammer_cli/output/adapter/json.rb +14 -3
- data/lib/hammer_cli/output/adapter/silent.rb +1 -1
- data/lib/hammer_cli/output/adapter/table.rb +27 -8
- data/lib/hammer_cli/output/adapter/yaml.rb +6 -3
- data/lib/hammer_cli/output/output.rb +2 -4
- data/lib/hammer_cli/settings.rb +2 -1
- data/lib/hammer_cli/subcommand.rb +25 -1
- data/lib/hammer_cli/testing/command_assertions.rb +2 -2
- data/lib/hammer_cli/utils.rb +22 -0
- data/lib/hammer_cli/version.rb +1 -1
- data/locale/ca/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/de/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/en/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/en_GB/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/es/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/fr/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/it/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/ja/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/ko/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/pt_BR/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/ru/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/zh_CN/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/zh_TW/LC_MESSAGES/hammer-cli.mo +0 -0
- data/test/unit/abstract_test.rb +23 -2
- data/test/unit/apipie/api_connection_test.rb +1 -0
- data/test/unit/apipie/option_builder_test.rb +8 -0
- data/test/unit/bash_test.rb +138 -0
- data/test/unit/command_extensions_test.rb +67 -49
- data/test/unit/exception_handler_test.rb +44 -0
- data/test/unit/help/builder_test.rb +22 -0
- data/test/unit/options/option_family_test.rb +48 -0
- data/test/unit/output/adapter/base_test.rb +58 -0
- data/test/unit/output/adapter/csv_test.rb +63 -1
- data/test/unit/output/adapter/json_test.rb +61 -0
- data/test/unit/output/adapter/table_test.rb +70 -1
- data/test/unit/output/adapter/yaml_test.rb +59 -0
- data/test/unit/output/output_test.rb +3 -3
- metadata +17 -6
- data/hammer_cli_complete +0 -13
data/lib/hammer_cli/main.rb
CHANGED
@@ -47,9 +47,11 @@ module HammerCLI
|
|
47
47
|
:context_target => :interactive
|
48
48
|
option ["--no-headers"], :flag, _("Hide headers from output")
|
49
49
|
option ["--csv"], :flag, _("Output as CSV (same as --output=csv)")
|
50
|
-
option [
|
51
|
-
format: HammerCLI::Options::Normalizers::Enum.new(
|
52
|
-
|
50
|
+
option ['--output'], 'ADAPTER', _('Set output format'),
|
51
|
+
format: HammerCLI::Options::Normalizers::Enum.new(
|
52
|
+
HammerCLI::Output::Output.adapters.keys.map(&:to_s)
|
53
|
+
),
|
54
|
+
context_target: :adapter
|
53
55
|
option ["--output-file"], "OUTPUT_FILE", _("Path to custom output file") do |filename|
|
54
56
|
begin
|
55
57
|
context[:output_file] = File.new(filename, 'w')
|
@@ -111,10 +111,10 @@ module HammerCLI
|
|
111
111
|
|
112
112
|
class ListNested < AbstractNormalizer
|
113
113
|
class Schema < Array
|
114
|
-
def description
|
114
|
+
def description(richtext: true)
|
115
115
|
'"' + reduce([]) do |schema, nested_param|
|
116
116
|
name = nested_param.name
|
117
|
-
name = HighLine.color(name, :bold) if nested_param.required?
|
117
|
+
name = HighLine.color(name, :bold) if nested_param.required? && richtext
|
118
118
|
values = nested_param.validator.scan(/<[^>]+>[\w]+<\/?[^>]+>/)
|
119
119
|
value_pattern = if values.empty?
|
120
120
|
"<#{nested_param.expected_type.downcase}>"
|
@@ -172,6 +172,9 @@ module HammerCLI
|
|
172
172
|
|
173
173
|
|
174
174
|
class Bool < AbstractNormalizer
|
175
|
+
def allowed_values
|
176
|
+
['yes', 'no', 'true', 'false', '1', '0']
|
177
|
+
end
|
175
178
|
|
176
179
|
def description
|
177
180
|
_('One of %s.') % ['true/false', 'yes/no', '1/0'].join(', ')
|
@@ -189,7 +192,7 @@ module HammerCLI
|
|
189
192
|
end
|
190
193
|
|
191
194
|
def complete(value)
|
192
|
-
|
195
|
+
allowed_values.map { |v| v + ' ' }
|
193
196
|
end
|
194
197
|
end
|
195
198
|
|
@@ -280,6 +283,7 @@ module HammerCLI
|
|
280
283
|
end
|
281
284
|
|
282
285
|
class EnumList < AbstractNormalizer
|
286
|
+
attr_reader :allowed_values
|
283
287
|
|
284
288
|
def initialize(allowed_values)
|
285
289
|
@allowed_values = allowed_values
|
@@ -22,14 +22,12 @@ module HammerCLI
|
|
22
22
|
|
23
23
|
class OptionDefinition < Clamp::Option::Definition
|
24
24
|
|
25
|
-
attr_accessor :value_formatter
|
26
|
-
attr_accessor :context_target
|
27
|
-
attr_accessor :deprecated_switches
|
25
|
+
attr_accessor :value_formatter, :context_target, :deprecated_switches
|
28
26
|
|
29
27
|
def initialize(switches, type, description, options = {})
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
@value_formatter = options[:format] || HammerCLI::Options::Normalizers::Default.new
|
29
|
+
@context_target = options[:context_target]
|
30
|
+
@deprecated_switches = options[:deprecated]
|
33
31
|
super
|
34
32
|
end
|
35
33
|
|
@@ -138,6 +136,28 @@ module HammerCLI
|
|
138
136
|
end
|
139
137
|
end
|
140
138
|
|
139
|
+
def completion_type(formatter = nil)
|
140
|
+
return { type: :flag } if @type == :flag
|
141
|
+
|
142
|
+
formatter ||= value_formatter
|
143
|
+
completion_type = case formatter
|
144
|
+
when HammerCLI::Options::Normalizers::Bool,
|
145
|
+
HammerCLI::Options::Normalizers::Enum
|
146
|
+
{ type: :enum, values: value_formatter.allowed_values }
|
147
|
+
when HammerCLI::Options::Normalizers::EnumList
|
148
|
+
{ type: :multienum, values: value_formatter.allowed_values }
|
149
|
+
when HammerCLI::Options::Normalizers::ListNested
|
150
|
+
{ type: :schema, schema: value_formatter.schema.description(richtext: false) }
|
151
|
+
when HammerCLI::Options::Normalizers::List
|
152
|
+
{ type: :list }
|
153
|
+
when HammerCLI::Options::Normalizers::KeyValueList
|
154
|
+
{ type: :key_value_list }
|
155
|
+
when HammerCLI::Options::Normalizers::File
|
156
|
+
{ type: :file }
|
157
|
+
end
|
158
|
+
completion_type || { type: :value }
|
159
|
+
end
|
160
|
+
|
141
161
|
private
|
142
162
|
|
143
163
|
def format_deprecation_msg(option_desc, deprecation_msg)
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HammerCLI
|
4
|
+
module Options
|
5
|
+
class OptionFamily
|
6
|
+
attr_reader :children
|
7
|
+
|
8
|
+
IDS_REGEX = /\s?([Ii][Dd][s]?)\W|([Ii][Dd][s]?\Z)/
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@all = []
|
12
|
+
@children = []
|
13
|
+
@options = options
|
14
|
+
@creator = options[:creator] || Class.new(HammerCLI::Apipie::Command)
|
15
|
+
@prefix = options[:prefix]
|
16
|
+
@description = options[:description]
|
17
|
+
@root = options[:root] || options[:aliased_resource] || options[:referenced_resource]
|
18
|
+
end
|
19
|
+
|
20
|
+
def description
|
21
|
+
types = all.map(&:type).map { |s| s.split('_').last.to_s }
|
22
|
+
.map(&:capitalize).join('/')
|
23
|
+
@description ||= @parent.help[1].gsub(IDS_REGEX) { |w| w.gsub(/\w+/, types) }
|
24
|
+
if @options[:deprecation].class <= String
|
25
|
+
format_deprecation_msg(@description, _('Deprecated: %{deprecated_msg}') % { deprecated_msg: @options[:deprecation] })
|
26
|
+
elsif @options[:deprecation].class <= Hash
|
27
|
+
full_msg = @options[:deprecation].map do |flag, msg|
|
28
|
+
_('%{flag} is deprecated: %{deprecated_msg}') % { flag: flag, deprecated_msg: msg }
|
29
|
+
end.join(', ')
|
30
|
+
format_deprecation_msg(@description, full_msg)
|
31
|
+
else
|
32
|
+
@description
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def switch
|
37
|
+
return if @parent.nil? && @children.empty?
|
38
|
+
return @parent.help_lhs.strip if @children.empty?
|
39
|
+
|
40
|
+
switch_start = main_switch.each_char
|
41
|
+
.zip(*all.map(&:switches).flatten.map(&:each_char))
|
42
|
+
.select { |a, b| a == b }.transpose.first.join
|
43
|
+
suffixes = all.map do |m|
|
44
|
+
m.switches.map { |s| s.gsub(switch_start, '') }
|
45
|
+
end.flatten.reject(&:empty?).sort { |x, y| x.size <=> y.size }
|
46
|
+
"#{switch_start}[#{suffixes.join('|')}]"
|
47
|
+
end
|
48
|
+
|
49
|
+
def head
|
50
|
+
@parent
|
51
|
+
end
|
52
|
+
|
53
|
+
def all
|
54
|
+
@children + [@parent].compact
|
55
|
+
end
|
56
|
+
|
57
|
+
def parent(switches, type, description, opts = {}, &block)
|
58
|
+
raise StandardError, 'Option family can have only one parent' if @parent
|
59
|
+
|
60
|
+
@parent = new_member(switches, type, description, opts, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def child(switches, type, description, opts = {}, &block)
|
64
|
+
child = new_member(switches, type, description, opts, &block)
|
65
|
+
@children << child
|
66
|
+
child
|
67
|
+
end
|
68
|
+
|
69
|
+
def adopt(child)
|
70
|
+
child.family = self
|
71
|
+
@children << child
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def format_deprecation_msg(option_desc, deprecation_msg)
|
77
|
+
"#{option_desc} (#{deprecation_msg})"
|
78
|
+
end
|
79
|
+
|
80
|
+
def new_member(switches, type, description, opts = {}, &block)
|
81
|
+
opts = opts.merge(@options)
|
82
|
+
opts[:family] = self
|
83
|
+
if opts[:deprecated]
|
84
|
+
handles = [switches].flatten
|
85
|
+
opts[:deprecated] = opts[:deprecated].select do |switch, _msg|
|
86
|
+
handles.include?(switch)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
@creator.instance_eval do
|
90
|
+
option(switches, type, description, opts, &block)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def main_switch
|
95
|
+
root = @root || @parent.aliased_resource || @parent.referenced_resource || common_root
|
96
|
+
"--#{@prefix}#{root}".tr('_', '-')
|
97
|
+
end
|
98
|
+
|
99
|
+
def common_root
|
100
|
+
switches = all.map(&:switches).flatten
|
101
|
+
shortest = switches.min_by(&:length)
|
102
|
+
max_len = shortest.length
|
103
|
+
max_len.downto(0) do |curr_len|
|
104
|
+
0.upto(max_len - curr_len) do |start|
|
105
|
+
root = shortest[start, curr_len]
|
106
|
+
if switches.all? { |switch| switch.include?(root) }
|
107
|
+
return root[2..-1].chomp('-')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -6,7 +6,7 @@ module HammerCLI
|
|
6
6
|
module Predefined
|
7
7
|
OPTIONS = {
|
8
8
|
fields: [['--fields'], 'FIELDS',
|
9
|
-
_('Show specified
|
9
|
+
_('Show specified fields or predefined field sets only. (See below)'),
|
10
10
|
format: HammerCLI::Options::Normalizers::List.new,
|
11
11
|
context_target: :fields]
|
12
12
|
}.freeze
|
@@ -44,14 +44,10 @@ module HammerCLI::Output::Adapter
|
|
44
44
|
raise NotImplementedError
|
45
45
|
end
|
46
46
|
|
47
|
-
def print_collection(fields, collection)
|
47
|
+
def print_collection(fields, collection, options = {})
|
48
48
|
raise NotImplementedError
|
49
49
|
end
|
50
50
|
|
51
|
-
def reset_context
|
52
|
-
@context.delete(:fields)
|
53
|
-
end
|
54
|
-
|
55
51
|
protected
|
56
52
|
|
57
53
|
def filter_fields(fields)
|
@@ -14,7 +14,7 @@ module HammerCLI::Output::Adapter
|
|
14
14
|
print_collection(fields, [record].flatten(1))
|
15
15
|
end
|
16
16
|
|
17
|
-
def print_collection(fields, collection)
|
17
|
+
def print_collection(fields, collection, options = {})
|
18
18
|
collection.each do |data|
|
19
19
|
output_stream.puts render_fields(fields, data)
|
20
20
|
output_stream.puts
|
@@ -149,7 +149,8 @@ module HammerCLI::Output::Adapter
|
|
149
149
|
print_collection(fields, [record].flatten(1))
|
150
150
|
end
|
151
151
|
|
152
|
-
def print_collection(fields, collection)
|
152
|
+
def print_collection(fields, collection, options = {})
|
153
|
+
current_chunk = options[:current_chunk] || :single
|
153
154
|
fields = filter_fields(fields).filter_by_classes
|
154
155
|
.filter_by_sets
|
155
156
|
.filter_by_data(collection.first,
|
@@ -161,7 +162,7 @@ module HammerCLI::Output::Adapter
|
|
161
162
|
# or use headers from output definition
|
162
163
|
headers ||= default_headers(fields)
|
163
164
|
csv_string = generate do |csv|
|
164
|
-
csv << headers if headers && !@context[:no_headers]
|
165
|
+
csv << headers if headers && !@context[:no_headers] && %i[first single].include?(current_chunk)
|
165
166
|
rows.each do |row|
|
166
167
|
csv << Cell.values(headers, row)
|
167
168
|
end
|
@@ -6,9 +6,20 @@ module HammerCLI::Output::Adapter
|
|
6
6
|
output_stream.puts JSON.pretty_generate(result.first)
|
7
7
|
end
|
8
8
|
|
9
|
-
def print_collection(fields, collection)
|
10
|
-
|
11
|
-
|
9
|
+
def print_collection(fields, collection, options = {})
|
10
|
+
current_chunk = options[:current_chunk] || :single
|
11
|
+
prepared = prepare_collection(fields, collection)
|
12
|
+
result = JSON.pretty_generate(prepared)
|
13
|
+
if current_chunk != :single
|
14
|
+
result = if current_chunk == :first
|
15
|
+
result[0...-2] + ','
|
16
|
+
elsif current_chunk == :last
|
17
|
+
result[2..-1]
|
18
|
+
else
|
19
|
+
result[2...-2] + ','
|
20
|
+
end
|
21
|
+
end
|
22
|
+
output_stream.puts result
|
12
23
|
end
|
13
24
|
|
14
25
|
def print_message(msg, msg_params={})
|
@@ -2,6 +2,11 @@ require File.join(File.dirname(__FILE__), 'wrapper_formatter')
|
|
2
2
|
|
3
3
|
module HammerCLI::Output::Adapter
|
4
4
|
class Table < Abstract
|
5
|
+
def initialize(context = {}, formatters = {}, filters = {})
|
6
|
+
super
|
7
|
+
@printed = 0
|
8
|
+
end
|
9
|
+
|
5
10
|
def features
|
6
11
|
return %i[rich_text serialized inline] if tags.empty?
|
7
12
|
|
@@ -12,7 +17,8 @@ module HammerCLI::Output::Adapter
|
|
12
17
|
print_collection(fields, [record].flatten(1))
|
13
18
|
end
|
14
19
|
|
15
|
-
def print_collection(all_fields, collection)
|
20
|
+
def print_collection(all_fields, collection, options = {})
|
21
|
+
current_chunk = options[:current_chunk] || :single
|
16
22
|
fields = filter_fields(all_fields).filter_by_classes
|
17
23
|
.filter_by_sets
|
18
24
|
.filter_by_data(collection.first,
|
@@ -27,13 +33,26 @@ module HammerCLI::Output::Adapter
|
|
27
33
|
table_gen = HammerCLI::Output::Generators::Table.new(
|
28
34
|
columns, formatted_collection, no_headers: @context[:no_headers]
|
29
35
|
)
|
30
|
-
output_stream.print(table_gen.result)
|
31
36
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
+
meta = collection.respond_to?(:meta) ? collection.meta : nil
|
38
|
+
|
39
|
+
output_stream.print(table_gen.header) if %i[first single].include?(current_chunk)
|
40
|
+
|
41
|
+
output_stream.print(table_gen.body)
|
42
|
+
|
43
|
+
@printed += collection.count
|
44
|
+
|
45
|
+
# print closing line only after the last chunk
|
46
|
+
output_stream.print(table_gen.footer) if %i[last single].include?(current_chunk)
|
47
|
+
|
48
|
+
return unless meta && meta.pagination_set?
|
49
|
+
|
50
|
+
leftovers = %i[last single].include?(current_chunk) && @printed < meta.subtotal
|
51
|
+
if @context[:verbosity] >= meta.pagination_verbosity &&
|
52
|
+
collection.count < meta.subtotal &&
|
53
|
+
leftovers
|
54
|
+
pages = (meta.subtotal.to_f / meta.per_page).ceil
|
55
|
+
puts _("Page %{page} of %{total} (use --page and --per-page for navigation).") % {:page => meta.page, :total => pages}
|
37
56
|
end
|
38
57
|
end
|
39
58
|
|
@@ -47,7 +66,7 @@ module HammerCLI::Output::Adapter
|
|
47
66
|
collection.collect do |d|
|
48
67
|
fields.inject({}) do |row, f|
|
49
68
|
formatter = WrapperFormatter.new(@formatters.formatter_for_type(f.class), f.parameters)
|
50
|
-
row.update(f.label => formatter.format(data_for_field(f, d)
|
69
|
+
row.update(f.label => formatter.format(data_for_field(f, d)).to_s)
|
51
70
|
end
|
52
71
|
end
|
53
72
|
end
|
@@ -6,9 +6,12 @@ module HammerCLI::Output::Adapter
|
|
6
6
|
output_stream.puts YAML.dump(result.first)
|
7
7
|
end
|
8
8
|
|
9
|
-
def print_collection(fields, collection)
|
10
|
-
|
11
|
-
|
9
|
+
def print_collection(fields, collection, options = {})
|
10
|
+
current_chunk = options[:current_chunk] || :single
|
11
|
+
prepared = prepare_collection(fields, collection)
|
12
|
+
result = YAML.dump(prepared)
|
13
|
+
result = result[4..-1] unless %i[first single].include?(current_chunk)
|
14
|
+
output_stream.puts result
|
12
15
|
end
|
13
16
|
|
14
17
|
def print_message(msg, msg_params={})
|
@@ -25,15 +25,13 @@ module HammerCLI::Output
|
|
25
25
|
|
26
26
|
def print_record(definition, record)
|
27
27
|
adapter.print_record(definition.fields, record) if appropriate_verbosity?(:record)
|
28
|
-
adapter.reset_context
|
29
28
|
end
|
30
29
|
|
31
|
-
def print_collection(definition, collection)
|
30
|
+
def print_collection(definition, collection, options = {})
|
32
31
|
unless collection.class <= HammerCLI::Output::RecordCollection
|
33
32
|
collection = HammerCLI::Output::RecordCollection.new([collection].flatten(1))
|
34
33
|
end
|
35
|
-
adapter.print_collection(definition.fields, collection) if appropriate_verbosity?(:collection)
|
36
|
-
adapter.reset_context
|
34
|
+
adapter.print_collection(definition.fields, collection, options) if appropriate_verbosity?(:collection)
|
37
35
|
end
|
38
36
|
|
39
37
|
def adapter
|
data/lib/hammer_cli/settings.rb
CHANGED
@@ -23,6 +23,11 @@ module HammerCLI
|
|
23
23
|
@subcommand_class
|
24
24
|
end
|
25
25
|
|
26
|
+
def help
|
27
|
+
names = HammerCLI.context[:full_help] ? @names.join(", ") : @names.first
|
28
|
+
[names, description]
|
29
|
+
end
|
30
|
+
|
26
31
|
attr_reader :warning
|
27
32
|
end
|
28
33
|
|
@@ -90,8 +95,27 @@ module HammerCLI
|
|
90
95
|
logger.info "subcommand #{name} (#{subcommand_class_name}) was created."
|
91
96
|
end
|
92
97
|
|
98
|
+
def find_subcommand(name, fuzzy: true)
|
99
|
+
subcommand = super(name)
|
100
|
+
if subcommand.nil? && fuzzy
|
101
|
+
find_subcommand_starting_with(name)
|
102
|
+
else
|
103
|
+
subcommand
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def find_subcommand_starting_with(name)
|
108
|
+
subcommands = recognised_subcommands.select { |sc| sc.names.any? { |n| n.start_with?(name) } }
|
109
|
+
if subcommands.size > 1
|
110
|
+
raise HammerCLI::CommandConflict, _('Found more than one command.') + "\n\n" +
|
111
|
+
_('Did you mean one of these?') + "\n\t" +
|
112
|
+
subcommands.collect(&:names).flatten.select { |n| n.start_with?(name) }.join("\n\t")
|
113
|
+
end
|
114
|
+
subcommands.first
|
115
|
+
end
|
116
|
+
|
93
117
|
def define_subcommand(name, subcommand_class, definition, &block)
|
94
|
-
existing = find_subcommand(name)
|
118
|
+
existing = find_subcommand(name, fuzzy: false)
|
95
119
|
if existing
|
96
120
|
raise HammerCLI::CommandConflict, _("Can't replace subcommand %<name>s (%<existing_class>s) with %<name>s (%<new_class>s).") % {
|
97
121
|
:name => name,
|