hammer_cli 3.0.2 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
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