claide 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/claide.rb +6 -704
  3. metadata +6 -8
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d9085e38c36d1128b294f24e832a757683f3c9a2
4
+ data.tar.gz: 8d85b685edcf81f078b1961f164e34df77981573
5
+ SHA512:
6
+ metadata.gz: df44b05b8e3e0fce7a7f9109155cf4e37b91c26ce7a48d1694f9df81165d1345e099eae6367bee698ebac620a43961a59d48199cc912b2b1151b33ce58024121
7
+ data.tar.gz: 8f78e80a5f12aa9a135698319bd5ca173798655784c2a79e63066f8f0768447768fe5b55bdf4690aac330f3f33cf850d489161199214bd357d16f98eb2759695
@@ -4,714 +4,16 @@
4
4
  # {CLAide::InformativeError}
5
5
  #
6
6
  module CLAide
7
+
7
8
  # @return [String]
8
9
  #
9
10
  # CLAide’s version, following [semver](http://semver.org).
10
11
  #
11
- VERSION = '0.2.0'
12
-
13
- # This class is responsible for parsing the parameters specified by the user,
14
- # accessing individual parameters, and keep state by removing handled
15
- # parameters.
16
- #
17
- class ARGV
18
-
19
- # @param [Array<String>] argv
20
- #
21
- # A list of parameters. Each entry is ensured to be a string by calling
22
- # `#to_s` on it.
23
- #
24
- def initialize(argv)
25
- @entries = self.class.parse(argv)
26
- end
27
-
28
- # @return [Boolean]
29
- #
30
- # Returns wether or not there are any remaining unhandled parameters.
31
- #
32
- def empty?
33
- @entries.empty?
34
- end
35
-
36
- # @return [Array<String>]
37
- #
38
- # A list of the remaining unhandled parameters, in the same format a user
39
- # specifies it in.
40
- #
41
- # @example
42
- #
43
- # argv = CLAide::ARGV.new(['tea', '--no-milk', '--sweetner=honey'])
44
- # argv.shift_argument # => 'tea'
45
- # argv.remainder # => ['--no-milk', '--sweetner=honey']
46
- #
47
- def remainder
48
- @entries.map do |type, (key, value)|
49
- case type
50
- when :arg
51
- key
52
- when :flag
53
- "--#{'no-' if value == false}#{key}"
54
- when :option
55
- "--#{key}=#{value}"
56
- end
57
- end
58
- end
59
-
60
- # @return [Hash]
61
- #
62
- # A hash that consists of the remaining flags and options and their
63
- # values.
64
- #
65
- # @example
66
- #
67
- # argv = CLAide::ARGV.new(['tea', '--no-milk', '--sweetner=honey'])
68
- # argv.options # => { 'milk' => false, 'sweetner' => 'honey' }
69
- #
70
- def options
71
- options = {}
72
- @entries.each do |type, (key, value)|
73
- options[key] = value unless type == :arg
74
- end
75
- options
76
- end
77
-
78
- # @return [Array<String>]
79
- #
80
- # A list of the remaining arguments.
81
- #
82
- # @example
83
- #
84
- # argv = CLAide::ARGV.new(['tea', 'white', '--no-milk', 'biscuit'])
85
- # argv.shift_argument # => 'tea'
86
- # argv.arguments # => ['white', 'biscuit']
87
- #
88
- def arguments
89
- @entries.map { |type, value| value if type == :arg }.compact
90
- end
91
-
92
- # @return [Array<String>]
93
- #
94
- # A list of the remaining arguments.
95
- #
96
- # @note
97
- #
98
- # This version also removes the arguments from the remaining parameters.
99
- #
100
- # @example
101
- #
102
- # argv = CLAide::ARGV.new(['tea', 'white', '--no-milk', 'biscuit'])
103
- # argv.arguments # => ['tea', 'white', 'biscuit']
104
- # argv.arguments! # => ['tea', 'white', 'biscuit']
105
- # argv.arguments # => []
106
- #
107
- def arguments!
108
- arguments = []
109
- while arg = shift_argument
110
- arguments << arg
111
- end
112
- arguments
113
- end
114
-
115
- # @return [String]
116
- #
117
- # The first argument in the remaining parameters.
118
- #
119
- # @note
120
- #
121
- # This will remove the argument from the remaining parameters.
122
- #
123
- # @example
124
- #
125
- # argv = CLAide::ARGV.new(['tea', 'white'])
126
- # argv.shift_argument # => 'tea'
127
- # argv.arguments # => ['white']
128
- #
129
- def shift_argument
130
- if index = @entries.find_index { |type, _| type == :arg }
131
- entry = @entries[index]
132
- @entries.delete_at(index)
133
- entry.last
134
- end
135
- end
136
-
137
- # @return [Boolean, nil]
138
- #
139
- # Returns `true` if the flag by the specified `name` is among the
140
- # remaining parameters and is not negated.
141
- #
142
- # @param [String] name
143
- #
144
- # The name of the flag to look for among the remaining parameters.
145
- #
146
- # @param [Boolean] default
147
- #
148
- # The value that is returned in case the flag is not among the remaining
149
- # parameters.
150
- #
151
- # @note
152
- #
153
- # This will remove the flag from the remaining parameters.
154
- #
155
- # @example
156
- #
157
- # argv = CLAide::ARGV.new(['tea', '--no-milk', '--sweetner=honey'])
158
- # argv.flag?('milk') # => false
159
- # argv.flag?('milk') # => nil
160
- # argv.flag?('milk', true) # => true
161
- # argv.remainder # => ['tea', '--sweetner=honey']
162
- #
163
- def flag?(name, default = nil)
164
- delete_entry(:flag, name, default)
165
- end
166
-
167
- # @return [String, nil]
168
- #
169
- # Returns the value of the option by the specified `name` is among the
170
- # remaining parameters.
171
- #
172
- # @param [String] name
173
- #
174
- # The name of the option to look for among the remaining parameters.
175
- #
176
- # @param [String] default
177
- #
178
- # The value that is returned in case the option is not among the
179
- # remaining parameters.
180
- #
181
- # @note
182
- #
183
- # This will remove the option from the remaining parameters.
184
- #
185
- # @example
186
- #
187
- # argv = CLAide::ARGV.new(['tea', '--no-milk', '--sweetner=honey'])
188
- # argv.option('sweetner') # => 'honey'
189
- # argv.option('sweetner') # => nil
190
- # argv.option('sweetner', 'sugar') # => 'sugar'
191
- # argv.remainder # => ['tea', '--no-milk']
192
- #
193
- def option(name, default = nil)
194
- delete_entry(:option, name, default)
195
- end
196
-
197
- private
198
-
199
- def delete_entry(requested_type, requested_key, default)
200
- result = nil
201
- @entries.delete_if do |type, (key, value)|
202
- if requested_key == key && requested_type == type
203
- result = value
204
- true
205
- end
206
- end
207
- result.nil? ? default : result
208
- end
209
-
210
- # @return [Array<Array>]
211
- #
212
- # A list of tuples for each parameter, where the first entry is the
213
- # `type` and the second entry the actual parsed parameter.
214
- #
215
- # @example
216
- #
217
- # list = parse(['tea', '--no-milk', '--sweetner=honey'])
218
- # list # => [[:arg, "tea"],
219
- # [:flag, ["milk", false]],
220
- # [:option, ["sweetner", "honey"]]]
221
- #
222
- def self.parse(argv)
223
- entries = []
224
- copy = argv.map(&:to_s)
225
- while x = copy.shift
226
- type = key = value = nil
227
- if is_arg?(x)
228
- # A regular argument (e.g. a command)
229
- type, value = :arg, x
230
- else
231
- key = x[2..-1]
232
- if key.include?('=')
233
- # An option with a value
234
- type = :option
235
- key, value = key.split('=', 2)
236
- else
237
- # A boolean flag
238
- type = :flag
239
- value = true
240
- if key[0,3] == 'no-'
241
- # A negated boolean flag
242
- key = key[3..-1]
243
- value = false
244
- end
245
- end
246
- value = [key, value]
247
- end
248
- entries << [type, value]
249
- end
250
- entries
251
- end
252
-
253
- def self.is_arg?(x)
254
- x[0,2] != '--'
255
- end
256
- end
257
-
258
- # Including this module into an exception class will ensure that when raised,
259
- # while running {Command.run}, only the message of the exception will be
260
- # shown to the user. Unless disabled with the `--verbose` flag.
261
- #
262
- # In addition, the message will be colored red, if {Command.colorize_output}
263
- # is set to `true`.
264
- #
265
- module InformativeError
266
- attr_writer :exit_status
267
-
268
- # @return [Numeric]
269
- #
270
- # The exist status code that should be used to terminate the program with.
271
- #
272
- # Defaults to `1`.
273
- #
274
- def exit_status
275
- @exit_status ||= 1
276
- end
277
- end
278
-
279
- # The exception class that is raised to indicate a help banner should be
280
- # shown while running {Command.run}.
281
- #
282
- class Help < StandardError
283
- include InformativeError
284
-
285
- # @return [Command]
286
- #
287
- # The command instance for which a help banner should be shown.
288
- #
289
- attr_reader :command
290
-
291
- # @return [String]
292
- #
293
- # The optional error message that will be shown before the help banner.
294
- #
295
- attr_reader :error_message
296
-
297
- # @param [Command] command
298
- #
299
- # An instance of a command class for which a help banner should be shown.
300
- #
301
- # @param [String] error_message
302
- #
303
- # An optional error message that will be shown before the help banner.
304
- # If specified, the exit status, used to terminate the program with, will
305
- # be set to `1`, otherwise a {Help} exception is treated as not being a
306
- # real error and exits with `0`.
307
- #
308
- def initialize(command, error_message = nil)
309
- @command, @error_message = command, error_message
310
- @exit_status = @error_message.nil? ? 0 : 1
311
- end
312
-
313
- # @return [String]
314
- #
315
- # The optional error message, colored in red if {Command.colorize_output}
316
- # is set to `true`.
317
- #
318
- def formatted_error_message
319
- if @error_message
320
- message = "[!] #{@error_message}"
321
- @command.colorize_output? ? message.red : message
322
- end
323
- end
324
-
325
- # @return [String]
326
- #
327
- # The optional error message, combined with the help banner of the
328
- # command.
329
- #
330
- def message
331
- [formatted_error_message, @command.formatted_banner].compact.join("\n\n")
332
- end
333
- end
334
-
335
- # This class is used to build a command-line interface
336
- #
337
- # Each command is represented by a subclass of this class, which may be
338
- # nested to create more granular commands.
339
- #
340
- # Following is an overview of the types of commands and what they should do.
341
- #
342
- # ### Any command type
343
- #
344
- # * Inherit from the command class under which the command should be nested.
345
- # * Set {Command.summary} to a brief description of the command.
346
- # * Override {Command.options} to return the options it handles and their
347
- # descriptions and prepending them to the results of calling `super`.
348
- # * Override {Command#initialize} if it handles any parameters.
349
- # * Override {Command#validate!} to check if the required parameters the
350
- # command handles are valid, or call {Command#help!} in case they’re not.
351
- #
352
- # ### Abstract command
353
- #
354
- # The following is needed for an abstract command:
355
- #
356
- # * Set {Command.abstract_command} to `true`.
357
- # * Subclass the command.
358
- #
359
- # When the optional {Command.description} is specified, it will be shown at
360
- # the top of the command’s help banner.
361
- #
362
- # ### Normal command
363
- #
364
- # The following is needed for a normal command:
365
- #
366
- # * Set {Command.arguments} to the description of the arguments this command
367
- # handles.
368
- # * Override {Command#run} to perform the actual work.
369
- #
370
- # When the optional {Command.description} is specified, it will be shown
371
- # underneath the usage section of the command’s help banner. Otherwise this
372
- # defaults to {Command.summary}.
373
- #
374
- class Command
375
- class << self
376
- # @return [Boolean]
377
- #
378
- # Indicates wether or not this command can actually perform work of
379
- # itself, or that it only contains subcommands.
380
- #
381
- attr_accessor :abstract_command
382
- alias_method :abstract_command?, :abstract_command
383
-
384
- # @return [String]
385
- #
386
- # A brief description of the command, which is shown next to the
387
- # command in the help banner of a parent command.
388
- #
389
- attr_accessor :summary
390
-
391
- # @return [String]
392
- #
393
- # A longer description of the command, which is shown underneath the
394
- # usage section of the command’s help banner. Any indentation in this
395
- # value will be ignored.
396
- #
397
- attr_accessor :description
398
-
399
- # @return [String]
400
- #
401
- # A list of arguments the command handles. This is shown in the usage
402
- # section of the command’s help banner.
403
- #
404
- attr_accessor :arguments
405
-
406
- # @return [Boolean]
407
- #
408
- # The default value for {Command#colorize_output}. This defaults to
409
- # `true` if `String` has the instance methods `#green` and `#red`.
410
- # Which are defined by, for instance, the
411
- # [colored](https://github.com/defunkt/colored) gem.
412
- #
413
- def colorize_output
414
- if @colorize_output.nil?
415
- @colorize_output = String.method_defined?(:red) &&
416
- String.method_defined?(:green)
417
- end
418
- @colorize_output
419
- end
420
- attr_writer :colorize_output
421
- alias_method :colorize_output?, :colorize_output
422
-
423
- # @return [String]
424
- #
425
- # The name of the command. Defaults to a snake-cased version of the
426
- # class’ name.
427
- #
428
- def command
429
- @command ||= name.split('::').last.gsub(/[A-Z]+[a-z]*/) do |part|
430
- part.downcase << '-'
431
- end[0..-2]
432
- end
433
- attr_writer :command
434
-
435
- # @return [String]
436
- #
437
- # The full command up-to this command.
438
- #
439
- # @example
440
- #
441
- # BevarageMaker::Tea.full_command # => "beverage-maker tea"
442
- #
443
- def full_command
444
- if superclass == Command
445
- "#{command}"
446
- else
447
- "#{superclass.full_command} #{command}"
448
- end
449
- end
450
-
451
- # @return [Array<Command>]
452
- #
453
- # A list of command classes that are nested under this command.
454
- #
455
- def subcommands
456
- @subcommands ||= []
457
- end
458
-
459
- # @visibility private
460
- #
461
- # Automatically registers a subclass as a subcommand.
462
- #
463
- def inherited(subcommand)
464
- subcommands << subcommand
465
- end
466
-
467
- # Should be overriden by a subclass if it handles any options.
468
- #
469
- # The subclass has to combine the result of calling `super` and its own
470
- # list of options. The recommended way of doing this is by concatenating
471
- # concatening to this classes’ own options.
472
- #
473
- # @return [Array<Array>]
474
- #
475
- # A list of option name and description tuples.
476
- #
477
- # @example
478
- #
479
- # def self.options
480
- # [
481
- # ['--verbose', 'Print more info'],
482
- # ['--help', 'Print help banner'],
483
- # ].concat(super)
484
- # end
485
- #
486
- def options
487
- options = [
488
- ['--verbose', 'Show more debugging information'],
489
- ['--help', 'Show help banner of specified command'],
490
- ]
491
- if Command.colorize_output?
492
- options.unshift(['--no-color', 'Show output without color'])
493
- end
494
- options
495
- end
496
-
497
- # @param [Array, ARGV] argv
498
- #
499
- # A list of (remaining) parameters.
500
- #
501
- # @return [Command]
502
- #
503
- # An instance of the command class that was matched by going through
504
- # the arguments in the parameters and drilling down command classes.
505
- #
506
- def parse(argv)
507
- argv = ARGV.new(argv) unless argv.is_a?(ARGV)
508
- cmd = argv.arguments.first
509
- if cmd && subcommand = subcommands.find { |sc| sc.command == cmd }
510
- argv.shift_argument
511
- subcommand.parse(argv)
512
- else
513
- new(argv)
514
- end
515
- end
516
-
517
- # Instantiates the command class matching the parameters through
518
- # {Command.parse}, validates it through {Command#validate!}, and runs it
519
- # through {Command#run}.
520
- #
521
- # @note
522
- #
523
- # You should normally call this on
524
- #
525
- # @param [Array, ARGV] argv
526
- #
527
- # A list of parameters. For instance, the standard `ARGV` constant,
528
- # which contains the parameters passed to the program.
529
- #
530
- # @return [void]
531
- #
532
- def run(argv)
533
- command = parse(argv)
534
- command.validate!
535
- command.run
536
- rescue Exception => exception
537
- if exception.is_a?(InformativeError)
538
- puts exception.message
539
- if command.verbose?
540
- puts
541
- puts *exception.backtrace
542
- end
543
- exit exception.exit_status
544
- else
545
- report_error(exception)
546
- end
547
- end
548
-
549
- # Allows the application to perform custom error reporting, by overriding
550
- # this method.
551
- #
552
- # @param [Exception] exception
553
- #
554
- # An exception that occurred while running a command through
555
- # {Command.run}.
556
- #
557
- # @raise
558
- #
559
- # By default re-raises the specified exception.
560
- #
561
- # @return [void]
562
- #
563
- def report_error(exception)
564
- raise exception
565
- end
566
- end
567
-
568
- # Set to `true` if the user specifies the `--verbose` option.
569
- #
570
- # @note
571
- #
572
- # If you want to make use of this value for your own configuration, you
573
- # should check the value _after_ calling the `super` {Command#initialize}
574
- # implementation.
575
- #
576
- # @return [Boolean]
577
- #
578
- # Wether or not backtraces should be included when presenting the user an
579
- # exception that includes the {InformativeError} module.
580
- #
581
- attr_accessor :verbose
582
- alias_method :verbose?, :verbose
583
-
584
- # Set to `true` if {Command.colorize_output} returns `true` and the user
585
- # did **not** specify the `--no-color` option.
586
- #
587
- # @note (see #verbose)
588
- #
589
- # @return [Boolean]
590
- #
591
- # Wether or not to color {InformativeError} exception messages red and
592
- # subcommands in help banners green.
593
- #
594
- attr_accessor :colorize_output
595
- alias_method :colorize_output?, :colorize_output
596
-
597
- # Subclasses should override this method to remove the arguments/options
598
- # they support from `argv` _before_ calling `super`.
599
- #
600
- # The `super` implementation sets the {#verbose} attribute based on wether
601
- # or not the `--verbose` option is specified; and the {#colorize_output}
602
- # attribute to `false` if {Command.colorize_output} returns `true`, but the
603
- # user specified the `--no-color` option.
604
- #
605
- # @param [ARGV] argv
606
- #
607
- # A list of (user-supplied) params that should be handled.
608
- #
609
- def initialize(argv)
610
- @verbose = argv.flag?('verbose')
611
- @colorize_output = argv.flag?('color', Command.colorize_output?)
612
- @argv = argv
613
- end
614
-
615
- # Raises a Help exception if the `--help` option is specified, if `argv`
616
- # still contains remaining arguments/options by the time it reaches this
617
- # implementation, or when called on an ‘abstract command’.
618
- #
619
- # Subclasses should call `super` _before_ doing their own validation. This
620
- # way when the user specifies the `--help` flag a help banner is shown,
621
- # instead of possible actual validation errors.
622
- #
623
- # @raise [Help]
624
- #
625
- # @return [void]
626
- #
627
- def validate!
628
- help! if @argv.flag?('help')
629
- help! "Unknown arguments: #{@argv.remainder.join(' ')}" if !@argv.empty?
630
- help! if self.class.abstract_command?
631
- end
632
-
633
- # This method should be overriden by the command class to perform its work.
634
- #
635
- # @return [void
636
- #
637
- def run
638
- raise "A subclass should override the Command#run method to actually " \
639
- "perform some work."
640
- end
641
-
642
- # @visibility private
643
- def formatted_options_description
644
- opts = self.class.options
645
- size = opts.map { |opt| opt.first.size }.max
646
- opts.map { |key, desc| " #{key.ljust(size)} #{desc}" }.join("\n")
647
- end
648
-
649
- # @visibility private
650
- def formatted_usage_description
651
- if message = self.class.description || self.class.summary
652
- message = strip_heredoc(message)
653
- message = message.split("\n").map { |line| " #{line}" }.join("\n")
654
- args = " #{self.class.arguments}" if self.class.arguments
655
- " $ #{self.class.full_command}#{args}\n\n#{message}"
656
- end
657
- end
658
-
659
- # @visibility private
660
- def formatted_subcommand_summaries
661
- subcommands = self.class.subcommands.reject do |subcommand|
662
- subcommand.summary.nil?
663
- end.sort_by(&:command)
664
- unless subcommands.empty?
665
- command_size = subcommands.map { |cmd| cmd.command.size }.max
666
- subcommands.map do |subcommand|
667
- command = subcommand.command.ljust(command_size)
668
- command = command.green if colorize_output?
669
- " * #{command} #{subcommand.summary}"
670
- end.join("\n")
671
- end
672
- end
673
-
674
- # @visibility private
675
- def formatted_banner
676
- banner = []
677
- if self.class.abstract_command?
678
- banner << self.class.description if self.class.description
679
- elsif usage = formatted_usage_description
680
- banner << 'Usage:'
681
- banner << usage
682
- end
683
- if commands = formatted_subcommand_summaries
684
- banner << 'Commands:'
685
- banner << commands
686
- end
687
- banner << 'Options:'
688
- banner << formatted_options_description
689
- banner.join("\n\n")
690
- end
691
-
692
- protected
693
-
694
- # @raise [Help]
695
- #
696
- # Signals CLAide that a help banner for this command should be shown,
697
- # with an optional error message.
698
- #
699
- # @return [void]
700
- #
701
- def help!(error_message = nil)
702
- raise Help.new(self, error_message)
703
- end
704
-
705
- private
12
+ VERSION = '0.3.0'
706
13
 
707
- # Lifted straight from ActiveSupport. Thanks guys!
708
- def strip_heredoc(string)
709
- if min = string.scan(/^[ \t]*(?=\S)/).min
710
- string.gsub(/^[ \t]{#{min.size}}/, '')
711
- else
712
- string
713
- end
714
- end
715
- end
14
+ require 'claide/argv.rb'
15
+ require 'claide/command.rb'
16
+ require 'claide/help.rb'
17
+ require 'claide/informative_error.rb'
716
18
 
717
19
  end
metadata CHANGED
@@ -1,8 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claide
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
5
- prerelease:
4
+ version: 0.3.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Eloy Duran
@@ -10,7 +9,7 @@ authors:
10
9
  autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2012-10-30 00:00:00.000000000 Z
12
+ date: 2013-05-23 00:00:00.000000000 Z
14
13
  dependencies: []
15
14
  description:
16
15
  email:
@@ -26,25 +25,24 @@ files:
26
25
  homepage: https://github.com/CocoaPods/CLAide
27
26
  licenses:
28
27
  - MIT
28
+ metadata: {}
29
29
  post_install_message:
30
30
  rdoc_options: []
31
31
  require_paths:
32
32
  - lib
33
33
  required_ruby_version: !ruby/object:Gem::Requirement
34
- none: false
35
34
  requirements:
36
- - - ! '>='
35
+ - - '>='
37
36
  - !ruby/object:Gem::Version
38
37
  version: '0'
39
38
  required_rubygems_version: !ruby/object:Gem::Requirement
40
- none: false
41
39
  requirements:
42
- - - ! '>='
40
+ - - '>='
43
41
  - !ruby/object:Gem::Version
44
42
  version: '0'
45
43
  requirements: []
46
44
  rubyforge_project:
47
- rubygems_version: 1.8.23
45
+ rubygems_version: 2.0.3
48
46
  signing_key:
49
47
  specification_version: 3
50
48
  summary: A small command-line interface framework.