claide 0.2.0 → 0.3.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.
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.