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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +128 -1
- data/README.md +97 -71
- data/examples/README.md +12 -0
- data/examples/hbase +58 -0
- data/examples/my-command +63 -0
- data/examples/numbers +55 -0
- data/examples/vagrant +90 -0
- data/lib/cliqr.rb +17 -2
- data/lib/cliqr/argument_validation/argument_type_validator.rb +2 -2
- data/lib/cliqr/argument_validation/validator.rb +1 -1
- data/lib/cliqr/cli/argument_operator.rb +44 -0
- data/lib/cliqr/cli/argument_operator_context.rb +20 -0
- data/lib/cliqr/cli/command.rb +1 -1
- data/lib/cliqr/cli/command_context.rb +93 -12
- data/lib/cliqr/cli/command_runner_factory.rb +2 -2
- data/lib/cliqr/cli/config.rb +301 -33
- data/lib/cliqr/cli/executor.rb +14 -9
- data/lib/cliqr/cli/interface.rb +22 -7
- data/lib/cliqr/cli/router.rb +6 -2
- data/lib/cliqr/cli/shell_command.rb +69 -0
- data/lib/cliqr/cli/usage_builder.rb +185 -0
- data/lib/cliqr/config_validation/validator_factory.rb +59 -5
- data/lib/cliqr/error.rb +10 -4
- data/lib/cliqr/parser/action_token.rb +23 -0
- data/lib/cliqr/parser/argument_parser.rb +1 -1
- data/lib/cliqr/parser/argument_token.rb +1 -4
- data/lib/cliqr/parser/argument_tree_walker.rb +40 -8
- data/lib/cliqr/parser/option_token.rb +2 -1
- data/lib/cliqr/parser/parsed_input.rb +21 -2
- data/lib/cliqr/parser/parsed_input_builder.rb +11 -7
- data/lib/cliqr/parser/token.rb +3 -9
- data/lib/cliqr/parser/token_factory.rb +1 -1
- data/lib/cliqr/util.rb +135 -0
- data/lib/cliqr/version.rb +1 -1
- data/spec/argument_parser_spec_helper.rb +15 -0
- data/spec/config/action_config_validator_spec.rb +146 -0
- data/spec/config/config_finalize_spec.rb +1 -1
- data/spec/config/config_validator_spec.rb +29 -19
- data/spec/config/option_config_validator_spec.rb +13 -13
- data/spec/dsl/interface_spec.rb +1 -168
- data/spec/dsl/usage_spec.rb +705 -0
- data/spec/executor/action_executor_spec.rb +205 -0
- data/spec/executor/executor_spec.rb +405 -17
- data/spec/executor/help_executor_spec.rb +424 -0
- data/spec/executor/shell_executor_spec.rb +233 -0
- data/spec/fixtures/action_reader_command.rb +12 -0
- data/spec/fixtures/csv_argument_operator.rb +8 -0
- data/spec/fixtures/test_option_type_checker_command.rb +8 -0
- data/spec/parser/action_argument_parser_spec.rb +113 -0
- data/spec/parser/argument_parser_spec.rb +37 -44
- data/spec/spec_helper.rb +1 -0
- data/spec/validation/action_argument_validator_spec.rb +50 -0
- data/spec/validation/{argument_validation_spec.rb → command_argument_validation_spec.rb} +36 -18
- data/spec/validation/error_spec.rb +1 -1
- data/tasks/rdoc.rake +16 -0
- data/tasks/rubucop.rake +14 -0
- data/tasks/yard.rake +21 -0
- data/templates/usage.erb +39 -0
- metadata +48 -11
data/lib/cliqr/cli/executor.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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::
|
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
|
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,
|
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
|
data/lib/cliqr/cli/interface.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/cliqr/cli/router.rb
CHANGED
@@ -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
|
27
|
+
handler = @config.handler
|
28
28
|
runner = CommandRunnerFactory.get(options)
|
29
29
|
runner.run do
|
30
|
-
handler.
|
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
|
-
|
195
|
-
|
196
|
-
|
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
|
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
|