hammer_cli 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/doc/commands_extension.md +12 -0
  3. data/doc/creating_commands.md +73 -0
  4. data/doc/release_notes.md +7 -0
  5. data/lib/hammer_cli/abstract.rb +29 -2
  6. data/lib/hammer_cli/apipie/option_builder.rb +15 -13
  7. data/lib/hammer_cli/apipie/option_definition.rb +9 -7
  8. data/lib/hammer_cli/command_extensions.rb +21 -1
  9. data/lib/hammer_cli/exception_handler.rb +1 -1
  10. data/lib/hammer_cli/full_help.rb +8 -1
  11. data/lib/hammer_cli/help/builder.rb +29 -3
  12. data/lib/hammer_cli/options/option_definition.rb +4 -6
  13. data/lib/hammer_cli/options/option_family.rb +114 -0
  14. data/lib/hammer_cli/subcommand.rb +25 -1
  15. data/lib/hammer_cli/testing/command_assertions.rb +2 -2
  16. data/lib/hammer_cli/utils.rb +17 -0
  17. data/lib/hammer_cli/version.rb +1 -1
  18. data/locale/ca/LC_MESSAGES/hammer-cli.mo +0 -0
  19. data/locale/de/LC_MESSAGES/hammer-cli.mo +0 -0
  20. data/locale/en/LC_MESSAGES/hammer-cli.mo +0 -0
  21. data/locale/en_GB/LC_MESSAGES/hammer-cli.mo +0 -0
  22. data/locale/es/LC_MESSAGES/hammer-cli.mo +0 -0
  23. data/locale/fr/LC_MESSAGES/hammer-cli.mo +0 -0
  24. data/locale/it/LC_MESSAGES/hammer-cli.mo +0 -0
  25. data/locale/ja/LC_MESSAGES/hammer-cli.mo +0 -0
  26. data/locale/ko/LC_MESSAGES/hammer-cli.mo +0 -0
  27. data/locale/pt_BR/LC_MESSAGES/hammer-cli.mo +0 -0
  28. data/locale/ru/LC_MESSAGES/hammer-cli.mo +0 -0
  29. data/locale/zh_CN/LC_MESSAGES/hammer-cli.mo +0 -0
  30. data/locale/zh_TW/LC_MESSAGES/hammer-cli.mo +0 -0
  31. data/test/unit/abstract_test.rb +23 -2
  32. data/test/unit/apipie/option_builder_test.rb +8 -0
  33. data/test/unit/command_extensions_test.rb +67 -49
  34. data/test/unit/help/builder_test.rb +22 -0
  35. data/test/unit/options/option_family_test.rb +48 -0
  36. metadata +7 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1beb690641cf038528a35bee7964c73e60acbe8a70e94bb9d1a72af372218ea1
4
- data.tar.gz: 516fd63e88f8df2996aefd0d3276a9ff56e1bf5f17c1a7a8b656210512f2896c
3
+ metadata.gz: f5401e68d37a678eb6193e892c4d148c165260a0e90f3a11c9dc2e2d49d464ae
4
+ data.tar.gz: 21d2f0466c0c433033e2bdbaf8aad4d8f3ea6a51261f4f695a135cb1e9f5b5c0
5
5
  SHA512:
6
- metadata.gz: 8b35646b4dc9c494687b7bbd38c578ce5cf70e90080025f622b89a28f1c1d29abeafc11dbe1e16cb0786fe8ae3ac4c8673b905e8bf03cf87de86383817c653a6
7
- data.tar.gz: 3a01e546cf71e7f015ec27d8c127328e380c3736106bc01b95d1b81593f2c7fa0a027e90a95bbf179407a953da871911e8dd0349c0ed980ed5cad613bd98363b
6
+ metadata.gz: c50b60e009c0f48736b540a4d7dcf39db860a8b128542aa15495ac5ae6bdc2c0c3eccf92e098b7c81ac55e90dbe295c5310fa1fd5e39d084e790c379171eeba2
7
+ data.tar.gz: 2482f77220d681fba21493cf80473132379127ef580c45dfe8293f2e5fb5ff420ba75c4276e4dfbad410f30d3ab0c880135a77b26d5a67d9ffbfb0e5914bd03d
@@ -10,6 +10,11 @@ Each command can be easily extended with one ore more `HammerCLI::CommandExtensi
10
10
  inheritable true
11
11
  # Simply add a new option to a command is being extended
12
12
  option(option_params)
13
+ # Add option family to a command
14
+ option_family(common_options = {}) do
15
+ parent option_params
16
+ child option_params
17
+ end
13
18
  # Extend hash with data returned from server before it is printed
14
19
  before_print do |data|
15
20
  # data modifications
@@ -65,6 +70,13 @@ class MyCommandExtensions < HammerCLI::CommandExtensions
65
70
 
66
71
  option ['--new-option'], 'TYPE', _('Option description')
67
72
 
73
+ option_family(
74
+ description: _('Common description')
75
+ ) do
76
+ parent ['--new-option'], 'TYPE', _('Option description')
77
+ child ['--new-option-ver2'], 'TYPE', _('Option description')
78
+ end
79
+
68
80
  before_print do |data|
69
81
  data['results'].each do |result|
70
82
  result['status'] = process_errors(result['errors'])
@@ -173,6 +173,54 @@ Here is the list of predefined options:
173
173
  * `:fields` Expects a list with fields to show in output, see [example](creating_commands.md#printing-hash-records).
174
174
 
175
175
 
176
+ ### Option family
177
+ Option family is the way to unify options which have the same meaning or purpose,
178
+ but contain some differences in their definitions (e.g. the name/switch of an option).
179
+ Mainly serves as a container for options, which purpose is to show less repetitive
180
+ output in commands' help. Option builders use it by default.
181
+
182
+ To define an option family, use the following DSL:
183
+ ```ruby
184
+ # options is a Hash with options for family/each defined option within it
185
+ option_family(options = {}) do
186
+ # parent is the main option. Must be single, option family can have only one parent.
187
+ parent switches, type, description, options
188
+ # child is an additional option. Could be none or more than one. Aren't shown in the help output.
189
+ child switches, type, description, options
190
+ end
191
+ ```
192
+
193
+ ##### Example
194
+
195
+ ```ruby
196
+ option_family(
197
+ aliased_resource: 'environment',
198
+ description: _('Puppet environment'),
199
+ deprecation: _("Use %s instead") % '--puppet-environment[-id]'
200
+ deprecated: { '--environment' => _("Use %s instead") % '--puppet-environment[-id]',
201
+ '--environment-id' => _("Use %s instead") % '--puppet-environment[-id]'}
202
+ ) do
203
+ parent '--environment-id', 'ENVIRONMENT_ID', _(''),
204
+ format: HammerCLI::Options::Normalizers::Number.new,
205
+ attribute_name: :option_environment_id
206
+ child '--environment', 'ENVIRONMENT_NAME', _('Environment name'),
207
+ attribute_name: :option_environment_name
208
+ end
209
+
210
+ # $ hammer command --help:
211
+ # ...
212
+ # Options:
213
+ # --environment[-id] Puppet environment (Deprecated: Use --puppet-environment[-id] instead)
214
+ # ...
215
+
216
+ # $ hammer full-help:
217
+ # ...
218
+ # Options:
219
+ # --environment ENVIRONMENT_NAME Environment name (--environment is deprecated: Use --puppet-environment[-id] instead)
220
+ # --environment-id ENVIRONMENT_ID (--environment-id is deprecated: Use --puppet-environment[-id] instead)
221
+ # ...
222
+ ```
223
+
176
224
  ### Option builders
177
225
  Hammer commands offer option builders that can be used for automatic option generation.
178
226
  See [documentation page](option_builders.md#option-builders) dedicated to this topic for more details.
@@ -406,6 +454,31 @@ Options:
406
454
  -h, --help print help
407
455
  ```
408
456
 
457
+ #### Aliasing subcommands
458
+
459
+ Commands can have two or more names, e.g. aliases. To support such functionality
460
+ simple name addition could be used via `command_name` or `command_names` method:
461
+ ```ruby
462
+ module HammerCLIHello
463
+
464
+ class SayCommand < HammerCLI::AbstractCommand
465
+
466
+ class GreetingsCommand < HammerCLI::AbstractCommand
467
+ command_name 'hello'
468
+ command_name 'hi'
469
+ # or use can use other method:
470
+ command_names 'hello', 'hi'
471
+
472
+ desc 'Say Hello World!'
473
+ # ...
474
+ end
475
+
476
+ autoload_subcommands
477
+ end
478
+
479
+ HammerCLI::MainCommand.subcommand 'say', "Say something", HammerCLIHello::SayCommand
480
+ end
481
+ ```
409
482
 
410
483
  ### Conflicting subcommands
411
484
  It can happen that two different plugins define subcommands with the same name by accident.
@@ -1,5 +1,12 @@
1
1
  Release notes
2
2
  =============
3
+ ### 2.1.0 (2020-05-14)
4
+ * Hammer full-help returns correct output, [#29697](http://projects.theforeman.org/issues/29697)
5
+ * Add fuzzy subcommand matching, [#29413](http://projects.theforeman.org/issues/29413)
6
+ * Help contains squeezed options, [#28440](http://projects.theforeman.org/issues/28440)
7
+ * Keep referenced resource in option options, [#29015](http://projects.theforeman.org/issues/29015)
8
+ * Bump to 2.1.0-develop
9
+
3
10
  ### 2.0.0 (2020-02-12)
4
11
  * Bump version to 2.0.0
5
12
  * Bump version to 2.0 ([PR #324](https://github.com/theforeman/hammer-cli/pull/324))
@@ -13,6 +13,7 @@ require 'hammer_cli/options/predefined'
13
13
  require 'hammer_cli/help/builder'
14
14
  require 'hammer_cli/help/text_builder'
15
15
  require 'hammer_cli/command_extensions'
16
+ require 'hammer_cli/options/option_family'
16
17
  require 'logging'
17
18
 
18
19
  module HammerCLI
@@ -191,6 +192,16 @@ module HammerCLI
191
192
  # skip switches that are already defined
192
193
  next if option.nil? or option.switches.any? {|s| find_option(s) }
193
194
 
195
+ if option.respond_to?(:referenced_resource)
196
+ # Collect options that don't have family, but related to this parent.
197
+ children = find_options(
198
+ referenced_resource: option.referenced_resource.to_s,
199
+ aliased_resource: option.aliased_resource.to_s
200
+ ).select { |o| o.family.nil? || o.family.head.nil? }
201
+ children.each do |child|
202
+ option.family.adopt(child) if option.family
203
+ end
204
+ end
194
205
  declared_options << option
195
206
  block ||= option.default_conversion_block
196
207
  define_accessors_for(option, &block)
@@ -207,6 +218,7 @@ module HammerCLI
207
218
  extension.delegatee(self)
208
219
  extension.extend_predefined_options(self)
209
220
  extension.extend_options(self)
221
+ extension.extend_option_family(self)
210
222
  extension.extend_output(self)
211
223
  extension.extend_help(self)
212
224
  logger('Extensions').info "Applied #{extension.details} on #{self}."
@@ -222,6 +234,12 @@ module HammerCLI
222
234
 
223
235
  protected
224
236
 
237
+ def self.option_family(options = {}, &block)
238
+ options[:creator] ||= self
239
+ family = HammerCLI::Options::OptionFamily.new(options)
240
+ family.instance_eval(&block)
241
+ end
242
+
225
243
  def self.find_options(switch_filter, other_filters={})
226
244
  filters = other_filters
227
245
  if switch_filter.is_a? Hash
@@ -285,8 +303,17 @@ module HammerCLI
285
303
  end
286
304
 
287
305
  def self.command_name(name=nil)
288
- @name = name if name
289
- @name || (superclass.respond_to?(:command_name) ? superclass.command_name : nil)
306
+ if @names && name
307
+ @names << name if !@names.include?(name)
308
+ else
309
+ @names = [name] if name
310
+ end
311
+ @names || (superclass.respond_to?(:command_names) ? superclass.command_names : nil)
312
+ end
313
+
314
+ def self.command_names(*names)
315
+ @names = names unless names.empty?
316
+ @names || (superclass.respond_to?(:command_names) ? superclass.command_names : nil)
290
317
  end
291
318
 
292
319
  def self.warning(message = nil)
@@ -41,12 +41,11 @@ module HammerCLI::Apipie
41
41
  end
42
42
 
43
43
  def create_option(param, resource_name_map)
44
- option(
45
- option_switch(param, resource_name_map),
46
- option_type(param, resource_name_map),
47
- option_desc(param),
48
- option_opts(param)
49
- )
44
+ family = HammerCLI::Options::OptionFamily.new
45
+ family.parent(option_switch(param, resource_name_map),
46
+ option_type(param, resource_name_map),
47
+ option_desc(param),
48
+ option_opts(param, resource_name_map))
50
49
  end
51
50
 
52
51
  def option_switch(param, resource_name_map)
@@ -61,7 +60,7 @@ module HammerCLI::Apipie
61
60
  param.description || " "
62
61
  end
63
62
 
64
- def option_opts(param)
63
+ def option_opts(param, resource_name_map)
65
64
  opts = {}
66
65
  opts[:required] = true if (param.required? and require_options?)
67
66
  if param.expected_type.to_s == 'array'
@@ -80,19 +79,22 @@ module HammerCLI::Apipie
80
79
  end
81
80
  opts[:attribute_name] = HammerCLI.option_accessor_name(param.name)
82
81
  opts[:referenced_resource] = resource_name(param)
82
+ opts[:aliased_resource] = aliased_name(resource_name(param), resource_name_map)
83
83
 
84
84
  return opts
85
85
  end
86
86
 
87
+ def aliased_name(name, resource_name_map)
88
+ return if name.nil?
89
+
90
+ resource_name_map[name.to_s] || resource_name_map[name.to_sym] || name
91
+ end
92
+
87
93
  def aliased(param, resource_name_map)
88
94
  resource_name = resource_name(param)
95
+ return param.name if resource_name.nil?
89
96
 
90
- if resource_name.nil?
91
- return param.name
92
- else
93
- aliased_name = resource_name_map[resource_name.to_s] || resource_name_map[resource_name.to_sym] || resource_name
94
- return param.name.gsub(resource_name, aliased_name.to_s)
95
- end
97
+ param.name.gsub(resource_name, aliased_name(resource_name, resource_name_map).to_s)
96
98
  end
97
99
 
98
100
  def resource_name(param)
@@ -1,21 +1,23 @@
1
1
  require File.join(File.dirname(__FILE__), 'options')
2
2
 
3
3
  module HammerCLI::Apipie
4
-
5
4
  class OptionDefinition < HammerCLI::Options::OptionDefinition
6
-
7
- attr_accessor :referenced_resource
5
+ attr_accessor :referenced_resource, :aliased_resource, :family
8
6
 
9
7
  def initialize(switches, type, description, options = {})
10
- if options.has_key? :referenced_resource
11
- self.referenced_resource = options.delete(:referenced_resource).to_s if options[:referenced_resource]
12
- end
8
+ @referenced_resource = options[:referenced_resource].to_s if options[:referenced_resource]
9
+ @aliased_resource = options[:aliased_resource].to_s if options[:aliased_resource]
10
+ @family = options[:family]
13
11
  super
14
12
  # Apipie currently sends descriptions as escaped HTML once this is changed this should be removed.
15
13
  # See #15198 on Redmine.
16
14
  @description = CGI::unescapeHTML(description)
17
15
  end
18
16
 
19
- end
17
+ def child?
18
+ return unless @family
20
19
 
20
+ @family.children.include?(self)
21
+ end
22
+ end
21
23
  end
@@ -15,7 +15,7 @@ module HammerCLI
15
15
  ALLOWED_EXTENSIONS = %i[
16
16
  option command_options before_print data output help request
17
17
  request_headers headers request_options options request_params params
18
- option_sources predefined_options use_option
18
+ option_sources predefined_options use_option option_family
19
19
  ].freeze
20
20
 
21
21
  def initialize(options = {})
@@ -86,6 +86,11 @@ module HammerCLI
86
86
  @option_sources_block = block
87
87
  end
88
88
 
89
+ def self.option_family(options = {}, &block)
90
+ @option_family_opts = options
91
+ @option_family_block = block
92
+ end
93
+
89
94
  # Object
90
95
 
91
96
  def extend_options(command_class)
@@ -151,6 +156,13 @@ module HammerCLI
151
156
  self.class.extend_option_sources(sources, command)
152
157
  end
153
158
 
159
+ def extend_option_family(command_class)
160
+ allowed = @only & %i[option_family]
161
+ return if allowed.empty? || (allowed & @except).any?
162
+
163
+ self.class.extend_option_family(command_class)
164
+ end
165
+
154
166
  def delegatee(command_class)
155
167
  self.class.delegatee = command_class
156
168
  end
@@ -234,5 +246,13 @@ module HammerCLI
234
246
  @option_sources_block.call(sources, command)
235
247
  logger.debug("Called block for #{@delegatee} option sources:\n\t#{@option_sources_block}")
236
248
  end
249
+
250
+ def self.extend_option_family(command_class)
251
+ return if @option_family_block.nil?
252
+
253
+ @option_family_opts[:creator] = command_class
254
+ command_class.send(:option_family, @option_family_opts, &@option_family_block)
255
+ logger.debug("Called option family block for #{command_class}:\n\t#{@option_family_block}")
256
+ end
237
257
  end
238
258
  end
@@ -69,7 +69,7 @@ module HammerCLI
69
69
 
70
70
  def handle_usage_exception(e)
71
71
  print_error (_("Error: %{message}") + "\n\n" +
72
- _("See: '%{path} --help'.")) % {:message => e.message, :path => e.command.invocation_path}
72
+ _("See: '%{path} --help'.")) % { message: e.message, path: HammerCLI.expand_invocation_path(e.command.invocation_path) }
73
73
  log_full_error e
74
74
  HammerCLI::EX_USAGE
75
75
  end
@@ -6,8 +6,11 @@ module HammerCLI
6
6
 
7
7
  def execute
8
8
  @adapter = option_md? ? MDAdapter.new : TxtAdapter.new
9
+ HammerCLI.context[:full_help] = true
10
+ @invocation_paths = {}
9
11
  print_heading
10
12
  print_help
13
+ HammerCLI.context[:full_help] = false
11
14
  HammerCLI::EX_OK
12
15
  end
13
16
 
@@ -19,9 +22,13 @@ module HammerCLI
19
22
  end
20
23
 
21
24
  def print_help(name='hammer', command=HammerCLI::MainCommand, desc='')
22
- @adapter.print_command(name, desc, command.new(name).help)
25
+ @invocation_paths[name] ||= []
26
+ @adapter.print_command(name, desc, command.new(name, path: @invocation_paths[name]).help)
23
27
 
24
28
  command.recognised_subcommands.each do |sub_cmd|
29
+ path = "#{name} #{sub_cmd.names.first}"
30
+ @invocation_paths[path] ||= []
31
+ @invocation_paths[path] += @invocation_paths[name]
25
32
  print_help(@adapter.command_name(name, sub_cmd.names.first), sub_cmd.subcommand_class, sub_cmd.description)
26
33
  end
27
34
  end
@@ -14,7 +14,7 @@ module HammerCLI
14
14
  def add_usage(invocation_path, usage_descriptions)
15
15
  heading(Clamp.message(:usage_heading))
16
16
  usage_descriptions.each do |usage|
17
- puts " #{invocation_path} #{usage}".rstrip
17
+ puts " #{HammerCLI.expand_invocation_path(invocation_path)} #{usage}".rstrip
18
18
  end
19
19
  end
20
20
 
@@ -29,12 +29,19 @@ module HammerCLI
29
29
 
30
30
  label_width = DEFAULT_LABEL_INDENT
31
31
  items.each do |item|
32
- label, description = item.help
32
+ label = item.help.first
33
33
  label_width = label.size if label.size > label_width
34
34
  end
35
35
 
36
36
  items.each do |item|
37
- label, description = item.help
37
+ if item.respond_to?(:child?) && item.child?
38
+ next unless HammerCLI.context[:full_help]
39
+ end
40
+ label, description = if !HammerCLI.context[:full_help] && item.respond_to?(:family) && item.family
41
+ [item.family.switch, item.family.description || item.help[1]]
42
+ else
43
+ item.help
44
+ end
38
45
  description.gsub(/^(.)/) { Unicode::capitalize($1) }.each_line do |line|
39
46
  puts " %-#{label_width}s %s" % [label, line]
40
47
  label = ''
@@ -54,6 +61,25 @@ module HammerCLI
54
61
  label = HighLine.color(label, :bold) if @richtext
55
62
  puts label
56
63
  end
64
+
65
+ private
66
+
67
+ def expand_invocation_path(path)
68
+ bits = path.split(' ')
69
+ parent_command = HammerCLI::MainCommand
70
+ new_path = (bits[1..-1] || []).each_with_object([]) do |bit, names|
71
+ subcommand = parent_command.find_subcommand(bit)
72
+ next if subcommand.nil?
73
+
74
+ names << if subcommand.names.size > 1
75
+ "<#{subcommand.names.join('|')}>"
76
+ else
77
+ subcommand.names.first
78
+ end
79
+ parent_command = subcommand.subcommand_class
80
+ end
81
+ new_path.unshift(bits.first).join(' ')
82
+ end
57
83
  end
58
84
  end
59
85
  end
@@ -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
- self.value_formatter = options[:format] || HammerCLI::Options::Normalizers::Default.new
31
- self.context_target = options[:context_target]
32
- self.deprecated_switches = options[:deprecated]
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
 
@@ -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
@@ -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,
@@ -71,13 +71,13 @@ module HammerCLI
71
71
  if heading.nil?
72
72
  ["Error: #{message}",
73
73
  "",
74
- "See: '#{command} --help'.",
74
+ "See: '#{HammerCLI.expand_invocation_path(command)} --help'.",
75
75
  ""].join("\n")
76
76
  else
77
77
  ["#{heading}:",
78
78
  " Error: #{message}",
79
79
  " ",
80
- " See: '#{command} --help'.",
80
+ " See: '#{HammerCLI.expand_invocation_path(command)} --help'.",
81
81
  ""].join("\n")
82
82
  end
83
83
  end
@@ -116,4 +116,21 @@ module HammerCLI
116
116
 
117
117
  array.insert(idx, *new_items)
118
118
  end
119
+
120
+ def self.expand_invocation_path(path)
121
+ bits = path.split(' ')
122
+ parent_command = HammerCLI::MainCommand
123
+ new_path = (bits[1..-1] || []).each_with_object([]) do |bit, names|
124
+ subcommand = parent_command.find_subcommand(bit)
125
+ next if subcommand.nil?
126
+
127
+ names << if subcommand.names.size > 1
128
+ "<#{subcommand.names.join('|')}>"
129
+ else
130
+ subcommand.names.first
131
+ end
132
+ parent_command = subcommand.subcommand_class
133
+ end
134
+ new_path.unshift(bits.first).join(' ')
135
+ end
119
136
  end
@@ -1,5 +1,5 @@
1
1
  module HammerCLI
2
2
  def self.version
3
- @version ||= Gem::Version.new "2.0.0"
3
+ @version ||= Gem::Version.new "2.1.0"
4
4
  end
5
5
  end
@@ -262,6 +262,27 @@ describe HammerCLI::AbstractCommand do
262
262
  end
263
263
 
264
264
  end
265
+
266
+ describe 'find_subcommand' do
267
+ it 'should find by full name' do
268
+ main_cmd.find_subcommand('some_command').wont_be_nil
269
+ end
270
+
271
+ it 'should find by partial name' do
272
+ main_cmd.find_subcommand('some_').wont_be_nil
273
+ end
274
+
275
+ it 'should not find by wrong name' do
276
+ main_cmd.find_subcommand('not_existing').must_be_nil
277
+ end
278
+
279
+ it 'should raise if more than one were found' do
280
+ main_cmd.subcommand('pong', 'description', Subcommand2)
281
+ proc do
282
+ main_cmd.find_subcommand('p')
283
+ end.must_raise HammerCLI::CommandConflict
284
+ end
285
+ end
265
286
  end
266
287
 
267
288
  describe "options" do
@@ -413,7 +434,7 @@ describe HammerCLI::AbstractCommand do
413
434
  class CmdName2 < CmdName1
414
435
  end
415
436
 
416
- CmdName2.command_name.must_equal 'cmd'
437
+ CmdName2.command_name.must_equal ['cmd']
417
438
  end
418
439
 
419
440
  it "should inherit output definition" do
@@ -525,7 +546,7 @@ describe HammerCLI::AbstractCommand do
525
546
  opt = cmd.find_option('--flag')
526
547
  opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal true
527
548
  cmd.output_definition.empty?.must_equal false
528
- cmd.new({}).help.must_match(/.*text.*/)
549
+ cmd.new('', {}).help.must_match(/.*text.*/)
529
550
  end
530
551
 
531
552
  it 'should store more than one extension' do
@@ -37,6 +37,14 @@ describe HammerCLI::Apipie::OptionBuilder do
37
37
  it "should set description with html tags stripped" do
38
38
  options[0].description.must_equal 'filter results'
39
39
  end
40
+
41
+ it "should build option with default family" do
42
+ options[0].family.wont_be_nil
43
+ end
44
+
45
+ it "should build parent option within default family" do
46
+ options[0].child?.must_equal false
47
+ end
40
48
  end
41
49
 
42
50
 
@@ -23,6 +23,12 @@ describe HammerCLI::CommandExtensions do
23
23
 
24
24
  class CmdExtensions < HammerCLI::CommandExtensions
25
25
  option '--ext', 'EXT', 'ext'
26
+ option_family(
27
+ description: 'Test',
28
+ ) do
29
+ parent '--test-one', '', ''
30
+ child '--test-two', '', ''
31
+ end
26
32
  before_print do |data|
27
33
  data['key'] = 'value'
28
34
  end
@@ -64,49 +70,55 @@ describe HammerCLI::CommandExtensions do
64
70
 
65
71
  it 'should extend help only' do
66
72
  cmd.extend_with(CmdExtensions.new(only: :help))
67
- cmd.new({}).help.must_match(/.*Section.*/)
68
- cmd.new({}).help.must_match(/.*text.*/)
73
+ cmd.new('', {}).help.must_match(/.*Section.*/)
74
+ cmd.new('', {}).help.must_match(/.*text.*/)
69
75
  end
70
76
 
71
77
  it 'should extend params only' do
72
78
  cmd.extend_with(CmdExtensions.new(only: :request_params))
73
- cmd.new({}).extended_request[0].must_equal(thin: true)
74
- cmd.new({}).extended_request[1].must_equal({})
75
- cmd.new({}).extended_request[2].must_equal({})
79
+ cmd.new('', {}).extended_request[0].must_equal(thin: true)
80
+ cmd.new('', {}).extended_request[1].must_equal({})
81
+ cmd.new('', {}).extended_request[2].must_equal({})
76
82
  end
77
83
 
78
84
  it 'should extend headers only' do
79
85
  cmd.extend_with(CmdExtensions.new(only: :request_headers))
80
- cmd.new({}).extended_request[0].must_equal({})
81
- cmd.new({}).extended_request[1].must_equal(ssl: true)
82
- cmd.new({}).extended_request[2].must_equal({})
86
+ cmd.new('', {}).extended_request[0].must_equal({})
87
+ cmd.new('', {}).extended_request[1].must_equal(ssl: true)
88
+ cmd.new('', {}).extended_request[2].must_equal({})
83
89
  end
84
90
 
85
91
  it 'should extend options only' do
86
92
  cmd.extend_with(CmdExtensions.new(only: :request_options))
87
- cmd.new({}).extended_request[0].must_equal({})
88
- cmd.new({}).extended_request[1].must_equal({})
89
- cmd.new({}).extended_request[2].must_equal(with_authentication: true)
93
+ cmd.new('', {}).extended_request[0].must_equal({})
94
+ cmd.new('', {}).extended_request[1].must_equal({})
95
+ cmd.new('', {}).extended_request[2].must_equal(with_authentication: true)
90
96
  end
91
97
 
92
98
  it 'should extend params and options and headers' do
93
99
  cmd.extend_with(CmdExtensions.new(only: :request))
94
- cmd.new({}).extended_request[0].must_equal(thin: true)
95
- cmd.new({}).extended_request[1].must_equal(ssl: true)
96
- cmd.new({}).extended_request[2].must_equal(with_authentication: true)
100
+ cmd.new('', {}).extended_request[0].must_equal(thin: true)
101
+ cmd.new('', {}).extended_request[1].must_equal(ssl: true)
102
+ cmd.new('', {}).extended_request[2].must_equal(with_authentication: true)
97
103
  end
98
104
 
99
105
  it 'should extend data only' do
100
106
  cmd.extend_with(CmdExtensions.new(only: :data))
101
- cmd.new({}).help.wont_match(/.*Section.*/)
102
- cmd.new({}).help.wont_match(/.*text.*/)
107
+ cmd.new('', {}).help.wont_match(/.*Section.*/)
108
+ cmd.new('', {}).help.wont_match(/.*text.*/)
103
109
  cmd.output_definition.empty?.must_equal true
104
110
  opt = cmd.find_option('--ext')
105
111
  opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal false
106
- cmd.new({}).extended_request[0].must_equal({})
107
- cmd.new({}).extended_request[1].must_equal({})
108
- cmd.new({}).extended_request[2].must_equal({})
109
- cmd.new({}).extended_data({}).must_equal('key' => 'value')
112
+ cmd.new('', {}).extended_request[0].must_equal({})
113
+ cmd.new('', {}).extended_request[1].must_equal({})
114
+ cmd.new('', {}).extended_request[2].must_equal({})
115
+ cmd.new('', {}).extended_data({}).must_equal('key' => 'value')
116
+ end
117
+
118
+ it 'should extend option family only' do
119
+ cmd.extend_with(CmdExtensions.new(only: :option_family))
120
+ cmd.output_definition.empty?.must_equal true
121
+ cmd.recognised_options.map(&:switches).flatten.must_equal ['--test-one', '--test-two', '-h', '--help']
110
122
  end
111
123
  end
112
124
 
@@ -116,9 +128,9 @@ describe HammerCLI::CommandExtensions do
116
128
  opt = cmd.find_option('--ext')
117
129
  opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal false
118
130
  cmd.output_definition.empty?.must_equal false
119
- cmd.new({}).extended_request[0].must_equal(thin: true)
120
- cmd.new({}).extended_request[1].must_equal(ssl: true)
121
- cmd.new({}).extended_request[2].must_equal(with_authentication: true)
131
+ cmd.new('', {}).extended_request[0].must_equal(thin: true)
132
+ cmd.new('', {}).extended_request[1].must_equal(ssl: true)
133
+ cmd.new('', {}).extended_request[2].must_equal(with_authentication: true)
122
134
  end
123
135
 
124
136
  it 'should extend all except output' do
@@ -126,62 +138,68 @@ describe HammerCLI::CommandExtensions do
126
138
  cmd.output_definition.empty?.must_equal true
127
139
  opt = cmd.find_option('--ext')
128
140
  opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal true
129
- cmd.new({}).extended_request[0].must_equal(thin: true)
130
- cmd.new({}).extended_request[1].must_equal(ssl: true)
131
- cmd.new({}).extended_request[2].must_equal(with_authentication: true)
141
+ cmd.new('', {}).extended_request[0].must_equal(thin: true)
142
+ cmd.new('', {}).extended_request[1].must_equal(ssl: true)
143
+ cmd.new('', {}).extended_request[2].must_equal(with_authentication: true)
132
144
  end
133
145
 
134
146
  it 'should extend all except help' do
135
147
  cmd.extend_with(CmdExtensions.new(except: :help))
136
- cmd.new({}).help.wont_match(/.*Section.*/)
137
- cmd.new({}).help.wont_match(/.*text.*/)
148
+ cmd.new('', {}).help.wont_match(/.*Section.*/)
149
+ cmd.new('', {}).help.wont_match(/.*text.*/)
138
150
  cmd.output_definition.empty?.must_equal false
139
151
  opt = cmd.find_option('--ext')
140
152
  opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal true
141
- cmd.new({}).extended_request[0].must_equal(thin: true)
142
- cmd.new({}).extended_request[1].must_equal(ssl: true)
143
- cmd.new({}).extended_request[2].must_equal(with_authentication: true)
153
+ cmd.new('', {}).extended_request[0].must_equal(thin: true)
154
+ cmd.new('', {}).extended_request[1].must_equal(ssl: true)
155
+ cmd.new('', {}).extended_request[2].must_equal(with_authentication: true)
144
156
  end
145
157
 
146
158
  it 'should extend all except params' do
147
159
  cmd.extend_with(CmdExtensions.new(except: :request_params))
148
- cmd.new({}).extended_request[0].must_equal({})
149
- cmd.new({}).extended_request[1].must_equal(ssl: true)
150
- cmd.new({}).extended_request[2].must_equal(with_authentication: true)
160
+ cmd.new('', {}).extended_request[0].must_equal({})
161
+ cmd.new('', {}).extended_request[1].must_equal(ssl: true)
162
+ cmd.new('', {}).extended_request[2].must_equal(with_authentication: true)
151
163
  end
152
164
 
153
165
  it 'should extend all except headers' do
154
166
  cmd.extend_with(CmdExtensions.new(except: :request_headers))
155
- cmd.new({}).extended_request[0].must_equal(thin: true)
156
- cmd.new({}).extended_request[1].must_equal({})
157
- cmd.new({}).extended_request[2].must_equal(with_authentication: true)
167
+ cmd.new('', {}).extended_request[0].must_equal(thin: true)
168
+ cmd.new('', {}).extended_request[1].must_equal({})
169
+ cmd.new('', {}).extended_request[2].must_equal(with_authentication: true)
158
170
  end
159
171
 
160
172
  it 'should extend all except options' do
161
173
  cmd.extend_with(CmdExtensions.new(except: :request_options))
162
- cmd.new({}).extended_request[0].must_equal(thin: true)
163
- cmd.new({}).extended_request[1].must_equal(ssl: true)
164
- cmd.new({}).extended_request[2].must_equal({})
174
+ cmd.new('', {}).extended_request[0].must_equal(thin: true)
175
+ cmd.new('', {}).extended_request[1].must_equal(ssl: true)
176
+ cmd.new('', {}).extended_request[2].must_equal({})
165
177
  end
166
178
 
167
179
  it 'should extend all except params and options and headers' do
168
180
  cmd.extend_with(CmdExtensions.new(except: :request))
169
- cmd.new({}).extended_request[0].must_equal({})
170
- cmd.new({}).extended_request[1].must_equal({})
171
- cmd.new({}).extended_request[2].must_equal({})
181
+ cmd.new('', {}).extended_request[0].must_equal({})
182
+ cmd.new('', {}).extended_request[1].must_equal({})
183
+ cmd.new('', {}).extended_request[2].must_equal({})
172
184
  end
173
185
 
174
186
  it 'should extend all except data' do
175
187
  cmd.extend_with(CmdExtensions.new(except: :data))
176
- cmd.new({}).help.must_match(/.*Section.*/)
177
- cmd.new({}).help.must_match(/.*text.*/)
188
+ cmd.new('', {}).help.must_match(/.*Section.*/)
189
+ cmd.new('', {}).help.must_match(/.*text.*/)
178
190
  cmd.output_definition.empty?.must_equal false
179
191
  opt = cmd.find_option('--ext')
180
192
  opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal true
181
- cmd.new({}).extended_request[0].must_equal(thin: true)
182
- cmd.new({}).extended_request[1].must_equal(ssl: true)
183
- cmd.new({}).extended_request[2].must_equal(with_authentication: true)
184
- cmd.new({}).extended_data({}).must_equal({})
193
+ cmd.new('', {}).extended_request[0].must_equal(thin: true)
194
+ cmd.new('', {}).extended_request[1].must_equal(ssl: true)
195
+ cmd.new('', {}).extended_request[2].must_equal(with_authentication: true)
196
+ cmd.new('', {}).extended_data({}).must_equal({})
197
+ end
198
+
199
+ it 'should extend all except option family' do
200
+ cmd.extend_with(CmdExtensions.new(except: :option_family))
201
+ cmd.output_definition.empty?.must_equal false
202
+ cmd.recognised_options.map(&:switches).flatten.must_equal ['--ext', '-h', '--help']
185
203
  end
186
204
  end
187
205
 
@@ -69,4 +69,26 @@ describe HammerCLI::Help::Builder do
69
69
  help.string.strip.must_equal expected_output
70
70
  end
71
71
  end
72
+
73
+ describe 'option family' do
74
+ let(:family) { Class.new(HammerCLI::Options::OptionFamily) }
75
+
76
+ it 'prints option families' do
77
+ fm1 = family.new
78
+ fm1.parent(['--option-zzz'], 'OPT', 'Some description')
79
+ fm1.child(['--option-aaa'], 'OPT', 'Some description')
80
+ fm2 = family.new
81
+ fm2.parent(['--option-bbb'], 'OPT', 'Some description')
82
+ fm2.child(['--option-yyy'], 'OPT', 'Some description')
83
+
84
+ options = fm1.all + fm2.all
85
+ help.add_list('Options', options)
86
+
87
+ help.string.strip.must_equal [
88
+ 'Options:',
89
+ ' --option[-yyy|-bbb] Some description',
90
+ ' --option[-aaa|-zzz] Some description',
91
+ ].join("\n")
92
+ end
93
+ end
72
94
  end
@@ -0,0 +1,48 @@
1
+ require_relative '../test_helper'
2
+
3
+ describe HammerCLI::Options::OptionFamily do
4
+ let(:family) do
5
+ HammerCLI::Options::OptionFamily.new(
6
+ deprecated: { '--test-one' => 'Use --test-two instead' }
7
+ )
8
+ end
9
+ let(:first_option) { HammerCLI::Apipie::OptionDefinition.new("--test-one", '', '') }
10
+ let(:second_option) { HammerCLI::Apipie::OptionDefinition.new("--test-two", '', '') }
11
+ let(:third_option) { HammerCLI::Apipie::OptionDefinition.new("--test-three", '', '') }
12
+ let(:full_family) do
13
+ family.parent('--test-one', '', 'Test').family.child('--test-two', '', '').family
14
+ end
15
+
16
+ describe 'switch' do
17
+ it 'returns nil if family is empty' do
18
+ family.switch.must_be_nil
19
+ end
20
+
21
+ it 'returns parent switch if family has no children' do
22
+ family.parent('--test-one', '', '')
23
+ family.switch.must_equal '--test-one'
24
+ end
25
+
26
+ it 'returns switch based on members' do
27
+ full_family.switch.must_equal '--test[-two|-one]'
28
+ end
29
+ end
30
+
31
+ describe 'description' do
32
+ it 'returns parent description if nothing passed to initializer' do
33
+ full_family.description.must_equal full_family.head.help[1]
34
+ end
35
+
36
+ it 'returns description with deprecation message' do
37
+ full_family.description.must_equal 'Test (--test-one is deprecated: Use --test-two instead)'
38
+ end
39
+ end
40
+
41
+ describe 'adopt' do
42
+ it 'appends an option to children' do
43
+ full_family.adopt(third_option)
44
+ full_family.children.size.must_equal 2
45
+ third_option.family.must_equal full_family
46
+ end
47
+ end
48
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hammer_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Bačovský
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-02-12 00:00:00.000000000 Z
12
+ date: 2020-05-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: clamp
@@ -145,7 +145,7 @@ dependencies:
145
145
  version: 0.2.0
146
146
  description: 'Hammer cli provides universal extendable CLI interface for ruby apps
147
147
 
148
- '
148
+ '
149
149
  email: mbacovsk@redhat.com
150
150
  executables:
151
151
  - hammer
@@ -252,6 +252,7 @@ files:
252
252
  - lib/hammer_cli/options/normalizers.rb
253
253
  - lib/hammer_cli/options/option_collector.rb
254
254
  - lib/hammer_cli/options/option_definition.rb
255
+ - lib/hammer_cli/options/option_family.rb
255
256
  - lib/hammer_cli/options/option_processor.rb
256
257
  - lib/hammer_cli/options/predefined.rb
257
258
  - lib/hammer_cli/options/processor_list.rb
@@ -353,6 +354,7 @@ files:
353
354
  - test/unit/options/normalizers_test.rb
354
355
  - test/unit/options/option_collector_test.rb
355
356
  - test/unit/options/option_definition_test.rb
357
+ - test/unit/options/option_family_test.rb
356
358
  - test/unit/options/processor_list_test.rb
357
359
  - test/unit/options/sources/command_line_test.rb
358
360
  - test/unit/options/sources/saved_defaults_test.rb
@@ -392,7 +394,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
392
394
  - !ruby/object:Gem::Version
393
395
  version: '0'
394
396
  requirements: []
395
- rubygems_version: 3.0.3
397
+ rubygems_version: 3.0.8
396
398
  signing_key:
397
399
  specification_version: 4
398
400
  summary: Universal command-line interface
@@ -414,6 +416,7 @@ test_files:
414
416
  - test/unit/command_extensions_test.rb
415
417
  - test/unit/main_test.rb
416
418
  - test/unit/options/processor_list_test.rb
419
+ - test/unit/options/option_family_test.rb
417
420
  - test/unit/options/validators/dsl_test.rb
418
421
  - test/unit/options/sources/command_line_test.rb
419
422
  - test/unit/options/sources/saved_defaults_test.rb