claide 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/claide.rb +1 -3
- data/lib/claide/argument.rb +1 -1
- data/lib/claide/argv.rb +82 -4
- data/lib/claide/command.rb +127 -24
- data/lib/claide/command/argument_suggester.rb +102 -0
- data/lib/claide/command/banner.rb +127 -19
- data/lib/claide/command/{plugins_helper.rb → plugin_manager.rb} +19 -19
- metadata +4 -13
- data/lib/claide/argv/parser.rb +0 -83
- data/lib/claide/command/banner/prettifier.rb +0 -61
- data/lib/claide/command/options.rb +0 -86
- data/lib/claide/command/parser.rb +0 -47
- data/lib/claide/command/shell_completion_helper.rb +0 -39
- data/lib/claide/command/shell_completion_helper/zsh_completion_generator.rb +0 -191
- data/lib/claide/command/validation_helper.rb +0 -102
- data/lib/claide/helper.rb +0 -115
- data/lib/claide/mixins.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39fe6340165dbe39cd732747424e3360f6fde1ca
|
4
|
+
data.tar.gz: 96a7229736da06c1c4ec61c9a2fc1a9a972fe1b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4cbd2c94407f1083a03629754892b8acbda7c2c5335f004c26e59f7040fb3532a161f082b7171a15f89f05db1cc80f2d447c645a35ff4fd5c45cb034126e6c25
|
7
|
+
data.tar.gz: c9af023661f85713f91b4fb2b4480b73599c2506272ad1362fa6fa5a3feb110066c02e5479926890151bb16bb95ca2d890e46875f7c87471dc1697819e0f95c2
|
data/lib/claide.rb
CHANGED
@@ -8,14 +8,12 @@ module CLAide
|
|
8
8
|
#
|
9
9
|
# CLAide’s version, following [semver](http://semver.org).
|
10
10
|
#
|
11
|
-
VERSION = '0.
|
11
|
+
VERSION = '0.8.0'
|
12
12
|
|
13
13
|
require 'claide/ansi'
|
14
14
|
require 'claide/argument'
|
15
15
|
require 'claide/argv'
|
16
16
|
require 'claide/command'
|
17
17
|
require 'claide/help'
|
18
|
-
require 'claide/helper'
|
19
18
|
require 'claide/informative_error'
|
20
|
-
require 'claide/mixins'
|
21
19
|
end
|
data/lib/claide/argument.rb
CHANGED
data/lib/claide/argv.rb
CHANGED
@@ -1,19 +1,17 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require 'claide/argv/parser'
|
4
|
-
|
5
3
|
module CLAide
|
6
4
|
# This class is responsible for parsing the parameters specified by the user,
|
7
5
|
# accessing individual parameters, and keep state by removing handled
|
8
6
|
# parameters.
|
9
7
|
#
|
10
8
|
class ARGV
|
11
|
-
# @return [ARGV]
|
9
|
+
# @return [ARGV] Coerces an object to the ARGV class if needed.
|
12
10
|
#
|
13
11
|
# @param [Object] argv
|
14
12
|
# The object which should be converted to the ARGV class.
|
15
13
|
#
|
16
|
-
def self.
|
14
|
+
def self.coerce(argv)
|
17
15
|
if argv.is_a?(ARGV)
|
18
16
|
argv
|
19
17
|
else
|
@@ -203,5 +201,85 @@ module CLAide
|
|
203
201
|
end
|
204
202
|
result.nil? ? default : result
|
205
203
|
end
|
204
|
+
|
205
|
+
module Parser
|
206
|
+
# @return [Array<Array<Symbol, String, Array>>] A list of tuples for each
|
207
|
+
# parameter, where the first entry is the `type` and the second
|
208
|
+
# entry the actual parsed parameter.
|
209
|
+
#
|
210
|
+
# @example
|
211
|
+
#
|
212
|
+
# list = parse(['tea', '--no-milk', '--sweetner=honey'])
|
213
|
+
# list # => [[:arg, "tea"],
|
214
|
+
# [:flag, ["milk", false]],
|
215
|
+
# [:option, ["sweetner", "honey"]]]
|
216
|
+
#
|
217
|
+
def self.parse(argv)
|
218
|
+
entries = []
|
219
|
+
copy = argv.map(&:to_s)
|
220
|
+
while argument = copy.shift
|
221
|
+
type = argument_type(argument)
|
222
|
+
parsed_argument = parse_argument(type, argument)
|
223
|
+
entries << [type, parsed_argument]
|
224
|
+
end
|
225
|
+
entries
|
226
|
+
end
|
227
|
+
|
228
|
+
# @return [Symbol] Returns the type of an argument. The types can be
|
229
|
+
# either: `:arg`, `:flag`, `:option`.
|
230
|
+
#
|
231
|
+
# @param [String] argument
|
232
|
+
# The argument to check.
|
233
|
+
#
|
234
|
+
def self.argument_type(argument)
|
235
|
+
if argument.start_with?('--')
|
236
|
+
if argument.include?('=')
|
237
|
+
:option
|
238
|
+
else
|
239
|
+
:flag
|
240
|
+
end
|
241
|
+
else
|
242
|
+
:arg
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# @return [String, Array<String, String>] Returns the argument itself for
|
247
|
+
# normal arguments (like commands) and a tuple with the key and
|
248
|
+
# the value for options and flags.
|
249
|
+
#
|
250
|
+
# @param [Symbol] type
|
251
|
+
# The type of the argument.
|
252
|
+
#
|
253
|
+
# @param [String] argument
|
254
|
+
# The argument to check.
|
255
|
+
#
|
256
|
+
def self.parse_argument(type, argument)
|
257
|
+
case type
|
258
|
+
when :arg
|
259
|
+
return argument
|
260
|
+
when :flag
|
261
|
+
return parse_flag(argument)
|
262
|
+
when :option
|
263
|
+
return argument[2..-1].split('=', 2)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# @return [String, Array<String, String>] Returns the parameter
|
268
|
+
# describing a flag arguments.
|
269
|
+
#
|
270
|
+
# @param [String] argument
|
271
|
+
# The flag argument to check.
|
272
|
+
#
|
273
|
+
def self.parse_flag(argument)
|
274
|
+
if argument.start_with?('--no-')
|
275
|
+
key = argument[5..-1]
|
276
|
+
value = false
|
277
|
+
else
|
278
|
+
key = argument[2..-1]
|
279
|
+
value = true
|
280
|
+
end
|
281
|
+
[key, value]
|
282
|
+
end
|
283
|
+
end
|
206
284
|
end
|
207
285
|
end
|
data/lib/claide/command.rb
CHANGED
@@ -1,11 +1,8 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
require 'claide/command/banner'
|
4
|
-
require 'claide/command/
|
5
|
-
require 'claide/command/
|
6
|
-
require 'claide/command/options'
|
7
|
-
require 'claide/command/shell_completion_helper'
|
8
|
-
require 'claide/command/validation_helper'
|
4
|
+
require 'claide/command/plugin_manager'
|
5
|
+
require 'claide/command/argument_suggester'
|
9
6
|
|
10
7
|
module CLAide
|
11
8
|
# This class is used to build a command-line interface
|
@@ -83,11 +80,14 @@ module CLAide
|
|
83
80
|
#
|
84
81
|
attr_accessor :description
|
85
82
|
|
86
|
-
# @return [String] The
|
87
|
-
#
|
88
|
-
#
|
83
|
+
# @return [Array<String>] The prefixes used t osearch for CLAide plugins.
|
84
|
+
# Plugins are loaded via their `<plugin_prefix>_plugin.rb` file.
|
85
|
+
# Defaults to search for `claide` plugins.
|
89
86
|
#
|
90
|
-
|
87
|
+
def plugin_prefixes
|
88
|
+
@plugin_prefixes ||= ['claide']
|
89
|
+
end
|
90
|
+
attr_writer :plugin_prefixes
|
91
91
|
|
92
92
|
# @return [Array<Argument>]
|
93
93
|
# A list of arguments the command handles. This is shown
|
@@ -219,6 +219,16 @@ module CLAide
|
|
219
219
|
subcommands << subcommand
|
220
220
|
end
|
221
221
|
|
222
|
+
DEFAULT_ROOT_OPTIONS = [
|
223
|
+
['--version', 'Show the version of the tool'],
|
224
|
+
]
|
225
|
+
|
226
|
+
DEFAULT_OPTIONS = [
|
227
|
+
['--verbose', 'Show more debugging information'],
|
228
|
+
['--no-ansi', 'Show output without ANSI codes'],
|
229
|
+
['--help', 'Show help banner of specified command'],
|
230
|
+
]
|
231
|
+
|
222
232
|
# Should be overridden by a subclass if it handles any options.
|
223
233
|
#
|
224
234
|
# The subclass has to combine the result of calling `super` and its own
|
@@ -239,15 +249,44 @@ module CLAide
|
|
239
249
|
# end
|
240
250
|
#
|
241
251
|
def self.options
|
242
|
-
|
252
|
+
if root_command?
|
253
|
+
DEFAULT_ROOT_OPTIONS + DEFAULT_OPTIONS
|
254
|
+
else
|
255
|
+
DEFAULT_OPTIONS
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Handles root commands options if appropriate.
|
260
|
+
#
|
261
|
+
# @param [ARGV] argv
|
262
|
+
# The parameters of the command.
|
263
|
+
#
|
264
|
+
# @return [Bool] Whether any root command option was handled.
|
265
|
+
#
|
266
|
+
def handle_root_options(argv)
|
267
|
+
return false unless self.class.root_command?
|
268
|
+
if argv.flag?('version')
|
269
|
+
print_version
|
270
|
+
return true
|
271
|
+
end
|
272
|
+
false
|
273
|
+
end
|
274
|
+
|
275
|
+
# Prints the version of the command optionally including plugins.
|
276
|
+
#
|
277
|
+
def print_version
|
278
|
+
puts self.class.version
|
279
|
+
if verbose?
|
280
|
+
PluginManager.specifications.each do |spec|
|
281
|
+
puts "#{spec.name}: #{spec.version}"
|
282
|
+
end
|
283
|
+
end
|
243
284
|
end
|
244
285
|
|
245
286
|
# Instantiates the command class matching the parameters through
|
246
287
|
# {Command.parse}, validates it through {Command#validate!}, and runs it
|
247
288
|
# through {Command#run}.
|
248
289
|
#
|
249
|
-
# @note You should normally call this on
|
250
|
-
#
|
251
290
|
# @note The ANSI support is configured before running a command to allow
|
252
291
|
# the same process to run multiple commands with different
|
253
292
|
# settings. For example a process with ANSI output enabled might
|
@@ -261,12 +300,14 @@ module CLAide
|
|
261
300
|
# @return [void]
|
262
301
|
#
|
263
302
|
def self.run(argv = [])
|
264
|
-
|
265
|
-
|
266
|
-
|
303
|
+
plugin_prefixes.each do |plugin_prefix|
|
304
|
+
PluginManager.load_plugins(plugin_prefix)
|
305
|
+
end
|
267
306
|
|
307
|
+
argv = ARGV.coerce(argv)
|
308
|
+
command = parse(argv)
|
268
309
|
ANSI.disabled = !command.ansi_output?
|
269
|
-
unless
|
310
|
+
unless command.handle_root_options(argv)
|
270
311
|
command.validate!
|
271
312
|
command.run
|
272
313
|
end
|
@@ -274,14 +315,47 @@ module CLAide
|
|
274
315
|
handle_exception(command, exception)
|
275
316
|
end
|
276
317
|
|
318
|
+
# @param [Array, ARGV] argv
|
319
|
+
# A list of (remaining) parameters.
|
320
|
+
#
|
321
|
+
# @return [Command] An instance of the command class that was matched by
|
322
|
+
# going through the arguments in the parameters and drilling down
|
323
|
+
# command classes.
|
324
|
+
#
|
277
325
|
def self.parse(argv)
|
278
|
-
|
326
|
+
argv = ARGV.coerce(argv)
|
327
|
+
cmd = argv.arguments.first
|
328
|
+
if cmd && subcommand = find_subcommand(cmd)
|
329
|
+
argv.shift_argument
|
330
|
+
subcommand.parse(argv)
|
331
|
+
elsif abstract_command? && default_subcommand
|
332
|
+
load_default_subcommand(argv)
|
333
|
+
else
|
334
|
+
new(argv)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# @param [Array, ARGV] argv
|
339
|
+
# A list of (remaining) parameters.
|
340
|
+
#
|
341
|
+
# @return [Command] Returns the default subcommand initialized with the
|
342
|
+
# given arguments.
|
343
|
+
#
|
344
|
+
def self.load_default_subcommand(argv)
|
345
|
+
unless subcommand = find_subcommand(default_subcommand)
|
346
|
+
raise 'Unable to find the default subcommand ' \
|
347
|
+
"`#{default_subcommand}` for command `#{self}`."
|
348
|
+
end
|
349
|
+
result = subcommand.parse(argv)
|
350
|
+
result.invoked_as_default = true
|
351
|
+
result
|
279
352
|
end
|
280
353
|
|
281
|
-
# Presents an exception to the user
|
354
|
+
# Presents an exception to the user in a short manner in case of an
|
355
|
+
# `InformativeError` or in long form in other cases,
|
282
356
|
#
|
283
|
-
# @param [Command] command
|
284
|
-
# The command
|
357
|
+
# @param [Command, nil] command
|
358
|
+
# The command from where the exception originated.
|
285
359
|
#
|
286
360
|
# @param [Object] exception
|
287
361
|
# The exception to present.
|
@@ -291,7 +365,7 @@ module CLAide
|
|
291
365
|
def self.handle_exception(command, exception)
|
292
366
|
if exception.is_a?(InformativeError)
|
293
367
|
puts exception.message
|
294
|
-
if command.verbose?
|
368
|
+
if command.nil? || command.verbose?
|
295
369
|
puts
|
296
370
|
puts(*exception.backtrace)
|
297
371
|
end
|
@@ -316,7 +390,7 @@ module CLAide
|
|
316
390
|
# @return [void]
|
317
391
|
#
|
318
392
|
def self.report_error(exception)
|
319
|
-
plugins =
|
393
|
+
plugins = PluginManager.plugins_involved_in_exception(exception)
|
320
394
|
unless plugins.empty?
|
321
395
|
puts '[!] The exception involves the following plugins:' \
|
322
396
|
"\n - #{plugins.join("\n - ")}\n".ansi.yellow
|
@@ -414,12 +488,30 @@ module CLAide
|
|
414
488
|
# A list of (user-supplied) params that should be handled.
|
415
489
|
#
|
416
490
|
def initialize(argv)
|
417
|
-
argv = ARGV.
|
491
|
+
argv = ARGV.coerce(argv)
|
418
492
|
@verbose = argv.flag?('verbose')
|
419
493
|
@ansi_output = argv.flag?('ansi', Command.ansi_output?)
|
420
494
|
@argv = argv
|
421
495
|
end
|
422
496
|
|
497
|
+
# Convenience method.
|
498
|
+
# Instantiate the command and run it with the provided arguments at once.
|
499
|
+
#
|
500
|
+
# @note This method validate! the command before running it, but contrary to
|
501
|
+
# CLAide::Command::run, it does not load plugins nor exit on failure.
|
502
|
+
# It is up to the caller to rescue any possible exception raised.
|
503
|
+
#
|
504
|
+
# @param [String..., Array<String>] args
|
505
|
+
# The arguments to initialize the command with
|
506
|
+
#
|
507
|
+
# @raise [Help] If validate! fails
|
508
|
+
#
|
509
|
+
def self.invoke(*args)
|
510
|
+
command = new(ARGV.new(args.flatten))
|
511
|
+
command.validate!
|
512
|
+
command.run
|
513
|
+
end
|
514
|
+
|
423
515
|
# @return [Bool] Whether the command was invoked by an abstract command by
|
424
516
|
# default.
|
425
517
|
#
|
@@ -441,7 +533,8 @@ module CLAide
|
|
441
533
|
def validate!
|
442
534
|
banner! if @argv.flag?('help')
|
443
535
|
unless @argv.empty?
|
444
|
-
|
536
|
+
argument = @argv.remainder.first
|
537
|
+
help! ArgumentSuggester.new(argument, self.class).suggestion
|
445
538
|
end
|
446
539
|
help! if self.class.abstract_command?
|
447
540
|
end
|
@@ -530,5 +623,15 @@ module CLAide
|
|
530
623
|
end
|
531
624
|
end
|
532
625
|
end
|
626
|
+
|
627
|
+
# Handle depracted form of assigning a plugin prefix.
|
628
|
+
#
|
629
|
+
# @todo Remove deprecated form.
|
630
|
+
#
|
631
|
+
def self.plugin_prefix=(prefix)
|
632
|
+
warn '[!] The specification of a singular plugin prefix has been ' \
|
633
|
+
"deprecated. Use `#{self}::plugin_prefixes` instead."
|
634
|
+
plugin_prefixes << prefix
|
635
|
+
end
|
533
636
|
end
|
534
637
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module CLAide
|
4
|
+
class Command
|
5
|
+
class ArgumentSuggester
|
6
|
+
# @param [String] argument
|
7
|
+
# The unrecognized argument for which to make a suggestion.
|
8
|
+
#
|
9
|
+
# @param [Class] command_class
|
10
|
+
# The class of the command which encountered the unrecognized
|
11
|
+
# arguments.
|
12
|
+
#
|
13
|
+
def initialize(argument, command_class)
|
14
|
+
@argument, @command_class = argument, command_class
|
15
|
+
@argument_type = ARGV::Parser.argument_type(@argument)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Array<String>] The list of the valid arguments for a command
|
19
|
+
# according to the type of the argument.
|
20
|
+
#
|
21
|
+
def possibilities
|
22
|
+
case @argument_type
|
23
|
+
when :option, :flag
|
24
|
+
@command_class.options.map(&:first)
|
25
|
+
when :arg
|
26
|
+
@command_class.subcommands_for_command_lookup.map(&:command)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [String] Returns a suggested argument from `possibilities` based
|
31
|
+
# on the `levenshtein_distance` score.
|
32
|
+
#
|
33
|
+
def suggested_argument
|
34
|
+
possibilities.sort_by do |element|
|
35
|
+
self.class.levenshtein_distance(@argument, element)
|
36
|
+
end.first
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [String] Returns a message including a suggestion for the given
|
40
|
+
# suggestion.
|
41
|
+
#
|
42
|
+
def suggestion
|
43
|
+
argument_description = @argument_type == :arg ? 'command' : 'option'
|
44
|
+
if suggestion = suggested_argument
|
45
|
+
pretty_suggestion = self.class.prettify_suggestion(suggestion,
|
46
|
+
@argument_type)
|
47
|
+
"Unknown #{argument_description}: `#{@argument}`\n" \
|
48
|
+
"Did you mean: #{pretty_suggestion}"
|
49
|
+
else
|
50
|
+
"Unknown #{argument_description}: `#{@argument}`"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Prettifies the given validation suggestion according to the type.
|
55
|
+
#
|
56
|
+
# @param [String] suggestion
|
57
|
+
# The suggestion to prettify.
|
58
|
+
#
|
59
|
+
# @param [Type] argument_type
|
60
|
+
# The type of the suggestion: either `:command` or `:option`.
|
61
|
+
#
|
62
|
+
# @return [String] A handsome suggestion.
|
63
|
+
#
|
64
|
+
def self.prettify_suggestion(suggestion, argument_type)
|
65
|
+
case argument_type
|
66
|
+
when :option, :flag
|
67
|
+
suggestion = "#{suggestion}"
|
68
|
+
suggestion.ansi.blue
|
69
|
+
when :arg
|
70
|
+
suggestion.ansi.green
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the Levenshtein distance between the given strings.
|
75
|
+
# From: http://rosettacode.org/wiki/Levenshtein_distance#Ruby
|
76
|
+
#
|
77
|
+
# @param [String] a
|
78
|
+
# The first string to compare.
|
79
|
+
#
|
80
|
+
# @param [String] b
|
81
|
+
# The second string to compare.
|
82
|
+
#
|
83
|
+
# @return [Fixnum] The distance between the strings.
|
84
|
+
#
|
85
|
+
# rubocop:disable all
|
86
|
+
def self.levenshtein_distance(a, b)
|
87
|
+
a, b = a.downcase, b.downcase
|
88
|
+
costs = Array(0..b.length)
|
89
|
+
(1..a.length).each do |i|
|
90
|
+
costs[0], nw = i, i - 1
|
91
|
+
(1..b.length).each do |j|
|
92
|
+
costs[j], nw = [
|
93
|
+
costs[j] + 1, costs[j - 1] + 1, a[i - 1] == b[j - 1] ? nw : nw + 1
|
94
|
+
].min, costs[j]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
costs[b.length]
|
98
|
+
end
|
99
|
+
# rubocop:enable all
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|