claide 0.1.0

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