hammer_cli 2.0.0 → 2.1.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.
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