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.
- data/LICENSE +21 -0
- data/README.markdown +113 -0
- data/lib/claide.rb +702 -0
- 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:
|