cliqr 1.1.0 → 1.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -1
  3. data/README.md +97 -71
  4. data/examples/README.md +12 -0
  5. data/examples/hbase +58 -0
  6. data/examples/my-command +63 -0
  7. data/examples/numbers +55 -0
  8. data/examples/vagrant +90 -0
  9. data/lib/cliqr.rb +17 -2
  10. data/lib/cliqr/argument_validation/argument_type_validator.rb +2 -2
  11. data/lib/cliqr/argument_validation/validator.rb +1 -1
  12. data/lib/cliqr/cli/argument_operator.rb +44 -0
  13. data/lib/cliqr/cli/argument_operator_context.rb +20 -0
  14. data/lib/cliqr/cli/command.rb +1 -1
  15. data/lib/cliqr/cli/command_context.rb +93 -12
  16. data/lib/cliqr/cli/command_runner_factory.rb +2 -2
  17. data/lib/cliqr/cli/config.rb +301 -33
  18. data/lib/cliqr/cli/executor.rb +14 -9
  19. data/lib/cliqr/cli/interface.rb +22 -7
  20. data/lib/cliqr/cli/router.rb +6 -2
  21. data/lib/cliqr/cli/shell_command.rb +69 -0
  22. data/lib/cliqr/cli/usage_builder.rb +185 -0
  23. data/lib/cliqr/config_validation/validator_factory.rb +59 -5
  24. data/lib/cliqr/error.rb +10 -4
  25. data/lib/cliqr/parser/action_token.rb +23 -0
  26. data/lib/cliqr/parser/argument_parser.rb +1 -1
  27. data/lib/cliqr/parser/argument_token.rb +1 -4
  28. data/lib/cliqr/parser/argument_tree_walker.rb +40 -8
  29. data/lib/cliqr/parser/option_token.rb +2 -1
  30. data/lib/cliqr/parser/parsed_input.rb +21 -2
  31. data/lib/cliqr/parser/parsed_input_builder.rb +11 -7
  32. data/lib/cliqr/parser/token.rb +3 -9
  33. data/lib/cliqr/parser/token_factory.rb +1 -1
  34. data/lib/cliqr/util.rb +135 -0
  35. data/lib/cliqr/version.rb +1 -1
  36. data/spec/argument_parser_spec_helper.rb +15 -0
  37. data/spec/config/action_config_validator_spec.rb +146 -0
  38. data/spec/config/config_finalize_spec.rb +1 -1
  39. data/spec/config/config_validator_spec.rb +29 -19
  40. data/spec/config/option_config_validator_spec.rb +13 -13
  41. data/spec/dsl/interface_spec.rb +1 -168
  42. data/spec/dsl/usage_spec.rb +705 -0
  43. data/spec/executor/action_executor_spec.rb +205 -0
  44. data/spec/executor/executor_spec.rb +405 -17
  45. data/spec/executor/help_executor_spec.rb +424 -0
  46. data/spec/executor/shell_executor_spec.rb +233 -0
  47. data/spec/fixtures/action_reader_command.rb +12 -0
  48. data/spec/fixtures/csv_argument_operator.rb +8 -0
  49. data/spec/fixtures/test_option_type_checker_command.rb +8 -0
  50. data/spec/parser/action_argument_parser_spec.rb +113 -0
  51. data/spec/parser/argument_parser_spec.rb +37 -44
  52. data/spec/spec_helper.rb +1 -0
  53. data/spec/validation/action_argument_validator_spec.rb +50 -0
  54. data/spec/validation/{argument_validation_spec.rb → command_argument_validation_spec.rb} +36 -18
  55. data/spec/validation/error_spec.rb +1 -1
  56. data/tasks/rdoc.rake +16 -0
  57. data/tasks/rubucop.rake +14 -0
  58. data/tasks/yard.rake +21 -0
  59. data/templates/usage.erb +39 -0
  60. metadata +48 -11
@@ -14,7 +14,6 @@ module Cliqr
14
14
  # Create a new command executor
15
15
  def initialize(config)
16
16
  @config = config
17
- @router = Router.new(config)
18
17
  @validator = Cliqr::ArgumentValidation::Validator.new
19
18
  end
20
19
 
@@ -25,12 +24,17 @@ module Cliqr
25
24
  #
26
25
  # @return [Integer] Exit status of the command execution
27
26
  def execute(args, options)
28
- parsed_input = parse(args)
27
+ args = Cliqr::Util.sanitize_args(args, @config)
28
+ action_config, parsed_input = parse(args)
29
29
  begin
30
- command_context = CommandContext.build(parsed_input)
31
- @router.handle command_context, **options
30
+ command_context = CommandContext.build(action_config, parsed_input, options) \
31
+ do |forwarded_args, forwarded_options|
32
+ execute(forwarded_args, options.merge(forwarded_options))
33
+ end
34
+ Router.new(action_config).handle(command_context, **options)
32
35
  rescue StandardError => e
33
- raise Cliqr::Error::CommandRuntimeException.new("command '#{@config.basename}' failed", e)
36
+ raise Cliqr::Error::CommandRuntimeError.new(
37
+ "command '#{action_config.command}' failed", e)
34
38
  end
35
39
  end
36
40
 
@@ -43,11 +47,12 @@ module Cliqr
43
47
  # @throws [Cliqr::Error::ValidationError] If the input arguments do not satisfy validation
44
48
  # criteria
45
49
  #
46
- # @return [Cliqr::Parser::ParsedInput] Parsed arguments wrapper
50
+ # @return [Cliqr::Parser::ParsedInput] Parsed [Cliqr::CLI::Config] instance and arguments
51
+ # wrapper
47
52
  def parse(args)
48
- parsed_input = Parser.parse(@config, args)
49
- @validator.validate(parsed_input, @config)
50
- parsed_input
53
+ action_config, parsed_input = Parser.parse(@config, args)
54
+ @validator.validate(parsed_input, action_config)
55
+ [action_config, parsed_input]
51
56
  end
52
57
  end
53
58
  end
@@ -2,10 +2,18 @@
2
2
 
3
3
  require 'cliqr/error'
4
4
  require 'cliqr/cli/executor'
5
+ require 'cliqr/cli/usage_builder'
5
6
 
6
7
  module Cliqr
7
8
  # Definition and builder for command line interface
8
9
  module CLI
10
+ # Exit code hash map
11
+ EXIT_CODE = {
12
+ success: 0,
13
+ 'Cliqr::Error::CommandRuntimeError'.to_sym => 1,
14
+ 'Cliqr::Error::IllegalArgumentError'.to_sym => 2
15
+ }
16
+
9
17
  # A CLI interface instance which is the entry point for all CLI commands.
10
18
  #
11
19
  # @api private
@@ -27,12 +35,7 @@ module Cliqr
27
35
  #
28
36
  # @return [String] Defines usage of this interface
29
37
  def usage
30
- template_file_path = File.expand_path('../../../../templates/usage.erb', __FILE__)
31
- template = ERB.new(File.new(template_file_path).read, nil, '%')
32
- result = template.result(@config.instance_eval { binding })
33
-
34
- # remove multiple newlines from the end of usage
35
- "#{result.strip}\n"
38
+ UsageBuilder.build(config)
36
39
  end
37
40
 
38
41
  # Execute a command
@@ -42,8 +45,20 @@ module Cliqr
42
45
  #
43
46
  # @return [Integer] Exit code of the command execution
44
47
  def execute(args = [], **options)
48
+ execute_internal(args, options)
49
+ Cliqr::CLI::EXIT_CODE[:success]
50
+ rescue Cliqr::Error::CliqrError => e
51
+ puts e.message
52
+ Cliqr::CLI::EXIT_CODE[e.class.to_s.to_sym]
53
+ end
54
+
55
+ # Executes a command without handling error conditions
56
+ #
57
+ # @return [Integer] Exit code
58
+ def execute_internal(args = [], **options)
45
59
  options = {
46
- :output => :default
60
+ :output => :default,
61
+ :environment => :bash
47
62
  }.merge(options)
48
63
  @executor.execute(args, options)
49
64
  end
@@ -24,10 +24,14 @@ module Cliqr
24
24
  #
25
25
  # @return [Integer] Exit code of the command execution
26
26
  def handle(context, **options)
27
- handler = @config.handler.new
27
+ handler = @config.handler
28
28
  runner = CommandRunnerFactory.get(options)
29
29
  runner.run do
30
- handler.execute(context)
30
+ if handler.is_a?(Proc)
31
+ context.instance_eval(&handler)
32
+ else
33
+ handler.execute(context)
34
+ end
31
35
  end
32
36
  end
33
37
  end
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/cli/command'
4
+
5
+ module Cliqr
6
+ # @api private
7
+ module CLI
8
+ # The default command executed to run a shell action
9
+ #
10
+ # @api private
11
+ class ShellCommand < Cliqr::CLI::Command
12
+ # Start a shell in the context of some other command
13
+ #
14
+ # @return [Integer] Exit code
15
+ def execute(context)
16
+ fail(Cliqr::Error::IllegalCommandError,
17
+ 'Cannot run another shell within an already running shell') unless context.bash?
18
+
19
+ base_command = context.command[0...(context.command.rindex('shell'))].strip
20
+ puts "Starting shell for command \"#{base_command}\""
21
+ exit_code = ShellRunner.new(base_command, context).run
22
+ puts "shell exited with code #{exit_code}"
23
+ exit_code
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # The runner for shell command
30
+ class ShellRunner
31
+ # Create the runner instance
32
+ def initialize(base_command, context)
33
+ @base_command = base_command
34
+ @context = context
35
+ end
36
+
37
+ # Start shell
38
+ #
39
+ # @return [Integer] Exit code
40
+ def run
41
+ loop do
42
+ command = prompt("#{@base_command} > ")
43
+ execute(command) unless command == 'exit'
44
+ break if command == 'exit'
45
+ end
46
+ 0
47
+ end
48
+
49
+ private
50
+
51
+ # Execute a shell command
52
+ #
53
+ # @return [Integer] Exit code of the command executed
54
+ def execute(command)
55
+ @context.forward("#{@base_command} #{command}", :environment => :cliqr_shell)
56
+ rescue StandardError => e
57
+ puts e.message
58
+ end
59
+
60
+ # Show a prompt and ask for input
61
+ #
62
+ # @return [String]
63
+ def prompt(prefix = '')
64
+ print prefix
65
+ $stdin.gets.chomp
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,185 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/error'
4
+ require 'cliqr/cli/executor'
5
+ require 'erb'
6
+
7
+ module Cliqr
8
+ module CLI
9
+ # Builds the usage information based on the configuration settings
10
+ #
11
+ # @api private
12
+ class UsageBuilder
13
+ # Build the usage information
14
+ #
15
+ # @param [Cliqr::CLI::Config] config Configuration of the command line interface
16
+ #
17
+ # @return [String]
18
+ def self.build(config)
19
+ template_file_path = File.expand_path('../../../../templates/usage.erb', __FILE__)
20
+ template = ERB.new(File.new(template_file_path).read, nil, '%')
21
+ usage_context = CommandUsageContext.new(config)
22
+ result = template.result(usage_context.instance_eval { binding })
23
+
24
+ # remove multiple newlines from the end of usage
25
+ "#{result.strip}\n"
26
+ end
27
+ end
28
+
29
+ # The context in which the usage template will be executed
30
+ #
31
+ # @api private
32
+ class CommandUsageContext
33
+ # Name of the current command in context
34
+ #
35
+ # @return [String]
36
+ attr_reader :name
37
+
38
+ # Description of the current command
39
+ #
40
+ # @return [String]
41
+ attr_reader :description
42
+
43
+ # Pre-configured command's actions
44
+ #
45
+ # @return [Array<Cliqr::CLI::CommandUsageContext>]
46
+ attr_reader :actions
47
+
48
+ # List of options configured for current context
49
+ #
50
+ # @return [Array<Cliqr::CLI::OptionUsageContext>]
51
+ attr_reader :options
52
+
53
+ # Command for the current context
54
+ #
55
+ # @return [String]
56
+ attr_reader :command
57
+
58
+ # Wrap a [Cliqr::CLI::Config] instance for usage template
59
+ def initialize(config)
60
+ @config = config
61
+
62
+ @name = config.name
63
+ @description = config.description
64
+ @actions = @config.actions.map { |action| CommandUsageContext.new(action) }
65
+ @options = @config.options.map { |option| OptionUsageContext.new(option) }
66
+ @command = @config.command
67
+ end
68
+
69
+ # Check if command has a description
70
+ def description?
71
+ non_empty?(@description)
72
+ end
73
+
74
+ # Check if there are any preconfigured options
75
+ def options?
76
+ non_empty?(@config.options)
77
+ end
78
+
79
+ # Check if current command allows arguments
80
+ def arguments?
81
+ @config.arguments == Cliqr::CLI::ENABLE_CONFIG
82
+ end
83
+
84
+ # Check if current command has any actions
85
+ def actions?
86
+ non_empty?(@actions)
87
+ end
88
+
89
+ # Check if the help is enabled
90
+ #
91
+ # @return [Boolean]
92
+ def help?
93
+ @config.help?
94
+ end
95
+
96
+ private
97
+
98
+ # Check if a obj is non-empty
99
+ def non_empty?(obj)
100
+ !(obj.nil? || obj.empty?)
101
+ end
102
+ end
103
+
104
+ # Wrapper of [Cliqr::CLI::OptionConfig] to be used in usage rendering
105
+ #
106
+ # @api private
107
+ class OptionUsageContext
108
+ # Name of the option
109
+ #
110
+ # @return [String]
111
+ attr_reader :name
112
+
113
+ # Short name of the option
114
+ #
115
+ # @return [String]
116
+ attr_reader :short
117
+
118
+ # Option's type
119
+ #
120
+ # @return [Symbol]
121
+ attr_reader :type
122
+
123
+ # Option's description
124
+ #
125
+ # @return [String]
126
+ attr_reader :description
127
+
128
+ # Default value for this option
129
+ #
130
+ # @return [Object]
131
+ attr_reader :default
132
+
133
+ # Create a new option usage context
134
+ def initialize(option_config)
135
+ @option_config = option_config
136
+
137
+ @name = @option_config.name
138
+ @short = @option_config.short
139
+ @type = @option_config.type
140
+ @description = @option_config.description
141
+ @default = @option_config.default
142
+ end
143
+
144
+ # Check if current option is a boolean option
145
+ def boolean?
146
+ @option_config.boolean? && !help? && !version?
147
+ end
148
+
149
+ # Check if the option has a short name
150
+ def short?
151
+ @option_config.short?
152
+ end
153
+
154
+ # Check if the option has non-empty description
155
+ def description?
156
+ @option_config.description?
157
+ end
158
+
159
+ # Assert if the details of this options should be printed
160
+ def details?
161
+ @option_config.description? || @option_config.type? || @option_config.default?
162
+ end
163
+
164
+ # Check if the option has a non-default type
165
+ def type?
166
+ @option_config.type? && !help? && !version?
167
+ end
168
+
169
+ # check if the option should display default setting
170
+ def default?
171
+ @option_config.default? && !help? && !version?
172
+ end
173
+
174
+ # Check if the option is for getting help
175
+ def help?
176
+ @option_config.name == 'help'
177
+ end
178
+
179
+ # Check if the option is for version
180
+ def version?
181
+ @option_config.name == 'version'
182
+ end
183
+ end
184
+ end
185
+ end
@@ -191,9 +191,10 @@ module Cliqr
191
191
  #
192
192
  # @return [Boolean] <tt>true</tt> if there were any errors during validation
193
193
  def do_validate(name, value, errors)
194
- errors.add("value '#{value}' of type '#{value.class.name}' for '#{name}' " \
195
- "does not extend from '#{@super_type}'") \
196
- unless value.is_a?(@super_type) || value < @super_type
194
+ return if value.is_a?(@super_type) || \
195
+ (value.respond_to?(:<) ? value < @super_type : value.is_a?(@super_type))
196
+ errors.add("#{name} of type '#{value.class.name}' " \
197
+ "does not extend from '#{@super_type}'")
197
198
  end
198
199
  end
199
200
 
@@ -213,7 +214,13 @@ module Cliqr
213
214
  valid = true
214
215
  values.each_with_index do |value, index|
215
216
  valid = false unless value.valid?
216
- value.errors.each { |error| errors.add("#{name}[#{index + 1}] - #{error}") }
217
+ value.errors.each do |error|
218
+ if value.name.nil? || value.name.empty?
219
+ errors.add("#{name}[#{index + 1}] - #{error}")
220
+ else
221
+ errors.add("#{name.to_s.gsub(/s$/, '')} \"#{value.name}\" - #{error}")
222
+ end
223
+ end
217
224
  end
218
225
  valid
219
226
  end
@@ -238,6 +245,51 @@ module Cliqr
238
245
  end
239
246
  end
240
247
 
248
+ # Validate the type of a attribute's value
249
+ class TypeOfValidator < NonNilValidator
250
+ # Create a new <tt>:type_of</tt> validator
251
+ def initialize(type)
252
+ super(true)
253
+ @type = type
254
+ end
255
+
256
+ protected
257
+
258
+ # Run the <tt>:type_of</tt> validation check
259
+ #
260
+ # @return [Nothing]
261
+ def do_validate(name, value, errors)
262
+ errors.add("#{name} should be a '#{@type}' not '#{value.class}'") \
263
+ unless value.class == @type
264
+ end
265
+ end
266
+
267
+ # Run multiple validators on a value to assert at-least one of them passes
268
+ class OneOfValidator < Validator
269
+ # Create a new <tt>:one_of</tt> validator
270
+ def initialize(validators)
271
+ @validators = validators.map { |type, config| ValidatorFactory.get(type, config) }
272
+ end
273
+
274
+ protected
275
+
276
+ # Run each validator one by one until one passes
277
+ #
278
+ # @return [Nothing]
279
+ def do_validate(name, value, errors)
280
+ local_errors = ValidationErrors.new
281
+ passing_validator = @validators.find do |validator|
282
+ validator_errors = ValidationErrors.new.tap do |temp_errors|
283
+ validator.validate(name, value, temp_errors)
284
+ local_errors.merge(temp_errors)
285
+ end
286
+ validator_errors.empty?
287
+ end
288
+ errors.add("invalid value for #{name}; fix one of - [#{local_errors}]") \
289
+ if passing_validator.nil?
290
+ end
291
+ end
292
+
241
293
  # A hash of validator type id to validator class
242
294
  VALIDATORS = {
243
295
  :non_empty => NonEmptyValidator,
@@ -246,7 +298,9 @@ module Cliqr
246
298
  :format => FormatValidator,
247
299
  :extend => TypeHierarchyValidator,
248
300
  :collection => CollectionValidator,
249
- :inclusion => InclusionValidator
301
+ :inclusion => InclusionValidator,
302
+ :one_of => OneOfValidator,
303
+ :type_of => TypeOfValidator
250
304
  }
251
305
 
252
306
  # Get a new validator based on the type and config param