claide 0.7.0 → 0.8.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/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
|