hammer_cli 0.19.2 → 2.0.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 (55) 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/creating_commands.md +27 -0
  6. data/doc/installation.md +47 -4
  7. data/doc/release_notes.md +17 -9
  8. data/lib/hammer_cli.rb +1 -0
  9. data/lib/hammer_cli/abstract.rb +32 -2
  10. data/lib/hammer_cli/apipie/api_connection.rb +5 -1
  11. data/lib/hammer_cli/apipie/command.rb +3 -2
  12. data/lib/hammer_cli/bash.rb +2 -0
  13. data/lib/hammer_cli/bash/completion.rb +159 -0
  14. data/lib/hammer_cli/bash/prebuild_command.rb +21 -0
  15. data/lib/hammer_cli/connection.rb +4 -0
  16. data/lib/hammer_cli/exception_handler.rb +10 -1
  17. data/lib/hammer_cli/main.rb +5 -3
  18. data/lib/hammer_cli/options/normalizers.rb +7 -3
  19. data/lib/hammer_cli/options/option_definition.rb +22 -0
  20. data/lib/hammer_cli/output/adapter/abstract.rb +1 -5
  21. data/lib/hammer_cli/output/adapter/base.rb +1 -1
  22. data/lib/hammer_cli/output/adapter/csv.rb +3 -2
  23. data/lib/hammer_cli/output/adapter/json.rb +14 -3
  24. data/lib/hammer_cli/output/adapter/silent.rb +1 -1
  25. data/lib/hammer_cli/output/adapter/table.rb +26 -7
  26. data/lib/hammer_cli/output/adapter/yaml.rb +6 -3
  27. data/lib/hammer_cli/output/output.rb +2 -4
  28. data/lib/hammer_cli/settings.rb +2 -1
  29. data/lib/hammer_cli/utils.rb +5 -0
  30. data/lib/hammer_cli/version.rb +1 -1
  31. data/locale/ca/LC_MESSAGES/hammer-cli.mo +0 -0
  32. data/locale/de/LC_MESSAGES/hammer-cli.mo +0 -0
  33. data/locale/en/LC_MESSAGES/hammer-cli.mo +0 -0
  34. data/locale/en_GB/LC_MESSAGES/hammer-cli.mo +0 -0
  35. data/locale/es/LC_MESSAGES/hammer-cli.mo +0 -0
  36. data/locale/fr/LC_MESSAGES/hammer-cli.mo +0 -0
  37. data/locale/it/LC_MESSAGES/hammer-cli.mo +0 -0
  38. data/locale/ja/LC_MESSAGES/hammer-cli.mo +0 -0
  39. data/locale/ko/LC_MESSAGES/hammer-cli.mo +0 -0
  40. data/locale/pt_BR/LC_MESSAGES/hammer-cli.mo +0 -0
  41. data/locale/ru/LC_MESSAGES/hammer-cli.mo +0 -0
  42. data/locale/zh_CN/LC_MESSAGES/hammer-cli.mo +0 -0
  43. data/locale/zh_TW/LC_MESSAGES/hammer-cli.mo +0 -0
  44. data/man/hammer.1.gz +0 -0
  45. data/test/unit/apipie/api_connection_test.rb +1 -0
  46. data/test/unit/bash_test.rb +138 -0
  47. data/test/unit/exception_handler_test.rb +44 -0
  48. data/test/unit/output/adapter/base_test.rb +58 -0
  49. data/test/unit/output/adapter/csv_test.rb +63 -1
  50. data/test/unit/output/adapter/json_test.rb +61 -0
  51. data/test/unit/output/adapter/table_test.rb +70 -1
  52. data/test/unit/output/adapter/yaml_test.rb +59 -0
  53. data/test/unit/output/output_test.rb +3 -3
  54. metadata +75 -68
  55. data/hammer_cli_complete +0 -13
@@ -0,0 +1,21 @@
1
+ module HammerCLI
2
+ module Bash
3
+ class PrebuildCompletionCommand < HammerCLI::AbstractCommand
4
+ def execute
5
+ map = HammerCLI::MainCommand.completion_map
6
+ cache_file = HammerCLI::Settings.get(:completion_cache_file)
7
+ cache_dir = File.dirname(cache_file)
8
+ FileUtils.mkdir_p(cache_dir) unless File.directory?(cache_dir)
9
+ File.write(File.expand_path(cache_file), map.to_json)
10
+
11
+ HammerCLI::EX_OK
12
+ end
13
+ end
14
+ end
15
+
16
+ HammerCLI::MainCommand.subcommand(
17
+ 'prebuild-bash-completion',
18
+ _('Prepare map of options and subcommands for Bash completion'),
19
+ HammerCLI::Bash::PrebuildCompletionCommand
20
+ )
21
+ end
@@ -35,6 +35,10 @@ module HammerCLI
35
35
  connections[name]
36
36
  end
37
37
 
38
+ def available
39
+ connections.select { |k, v| !v.nil? }.values.first
40
+ end
41
+
38
42
  private
39
43
 
40
44
  def connections
@@ -138,7 +138,16 @@ module HammerCLI
138
138
  end
139
139
 
140
140
  def handle_apipie_missing_arguments_error(e)
141
- message = _("Missing arguments for %s") % "'#{e.params.join("', '")}'"
141
+ params = e.params.map do |p|
142
+ param = p[/\[.+\]/]
143
+ param = if param.nil?
144
+ p.tr('_', '-')
145
+ else
146
+ p.scan(/\[[^\[\]]+\]/).first[1...-1].tr('_', '-')
147
+ end
148
+ "--#{param}"
149
+ end
150
+ message = _("Missing arguments for %s.") % "'#{params.uniq.join("', '")}'"
142
151
  print_error message
143
152
  log_full_error e, message
144
153
  HammerCLI::EX_USAGE
@@ -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
@@ -138,6 +138,28 @@ module HammerCLI
138
138
  end
139
139
  end
140
140
 
141
+ def completion_type(formatter = nil)
142
+ return { type: :flag } if @type == :flag
143
+
144
+ formatter ||= value_formatter
145
+ completion_type = case formatter
146
+ when HammerCLI::Options::Normalizers::Bool,
147
+ HammerCLI::Options::Normalizers::Enum
148
+ { type: :enum, values: value_formatter.allowed_values }
149
+ when HammerCLI::Options::Normalizers::EnumList
150
+ { type: :multienum, values: value_formatter.allowed_values }
151
+ when HammerCLI::Options::Normalizers::ListNested
152
+ { type: :schema, schema: value_formatter.schema.description(richtext: false) }
153
+ when HammerCLI::Options::Normalizers::List
154
+ { type: :list }
155
+ when HammerCLI::Options::Normalizers::KeyValueList
156
+ { type: :key_value_list }
157
+ when HammerCLI::Options::Normalizers::File
158
+ { type: :file }
159
+ end
160
+ completion_type || { type: :value }
161
+ end
162
+
141
163
  private
142
164
 
143
165
  def format_deprecation_msg(option_desc, deprecation_msg)
@@ -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
 
@@ -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
 
@@ -60,6 +60,11 @@ module HammerCLI
60
60
  STDOUT.tty?
61
61
  end
62
62
 
63
+ def self.clear_cache
64
+ cache_file = File.expand_path(HammerCLI::Settings.get(:completion_cache_file))
65
+ File.delete(cache_file) if File.exist?(cache_file)
66
+ end
67
+
63
68
  def self.interactive?
64
69
  return HammerCLI::Settings.get(:_params, :interactive) unless HammerCLI::Settings.get(:_params, :interactive).nil?
65
70
  HammerCLI::Settings.get(:ui, :interactive) != false
@@ -1,5 +1,5 @@
1
1
  module HammerCLI
2
2
  def self.version
3
- @version ||= Gem::Version.new "0.19.2"
3
+ @version ||= Gem::Version.new "2.0.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
data/man/hammer.1.gz CHANGED
Binary file
@@ -30,6 +30,7 @@ describe HammerCLI::Apipie::ApiConnection do
30
30
  it "logs message when logger is available" do
31
31
  logger = stub()
32
32
  logger.expects(:debug).with('Apipie cache was cleared')
33
+ logger.expects(:debug).with('Completion cache was cleared')
33
34
 
34
35
  api_stub.expects(:clean_cache)
35
36
  HammerCLI::Apipie::ApiConnection.new(empty_params, :reload_cache => true, :logger => logger)
@@ -0,0 +1,138 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ describe HammerCLI::Bash::Completion do
4
+ describe '#complete' do
5
+ let(:dict) do
6
+ {
7
+ 'host' => {
8
+ 'create' => {
9
+ '--installed-products-attributes' => { type: :schema, schema: '"product_id=string\,product_name=string\,arch=string\,version=string, ... "' },
10
+ '--help' => { type: :flag },
11
+ '--build' => { type: :flag },
12
+ '--managed' => { type: :enum, values: %w[yes no] },
13
+ '--volume' => { type: :multienum, values: %w[first second third] },
14
+ '--config-group-ids' => { type: :list },
15
+ '--params' => { type: :key_value_list },
16
+ '--log' => { type: :file, filter: '.*\.log$' },
17
+ '--pool' => { type: :directory },
18
+ '-t' => { type: :value },
19
+ :params => [{ type: :directory }, { type: :value }, { type: :file }]
20
+ },
21
+ '--dry' => { type: :flag },
22
+ '--help' => { type: :flag },
23
+ },
24
+ '--interactive' => { type: :enum, values: %w[yes no] },
25
+ '--help' => { type: :flag },
26
+ '-h' => { type: :flag }
27
+ }
28
+ end
29
+
30
+ subject do
31
+ HammerCLI::Bash::Completion.new(JSON.load(dict.to_json))
32
+ end
33
+
34
+ it 'returns options when no input given' do
35
+ result = subject.complete('').sort
36
+ result.must_equal ['host ', '--interactive ', '--help ', '-h '].sort
37
+ end
38
+
39
+ it 'returns filtered options when partial input is given' do
40
+ result = subject.complete('-').sort
41
+ result.must_equal ['--help ', '-h ', '--interactive '].sort
42
+ end
43
+
44
+ it 'returns filtered options when partial input is given' do
45
+ result = subject.complete('host')
46
+ result.must_equal ['host ']
47
+ end
48
+
49
+ it 'returns options when subcommand is given' do
50
+ result = subject.complete('host ').sort
51
+ result.must_equal ['create ', '--help ', '--dry '].sort
52
+ end
53
+
54
+ it 'returns no options when subcommand is wrong' do
55
+ result = subject.complete('unknown -h')
56
+ result.must_equal []
57
+ end
58
+
59
+ it 'returns no options when there are no other params allowed' do
60
+ result = subject.complete('host create /tmp some /tmp extra')
61
+ result.must_equal []
62
+ end
63
+
64
+ it 'return hint for option-value pair without value' do
65
+ result = subject.complete('host create -t ')
66
+ result.must_equal ['--->', 'Add option <value>']
67
+ end
68
+
69
+ it 'return no options for option-value pair without complete value' do
70
+ result = subject.complete('host create -t x')
71
+ result.must_equal []
72
+ end
73
+
74
+ # multiple options in one subcommand
75
+ it 'allows mutiple options of the same subcommand' do
76
+ result = subject.complete('host create --build --he')
77
+ result.must_equal ['--help ']
78
+ end
79
+
80
+ # multiple options with values in one subcommand
81
+ it 'allows mutiple options with values of the same subcommand' do
82
+ result = subject.complete('host create -t value --he')
83
+ result.must_equal ['--help ']
84
+ end
85
+
86
+ # subcommand after options
87
+ it 'allows subcommand after options' do
88
+ result = subject.complete('host --dry crea')
89
+ result.must_equal ['create ']
90
+ end
91
+
92
+ describe 'completion by type' do
93
+ it 'completes :value' do
94
+ result = subject.complete('host create -t ')
95
+ result.must_equal ['--->', 'Add option <value>']
96
+ end
97
+
98
+ it 'completes :flag' do
99
+ result = subject.complete('host --h')
100
+ result.must_equal ['--help ']
101
+ end
102
+
103
+ it 'completes :schema' do
104
+ result = subject.complete('host create --installed-products-attributes ')
105
+ result.must_equal ["--->", 'Add value by following schema: "product_id=string\,product_name=string\,arch=string\,version=string, ... "']
106
+ end
107
+
108
+ it 'completes :enum' do
109
+ result = subject.complete('host create --managed ')
110
+ result.must_equal ['yes ', 'no ']
111
+ end
112
+
113
+ it 'completes :multienum' do
114
+ result = subject.complete('host create --volume ')
115
+ result.must_equal ['first', 'second', 'third']
116
+
117
+ result = subject.complete('host create --volume fir')
118
+ result.must_equal ['first']
119
+
120
+ result = subject.complete('host create --volume first,')
121
+ result.must_equal ['second', 'third']
122
+
123
+ result = subject.complete('host create --volume first,se')
124
+ result.must_equal ['first,second']
125
+ end
126
+
127
+ it 'completes :list' do
128
+ result = subject.complete('host create --config-group-ids ')
129
+ result.must_equal ['--->', 'Add comma-separated list of values']
130
+ end
131
+
132
+ it 'completes :key_value_list' do
133
+ result = subject.complete('host create --params ')
134
+ result.must_equal ['--->', 'Add comma-separated list of key=value']
135
+ end
136
+ end
137
+ end
138
+ end