claide 0.1.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 (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: