hammer_cli 0.19.1 → 2.2.0

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