hammer_cli 0.19.2 → 2.0.0

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