hammer_cli 3.0.2 → 3.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/doc/creating_commands.md +17 -0
  3. data/doc/release_notes.md +10 -4
  4. data/lib/hammer_cli/abstract.rb +58 -53
  5. data/lib/hammer_cli/apipie/command.rb +1 -1
  6. data/lib/hammer_cli/apipie/option_builder.rb +13 -10
  7. data/lib/hammer_cli/apipie/option_definition.rb +1 -8
  8. data/lib/hammer_cli/command_extensions.rb +11 -6
  9. data/lib/hammer_cli/help/builder.rb +10 -6
  10. data/lib/hammer_cli/options/normalizers.rb +126 -18
  11. data/lib/hammer_cli/options/option_definition.rb +17 -22
  12. data/lib/hammer_cli/options/option_family.rb +44 -8
  13. data/lib/hammer_cli/output/adapter/abstract.rb +6 -0
  14. data/lib/hammer_cli/output/adapter/base.rb +1 -1
  15. data/lib/hammer_cli/output/adapter/tree_structure.rb +1 -1
  16. data/lib/hammer_cli/output/field_filter.rb +1 -1
  17. data/lib/hammer_cli/utils.rb +6 -0
  18. data/lib/hammer_cli/version.rb +1 -1
  19. data/locale/ca/LC_MESSAGES/hammer-cli.mo +0 -0
  20. data/locale/de/LC_MESSAGES/hammer-cli.mo +0 -0
  21. data/locale/en/LC_MESSAGES/hammer-cli.mo +0 -0
  22. data/locale/en_GB/LC_MESSAGES/hammer-cli.mo +0 -0
  23. data/locale/es/LC_MESSAGES/hammer-cli.mo +0 -0
  24. data/locale/fr/LC_MESSAGES/hammer-cli.mo +0 -0
  25. data/locale/it/LC_MESSAGES/hammer-cli.mo +0 -0
  26. data/locale/ja/LC_MESSAGES/hammer-cli.mo +0 -0
  27. data/locale/ko/LC_MESSAGES/hammer-cli.mo +0 -0
  28. data/locale/pt_BR/LC_MESSAGES/hammer-cli.mo +0 -0
  29. data/locale/ru/LC_MESSAGES/hammer-cli.mo +0 -0
  30. data/locale/zh_CN/LC_MESSAGES/hammer-cli.mo +0 -0
  31. data/locale/zh_TW/LC_MESSAGES/hammer-cli.mo +0 -0
  32. data/test/unit/abstract_test.rb +7 -0
  33. data/test/unit/apipie/option_builder_test.rb +8 -3
  34. data/test/unit/command_extensions_test.rb +10 -2
  35. data/test/unit/help/builder_test.rb +20 -2
  36. data/test/unit/options/option_definition_test.rb +12 -1
  37. metadata +5 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7036ad620797bd887a62908eef5231092a47f019b5426ca4f4232c1c9e3c8791
4
- data.tar.gz: 49c8e269f005306bb0f12031c59366d0d3ffb892cd08fef9926644477caa2ffd
3
+ metadata.gz: 510f07df29c39c069ae58e923507361a276e12ef50cc38628868361794a7754c
4
+ data.tar.gz: 13790a4114574688531978c11a9bb479062381d1b5196168f6d0e70deddc6a43
5
5
  SHA512:
6
- metadata.gz: dbe31f4f8ab404b407754247f4887deb4687ad6b45cb0b2428757d3817192de9b8edd4f2c4cd95a3414047e92cfb9fd461eeef3ff200c47497a64d41da2500be
7
- data.tar.gz: 93e25574abdfe8cf2942427ac55cf7c6c7667df0f277c630fdb644f14ab3cecb70e3c15f54f009c6d3cd6d0e37a3fd602f4cd23560c3b64bdb3baf223b99643f
6
+ metadata.gz: a8bd5241448ccae9b7bd27f4260ebe5cbc8d75f2a79dcf76fb710c13e7bba42275cda6332acd6b67c33125572393b17e1f2d1575166ce8edfc76c7a81cdb9854
7
+ data.tar.gz: 9656ed10f1c8d44f35f0d321e1568d82a9607dc82b5ecf3e03999ec471eb67aa5dc119fcc31c90b1f540bab4a3d5abc5eff444d498eb275a7e0b061c61426fcf
@@ -190,6 +190,23 @@ To define an option family, use the following DSL:
190
190
  end
191
191
  ```
192
192
 
193
+ You can also add additional options for automatically built ones:
194
+ ```ruby
195
+ # ...
196
+ build_options
197
+ # If --resource-id option comes from the API params and you want to add options
198
+ # with searchables such as --resource-name, --resource-label
199
+ option_family(associate: 'resource') do
200
+ child '--resource-name', 'RESOURCE', _('Resource desc'), attribute_name: :option_resource_name
201
+ child '--resource-label', 'RESOURCE', _('Resource desc'), attribute_name: :option_resource_label
202
+ end
203
+ # $ hammer command --help:
204
+ # ...
205
+ # Options:
206
+ # --resource[-id|-name|-label] Resource desc
207
+ # ...
208
+ ```
209
+
193
210
  ##### Example
194
211
 
195
212
  ```ruby
data/doc/release_notes.md CHANGED
@@ -1,10 +1,16 @@
1
1
  Release notes
2
2
  =============
3
- ### 3.0.2 (2022-02-01)
4
- * Fix fr translation ([PR #358](https://github.com/theforeman/hammer-cli/pull/358)), [#34204](http://projects.theforeman.org/issues/34204)
5
-
6
- ### 3.0.1 (2021-11-02)
3
+ ### 3.1.0 (2021-11-10)
7
4
  * Remove a space in hammer's shebang, [#33810](http://projects.theforeman.org/issues/33810)
5
+ * Revert fix rake version
6
+ * Fix rake version
7
+ * Wrap option descriptions to 80 chars, [#33129](http://projects.theforeman.org/issues/33129)
8
+ * Don't store @context in field params, [#33259](http://projects.theforeman.org/issues/33259)
9
+ * Change from superficial copy to deep copy of fields ([PR #348](https://github.com/theforeman/hammer-cli/pull/348)), [#29093](http://projects.theforeman.org/issues/29093)
10
+ * Make api docs params to be the main options, [#33226](http://projects.theforeman.org/issues/33226)
11
+ * Show depr warning only on option usage, [#33225](http://projects.theforeman.org/issues/33225)
12
+ * Extract descs to option details section, [#32783](http://projects.theforeman.org/issues/32783)
13
+ * Bump to 3.1.0-develop
8
14
 
9
15
  ### 3.0.0 (2021-08-04)
10
16
  * Update rel-eng notebook ([PR #347](https://github.com/theforeman/hammer-cli/pull/347))
@@ -25,6 +25,18 @@ module HammerCLI
25
25
  class << self
26
26
  attr_accessor :validation_blocks
27
27
 
28
+ def family_registry
29
+ @family_registry ||= HammerCLI::Options::OptionFamilyRegistry.new
30
+ end
31
+
32
+ def option_families
33
+ ancestors.inject([]) do |registry, ancestor|
34
+ next registry unless ancestor <= HammerCLI::AbstractCommand
35
+
36
+ registry + ancestor.family_registry
37
+ end
38
+ end
39
+
28
40
  def help_extension_blocks
29
41
  @help_extension_blocks ||= []
30
42
  end
@@ -43,25 +55,37 @@ module HammerCLI
43
55
  extensions
44
56
  end
45
57
 
46
- def extend_options_help(option)
58
+ def add_option_schema(option)
47
59
  extend_help do |h|
60
+ option_details = h.find_item(:s_option_details)
48
61
  begin
49
- h.find_item(:s_option_details)
62
+ option_details.definition.find_item(:t_schema_help)
50
63
  rescue ArgumentError
51
- option_details = HammerCLI::Help::Section.new(_('Option details'), nil, id: :s_option_details, richtext: true)
52
64
  option_details.definition << HammerCLI::Help::Text.new(
53
65
  _('Following parameters accept format defined by its schema ' \
54
- '(bold are required; <> contain acceptable type; [] contain acceptable value):')
66
+ '(bold are required; <> contains acceptable type; [] contains acceptable value):'),
67
+ id: :t_schema_help
55
68
  )
56
- h.definition.unshift(option_details)
57
- ensure
58
- h.find_item(:s_option_details).definition << HammerCLI::Help::List.new([
59
- [option.switches.last, option.value_formatter.schema.description]
60
- ])
61
69
  end
70
+ option_details.definition << HammerCLI::Help::List.new([
71
+ [option.switches.last, option.value_formatter.schema.description]
72
+ ])
62
73
  end
63
74
  end
64
75
 
76
+ def add_option_details_section(help)
77
+ option_details = HammerCLI::Help::Section.new(_('Option details'), nil, id: :s_option_details, richtext: true)
78
+ option_details.definition << HammerCLI::Help::Text.new(
79
+ _('Here you can find option types and the value an option can accept:')
80
+ )
81
+ type_list = HammerCLI::Options::Normalizers.available.each_with_object([]) do |n, l|
82
+ l << [n.completion_type.to_s.upcase, n.common_description]
83
+ end.uniq(&:first).sort
84
+
85
+ option_details.definition << HammerCLI::Help::List.new(type_list)
86
+ help.definition.unshift(option_details)
87
+ end
88
+
65
89
  def add_sets_help(help)
66
90
  sets_details = HammerCLI::Help::Section.new(_('Predefined field sets'), nil, id: :s_sets_details, richtext: true)
67
91
  sets_details.definition << HammerCLI::Help::Text.new(output_definition.sets_table)
@@ -138,6 +162,7 @@ module HammerCLI
138
162
  super(invocation_path, builder)
139
163
  help_extension = HammerCLI::Help::TextBuilder.new(builder.richtext)
140
164
  fields_switch = HammerCLI::Options::Predefined::OPTIONS[:fields].first[0]
165
+ add_option_details_section(help_extension) if recognised_options.size > 1
141
166
  add_sets_help(help_extension) if find_option(fields_switch)
142
167
  unless help_extension_blocks.empty?
143
168
  help_extension_blocks.each do |extension_block|
@@ -194,18 +219,29 @@ module HammerCLI
194
219
  @option_builder
195
220
  end
196
221
 
222
+ def self.option(switches, type, description, opts = {}, &block)
223
+ option = HammerCLI::Options::OptionDefinition.new(switches, type, description, opts).tap do |option|
224
+ declared_options << option
225
+ block ||= option.default_conversion_block
226
+ define_accessors_for(option, &block)
227
+ add_option_schema(option) if option.value_formatter.is_a?(HammerCLI::Options::Normalizers::ListNested)
228
+ completion_type_for(option, opts)
229
+ end
230
+ option
231
+ end
232
+
197
233
  def self.build_options(builder_params={})
198
234
  builder_params = yield(builder_params) if block_given?
235
+ builder_params[:command] = self
199
236
 
200
237
  option_builder.build(builder_params).each do |option|
201
238
  # skip switches that are already defined
202
- next if option.nil? || option.switches.any? { |s| find_option(s) }
239
+ next if option.nil? || option.family || option.switches.any? { |s| find_option(s) }
203
240
 
204
- adjust_family(option) if option.respond_to?(:family)
205
241
  declared_options << option
206
242
  block ||= option.default_conversion_block
207
243
  define_accessors_for(option, &block)
208
- extend_options_help(option) if option.value_formatter.is_a?(HammerCLI::Options::Normalizers::ListNested)
244
+ add_option_schema(option) if option.value_formatter.is_a?(HammerCLI::Options::Normalizers::ListNested)
209
245
  completion_type_for(option)
210
246
  end
211
247
  end
@@ -233,14 +269,20 @@ module HammerCLI
233
269
  end
234
270
  end
235
271
 
236
- protected
237
-
238
272
  def self.option_family(options = {}, &block)
239
273
  options[:creator] ||= self
240
- family = HammerCLI::Options::OptionFamily.new(options)
241
- family.instance_eval(&block)
274
+ family = if options[:associate]
275
+ option_families.find { |f| f.root.to_s == options[:associate].to_s }
276
+ else
277
+ HammerCLI::Options::OptionFamily.new(options)
278
+ end
279
+ return family.instance_eval(&block) if family
280
+
281
+ logger('Option Family').debug "No family found for #{options[:associate]}, skipping"
242
282
  end
243
283
 
284
+ protected
285
+
244
286
  def self.find_options(switch_filter, other_filters={})
245
287
  filters = other_filters
246
288
  if switch_filter.is_a? Hash
@@ -339,17 +381,6 @@ module HammerCLI
339
381
  end
340
382
  end
341
383
 
342
- def self.option(switches, type, description, opts = {}, &block)
343
- option = HammerCLI::Options::OptionDefinition.new(switches, type, description, opts).tap do |option|
344
- declared_options << option
345
- block ||= option.default_conversion_block
346
- define_accessors_for(option, &block)
347
- completion_type_for(option, opts)
348
- end
349
- extend_options_help(option) if option.value_formatter.is_a?(HammerCLI::Options::Normalizers::ListNested)
350
- option
351
- end
352
-
353
384
  def all_options
354
385
  option_collector.all_options
355
386
  end
@@ -412,32 +443,6 @@ module HammerCLI
412
443
 
413
444
  private
414
445
 
415
- def self.adjust_family(option)
416
- # Collect options that should share the same family
417
- # If those options have family, adopt the current one
418
- # Else adopt those options to the family of the current option
419
- # NOTE: this shouldn't rewrite any options,
420
- # although options from similar family could be adopted (appended)
421
- options = find_options(
422
- aliased_resource: option.aliased_resource.to_s
423
- ).select { |o| o.family.nil? || o.family.formats.include?(option.value_formatter.class) }.group_by do |o|
424
- next :to_skip if option.family.children.include?(o)
425
- next :to_adopt if o.family.nil? || o.family.head.nil?
426
- next :to_skip if o.family.children.include?(option)
427
- # If both family heads handle the same switch
428
- # then `option` is probably from similar family and can be adopted
429
- next :adopt_by if option.family.head.nil? || o.family.head.handles?(option.family.head.long_switch)
430
-
431
- :to_skip
432
- end
433
- options[:to_adopt]&.each do |child|
434
- option.family&.adopt(child)
435
- end
436
- options[:adopt_by]&.map(&:family)&.uniq&.each do |family|
437
- family.adopt(option)
438
- end
439
- end
440
-
441
446
  def self.inherited_output_definition
442
447
  od = nil
443
448
  if superclass.respond_to? :output_definition
@@ -86,9 +86,9 @@ module HammerCLI::Apipie
86
86
  declared_options << option
87
87
  block ||= option.default_conversion_block
88
88
  define_accessors_for(option, &block)
89
+ add_option_schema(option) if option.value_formatter.is_a?(HammerCLI::Options::Normalizers::ListNested)
89
90
  completion_type_for(option, opts)
90
91
  end
91
- extend_options_help(option) if option.value_formatter.is_a?(HammerCLI::Options::Normalizers::ListNested)
92
92
  option
93
93
  end
94
94
 
@@ -9,11 +9,11 @@ module HammerCLI::Apipie
9
9
  @require_options = options[:require_options].nil? ? true : options[:require_options]
10
10
  end
11
11
 
12
- def build(builder_params={})
12
+ def build(builder_params = {})
13
13
  filter = Array(builder_params[:without])
14
14
  resource_name_map = builder_params[:resource_mapping] || {}
15
15
 
16
- options_for_params(@action.params, filter, resource_name_map)
16
+ options_for_params(@action.params, filter, resource_name_map, command: builder_params[:command])
17
17
  end
18
18
 
19
19
  attr_writer :require_options
@@ -27,21 +27,24 @@ module HammerCLI::Apipie
27
27
  HammerCLI::Apipie::OptionDefinition.new(*args)
28
28
  end
29
29
 
30
- def options_for_params(params, filter, resource_name_map)
31
- opts = []
30
+ def options_for_params(params, filter, resource_name_map, opts = {})
31
+ options = []
32
32
  params.each do |p|
33
- next if filter.include?(p.name) || filter.include?(p.name.to_sym)
33
+ exists = opts[:command].find_option(option_switch(p, resource_name_map))
34
+ next if filter.include?(p.name) || filter.include?(p.name.to_sym) || exists
35
+
34
36
  if p.expected_type == :hash
35
- opts += options_for_params(p.params, filter, resource_name_map)
37
+ options += options_for_params(p.params, filter, resource_name_map, opts)
36
38
  else
37
- opts << create_option(p, resource_name_map)
39
+ options << create_option(p, resource_name_map, opts)
38
40
  end
39
41
  end
40
- opts
42
+ options
41
43
  end
42
44
 
43
- def create_option(param, resource_name_map)
44
- family = HammerCLI::Options::OptionFamily.new
45
+ def create_option(param, resource_name_map, opts = {})
46
+ family = HammerCLI::Options::OptionFamily.new(creator: opts[:command])
47
+ # APIdoc params are considered to be the main options (parent) by default
45
48
  family.parent(option_switch(param, resource_name_map),
46
49
  option_type(param, resource_name_map),
47
50
  option_desc(param),
@@ -2,22 +2,15 @@ require File.join(File.dirname(__FILE__), 'options')
2
2
 
3
3
  module HammerCLI::Apipie
4
4
  class OptionDefinition < HammerCLI::Options::OptionDefinition
5
- attr_accessor :referenced_resource, :aliased_resource, :family
5
+ attr_accessor :referenced_resource, :aliased_resource
6
6
 
7
7
  def initialize(switches, type, description, options = {})
8
8
  @referenced_resource = options[:referenced_resource].to_s if options[:referenced_resource]
9
9
  @aliased_resource = options[:aliased_resource].to_s if options[:aliased_resource]
10
- @family = options[:family]
11
10
  super
12
11
  # Apipie currently sends descriptions as escaped HTML once this is changed this should be removed.
13
12
  # See #15198 on Redmine.
14
13
  @description = CGI::unescapeHTML(description)
15
14
  end
16
-
17
- def child?
18
- return unless @family
19
-
20
- @family.children.include?(self)
21
- end
22
15
  end
23
16
  end
@@ -87,8 +87,11 @@ module HammerCLI
87
87
  end
88
88
 
89
89
  def self.option_family(options = {}, &block)
90
- @option_family_opts = options
91
- @option_family_block = block
90
+ @option_family_extensions ||= []
91
+ @option_family_extensions << {
92
+ options: options,
93
+ block: block
94
+ }
92
95
  end
93
96
 
94
97
  # Object
@@ -256,11 +259,13 @@ module HammerCLI
256
259
  end
257
260
 
258
261
  def self.extend_option_family(command_class)
259
- return if @option_family_block.nil?
262
+ return if @option_family_extensions.nil?
260
263
 
261
- @option_family_opts[:creator] = command_class
262
- command_class.send(:option_family, @option_family_opts, &@option_family_block)
263
- logger.debug("Called option family block for #{command_class}:\n\t#{@option_family_block}")
264
+ @option_family_extensions.each do |extension|
265
+ extension[:options][:creator] = command_class
266
+ command_class.send(:option_family, extension[:options], &extension[:block])
267
+ logger.debug("Called option family block for #{command_class}:\n\t#{extension[:block]}")
268
+ end
264
269
  end
265
270
  end
266
271
  end
@@ -29,7 +29,11 @@ module HammerCLI
29
29
 
30
30
  label_width = DEFAULT_LABEL_INDENT
31
31
  items.each do |item|
32
- label = item.help.first
32
+ label = if !HammerCLI.context[:full_help] && item.respond_to?(:family) && item.family && !item.child?
33
+ item.family.help.first
34
+ else
35
+ item.help.first
36
+ end
33
37
  label_width = label.size if label.size > label_width
34
38
  end
35
39
 
@@ -38,11 +42,11 @@ module HammerCLI
38
42
  next unless HammerCLI.context[:full_help]
39
43
  end
40
44
  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
45
- description.gsub(/^(.)/) { Unicode::capitalize($1) }.each_line do |line|
45
+ item.family.help
46
+ else
47
+ item.help
48
+ end
49
+ description.gsub(/^(.)/) { Unicode.capitalize(Regexp.last_match(1)) }.wrap.each_line do |line|
46
50
  puts " %-#{label_width}s %s" % [label, line]
47
51
  label = ''
48
52
  end
@@ -4,8 +4,28 @@ require 'hammer_cli/csv_parser'
4
4
  module HammerCLI
5
5
  module Options
6
6
  module Normalizers
7
+ def self.available
8
+ AbstractNormalizer.available
9
+ end
7
10
 
8
11
  class AbstractNormalizer
12
+ class << self
13
+ attr_reader :available
14
+
15
+ def inherited(subclass)
16
+ @available ||= []
17
+ @available << subclass
18
+ end
19
+
20
+ def completion_type
21
+ :value
22
+ end
23
+
24
+ def common_description
25
+ _("Value described in the option's description. Mostly simple string")
26
+ end
27
+ end
28
+
9
29
  def description
10
30
  ""
11
31
  end
@@ -17,6 +37,10 @@ module HammerCLI
17
37
  def complete(val)
18
38
  []
19
39
  end
40
+
41
+ def completion_type
42
+ { type: self.class.completion_type }
43
+ end
20
44
  end
21
45
 
22
46
  class Default < AbstractNormalizer
@@ -30,9 +54,15 @@ module HammerCLI
30
54
  PAIR_RE = '([^,=]+)=([^,\{\[]+|[\{\[][^\{\}\[\]]*[\}\]])'
31
55
  FULL_RE = "^((%s)[,]?)+$" % PAIR_RE
32
56
 
33
- def description
34
- _("Comma-separated list of key=value.") + "\n" +
35
- _("JSON is acceptable and preferred way for complex parameters")
57
+ class << self
58
+ def completion_type
59
+ :key_value_list
60
+ end
61
+
62
+ def common_description
63
+ _('Comma-separated list of key=value.') + "\n" +
64
+ _('JSON is acceptable and preferred way for such parameters')
65
+ end
36
66
  end
37
67
 
38
68
  def format(val)
@@ -94,9 +124,16 @@ module HammerCLI
94
124
 
95
125
 
96
126
  class List < AbstractNormalizer
97
- def description
98
- _("Comma separated list of values. Values containing comma should be quoted or escaped with backslash.") + "\n" +
99
- _("JSON is acceptable and preferred way for complex parameters")
127
+ class << self
128
+ def completion_type
129
+ :list
130
+ end
131
+
132
+ def common_description
133
+ _('Comma separated list of values. Values containing comma should be quoted or escaped with backslash.') +
134
+ "\n" +
135
+ _('JSON is acceptable and preferred way for such parameters')
136
+ end
100
137
  end
101
138
 
102
139
  def format(val)
@@ -110,6 +147,18 @@ module HammerCLI
110
147
  end
111
148
 
112
149
  class ListNested < AbstractNormalizer
150
+ class << self
151
+ def completion_type
152
+ :schema
153
+ end
154
+
155
+ def common_description
156
+ _('Comma separated list of values defined by a schema.') +
157
+ "\n" +
158
+ _('JSON is acceptable and preferred way for such parameters')
159
+ end
160
+ end
161
+
113
162
  class Schema < Array
114
163
  def description(richtext: true)
115
164
  '"' + reduce([]) do |schema, nested_param|
@@ -135,11 +184,6 @@ module HammerCLI
135
184
  @schema = Schema.new(schema)
136
185
  end
137
186
 
138
- def description
139
- _("Comma separated list of values defined by a schema. See Option details section below.") + "\n" +
140
- _("JSON is acceptable and preferred way for complex parameters")
141
- end
142
-
143
187
  def format(val)
144
188
  return [] unless val.is_a?(String) && !val.empty?
145
189
  begin
@@ -152,9 +196,22 @@ module HammerCLI
152
196
  end
153
197
  end
154
198
  end
199
+
200
+ def completion_type
201
+ super.merge({ schema: schema.description(richtext: false) })
202
+ end
155
203
  end
156
204
 
157
205
  class Number < AbstractNormalizer
206
+ class << self
207
+ def completion_type
208
+ :number
209
+ end
210
+
211
+ def common_description
212
+ _('Numeric value. Integer')
213
+ end
214
+ end
158
215
 
159
216
  def format(val)
160
217
  if numeric?(val)
@@ -167,17 +224,22 @@ module HammerCLI
167
224
  def numeric?(val)
168
225
  Integer(val) != nil rescue false
169
226
  end
170
-
171
227
  end
172
228
 
173
229
 
174
230
  class Bool < AbstractNormalizer
175
- def allowed_values
176
- ['yes', 'no', 'true', 'false', '1', '0']
231
+ class << self
232
+ def completion_type
233
+ :boolean
234
+ end
235
+
236
+ def common_description
237
+ _('One of %s') % ['true/false', 'yes/no', '1/0'].join(', ')
238
+ end
177
239
  end
178
240
 
179
- def description
180
- _('One of %s.') % ['true/false', 'yes/no', '1/0'].join(', ')
241
+ def allowed_values
242
+ ['yes', 'no', 'true', 'false', '1', '0']
181
243
  end
182
244
 
183
245
  def format(bool)
@@ -194,10 +256,23 @@ module HammerCLI
194
256
  def complete(value)
195
257
  allowed_values.map { |v| v + ' ' }
196
258
  end
259
+
260
+ def completion_type
261
+ super.merge({ values: allowed_values })
262
+ end
197
263
  end
198
264
 
199
265
 
200
266
  class File < AbstractNormalizer
267
+ class << self
268
+ def completion_type
269
+ :file
270
+ end
271
+
272
+ def common_description
273
+ _('Path to a file')
274
+ end
275
+ end
201
276
 
202
277
  def format(path)
203
278
  ::File.read(::File.expand_path(path))
@@ -233,6 +308,16 @@ module HammerCLI
233
308
 
234
309
 
235
310
  class Enum < AbstractNormalizer
311
+ class << self
312
+ def completion_type
313
+ :enum
314
+ end
315
+
316
+ def common_description
317
+ _("Possible values are described in the option's description")
318
+ end
319
+ end
320
+
236
321
  attr_reader :allowed_values
237
322
 
238
323
  def initialize(allowed_values)
@@ -260,6 +345,10 @@ module HammerCLI
260
345
  Completer::finalize_completions(@allowed_values)
261
346
  end
262
347
 
348
+ def completion_type
349
+ super.merge({ values: allowed_values })
350
+ end
351
+
263
352
  private
264
353
 
265
354
  def quoted_values
@@ -269,9 +358,14 @@ module HammerCLI
269
358
 
270
359
 
271
360
  class DateTime < AbstractNormalizer
361
+ class << self
362
+ def completion_type
363
+ :datetime
364
+ end
272
365
 
273
- def description
274
- _("Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format")
366
+ def common_description
367
+ _('Date and time in YYYY-MM-DD HH:MM:SS or ISO 8601 format')
368
+ end
275
369
  end
276
370
 
277
371
  def format(date)
@@ -283,6 +377,16 @@ module HammerCLI
283
377
  end
284
378
 
285
379
  class EnumList < AbstractNormalizer
380
+ class << self
381
+ def completion_type
382
+ :multienum
383
+ end
384
+
385
+ def common_description
386
+ _("Any combination of possible values described in the option's description")
387
+ end
388
+ end
389
+
286
390
  attr_reader :allowed_values
287
391
 
288
392
  def initialize(allowed_values)
@@ -301,6 +405,10 @@ module HammerCLI
301
405
  Completer::finalize_completions(@allowed_values)
302
406
  end
303
407
 
408
+ def completion_type
409
+ super.merge({ values: allowed_values })
410
+ end
411
+
304
412
  private
305
413
 
306
414
  def quoted_values
@@ -22,12 +22,14 @@ module HammerCLI
22
22
 
23
23
  class OptionDefinition < Clamp::Option::Definition
24
24
 
25
- attr_accessor :value_formatter, :context_target, :deprecated_switches
25
+ attr_accessor :value_formatter, :context_target, :deprecated_switches,
26
+ :family
26
27
 
27
28
  def initialize(switches, type, description, options = {})
28
29
  @value_formatter = options[:format] || HammerCLI::Options::Normalizers::Default.new
29
30
  @context_target = options[:context_target]
30
31
  @deprecated_switches = options[:deprecated]
32
+ @family = options[:family]
31
33
  super
32
34
  end
33
35
 
@@ -36,7 +38,9 @@ module HammerCLI
36
38
  end
37
39
 
38
40
  def help_lhs
39
- super
41
+ lhs = switches.join(', ')
42
+ lhs += " #{completion_type[:type]}".upcase unless flag?
43
+ lhs
40
44
  end
41
45
 
42
46
  def help_rhs
@@ -50,14 +54,14 @@ module HammerCLI
50
54
  rhs.empty? ? " " : rhs
51
55
  end
52
56
 
53
- def handles?(switch)
57
+ def extract_value(switch, arguments)
54
58
  message = _("Warning: Option %{option} is deprecated. %{message}")
55
59
  if deprecated_switches.class <= String && switches.include?(switch)
56
- warn(message % { :option => switch, :message => deprecated_switches })
60
+ warn(message % { option: switch, message: deprecated_switches })
57
61
  elsif deprecated_switches.class <= Hash && deprecated_switches.keys.include?(switch)
58
- warn(message % { :option => switch, :message => deprecated_switches[switch] })
62
+ warn(message % { option: switch, message: deprecated_switches[switch] })
59
63
  end
60
- super(switch)
64
+ super(switch, arguments)
61
65
  end
62
66
 
63
67
  def deprecation_message(switch)
@@ -140,22 +144,13 @@ module HammerCLI
140
144
  return { type: :flag } if @type == :flag
141
145
 
142
146
  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 }
147
+ formatter.completion_type
148
+ end
149
+
150
+ def child?
151
+ return unless @family
152
+
153
+ @family.children.include?(self)
159
154
  end
160
155
 
161
156
  private
@@ -2,25 +2,33 @@
2
2
 
3
3
  module HammerCLI
4
4
  module Options
5
+ class OptionFamilyRegistry < Array
6
+ # rubocop:disable Style/Alias
7
+ alias_method :register, :push
8
+ alias_method :unregister, :delete
9
+ # rubocop:enable Style/Alias
10
+ end
11
+
5
12
  class OptionFamily
6
13
  attr_reader :children
7
14
 
8
- IDS_REGEX = /(\A[Ii][Dd][s]?)|\s([Ii][Dd][s]?)\W|([Ii][Dd][s]?\Z)/
15
+ IDS_REGEX = /(\A[Ii][Dd][s]?)|\s([Ii][Dd][s]?)\W|([Ii][Dd][s]?\Z)|(numeric identifier|identifier)/.freeze
9
16
 
10
17
  def initialize(options = {})
11
18
  @all = []
12
19
  @children = []
13
20
  @options = options
14
- @creator = options[:creator] || Class.new(HammerCLI::Apipie::Command)
21
+ @creator = options[:creator] || self
15
22
  @prefix = options[:prefix]
16
23
  @root = options[:root] || options[:aliased_resource] || options[:referenced_resource]
24
+ @creator.family_registry.register(self) if @creator != self
17
25
  end
18
26
 
19
27
  def description
20
28
  types = all.map(&:type).map { |s| s.split('_').last.to_s }
21
29
  .map(&:downcase).join('/')
22
- parent_desc = @parent.help[1].gsub(IDS_REGEX) { |w| w.gsub(/\w+/, types) }
23
- desc = parent_desc.strip.empty? ? @options[:description] : parent_desc
30
+ parent_desc = @parent.help[1].gsub(IDS_REGEX) { |w| w.gsub(/\b.+\b/, types) }
31
+ desc = @options[:description] || parent_desc.strip.empty? ? @options[:description] : parent_desc
24
32
  if @options[:deprecation].class <= String
25
33
  format_deprecation_msg(desc, _('Deprecated: %{deprecated_msg}') % { deprecated_msg: @options[:deprecation] })
26
34
  elsif @options[:deprecation].class <= Hash
@@ -33,6 +41,21 @@ module HammerCLI
33
41
  end
34
42
  end
35
43
 
44
+ def help
45
+ [help_lhs, help_rhs]
46
+ end
47
+
48
+ def help_lhs
49
+ return @parent&.help_lhs if @children.empty?
50
+
51
+ types = all.map(&:value_formatter).map { |f| f.completion_type[:type].to_s.upcase }
52
+ switch + ' ' + types.uniq.join('/')
53
+ end
54
+
55
+ def help_rhs
56
+ description || @parent.help[1]
57
+ end
58
+
36
59
  def formats
37
60
  return [@options[:format].class] if @options[:format]
38
61
 
@@ -41,7 +64,7 @@ module HammerCLI
41
64
 
42
65
  def switch
43
66
  return if @parent.nil? && @children.empty?
44
- return @parent.help_lhs.strip if @children.empty?
67
+ return @parent.switches.join(', ').strip if @children.empty?
45
68
 
46
69
  switch_start = main_switch.each_char
47
70
  .zip(*all.map(&:switches).flatten.map(&:each_char))
@@ -68,6 +91,8 @@ module HammerCLI
68
91
 
69
92
  def child(switches, type, description, opts = {}, &block)
70
93
  child = new_member(switches, type, description, opts, &block)
94
+ return unless child
95
+
71
96
  @children << child
72
97
  child
73
98
  end
@@ -80,6 +105,18 @@ module HammerCLI
80
105
  @children << child
81
106
  end
82
107
 
108
+ def root
109
+ @root || @parent&.aliased_resource || @parent&.referenced_resource || common_root
110
+ end
111
+
112
+ def option(*args)
113
+ HammerCLI::Apipie::OptionDefinition.new(*args)
114
+ end
115
+
116
+ def find_option(switch)
117
+ all.find { |m| m.handles?(switch) }
118
+ end
119
+
83
120
  private
84
121
 
85
122
  def format_deprecation_msg(option_desc, deprecation_msg)
@@ -90,18 +127,17 @@ module HammerCLI
90
127
  opts = opts.merge(@options)
91
128
  opts[:family] = self
92
129
  if opts[:deprecated]
93
- handles = [switches].flatten
130
+ handles = Array(switches)
94
131
  opts[:deprecated] = opts[:deprecated].select do |switch, _msg|
95
132
  handles.include?(switch)
96
133
  end
97
134
  end
98
135
  @creator.instance_eval do
99
- option(switches, type, description, opts, &block)
136
+ option(switches, type, description, opts, &block) unless Array(switches).any? { |s| find_option(s) }
100
137
  end
101
138
  end
102
139
 
103
140
  def main_switch
104
- root = @root || @parent.aliased_resource || @parent.referenced_resource || common_root
105
141
  "--#{@prefix}#{root}".tr('_', '-')
106
142
  end
107
143
 
@@ -97,6 +97,12 @@ module HammerCLI::Output::Adapter
97
97
  @context[:fields] || ['DEFAULT']
98
98
  end
99
99
 
100
+ def context_for_fields
101
+ {
102
+ show_ids: @context[:show_ids]
103
+ }
104
+ end
105
+
100
106
  private
101
107
 
102
108
  def filter_formatters(formatters_map)
@@ -76,7 +76,7 @@ module HammerCLI::Output::Adapter
76
76
  def render_value(field, data)
77
77
  formatter = @formatters.formatter_for_type(field.class)
78
78
  parameters = field.parameters
79
- parameters[:context] = @context
79
+ parameters[:context] = context_for_fields
80
80
  data = formatter.format(data, field.parameters) if formatter
81
81
  data.to_s
82
82
  end
@@ -55,7 +55,7 @@ module HammerCLI::Output::Adapter
55
55
  else
56
56
  formatter = @formatters.formatter_for_type(field.class)
57
57
  parameters = field.parameters
58
- parameters[:context] = @context
58
+ parameters[:context] = context_for_fields
59
59
  if formatter
60
60
  data = formatter.format(data, field.parameters)
61
61
  end
@@ -11,7 +11,7 @@ module HammerCLI::Output
11
11
 
12
12
  def fields=(fields)
13
13
  @fields = fields || []
14
- @filtered_fields = @fields.dup
14
+ @filtered_fields = Marshal.load(Marshal.dump(@fields))
15
15
  end
16
16
 
17
17
  def filter_by_classes(classes = nil)
@@ -40,6 +40,12 @@ class String
40
40
  HammerCLI.constant_path(self)[-1]
41
41
  end
42
42
 
43
+ # Rails implementation: https://github.com/rails/rails/blob/main/actionview/lib/action_view/helpers/text_helper.rb#L260
44
+ def wrap(line_width: 80, break_sequence: "\n")
45
+ split("\n").collect! do |line|
46
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line
47
+ end * break_sequence
48
+ end
43
49
  end
44
50
 
45
51
  class Hash
@@ -1,5 +1,5 @@
1
1
  module HammerCLI
2
2
  def self.version
3
- @version ||= Gem::Version.new "3.0.2"
3
+ @version ||= Gem::Version.new "3.1.0"
4
4
  end
5
5
  end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -308,6 +308,13 @@ describe HammerCLI::AbstractCommand do
308
308
  opt = TestOptionCmd.find_option('--fields')
309
309
  opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal true
310
310
  end
311
+
312
+ it 'should add option type and accepted value' do
313
+ help_str = TestOptionCmd.help('')
314
+ help_str.must_match(
315
+ /LIST Comma separated list of values. Values containing comma should be quoted or escaped with backslash./
316
+ )
317
+ end
311
318
  end
312
319
 
313
320
  describe "#options" do
@@ -17,7 +17,7 @@ describe HammerCLI::Apipie::OptionBuilder do
17
17
  let(:resource) {api.resource(:documented)}
18
18
  let(:action) {resource.action(:index)}
19
19
  let(:builder) { HammerCLI::Apipie::OptionBuilder.new(resource, action) }
20
- let(:builder_options) { {} }
20
+ let(:builder_options) { { command: Class.new(HammerCLI::Apipie::Command) } }
21
21
  let(:options) { builder.build(builder_options) }
22
22
 
23
23
  context "with one simple param" do
@@ -51,7 +51,7 @@ describe HammerCLI::Apipie::OptionBuilder do
51
51
  context "required options" do
52
52
 
53
53
  let(:action) {resource.action(:create)}
54
- let(:required_options) { builder.build.reject{|opt| !opt.required?} }
54
+ let(:required_options) { builder.build(builder_options).reject{|opt| !opt.required?} }
55
55
 
56
56
  it "should set required flag for the required options" do
57
57
  required_options.map(&:attribute_name).sort.must_equal [HammerCLI.option_accessor_name("array_param")]
@@ -146,7 +146,12 @@ describe HammerCLI::Apipie::OptionBuilder do
146
146
 
147
147
  context "aliasing resources" do
148
148
  let(:action) {resource.action(:action_with_ids)}
149
- let(:builder_options) { {:resource_mapping => {:organization => 'company', 'compute_resource' => :compute_provider}} }
149
+ let(:builder_options) do
150
+ {
151
+ resource_mapping: { organization: 'company', 'compute_resource' => :compute_provider },
152
+ command: Class.new(HammerCLI::Apipie::Command)
153
+ }
154
+ end
150
155
 
151
156
  it "renames options" do
152
157
  # builder_options[:resource_mapping] = {:organization => 'company', 'compute_resource' => :compute_provider}
@@ -118,7 +118,7 @@ describe HammerCLI::CommandExtensions do
118
118
  it 'should extend option family only' do
119
119
  cmd.extend_with(CmdExtensions.new(only: :option_family))
120
120
  cmd.output_definition.empty?.must_equal true
121
- cmd.recognised_options.map(&:switches).flatten.must_equal ['--test-one', '--test-two', '-h', '--help']
121
+ cmd.recognised_options.map(&:switches).flatten.must_equal ['-h', '--help', '--test-one', '--test-two']
122
122
  end
123
123
  end
124
124
 
@@ -203,5 +203,13 @@ describe HammerCLI::CommandExtensions do
203
203
  end
204
204
  end
205
205
 
206
-
206
+ context 'associate family' do
207
+ it 'should associate option family' do
208
+ cmd.extend_with(CmdExtensions.new(only: :option_family))
209
+ cmd.option_family associate: 'test' do
210
+ child '--test-three', '', ''
211
+ end
212
+ cmd.recognised_options.map(&:switches).flatten.must_equal ['-h', '--help', '--test-one', '--test-two', '--test-three']
213
+ end
214
+ end
207
215
  end
@@ -21,6 +21,24 @@ describe HammerCLI::Help::Builder do
21
21
  ' --zzz-option OPT_Z Some description'
22
22
  ].join("\n")
23
23
  end
24
+
25
+ it 'prints long option descriptions aligned' do
26
+ opt_a_desc = 'AAAAAAA ' * 20
27
+ opt_b_desc = 'BBBBBBB ' * 20
28
+ options = [
29
+ Clamp::Option::Definition.new(['--aaa-option'], 'OPT_A', opt_a_desc),
30
+ Clamp::Option::Definition.new(['--bbb-option'], 'OPT_B', opt_b_desc)
31
+ ]
32
+ help.add_list('Options', options)
33
+
34
+ help.string.strip.must_equal [
35
+ 'Options:',
36
+ ' --aaa-option OPT_A %s' % ('AAAAAAA ' * 10).strip,
37
+ ' %s' % ('AAAAAAA ' * 10).strip,
38
+ ' --bbb-option OPT_B %s' % ('BBBBBBB ' * 10).strip,
39
+ ' %s' % ('BBBBBBB ' * 10).strip
40
+ ].join("\n")
41
+ end
24
42
  end
25
43
 
26
44
  describe 'adding an option with lower case description' do
@@ -86,8 +104,8 @@ describe HammerCLI::Help::Builder do
86
104
 
87
105
  help.string.strip.must_equal [
88
106
  'Options:',
89
- ' --option[-yyy|-bbb] Some description',
90
- ' --option[-aaa|-zzz] Some description',
107
+ ' --option[-yyy|-bbb] VALUE Some description',
108
+ ' --option[-aaa|-zzz] VALUE Some description'
91
109
  ].join("\n")
92
110
  end
93
111
  end
@@ -25,6 +25,10 @@ describe HammerCLI::Options::OptionDefinition do
25
25
  option "--another-deprecated", "OLD_OPTION", "Test old option",
26
26
  :context_target => :old_option,
27
27
  :deprecated => "It is going to be removed"
28
+
29
+ def find_option(switch)
30
+ super(switch)
31
+ end
28
32
  end
29
33
 
30
34
  def opt_with_deprecation(deprecation)
@@ -73,6 +77,14 @@ describe HammerCLI::Options::OptionDefinition do
73
77
  context[:test_option].must_equal "VALUE"
74
78
  end
75
79
 
80
+ it "doesn't print deprecation warning if the option is not used" do
81
+ context = {}
82
+ cmd = TestDeprecatedOptionCmd.new('', context)
83
+ cmd.find_option('--deprecated')
84
+ _out, err = capture_io { cmd.run([]) }
85
+ err.must_equal ''
86
+ end
87
+
76
88
  it 'shows depracated message in help' do
77
89
  opt = opt_with_deprecation("Use --better-switch instead")
78
90
  opt.description.must_equal "Test option (Deprecated: Use --better-switch instead)"
@@ -98,4 +110,3 @@ describe HammerCLI::Options::OptionDefinition do
98
110
  end
99
111
  end
100
112
  end
101
-
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: 3.0.2
4
+ version: 3.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: 2022-02-01 00:00:00.000000000 Z
12
+ date: 2021-11-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: clamp
@@ -163,7 +163,7 @@ extra_rdoc_files:
163
163
  - doc/option_normalizers.md
164
164
  - doc/writing_a_plugin.md
165
165
  - doc/installation_source.md
166
- - doc/creating_commands.md
166
+ - doc/release_notes.md
167
167
  - doc/commands_extension.md
168
168
  - doc/commands_modification.md
169
169
  - doc/developer_docs.md
@@ -172,7 +172,7 @@ extra_rdoc_files:
172
172
  - doc/installation_rpm.md
173
173
  - doc/output.md
174
174
  - doc/review_checklist.md
175
- - doc/release_notes.md
175
+ - doc/creating_commands.md
176
176
  - config/cli.modules.d/module_config_template.yml
177
177
  - config/cli_config.template.yml
178
178
  - config/hammer.completion
@@ -443,8 +443,8 @@ test_files:
443
443
  - test/unit/options/sources/command_line_test.rb
444
444
  - test/unit/options/sources/saved_defaults_test.rb
445
445
  - test/unit/options/validators/dsl_test.rb
446
- - test/unit/options/option_family_test.rb
447
446
  - test/unit/options/normalizers_test.rb
447
+ - test/unit/options/option_family_test.rb
448
448
  - test/unit/options/option_definition_test.rb
449
449
  - test/unit/output/adapter/abstract_test.rb
450
450
  - test/unit/output/adapter/base_test.rb