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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fa421ae80f05ff7766c67d15049c75363ea796ca
4
- data.tar.gz: 6c85d3c205a7f365992cb83913663e923eaf89e0
3
+ metadata.gz: 39fe6340165dbe39cd732747424e3360f6fde1ca
4
+ data.tar.gz: 96a7229736da06c1c4ec61c9a2fc1a9a972fe1b2
5
5
  SHA512:
6
- metadata.gz: 125926a5f8beb325d3e5a8a8b834de3db972cf402d62dab2c7afcfba52dd8738f152c30dceb02ce41a42a5ed5715d135b49d155e0ecfd2c021a8f64c9dd3b5ab
7
- data.tar.gz: 99bf6357be57b12289b94501bd708c558b4ac45c9ada4321f0927b4e9f087e39c971bdfbdf20ed7cb0530ae42522c245d8fc70f0a853a00bbc71cba995d6f90a
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.7.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
@@ -56,7 +56,7 @@ module CLAide
56
56
  #
57
57
  def ==(other)
58
58
  other.is_a?(Argument) &&
59
- names == other.names && required == other.required
59
+ names == other.names && required == other.required
60
60
  end
61
61
  end
62
62
  end
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] Coherces an object to the ARGV class if needed.
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.coherce(argv)
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
@@ -1,11 +1,8 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'claide/command/banner'
4
- require 'claide/command/parser'
5
- require 'claide/command/plugins_helper'
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 prefix for loading CLAide plugins for this
87
- # command. Plugins are loaded via their
88
- # <plugin_prefix>_plugin.rb file.
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
- attr_accessor :plugin_prefix
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
- Options.default_options(self)
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
- argv = ARGV.coherce(argv)
265
- PluginsHelper.load_plugins(plugin_prefix)
266
- command = parse(argv)
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 Options.handle_root_option(command, argv)
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
- Parser.parse(self, argv)
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 according to class of the .
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 which originated the exception.
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 = PluginsHelper.plugins_involved_in_exception(exception)
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.new(argv) unless argv.is_a?(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
- help! ValidationHelper.argument_suggestion(@argv.remainder, self.class)
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