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 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