claide 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d9085e38c36d1128b294f24e832a757683f3c9a2
4
- data.tar.gz: 8d85b685edcf81f078b1961f164e34df77981573
3
+ metadata.gz: 8b17373043336a80f9e26e9c516bae69912b455c
4
+ data.tar.gz: 5ebac7336f852fae13e18cd2401aed67032a68f7
5
5
  SHA512:
6
- metadata.gz: df44b05b8e3e0fce7a7f9109155cf4e37b91c26ce7a48d1694f9df81165d1345e099eae6367bee698ebac620a43961a59d48199cc912b2b1151b33ce58024121
7
- data.tar.gz: 8f78e80a5f12aa9a135698319bd5ca173798655784c2a79e63066f8f0768447768fe5b55bdf4690aac330f3f33cf850d489161199214bd357d16f98eb2759695
6
+ metadata.gz: 40bf038269203c041b11b0b64a830103fc194b051472aec39cef958ca1de08a4dadab8dae285650a117a02cd58b1e709eb3397b22dedb7bff37b95d02b96b6df
7
+ data.tar.gz: e137fa4d2b3d6aa178ba9feb6a2b1197364d1603e579746be60033266872af863b076a2c1c53cfbddffc6a206ad5106fb563e2dcc0a73b5c4cc23d8da7c5e339
@@ -9,7 +9,7 @@ module CLAide
9
9
  #
10
10
  # CLAide’s version, following [semver](http://semver.org).
11
11
  #
12
- VERSION = '0.3.0'
12
+ VERSION = '0.3.1'
13
13
 
14
14
  require 'claide/argv.rb'
15
15
  require 'claide/command.rb'
@@ -0,0 +1,250 @@
1
+ module CLAide
2
+
3
+ # This class is responsible for parsing the parameters specified by the user,
4
+ # accessing individual parameters, and keep state by removing handled
5
+ # parameters.
6
+ #
7
+ class ARGV
8
+
9
+ # @param [Array<String>] argv
10
+ #
11
+ # A list of parameters. Each entry is ensured to be a string by calling
12
+ # `#to_s` on it.
13
+ #
14
+ def initialize(argv)
15
+ @entries = self.class.parse(argv)
16
+ end
17
+
18
+ # @return [Boolean]
19
+ #
20
+ # Returns whether or not there are any remaining unhandled parameters.
21
+ #
22
+ def empty?
23
+ @entries.empty?
24
+ end
25
+
26
+ # @return [Array<String>]
27
+ #
28
+ # A list of the remaining unhandled parameters, in the same format a user
29
+ # specifies it in.
30
+ #
31
+ # @example
32
+ #
33
+ # argv = CLAide::ARGV.new(['tea', '--no-milk', '--sweetner=honey'])
34
+ # argv.shift_argument # => 'tea'
35
+ # argv.remainder # => ['--no-milk', '--sweetner=honey']
36
+ #
37
+ def remainder
38
+ @entries.map do |type, (key, value)|
39
+ case type
40
+ when :arg
41
+ key
42
+ when :flag
43
+ "--#{'no-' if value == false}#{key}"
44
+ when :option
45
+ "--#{key}=#{value}"
46
+ end
47
+ end
48
+ end
49
+
50
+ # @return [Hash]
51
+ #
52
+ # A hash that consists of the remaining flags and options and their
53
+ # values.
54
+ #
55
+ # @example
56
+ #
57
+ # argv = CLAide::ARGV.new(['tea', '--no-milk', '--sweetner=honey'])
58
+ # argv.options # => { 'milk' => false, 'sweetner' => 'honey' }
59
+ #
60
+ def options
61
+ options = {}
62
+ @entries.each do |type, (key, value)|
63
+ options[key] = value unless type == :arg
64
+ end
65
+ options
66
+ end
67
+
68
+ # @return [Array<String>]
69
+ #
70
+ # A list of the remaining arguments.
71
+ #
72
+ # @example
73
+ #
74
+ # argv = CLAide::ARGV.new(['tea', 'white', '--no-milk', 'biscuit'])
75
+ # argv.shift_argument # => 'tea'
76
+ # argv.arguments # => ['white', 'biscuit']
77
+ #
78
+ def arguments
79
+ @entries.map { |type, value| value if type == :arg }.compact
80
+ end
81
+
82
+ # @return [Array<String>]
83
+ #
84
+ # A list of the remaining arguments.
85
+ #
86
+ # @note
87
+ #
88
+ # This version also removes the arguments from the remaining parameters.
89
+ #
90
+ # @example
91
+ #
92
+ # argv = CLAide::ARGV.new(['tea', 'white', '--no-milk', 'biscuit'])
93
+ # argv.arguments # => ['tea', 'white', 'biscuit']
94
+ # argv.arguments! # => ['tea', 'white', 'biscuit']
95
+ # argv.arguments # => []
96
+ #
97
+ def arguments!
98
+ arguments = []
99
+ while arg = shift_argument
100
+ arguments << arg
101
+ end
102
+ arguments
103
+ end
104
+
105
+ # @return [String]
106
+ #
107
+ # The first argument in the remaining parameters.
108
+ #
109
+ # @note
110
+ #
111
+ # This will remove the argument from the remaining parameters.
112
+ #
113
+ # @example
114
+ #
115
+ # argv = CLAide::ARGV.new(['tea', 'white'])
116
+ # argv.shift_argument # => 'tea'
117
+ # argv.arguments # => ['white']
118
+ #
119
+ def shift_argument
120
+ if index = @entries.find_index { |type, _| type == :arg }
121
+ entry = @entries[index]
122
+ @entries.delete_at(index)
123
+ entry.last
124
+ end
125
+ end
126
+
127
+ # @return [Boolean, nil]
128
+ #
129
+ # Returns `true` if the flag by the specified `name` is among the
130
+ # remaining parameters and is not negated.
131
+ #
132
+ # @param [String] name
133
+ #
134
+ # The name of the flag to look for among the remaining parameters.
135
+ #
136
+ # @param [Boolean] default
137
+ #
138
+ # The value that is returned in case the flag is not among the remaining
139
+ # parameters.
140
+ #
141
+ # @note
142
+ #
143
+ # This will remove the flag from the remaining parameters.
144
+ #
145
+ # @example
146
+ #
147
+ # argv = CLAide::ARGV.new(['tea', '--no-milk', '--sweetner=honey'])
148
+ # argv.flag?('milk') # => false
149
+ # argv.flag?('milk') # => nil
150
+ # argv.flag?('milk', true) # => true
151
+ # argv.remainder # => ['tea', '--sweetner=honey']
152
+ #
153
+ def flag?(name, default = nil)
154
+ delete_entry(:flag, name, default)
155
+ end
156
+
157
+ # @return [String, nil]
158
+ #
159
+ # Returns the value of the option by the specified `name` is among the
160
+ # remaining parameters.
161
+ #
162
+ # @param [String] name
163
+ #
164
+ # The name of the option to look for among the remaining parameters.
165
+ #
166
+ # @param [String] default
167
+ #
168
+ # The value that is returned in case the option is not among the
169
+ # remaining parameters.
170
+ #
171
+ # @note
172
+ #
173
+ # This will remove the option from the remaining parameters.
174
+ #
175
+ # @example
176
+ #
177
+ # argv = CLAide::ARGV.new(['tea', '--no-milk', '--sweetner=honey'])
178
+ # argv.option('sweetner') # => 'honey'
179
+ # argv.option('sweetner') # => nil
180
+ # argv.option('sweetner', 'sugar') # => 'sugar'
181
+ # argv.remainder # => ['tea', '--no-milk']
182
+ #
183
+ def option(name, default = nil)
184
+ delete_entry(:option, name, default)
185
+ end
186
+
187
+ private
188
+
189
+ attr_reader :entries
190
+
191
+ def delete_entry(requested_type, requested_key, default)
192
+ result = nil
193
+ @entries.delete_if do |type, (key, value)|
194
+ if requested_key == key && requested_type == type
195
+ result = value
196
+ true
197
+ end
198
+ end
199
+ result.nil? ? default : result
200
+ end
201
+
202
+ # @return [Array<Array>]
203
+ #
204
+ # A list of tuples for each parameter, where the first entry is the
205
+ # `type` and the second entry the actual parsed parameter.
206
+ #
207
+ # @example
208
+ #
209
+ # list = parse(['tea', '--no-milk', '--sweetner=honey'])
210
+ # list # => [[:arg, "tea"],
211
+ # [:flag, ["milk", false]],
212
+ # [:option, ["sweetner", "honey"]]]
213
+ #
214
+ def self.parse(argv)
215
+ entries = []
216
+ copy = argv.map(&:to_s)
217
+ while x = copy.shift
218
+ type = key = value = nil
219
+ if is_arg?(x)
220
+ # A regular argument (e.g. a command)
221
+ type, value = :arg, x
222
+ else
223
+ key = x[2..-1]
224
+ if key.include?('=')
225
+ # An option with a value
226
+ type = :option
227
+ key, value = key.split('=', 2)
228
+ else
229
+ # A boolean flag
230
+ type = :flag
231
+ value = true
232
+ if key[0,3] == 'no-'
233
+ # A negated boolean flag
234
+ key = key[3..-1]
235
+ value = false
236
+ end
237
+ end
238
+ value = [key, value]
239
+ end
240
+ entries << [type, value]
241
+ end
242
+ entries
243
+ end
244
+
245
+ def self.is_arg?(x)
246
+ x[0,2] != '--'
247
+ end
248
+
249
+ end
250
+ end
@@ -0,0 +1,370 @@
1
+ require 'claide/command/banner'
2
+
3
+ module CLAide
4
+
5
+ # This class is used to build a command-line interface
6
+ #
7
+ # Each command is represented by a subclass of this class, which may be
8
+ # nested to create more granular commands.
9
+ #
10
+ # Following is an overview of the types of commands and what they should do.
11
+ #
12
+ # ### Any command type
13
+ #
14
+ # * Inherit from the command class under which the command should be nested.
15
+ # * Set {Command.summary} to a brief description of the command.
16
+ # * Override {Command.options} to return the options it handles and their
17
+ # descriptions and prepending them to the results of calling `super`.
18
+ # * Override {Command#initialize} if it handles any parameters.
19
+ # * Override {Command#validate!} to check if the required parameters the
20
+ # command handles are valid, or call {Command#help!} in case they’re not.
21
+ #
22
+ # ### Abstract command
23
+ #
24
+ # The following is needed for an abstract command:
25
+ #
26
+ # * Set {Command.abstract_command} to `true`.
27
+ # * Subclass the command.
28
+ #
29
+ # When the optional {Command.description} is specified, it will be shown at
30
+ # the top of the command’s help banner.
31
+ #
32
+ # ### Normal command
33
+ #
34
+ # The following is needed for a normal command:
35
+ #
36
+ # * Set {Command.arguments} to the description of the arguments this command
37
+ # handles.
38
+ # * Override {Command#run} to perform the actual work.
39
+ #
40
+ # When the optional {Command.description} is specified, it will be shown
41
+ # underneath the usage section of the command’s help banner. Otherwise this
42
+ # defaults to {Command.summary}.
43
+ #
44
+ class Command
45
+
46
+ #-------------------------------------------------------------------------#
47
+
48
+ class << self
49
+
50
+ # @return [Boolean] Indicates whether or not this command can actually
51
+ # perform work of itself, or that it only contains subcommands.
52
+ #
53
+ attr_accessor :abstract_command
54
+ alias_method :abstract_command?, :abstract_command
55
+
56
+ # @return [String] The subcommand which an abstract command should invoke
57
+ # by default.
58
+ #
59
+ attr_accessor :default_subcommand
60
+
61
+ # @return [String] A brief description of the command, which is shown
62
+ # next to the command in the help banner of a parent command.
63
+ #
64
+ attr_accessor :summary
65
+
66
+ # @return [String] A longer description of the command, which is shown
67
+ # underneath the usage section of the command’s help banner. Any
68
+ # indentation in this value will be ignored.
69
+ #
70
+ attr_accessor :description
71
+
72
+ # @return [String] A list of arguments the command handles. This is shown
73
+ # in the usage section of the command’s help banner.
74
+ #
75
+ attr_accessor :arguments
76
+
77
+ # @return [Boolean] The default value for {Command#colorize_output}. This
78
+ # defaults to `true` if `String` has the instance methods
79
+ # `#green` and `#red`. Which are defined by, for instance, the
80
+ # [colored](https://github.com/defunkt/colored) gem.
81
+ #
82
+ def colorize_output
83
+ if @colorize_output.nil?
84
+ @colorize_output = String.method_defined?(:red) &&
85
+ String.method_defined?(:green)
86
+ end
87
+ @colorize_output
88
+ end
89
+ attr_writer :colorize_output
90
+ alias_method :colorize_output?, :colorize_output
91
+
92
+ # @return [String] The name of the command. Defaults to a snake-cased
93
+ # version of the class’ name.
94
+ #
95
+ def command
96
+ @command ||= name.split('::').last.gsub(/[A-Z]+[a-z]*/) do |part|
97
+ part.downcase << '-'
98
+ end[0..-2]
99
+ end
100
+ attr_writer :command
101
+
102
+ # @return [String] The full command up-to this command.
103
+ #
104
+ # @example
105
+ #
106
+ # BevarageMaker::Tea.full_command # => "beverage-maker tea"
107
+ #
108
+ def full_command
109
+ if superclass == Command
110
+ "#{command}"
111
+ else
112
+ "#{superclass.full_command} #{command}"
113
+ end
114
+ end
115
+
116
+ # @return [Array<Class>] A list of command classes that are nested under
117
+ # this command.
118
+ #
119
+ def subcommands
120
+ @subcommands ||= []
121
+ end
122
+
123
+ # @visibility private
124
+ #
125
+ # Automatically registers a subclass as a subcommand.
126
+ #
127
+ def inherited(subcommand)
128
+ subcommands << subcommand
129
+ end
130
+
131
+ # Should be overridden by a subclass if it handles any options.
132
+ #
133
+ # The subclass has to combine the result of calling `super` and its own
134
+ # list of options. The recommended way of doing this is by concatenating
135
+ # concatenating to this classes’ own options.
136
+ #
137
+ # @return [Array<Array>]
138
+ #
139
+ # A list of option name and description tuples.
140
+ #
141
+ # @example
142
+ #
143
+ # def self.options
144
+ # [
145
+ # ['--verbose', 'Print more info'],
146
+ # ['--help', 'Print help banner'],
147
+ # ].concat(super)
148
+ # end
149
+ #
150
+ def options
151
+ options = [
152
+ ['--verbose', 'Show more debugging information'],
153
+ ['--help', 'Show help banner of specified command'],
154
+ ]
155
+ if Command.colorize_output?
156
+ options.unshift(['--no-color', 'Show output without color'])
157
+ end
158
+ options
159
+ end
160
+
161
+ # @param [Array, ARGV] argv
162
+ # A list of (remaining) parameters.
163
+ #
164
+ # @return [Command] An instance of the command class that was matched by
165
+ # going through the arguments in the parameters and drilling down
166
+ # command classes.
167
+ #
168
+ def parse(argv)
169
+ argv = ARGV.new(argv) unless argv.is_a?(ARGV)
170
+ cmd = argv.arguments.first
171
+ if cmd && subcommand = subcommands.find { |sc| sc.command == cmd }
172
+ argv.shift_argument
173
+ subcommand.parse(argv)
174
+ elsif abstract_command? && default_subcommand
175
+ subcommand = subcommands.find { |sc| sc.command == default_subcommand }
176
+ unless subcommand
177
+ raise "Unable to find the default subcommand `#{default_subcommand}` " \
178
+ "for command `#{self}`."
179
+ end
180
+ result = subcommand.parse(argv)
181
+ result.invoked_as_default = true
182
+ result
183
+ else
184
+ new(argv)
185
+ end
186
+ end
187
+
188
+ # Instantiates the command class matching the parameters through
189
+ # {Command.parse}, validates it through {Command#validate!}, and runs it
190
+ # through {Command#run}.
191
+ #
192
+ # @note
193
+ #
194
+ # You should normally call this on
195
+ #
196
+ # @param [Array, ARGV] argv
197
+ #
198
+ # A list of parameters. For instance, the standard `ARGV` constant,
199
+ # which contains the parameters passed to the program.
200
+ #
201
+ # @return [void]
202
+ #
203
+ def run(argv)
204
+ command = parse(argv)
205
+ command.validate!
206
+ command.run
207
+ rescue Exception => exception
208
+ if exception.is_a?(InformativeError)
209
+ puts exception.message
210
+ if command.verbose?
211
+ puts
212
+ puts(*exception.backtrace)
213
+ end
214
+ exit exception.exit_status
215
+ else
216
+ report_error(exception)
217
+ end
218
+ end
219
+
220
+ # Allows the application to perform custom error reporting, by overriding
221
+ # this method.
222
+ #
223
+ # @param [Exception] exception
224
+ #
225
+ # An exception that occurred while running a command through
226
+ # {Command.run}.
227
+ #
228
+ # @raise
229
+ #
230
+ # By default re-raises the specified exception.
231
+ #
232
+ # @return [void]
233
+ #
234
+ def report_error(exception)
235
+ raise exception
236
+ end
237
+
238
+ # @visibility private
239
+ #
240
+ # @raise [Help]
241
+ #
242
+ # Signals CLAide that a help banner for this command should be shown,
243
+ # with an optional error message.
244
+ #
245
+ # @return [void]
246
+ #
247
+ def help!(error_message = nil, colorize = false)
248
+ raise Help.new(banner(colorize), error_message, colorize)
249
+ end
250
+
251
+ # @visibility private
252
+ #
253
+ # Returns the banner for the command.
254
+ #
255
+ # @param [Bool] colorize
256
+ # Whether the banner should be returned colorized.
257
+ #
258
+ # @return [String] The banner for the command.
259
+ #
260
+ def banner(colorize = false)
261
+ Banner.new(self, colorize).formatted_banner
262
+ end
263
+
264
+ end
265
+
266
+ #-------------------------------------------------------------------------#
267
+
268
+ # Set to `true` if the user specifies the `--verbose` option.
269
+ #
270
+ # @note
271
+ #
272
+ # If you want to make use of this value for your own configuration, you
273
+ # should check the value _after_ calling the `super` {Command#initialize}
274
+ # implementation.
275
+ #
276
+ # @return [Boolean]
277
+ #
278
+ # Wether or not backtraces should be included when presenting the user an
279
+ # exception that includes the {InformativeError} module.
280
+ #
281
+ attr_accessor :verbose
282
+ alias_method :verbose?, :verbose
283
+
284
+ # Set to `true` if {Command.colorize_output} returns `true` and the user
285
+ # did **not** specify the `--no-color` option.
286
+ #
287
+ # @note (see #verbose)
288
+ #
289
+ # @return [Boolean]
290
+ #
291
+ # Wether or not to color {InformativeError} exception messages red and
292
+ # subcommands in help banners green.
293
+ #
294
+ attr_accessor :colorize_output
295
+ alias_method :colorize_output?, :colorize_output
296
+
297
+ # @return [Bool] Whether the command was invoked by an abstract command by
298
+ # default.
299
+ #
300
+ attr_accessor :invoked_as_default
301
+ alias_method :invoked_as_default?, :invoked_as_default
302
+
303
+ # Subclasses should override this method to remove the arguments/options
304
+ # they support from `argv` _before_ calling `super`.
305
+ #
306
+ # The `super` implementation sets the {#verbose} attribute based on whether
307
+ # or not the `--verbose` option is specified; and the {#colorize_output}
308
+ # attribute to `false` if {Command.colorize_output} returns `true`, but the
309
+ # user specified the `--no-color` option.
310
+ #
311
+ # @param [ARGV, Array] argv
312
+ #
313
+ # A list of (user-supplied) params that should be handled.
314
+ #
315
+ def initialize(argv)
316
+ argv = ARGV.new(argv) unless argv.is_a?(ARGV)
317
+ @verbose = argv.flag?('verbose')
318
+ @colorize_output = argv.flag?('color', Command.colorize_output?)
319
+ @argv = argv
320
+ end
321
+
322
+ # Raises a Help exception if the `--help` option is specified, if `argv`
323
+ # still contains remaining arguments/options by the time it reaches this
324
+ # implementation, or when called on an ‘abstract command’.
325
+ #
326
+ # Subclasses should call `super` _before_ doing their own validation. This
327
+ # way when the user specifies the `--help` flag a help banner is shown,
328
+ # instead of possible actual validation errors.
329
+ #
330
+ # @raise [Help]
331
+ #
332
+ # @return [void]
333
+ #
334
+ def validate!
335
+ help! if @argv.flag?('help')
336
+ help! "Unknown arguments: #{@argv.remainder.join(' ')}" if !@argv.empty?
337
+ help! if self.class.abstract_command?
338
+ end
339
+
340
+ # This method should be overridden by the command class to perform its work.
341
+ #
342
+ # @return [void
343
+ #
344
+ def run
345
+ raise "A subclass should override the Command#run method to actually " \
346
+ "perform some work."
347
+ end
348
+
349
+ protected
350
+
351
+ # @raise [Help]
352
+ #
353
+ # Signals CLAide that a help banner for this command should be shown,
354
+ # with an optional error message.
355
+ #
356
+ # @return [void]
357
+ #
358
+ def help!(error_message = nil)
359
+ if invoked_as_default?
360
+ command = self.class.superclass
361
+ else
362
+ command = self.class
363
+ end
364
+ command = command.help!(error_message, colorize_output?)
365
+ end
366
+
367
+ #-------------------------------------------------------------------------#
368
+
369
+ end
370
+ end
@@ -0,0 +1,110 @@
1
+ module CLAide
2
+ class Command
3
+
4
+ # Creates the formatted banner to present as help of the provided command
5
+ # class.
6
+ #
7
+ class Banner
8
+
9
+ # @return [Class]
10
+ #
11
+ attr_accessor :command
12
+
13
+ # @return [Bool]
14
+ #
15
+ attr_accessor :colorize_output
16
+ alias_method :colorize_output?, :colorize_output
17
+
18
+ # @param [Class] command @see command
19
+ # @param [Class] colorize_output@see colorize_output
20
+ #
21
+ def initialize(command, colorize_output = false)
22
+ @command = command
23
+ @colorize_output = colorize_output
24
+ end
25
+
26
+ # @return [String]
27
+ #
28
+ def formatted_banner
29
+ banner = []
30
+ if command.abstract_command?
31
+ banner << command.description if command.description
32
+ elsif usage = formatted_usage_description
33
+ banner << 'Usage:'
34
+ banner << usage
35
+ end
36
+ if commands = formatted_subcommand_summaries
37
+ banner << 'Commands:'
38
+ banner << commands
39
+ end
40
+ banner << 'Options:'
41
+ banner << formatted_options_description
42
+ banner.join("\n\n")
43
+ end
44
+
45
+ private
46
+
47
+ # @!group Banner sections
48
+
49
+ #-----------------------------------------------------------------------#
50
+
51
+ # @return [String]
52
+ #
53
+ def formatted_options_description
54
+ opts = command.options
55
+ size = opts.map { |opt| opt.first.size }.max
56
+ opts.map { |key, desc| " #{key.ljust(size)} #{desc}" }.join("\n")
57
+ end
58
+
59
+ # @return [String]
60
+ #
61
+ def formatted_usage_description
62
+ if message = command.description || command.summary
63
+ message = strip_heredoc(message)
64
+ message = message.split("\n").map { |line| " #{line}" }.join("\n")
65
+ args = " #{command.arguments}" if command.arguments
66
+ " $ #{command.full_command}#{args}\n\n#{message}"
67
+ end
68
+ end
69
+
70
+ # @return [String]
71
+ #
72
+ def formatted_subcommand_summaries
73
+ subcommands = command.subcommands.reject do |subcommand|
74
+ subcommand.summary.nil?
75
+ end.sort_by(&:command)
76
+ unless subcommands.empty?
77
+ command_size = subcommands.map { |cmd| cmd.command.size }.max
78
+ subcommands.map do |subcommand|
79
+ subcommand_string = subcommand.command.ljust(command_size)
80
+ subcommand_string = subcommand_string.green if colorize_output?
81
+ is_default = subcommand.command == command.default_subcommand
82
+ if is_default
83
+ bullet_point = '-'
84
+ else
85
+ bullet_point = '*'
86
+ end
87
+ " #{bullet_point} #{subcommand_string} #{subcommand.summary}"
88
+ end.join("\n")
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ # @!group Private helpers
95
+
96
+ #-----------------------------------------------------------------------#
97
+
98
+ # @return [String] Lifted straight from ActiveSupport. Thanks guys!
99
+ #
100
+ def strip_heredoc(string)
101
+ if min = string.scan(/^[ \t]*(?=\S)/).min
102
+ string.gsub(/^[ \t]{#{min.size}}/, '')
103
+ else
104
+ string
105
+ end
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,57 @@
1
+ module CLAide
2
+
3
+ require 'claide/informative_error.rb'
4
+
5
+ # The exception class that is raised to indicate a help banner should be
6
+ # shown while running {Command.run}.
7
+ #
8
+ class Help < StandardError
9
+ include InformativeError
10
+
11
+ # @return [String] The banner containing the usage instructions of the
12
+ # command to show in the help.
13
+ #
14
+ attr_reader :banner
15
+
16
+ # @return [String] An optional error message that will be shown before the
17
+ # help banner.
18
+ #
19
+ attr_reader :error_message
20
+
21
+ # @return [Bool] Whether the error message should be colorized.
22
+ #
23
+ attr_reader :colorize
24
+ alias_method :colorize?, :colorize
25
+
26
+ # @param [String] banner @see banner
27
+ # @param [String] error_message @see error_message
28
+ #
29
+ # @note If an error message is provided, the exit status, used to
30
+ # terminate the program with, will be set to `1`, otherwise a {Help}
31
+ # exception is treated as not being a real error and exits with `0`.
32
+ #
33
+ def initialize(banner, error_message = nil, colorize = false)
34
+ @banner = banner
35
+ @error_message = error_message
36
+ @colorize = colorize
37
+ @exit_status = @error_message.nil? ? 0 : 1
38
+ end
39
+
40
+ # @return [String] The optional error message, colored in red if
41
+ # {Command.colorize_output} is set to `true`.
42
+ #
43
+ def formatted_error_message
44
+ if error_message
45
+ message = "[!] #{error_message}"
46
+ colorize? ? message.red : message
47
+ end
48
+ end
49
+
50
+ # @return [String] The optional error message, combined with the help
51
+ # banner of the command.
52
+ #
53
+ def message
54
+ [formatted_error_message, banner].compact.join("\n\n")
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,21 @@
1
+ module CLAide
2
+
3
+ # Including this module into an exception class will ensure that when raised,
4
+ # while running {Command.run}, only the message of the exception will be
5
+ # shown to the user. Unless disabled with the `--verbose` flag.
6
+ #
7
+ # In addition, the message will be colored red, if {Command.colorize_output}
8
+ # is set to `true`.
9
+ #
10
+ module InformativeError
11
+
12
+ # @return [Numeric] The exist status code that should be used to terminate
13
+ # the program with. Defaults to `1`.
14
+ #
15
+ attr_accessor :exit_status
16
+
17
+ def exit_status
18
+ @exit_status ||= 1
19
+ end
20
+ end
21
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claide
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eloy Duran
@@ -19,6 +19,11 @@ executables: []
19
19
  extensions: []
20
20
  extra_rdoc_files: []
21
21
  files:
22
+ - lib/claide/argv.rb
23
+ - lib/claide/command/banner.rb
24
+ - lib/claide/command.rb
25
+ - lib/claide/help.rb
26
+ - lib/claide/informative_error.rb
22
27
  - lib/claide.rb
23
28
  - README.markdown
24
29
  - LICENSE