hammer_cli 0.19.1 → 2.1.2

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/release_notes.md +32 -6
  9. data/lib/hammer_cli.rb +1 -0
  10. data/lib/hammer_cli/abstract.rb +61 -4
  11. data/lib/hammer_cli/apipie/api_connection.rb +5 -1
  12. data/lib/hammer_cli/apipie/command.rb +3 -2
  13. data/lib/hammer_cli/apipie/option_builder.rb +15 -13
  14. data/lib/hammer_cli/apipie/option_definition.rb +9 -7
  15. data/lib/hammer_cli/bash.rb +2 -0
  16. data/lib/hammer_cli/bash/completion.rb +159 -0
  17. data/lib/hammer_cli/bash/prebuild_command.rb +21 -0
  18. data/lib/hammer_cli/command_extensions.rb +21 -1
  19. data/lib/hammer_cli/connection.rb +4 -0
  20. data/lib/hammer_cli/exception_handler.rb +11 -2
  21. data/lib/hammer_cli/full_help.rb +8 -1
  22. data/lib/hammer_cli/help/builder.rb +10 -3
  23. data/lib/hammer_cli/logger_watch.rb +1 -1
  24. data/lib/hammer_cli/main.rb +5 -3
  25. data/lib/hammer_cli/options/normalizers.rb +7 -3
  26. data/lib/hammer_cli/options/option_definition.rb +26 -6
  27. data/lib/hammer_cli/options/option_family.rb +114 -0
  28. data/lib/hammer_cli/options/predefined.rb +1 -1
  29. data/lib/hammer_cli/output/adapter/abstract.rb +1 -5
  30. data/lib/hammer_cli/output/adapter/base.rb +1 -1
  31. data/lib/hammer_cli/output/adapter/csv.rb +3 -2
  32. data/lib/hammer_cli/output/adapter/json.rb +14 -3
  33. data/lib/hammer_cli/output/adapter/silent.rb +1 -1
  34. data/lib/hammer_cli/output/adapter/table.rb +27 -8
  35. data/lib/hammer_cli/output/adapter/yaml.rb +6 -3
  36. data/lib/hammer_cli/output/output.rb +2 -4
  37. data/lib/hammer_cli/settings.rb +2 -1
  38. data/lib/hammer_cli/subcommand.rb +25 -1
  39. data/lib/hammer_cli/testing/command_assertions.rb +2 -2
  40. data/lib/hammer_cli/utils.rb +22 -0
  41. data/lib/hammer_cli/version.rb +1 -1
  42. data/locale/ca/LC_MESSAGES/hammer-cli.mo +0 -0
  43. data/locale/de/LC_MESSAGES/hammer-cli.mo +0 -0
  44. data/locale/en/LC_MESSAGES/hammer-cli.mo +0 -0
  45. data/locale/en_GB/LC_MESSAGES/hammer-cli.mo +0 -0
  46. data/locale/es/LC_MESSAGES/hammer-cli.mo +0 -0
  47. data/locale/fr/LC_MESSAGES/hammer-cli.mo +0 -0
  48. data/locale/it/LC_MESSAGES/hammer-cli.mo +0 -0
  49. data/locale/ja/LC_MESSAGES/hammer-cli.mo +0 -0
  50. data/locale/ko/LC_MESSAGES/hammer-cli.mo +0 -0
  51. data/locale/pt_BR/LC_MESSAGES/hammer-cli.mo +0 -0
  52. data/locale/ru/LC_MESSAGES/hammer-cli.mo +0 -0
  53. data/locale/zh_CN/LC_MESSAGES/hammer-cli.mo +0 -0
  54. data/locale/zh_TW/LC_MESSAGES/hammer-cli.mo +0 -0
  55. data/man/hammer.1.gz +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 +82 -71
  71. data/hammer_cli_complete +0 -13
@@ -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 = ''
@@ -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')