cliqr 1.1.0 → 1.2.0

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