claide 0.3.0 → 0.3.1

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.
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