hammer_cli 0.19.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -10,7 +10,11 @@ module HammerCLI::Apipie
10
10
  @api = ApipieBindings::API.new(params, HammerCLI::SSLOptions.new.get_options(params[:uri]))
11
11
  if options[:reload_cache]
12
12
  @api.clean_cache
13
- @logger.debug 'Apipie cache was cleared' unless @logger.nil?
13
+ HammerCLI.clear_cache
14
+ unless @logger.nil?
15
+ @logger.debug 'Apipie cache was cleared'
16
+ @logger.debug 'Completion cache was cleared'
17
+ end
14
18
  end
15
19
  end
16
20
 
@@ -65,8 +65,8 @@ module HammerCLI::Apipie
65
65
  method_options(options)
66
66
  end
67
67
 
68
- def print_data(data)
69
- print_collection(output_definition, data) unless output_definition.empty?
68
+ def print_data(data, options = {})
69
+ print_collection(output_definition, data, options) unless output_definition.empty?
70
70
  print_success_message(data) unless success_message.nil?
71
71
  end
72
72
 
@@ -86,6 +86,7 @@ module HammerCLI::Apipie
86
86
  declared_options << option
87
87
  block ||= option.default_conversion_block
88
88
  define_accessors_for(option, &block)
89
+ completion_type_for(option, opts)
89
90
  end
90
91
  extend_options_help(option) if option.value_formatter.is_a?(HammerCLI::Options::Normalizers::ListNested)
91
92
  option
@@ -41,12 +41,11 @@ module HammerCLI::Apipie
41
41
  end
42
42
 
43
43
  def create_option(param, resource_name_map)
44
- option(
45
- option_switch(param, resource_name_map),
46
- option_type(param, resource_name_map),
47
- option_desc(param),
48
- option_opts(param)
49
- )
44
+ family = HammerCLI::Options::OptionFamily.new
45
+ family.parent(option_switch(param, resource_name_map),
46
+ option_type(param, resource_name_map),
47
+ option_desc(param),
48
+ option_opts(param, resource_name_map))
50
49
  end
51
50
 
52
51
  def option_switch(param, resource_name_map)
@@ -61,7 +60,7 @@ module HammerCLI::Apipie
61
60
  param.description || " "
62
61
  end
63
62
 
64
- def option_opts(param)
63
+ def option_opts(param, resource_name_map)
65
64
  opts = {}
66
65
  opts[:required] = true if (param.required? and require_options?)
67
66
  if param.expected_type.to_s == 'array'
@@ -80,19 +79,22 @@ module HammerCLI::Apipie
80
79
  end
81
80
  opts[:attribute_name] = HammerCLI.option_accessor_name(param.name)
82
81
  opts[:referenced_resource] = resource_name(param)
82
+ opts[:aliased_resource] = aliased_name(resource_name(param), resource_name_map)
83
83
 
84
84
  return opts
85
85
  end
86
86
 
87
+ def aliased_name(name, resource_name_map)
88
+ return if name.nil?
89
+
90
+ resource_name_map[name.to_s] || resource_name_map[name.to_sym] || name
91
+ end
92
+
87
93
  def aliased(param, resource_name_map)
88
94
  resource_name = resource_name(param)
95
+ return param.name if resource_name.nil?
89
96
 
90
- if resource_name.nil?
91
- return param.name
92
- else
93
- aliased_name = resource_name_map[resource_name.to_s] || resource_name_map[resource_name.to_sym] || resource_name
94
- return param.name.gsub(resource_name, aliased_name.to_s)
95
- end
97
+ param.name.gsub(resource_name, aliased_name(resource_name, resource_name_map).to_s)
96
98
  end
97
99
 
98
100
  def resource_name(param)
@@ -1,21 +1,23 @@
1
1
  require File.join(File.dirname(__FILE__), 'options')
2
2
 
3
3
  module HammerCLI::Apipie
4
-
5
4
  class OptionDefinition < HammerCLI::Options::OptionDefinition
6
-
7
- attr_accessor :referenced_resource
5
+ attr_accessor :referenced_resource, :aliased_resource, :family
8
6
 
9
7
  def initialize(switches, type, description, options = {})
10
- if options.has_key? :referenced_resource
11
- self.referenced_resource = options.delete(:referenced_resource).to_s if options[:referenced_resource]
12
- end
8
+ @referenced_resource = options[:referenced_resource].to_s if options[:referenced_resource]
9
+ @aliased_resource = options[:aliased_resource].to_s if options[:aliased_resource]
10
+ @family = options[:family]
13
11
  super
14
12
  # Apipie currently sends descriptions as escaped HTML once this is changed this should be removed.
15
13
  # See #15198 on Redmine.
16
14
  @description = CGI::unescapeHTML(description)
17
15
  end
18
16
 
19
- end
17
+ def child?
18
+ return unless @family
20
19
 
20
+ @family.children.include?(self)
21
+ end
22
+ end
21
23
  end
@@ -0,0 +1,2 @@
1
+ require 'hammer_cli/bash/prebuild_command'
2
+ require 'hammer_cli/bash/completion'
@@ -0,0 +1,159 @@
1
+ require 'json'
2
+
3
+ module HammerCLI
4
+ module Bash
5
+ class Completion
6
+ def initialize(dict)
7
+ @dict = dict
8
+ end
9
+
10
+ def complete(line)
11
+ @complete_line = line.end_with?(' ')
12
+ full_path = line.split(' ')
13
+ complete_path = @complete_line ? full_path : full_path[0..-2]
14
+ dict, path = traverse_tree(@dict, complete_path)
15
+
16
+ return [] unless path.empty? # lost during traversing
17
+
18
+ partial = @complete_line ? '' : full_path.last
19
+ finish_word(dict, partial)
20
+ end
21
+
22
+ def self.load_description(path)
23
+ JSON.load(File.open(path))
24
+ rescue Errno::ENOENT
25
+ {}
26
+ end
27
+
28
+ private
29
+
30
+ def finish_word(dict, incomplete)
31
+ finish_option_value(dict, incomplete) ||
32
+ (finish_option_or_subcommand(dict, incomplete) + finish_param(dict, incomplete))
33
+ end
34
+
35
+ def finish_option_or_subcommand(dict, incomplete)
36
+ dict.keys.select { |k| k.is_a?(String) && k =~ /^#{incomplete}/ }.map { |k| k + ' ' }
37
+ end
38
+
39
+ def complete_value(value_description, partial, is_param)
40
+ case value_description['type']
41
+ when 'value'
42
+ if !partial.empty?
43
+ []
44
+ elsif is_param
45
+ ['--->', 'Add parameter']
46
+ else
47
+ ['--->', 'Add option <value>']
48
+ end
49
+ when 'directory'
50
+ directories(partial)
51
+ when 'file'
52
+ files(partial, value_description)
53
+ when 'enum'
54
+ enum(partial, value_description['values'])
55
+ when 'multienum'
56
+ multienum(partial, value_description['values'])
57
+ when 'schema'
58
+ schema(value_description['schema'])
59
+ when 'list'
60
+ ['--->', 'Add comma-separated list of values']
61
+ when 'key_value_list'
62
+ ['--->', 'Add comma-separated list of key=value']
63
+ end
64
+ end
65
+
66
+ def finish_param(dict, incomplete)
67
+ if dict['params'] && !dict['params'].empty?
68
+ complete_value(dict['params'].first, incomplete, true)
69
+ else
70
+ []
71
+ end
72
+ end
73
+
74
+ def finish_option_value(dict, incomplete)
75
+ complete_value(dict, incomplete, false) if dict.key?('type')
76
+ end
77
+
78
+ def traverse_tree(dict, path)
79
+ return [dict, []] if path.nil? || path.empty?
80
+ result = if dict.key?(path.first)
81
+ if path.first.start_with?('-')
82
+ parse_option(dict, path)
83
+ else
84
+ parse_subcommand(dict, path)
85
+ end
86
+ elsif dict['params']
87
+ # traverse params one by one
88
+ parse_params(dict, path)
89
+ else
90
+ # not found
91
+ [{}, path]
92
+ end
93
+ result
94
+ end
95
+
96
+ def parse_params(dict, path)
97
+ traverse_tree({ 'params' => dict['params'][1..-1] }, path[1..-1])
98
+ end
99
+
100
+ def parse_subcommand(dict, path)
101
+ traverse_tree(dict[path.first], path[1..-1])
102
+ end
103
+
104
+ def parse_option(dict, path)
105
+ if dict[path.first]['type'] == 'flag' # flag
106
+ traverse_tree(dict, path[1..-1])
107
+ elsif path.length >= 2 # option with value
108
+ traverse_tree(dict, path[2..-1])
109
+ else # option with value missing
110
+ [dict[path.first], path[1..-1]]
111
+ end
112
+ end
113
+
114
+ def directories(partial = '')
115
+ dirs = []
116
+ dirs += Dir.glob("#{partial}*").select { |f| File.directory?(f) }
117
+ dirs = dirs.map { |d| d + '/' } if dirs.length == 1
118
+ dirs
119
+ end
120
+
121
+ def files(partial = '', opts = {})
122
+ filter = opts.fetch('filter', '.*')
123
+ file_names = []
124
+ file_names += Dir.glob("#{partial}*").select do |f|
125
+ File.directory?(f) || f =~ /#{filter}/
126
+ end
127
+ file_names.map { |f| File.directory?(f) ? f + '/' : f + ' ' }
128
+ end
129
+
130
+ def enum(partial = '', values = [])
131
+ values.select { |v| v.start_with?(partial) }.map { |v| v + ' ' }
132
+ end
133
+
134
+ def multienum(partial = '', values = [])
135
+ return values if partial.empty?
136
+
137
+ parts = partial.split(',')
138
+ resolved = []
139
+ to_complete = parts.each_with_object([]) do |part, res|
140
+ next resolved << part if values.include?(part)
141
+
142
+ res << part
143
+ end
144
+
145
+ hints = to_complete.map do |p|
146
+ values.select { |v| v.start_with?(p) }
147
+ end.flatten(1).uniq
148
+ return values - parts if hints.empty?
149
+ return [(resolved + hints).join(',')] if hints.size == 1
150
+
151
+ hints
152
+ end
153
+
154
+ def schema(template = '')
155
+ ['--->', "Add value by following schema: #{template}"]
156
+ end
157
+ end
158
+ end
159
+ end
@@ -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
@@ -15,7 +15,7 @@ module HammerCLI
15
15
  ALLOWED_EXTENSIONS = %i[
16
16
  option command_options before_print data output help request
17
17
  request_headers headers request_options options request_params params
18
- option_sources predefined_options use_option
18
+ option_sources predefined_options use_option option_family
19
19
  ].freeze
20
20
 
21
21
  def initialize(options = {})
@@ -86,6 +86,11 @@ module HammerCLI
86
86
  @option_sources_block = block
87
87
  end
88
88
 
89
+ def self.option_family(options = {}, &block)
90
+ @option_family_opts = options
91
+ @option_family_block = block
92
+ end
93
+
89
94
  # Object
90
95
 
91
96
  def extend_options(command_class)
@@ -151,6 +156,13 @@ module HammerCLI
151
156
  self.class.extend_option_sources(sources, command)
152
157
  end
153
158
 
159
+ def extend_option_family(command_class)
160
+ allowed = @only & %i[option_family]
161
+ return if allowed.empty? || (allowed & @except).any?
162
+
163
+ self.class.extend_option_family(command_class)
164
+ end
165
+
154
166
  def delegatee(command_class)
155
167
  self.class.delegatee = command_class
156
168
  end
@@ -234,5 +246,13 @@ module HammerCLI
234
246
  @option_sources_block.call(sources, command)
235
247
  logger.debug("Called block for #{@delegatee} option sources:\n\t#{@option_sources_block}")
236
248
  end
249
+
250
+ def self.extend_option_family(command_class)
251
+ return if @option_family_block.nil?
252
+
253
+ @option_family_opts[:creator] = command_class
254
+ command_class.send(:option_family, @option_family_opts, &@option_family_block)
255
+ logger.debug("Called option family block for #{command_class}:\n\t#{@option_family_block}")
256
+ end
237
257
  end
238
258
  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
@@ -69,7 +69,7 @@ module HammerCLI
69
69
 
70
70
  def handle_usage_exception(e)
71
71
  print_error (_("Error: %{message}") + "\n\n" +
72
- _("See: '%{path} --help'.")) % {:message => e.message, :path => e.command.invocation_path}
72
+ _("See: '%{path} --help'.")) % { message: e.message, path: HammerCLI.expand_invocation_path(e.command.invocation_path) }
73
73
  log_full_error e
74
74
  HammerCLI::EX_USAGE
75
75
  end
@@ -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
@@ -6,8 +6,11 @@ module HammerCLI
6
6
 
7
7
  def execute
8
8
  @adapter = option_md? ? MDAdapter.new : TxtAdapter.new
9
+ HammerCLI.context[:full_help] = true
10
+ @invocation_paths = {}
9
11
  print_heading
10
12
  print_help
13
+ HammerCLI.context[:full_help] = false
11
14
  HammerCLI::EX_OK
12
15
  end
13
16
 
@@ -19,9 +22,13 @@ module HammerCLI
19
22
  end
20
23
 
21
24
  def print_help(name='hammer', command=HammerCLI::MainCommand, desc='')
22
- @adapter.print_command(name, desc, command.new(name).help)
25
+ @invocation_paths[name] ||= []
26
+ @adapter.print_command(name, desc, command.new(name, path: @invocation_paths[name]).help)
23
27
 
24
28
  command.recognised_subcommands.each do |sub_cmd|
29
+ path = "#{name} #{sub_cmd.names.first}"
30
+ @invocation_paths[path] ||= []
31
+ @invocation_paths[path] += @invocation_paths[name]
25
32
  print_help(@adapter.command_name(name, sub_cmd.names.first), sub_cmd.subcommand_class, sub_cmd.description)
26
33
  end
27
34
  end
@@ -14,7 +14,7 @@ module HammerCLI
14
14
  def add_usage(invocation_path, usage_descriptions)
15
15
  heading(Clamp.message(:usage_heading))
16
16
  usage_descriptions.each do |usage|
17
- puts " #{invocation_path} #{usage}".rstrip
17
+ puts " #{HammerCLI.expand_invocation_path(invocation_path)} #{usage}".rstrip
18
18
  end
19
19
  end
20
20
 
@@ -29,12 +29,19 @@ module HammerCLI
29
29
 
30
30
  label_width = DEFAULT_LABEL_INDENT
31
31
  items.each do |item|
32
- label, description = item.help
32
+ label = item.help.first
33
33
  label_width = label.size if label.size > label_width
34
34
  end
35
35
 
36
36
  items.each do |item|
37
- label, description = item.help
37
+ if item.respond_to?(:child?) && item.child?
38
+ next unless HammerCLI.context[:full_help]
39
+ end
40
+ 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
38
45
  description.gsub(/^(.)/) { Unicode::capitalize($1) }.each_line do |line|
39
46
  puts " %-#{label_width}s %s" % [label, line]
40
47
  label = ''
@@ -54,6 +61,25 @@ module HammerCLI
54
61
  label = HighLine.color(label, :bold) if @richtext
55
62
  puts label
56
63
  end
64
+
65
+ private
66
+
67
+ def expand_invocation_path(path)
68
+ bits = path.split(' ')
69
+ parent_command = HammerCLI::MainCommand
70
+ new_path = (bits[1..-1] || []).each_with_object([]) do |bit, names|
71
+ subcommand = parent_command.find_subcommand(bit)
72
+ next if subcommand.nil?
73
+
74
+ names << if subcommand.names.size > 1
75
+ "<#{subcommand.names.join('|')}>"
76
+ else
77
+ subcommand.names.first
78
+ end
79
+ parent_command = subcommand.subcommand_class
80
+ end
81
+ new_path.unshift(bits.first).join(' ')
82
+ end
57
83
  end
58
84
  end
59
85
  end