mamertes 2.4.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.
Files changed (53) hide show
  1. checksums.yaml +15 -0
  2. data/.DS_Store +0 -0
  3. data/.gitignore +6 -0
  4. data/.travis-gemfile +15 -0
  5. data/.travis.yml +10 -0
  6. data/.yardopts +1 -0
  7. data/Gemfile +21 -0
  8. data/README.md +126 -0
  9. data/Rakefile +29 -0
  10. data/doc/Mamertes.html +155 -0
  11. data/doc/Mamertes/Application.html +3057 -0
  12. data/doc/Mamertes/Command.html +7031 -0
  13. data/doc/Mamertes/CommandMethods.html +125 -0
  14. data/doc/Mamertes/CommandMethods/Children.html +1286 -0
  15. data/doc/Mamertes/CommandMethods/Help.html +209 -0
  16. data/doc/Mamertes/Error.html +631 -0
  17. data/doc/Mamertes/Localizer.html +376 -0
  18. data/doc/Mamertes/Option.html +6671 -0
  19. data/doc/Mamertes/Parser.html +276 -0
  20. data/doc/Mamertes/ParserMethods.html +125 -0
  21. data/doc/Mamertes/ParserMethods/General.html +134 -0
  22. data/doc/Mamertes/ParserMethods/General/ClassMethods.html +574 -0
  23. data/doc/Mamertes/Version.html +189 -0
  24. data/doc/_index.html +276 -0
  25. data/doc/class_list.html +54 -0
  26. data/doc/css/common.css +1 -0
  27. data/doc/css/full_list.css +57 -0
  28. data/doc/css/style.css +338 -0
  29. data/doc/file.README.html +198 -0
  30. data/doc/file_list.html +56 -0
  31. data/doc/frames.html +28 -0
  32. data/doc/index.html +198 -0
  33. data/doc/js/app.js +214 -0
  34. data/doc/js/full_list.js +178 -0
  35. data/doc/js/jquery.js +4 -0
  36. data/doc/method_list.html +509 -0
  37. data/doc/top-level-namespace.html +112 -0
  38. data/lib/mamertes.rb +18 -0
  39. data/lib/mamertes/application.rb +206 -0
  40. data/lib/mamertes/command.rb +529 -0
  41. data/lib/mamertes/option.rb +236 -0
  42. data/lib/mamertes/parser.rb +317 -0
  43. data/lib/mamertes/version.rb +24 -0
  44. data/locales/en.yml +40 -0
  45. data/locales/it.yml +40 -0
  46. data/mamertes.gemspec +30 -0
  47. data/spec/coverage_helper.rb +20 -0
  48. data/spec/mamertes/application_spec.rb +181 -0
  49. data/spec/mamertes/command_spec.rb +526 -0
  50. data/spec/mamertes/option_spec.rb +274 -0
  51. data/spec/mamertes/parser_spec.rb +126 -0
  52. data/spec/spec_helper.rb +15 -0
  53. metadata +115 -0
@@ -0,0 +1,236 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the mamertes gem. Copyright (C) 2013 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ module Mamertes
8
+ # List of valid option types.
9
+ #
10
+ # Values are the default values for that type.
11
+ #
12
+ # For any unknown type, the default value is `false`, it means that any unknown type is managed as a Boolean value with no argument.
13
+ OPTION_TYPES = {String => "", Integer => 0, Float => 0.0, Array => []}
14
+ OPTION_TYPES.default = false
15
+
16
+ # This class represents an option for a command.
17
+ #
18
+ # @attribute name
19
+ # @return [String] The name of this option.
20
+ # @attribute short
21
+ # @return [String] The short form (i.e.: `-h`) for this option.
22
+ # @attribute long
23
+ # @return [String] The long form (i.e.: `--help`) for this option.
24
+ # @attribute type
25
+ # @return [Class] The type of this option.
26
+ # @attribute required
27
+ # @return [Boolean] If this option is required.
28
+ # @attribute default
29
+ # @return [Object] The default value of this option.
30
+ # @attribute meta
31
+ # @return [String] The META argument for this option, used only when showing the help.
32
+ # @attribute help
33
+ # @return [String] An help message for this option.
34
+ # @attribute value
35
+ # @return [Object] The current value of this option.
36
+ # @attribute action
37
+ # @return [Proc] The action associated to this option.
38
+ # @attribute validator
39
+ # @return [Array|Regexp] or A constraint for valid values. Can be an Array of valid values or a Regexp.
40
+ # @attribute parent
41
+ # @return [Command] The parent of this option.
42
+ class Option
43
+ attr_accessor :name
44
+ attr_accessor :short
45
+ attr_accessor :long
46
+ attr_accessor :type
47
+ attr_accessor :required
48
+ attr_accessor :default
49
+ attr_accessor :meta
50
+ attr_accessor :help
51
+ attr_accessor :value
52
+ attr_accessor :action
53
+ attr_accessor :validator
54
+ attr_accessor :parent
55
+
56
+ # Creates a new option.
57
+ #
58
+ # @param name [String] The name of this option. Must be unique.
59
+ # @param forms [Array] An array of short and long forms for this option. Missing forms will be inferred by the name.
60
+ # @param options [Hash] The settings for this option.
61
+ # @param action [Proc] The action of this option.
62
+ def initialize(name, forms = [], options = {}, &action)
63
+ @name = name.ensure_string
64
+ @provided = false
65
+ setup_forms(forms)
66
+ setup_options(options)
67
+ setup_action(action)
68
+ end
69
+
70
+ # Sets the short form of this option.
71
+ #
72
+ # @param value [String] The short form of this option.
73
+ def short=(value)
74
+ value = @name[0, 1] if !value.present?
75
+
76
+ # Clean value
77
+ final_value = value.to_s.match(/^-{0,2}([a-z0-9])(.*)$/i)[1]
78
+
79
+ @short = final_value if final_value.present?
80
+ end
81
+
82
+ # Sets the long form of this option.
83
+ #
84
+ # @param value [String] The short form of this option.
85
+ def long=(value)
86
+ value = @name if !value.present?
87
+
88
+ # Clean value
89
+ final_value = value.to_s.match(/^-{0,2}(.+)$/)[1]
90
+
91
+ @long = final_value if final_value.present?
92
+ end
93
+
94
+ # Sets the long form of this option. Can be a Object, an Array or a Regexp.
95
+ #
96
+ # @param value [String] The validator of this option.
97
+ def validator=(value)
98
+ value = nil if value.blank? || (value.is_a?(Regexp) && value.source.blank?)
99
+ value = value.ensure_array(nil, true, true, true, :ensure_string) if !value.nil? && !value.is_a?(Regexp)
100
+ @validator = value
101
+ end
102
+
103
+ # Returns the short form with a dash prepended.
104
+ #
105
+ # @return [String] The short form with a dash prepended.
106
+ def complete_short
107
+ "-#{@short}"
108
+ end
109
+
110
+ # Returns the long form with two dashes prepended.
111
+ #
112
+ # @return [String] The short form with two dashes prepended.
113
+ def complete_long
114
+ "--#{@long}"
115
+ end
116
+
117
+ # Returns a label for this option, combining short and long forms.
118
+ #
119
+ # @return [String] A label for this option.
120
+ def label
121
+ [complete_short, complete_long].compact.join("/")
122
+ end
123
+
124
+ # Returns the meta argument for this option.
125
+ #
126
+ # @return [String|NilClass] Returns the current meta argument for this option (the default value is the option name uppercased) or `nil`, if this option doesn't require a meta argument.
127
+ def meta
128
+ requires_argument? ? (@meta.present? ? @meta : @name.upcase) : nil
129
+ end
130
+
131
+ # Get the current default value for this option.
132
+ #
133
+ # @return [Object] The default value for this option.
134
+ def default
135
+ @default || ::Mamertes::OPTION_TYPES[@type]
136
+ end
137
+
138
+ # Check if the current option has a default value.
139
+ #
140
+ # @return [Boolean] If the current option has a default value.
141
+ def has_default?
142
+ !@default.nil?
143
+ end
144
+
145
+ # Sets the value of this option and also make sure that it is validated.
146
+ #
147
+ # @param value [Object] The new value of this option.
148
+ # @param raise_error [Boolean] If raise an ArgumentError in case of validation errors.
149
+ # @return [Boolean] `true` if operation succeeded, `false` otherwise.
150
+ def set(value, raise_error = true)
151
+ vs = @validator.present? ? (@validator.is_a?(Array) ? :array : :regexp) : false # Check we have a validator
152
+ rv = vs ? @validator.send(vs == :array ? "include?" : "match", value) : true
153
+
154
+ if rv then
155
+ @value = value
156
+ @provided = true
157
+ elsif raise_error then # Validation failed
158
+ handle_set_failure(vs)
159
+ else
160
+ false
161
+ end
162
+ end
163
+
164
+ # Executes the action associated to this option.
165
+ def execute_action
166
+ if @action.present? then
167
+ @provided = true
168
+ @action.call(parent, self)
169
+ end
170
+ end
171
+
172
+ # Checks if this option requires an argument.
173
+ #
174
+ # @return [Boolean] `true` if this option requires an argument, `false` otherwise.
175
+ def requires_argument?
176
+ [String, Integer, Float, Array].include?(@type) && @action.blank?
177
+ end
178
+
179
+ # If this option was provided.
180
+ #
181
+ # @return [Boolean] `true` if this option was provided, `false` otherwise.
182
+ def provided?
183
+ @provided
184
+ end
185
+
186
+ # Check if this command has a help.
187
+ #
188
+ # @return [Boolean] `true` if this command has a help, `false` otherwise.
189
+ def has_help?
190
+ @help.present?
191
+ end
192
+
193
+ # Get the current value for this option.
194
+ #
195
+ # @return [Object] The current value of this option.
196
+ def value
197
+ provided? ? @value : default
198
+ end
199
+
200
+ private
201
+ # Setups the forms of the this option.
202
+ #
203
+ # @param forms [Array] An array of short and long forms for this option. Missing forms will be inferred by the name.
204
+ def setup_forms(forms)
205
+ self.short = forms.length > 0 ? forms[0] : @name[0, 1]
206
+ self.long = forms.length == 2 ? forms[1] : @name
207
+ end
208
+
209
+ # Setups the settings of the this option.
210
+ #
211
+ # @param options [Hash] The settings for this option.
212
+ def setup_options(options)
213
+ (options.is_a?(::Hash) ? options : {}).each_pair do |option, value|
214
+ send("#{option}=", value) if respond_to?("#{option}=")
215
+ end
216
+ end
217
+
218
+ # Setups the action of the this option.
219
+ #
220
+ # @param action [Proc] The action of this option.
221
+ def setup_action(action)
222
+ @action = action if action.present? && action.respond_to?(:call) && action.try(:arity) == 2
223
+ end
224
+
225
+ # Handle failure in setting an option.
226
+ #
227
+ # @param vs [Symbol] The type of validator.
228
+ def handle_set_failure(vs)
229
+ if vs == :array then
230
+ raise ::Mamertes::Error.new(self, :validation_failed, @parent.i18n.invalid_value(label, ::Mamertes::Parser.smart_join(@validator)))
231
+ else
232
+ raise ::Mamertes::Error.new(self, :validation_failed, @parent.i18n.invalid_for_regexp(label, @validator.inspect))
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,317 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the mamertes gem. Copyright (C) 2013 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ module Mamertes
8
+ # Methods for the {Parser Parser} class.
9
+ module ParserMethods
10
+ # General methods.
11
+ module General
12
+ extend ActiveSupport::Concern
13
+
14
+ # Class methods
15
+ module ClassMethods
16
+ # Joins an array using multiple separators.
17
+ #
18
+ # @param array [Array] The array to join.
19
+ # @param separator [String] The separator to use for all but last join.
20
+ # @param last_separator [String] The separator to use for the last join.
21
+ # @param quote [String] If not nil, elements are quoted with that element.
22
+ # @return [String] The joined array.
23
+ def smart_join(array, separator = ", ", last_separator = " and ", quote = "\"")
24
+ separator = separator.ensure_string
25
+ last_separator = last_separator.ensure_string
26
+ array = array.ensure_array {|a| quote.present? ? "#{quote}#{a}#{quote}" : a.ensure_string }
27
+ array.length < 2 ? (array[0] || "") : (array[0, array.length - 1].join(separator) + last_separator + array[-1])
28
+ end
29
+
30
+ # Finds a command which corresponds to an argument.
31
+ #
32
+ # @param arg [String] The string to match.
33
+ # @param command [Command] The command to search subcommand in.
34
+ # @param args [String] The complete list of arguments passed.
35
+ # @param separator [String] The separator for joined syntax commands.
36
+ # @return [Hash|NilClass] An hash with `name` and `args` keys if a valid subcommand is found, `nil` otherwise.
37
+ def find_command(arg, command, args, separator = ":")
38
+ args = args.ensure_array.dup
39
+
40
+ if command.commands.present? then
41
+ arg, args = adjust_command(arg, args, separator)
42
+
43
+ matching = match_subcommands(arg, command)
44
+ if matching.length == 1 # Found a command
45
+ {name: matching[0], args: args}
46
+ elsif matching.length > 1 # Ambiguous match
47
+ raise ::Mamertes::Error.new(command, :ambiguous_command, command.i18n.ambigous_command(arg, ::Mamertes::Parser.smart_join(matching)))
48
+ end
49
+ else
50
+ nil
51
+ end
52
+ end
53
+
54
+ # Parses a command/application.
55
+ #
56
+ # @param command [Command] The command or application to parse.
57
+ # @param args [Array] The arguments to parse.
58
+ # @return [Hash|NilClass] An hash with `name` (of a subcommand to execute) and `args` keys if a valid subcommand is found, `nil` otherwise.
59
+ def parse(command, args)
60
+ ::Mamertes::Parser.new.parse(command, args)
61
+ end
62
+
63
+ private
64
+ # Adjusts a command so that it only specify a single command.
65
+ #
66
+ # @param arg [String] The string to match.
67
+ # @param args [String] The complete list of arguments passed.
68
+ # @param separator [String] The separator for joined syntax commands.
69
+ # @return [Array] Adjust command and arguments.
70
+ def adjust_command(arg, args, separator)
71
+ if arg.index(separator) then
72
+ tokens = arg.split(separator, 2)
73
+ arg = tokens[0]
74
+ args.insert(0, tokens[1])
75
+ end
76
+
77
+ [arg, args]
78
+ end
79
+
80
+ # Matches a string against a command's subcommands.
81
+ #
82
+ # @param arg [String] The string to match.
83
+ # @param command [Command] The command to search subcommand in.
84
+ # @return [Array] The matching subcommands.
85
+ def match_subcommands(arg, command)
86
+ command.commands.keys.select {|c| c =~ /^(#{Regexp.quote(arg)})/ }.compact
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ # The parser for the command line.
93
+ class Parser
94
+ include ::Mamertes::ParserMethods::General
95
+
96
+ # Parses a command/application.
97
+ #
98
+ # @param command [Command] The command or application to parse.
99
+ # @param args [Array] The arguments to parse.
100
+ # @return [Hash|NilClass] An hash with `name` (of a subcommand to execute) and `args` keys if a valid subcommand is found, `nil` otherwise.
101
+ def parse(command, args)
102
+ args = args.ensure_array.dup
103
+ forms, parser = create_parser(command)
104
+ perform_parsing(parser, command, args, forms)
105
+ end
106
+
107
+ private
108
+ # Creates a new option parser.
109
+ #
110
+ # @param command [Command] The command or application to parse.
111
+ # @return [OptionParser] The new parser
112
+ def create_parser(command)
113
+ forms = {}
114
+ parser = OptionParser.new do |opts|
115
+ # Add every option
116
+ command.options.each_pair do |_, option|
117
+ check_unique(command, forms, option)
118
+ setup_option(command, opts, option)
119
+ end
120
+ end
121
+
122
+ [forms, parser]
123
+ end
124
+
125
+ # Perform the parsing
126
+ #
127
+ # @param parser [OptionParser] The option parser.
128
+ # @param command [Command] The command or application to parse.
129
+ # @param args [Array] The arguments to parse.
130
+ # @param forms [Hash] The current forms.
131
+ def perform_parsing(parser, command, args, forms)
132
+ rv = nil
133
+
134
+ begin
135
+ rv = execute_parsing(parser, command, args)
136
+ rescue OptionParser::NeedlessArgument, OptionParser::MissingArgument, OptionParser::InvalidOption => oe
137
+ type = oe.class.to_s.gsub("OptionParser::", "").underscore.to_sym
138
+ opt = oe.args.first
139
+ raise ::Mamertes::Error.new(forms[opt], type, command.i18n.send(type, opt))
140
+ rescue => e
141
+ raise e
142
+ end
143
+
144
+ rv
145
+ end
146
+
147
+ # Executes the parsing.
148
+ #
149
+ # @param parser [OptionParser] The option parser.
150
+ # @param command [Command] The command or application to parse.
151
+ # @param args [Array] The arguments to parse.
152
+ # @return [Command|nil] A command to execute or `nil` if no valid command was found.
153
+ def execute_parsing(parser, command, args)
154
+ rv = nil
155
+
156
+ if command.options.present? then
157
+ rv = parse_options(parser, command, args)
158
+ check_required_options(command)
159
+ elsif args.present? then
160
+ rv = find_command_to_execute(command, args)
161
+ end
162
+
163
+ rv
164
+ end
165
+
166
+ # Setups an option for a command.
167
+ #
168
+ # @param command [Command] The command or application to parse.
169
+ # @param opts [Object] The current set options.
170
+ # @param option [Option] The option to set.
171
+ def setup_option(command, opts, option)
172
+ case option.type.to_s
173
+ when "String" then parse_string(command, opts, option)
174
+ when "Integer" then parse_number(command, opts, option, :is_integer?, :to_integer, command.i18n.invalid_integer(option.label))
175
+ when "Float" then parse_number(command, opts, option, :is_float?, :to_float, command.i18n.invalid_float(option.label))
176
+ when "Array" then parse_array(command, opts, option)
177
+ else option.action.present? ? parse_action(opts, option) : parse_boolean(opts, option)
178
+ end
179
+ end
180
+
181
+ # Checks if a option is unique.
182
+ #
183
+ # @param command [Command] The command or application to parse.
184
+ # @param forms [Hash] The current forms.
185
+ # @param option [Option] The option to set.
186
+ def check_unique(command, forms, option)
187
+ if forms[option.complete_short] || forms[option.complete_long] then
188
+ raise ::Mamertes::Error.new(command, :ambiguous_form, command.i18n.conflicting_options(option.label, forms[option.complete_short].label))
189
+ else
190
+ forms[option.complete_short] = option.dup
191
+ forms[option.complete_long] = option.dup
192
+ end
193
+ end
194
+
195
+ # Parses an action option. A block must be provided to deal with the value.
196
+ #
197
+ # @param command [Command] The command or application to parse.
198
+ # @param opts [Object] The current set options.
199
+ # @param option [Option] The option to set.
200
+ def parse_option(command, opts, option)
201
+ opts.on("#{option.complete_short} #{option.meta || command.i18n.help_arg}", "#{option.complete_long} #{option.meta || command.i18n.help_arg}") do |value|
202
+ yield(value)
203
+ end
204
+ end
205
+
206
+ # Parses an action option.
207
+ #
208
+ # @param opts [Object] The current set options.
209
+ # @param option [Option] The option to set.
210
+ def parse_action(opts, option)
211
+ opts.on("-#{option.short}", "--#{option.long}") do |_|
212
+ option.execute_action
213
+ end
214
+ end
215
+
216
+ # Parses a string option.
217
+ #
218
+ # @param command [Command] The command or application to parse.
219
+ # @param opts [Object] The current set options.
220
+ # @param option [Option] The option to set.
221
+ def parse_string(command, opts, option)
222
+ parse_option(command, opts, option) { |value| option.set(value) }
223
+ end
224
+
225
+ # Parses a number option.
226
+ #
227
+ # @param command [Command] The command or application to parse.
228
+ # @param opts [Object] The current set options.
229
+ # @param option [Option] The option to set.
230
+ # @param check_method [Symbol] The method to execute to check option validity. Must return a boolean.
231
+ # @param convert_method [Symbol] The method to execute to convert option.
232
+ # @param invalid_message [String] The string to send in case of invalid arguments.
233
+ def parse_number(command, opts, option, check_method, convert_method, invalid_message)
234
+ parse_option(command, opts, option) do |value|
235
+ raise ::Mamertes::Error.new(option, :invalid_argument, invalid_message) if !value.send(check_method)
236
+ option.set(value.send(convert_method))
237
+ end
238
+ end
239
+
240
+ # Parses an array option.
241
+ #
242
+ # @param command [Command] The command or application to parse.
243
+ # @param opts [Object] The current set options.
244
+ # @param option [Option] The option to set.
245
+ def parse_array(command, opts, option)
246
+ opts.on("#{option.complete_short} #{option.meta || command.i18n.help_arg}", "#{option.complete_long} #{option.meta || command.i18n.help_arg}", Array) do |value|
247
+ option.set(value.ensure_array)
248
+ end
249
+ end
250
+
251
+ # Parses an action option.
252
+ #
253
+ # @param opts [Object] The current set options.
254
+ # @param option [Option] The option to set.
255
+ def parse_boolean(opts, option)
256
+ opts.on("-#{option.short}", "--#{option.long}") do |value|
257
+ option.set(value.to_boolean)
258
+ end
259
+ end
260
+
261
+ # Parses options of a command.
262
+ #
263
+ # @param parser [OptionParser] The option parser.
264
+ # @param command [Command] The command or application to parse.
265
+ # @param args [Array] The arguments to parse.
266
+ # @return [Command|nil] A command to execute or `nil` if no command was found.
267
+ def parse_options(parser, command, args)
268
+ rv = nil
269
+
270
+ # Parse options
271
+ parser.order!(args) do |arg|
272
+ fc = ::Mamertes::Parser.find_command(arg, command, args)
273
+
274
+ if fc.present? then
275
+ rv = fc
276
+ parser.terminate
277
+ else
278
+ command.argument(arg)
279
+ end
280
+ end
281
+
282
+ rv
283
+ end
284
+
285
+ # Checks if all options of a command are present.
286
+ #
287
+ # @param command [Command] The command or application to parse.
288
+ def check_required_options(command)
289
+ # Check if any required option is missing.
290
+ command.options.each_pair do |name, option|
291
+ raise ::Mamertes::Error.new(option, :missing_option, command.i18n.missing_option(option.label)) if option.required && !option.provided?
292
+ end
293
+ end
294
+
295
+ # Finds a command to execute
296
+ #
297
+ # @param command [Command] The command or application to parse.
298
+ # @param args [Array] The arguments to parse.
299
+ # @return [Command|nil] A command to execute or `nil` if no command was found.
300
+ def find_command_to_execute(command, args)
301
+ rv = nil
302
+
303
+ # Try to find a command into the first argument
304
+ fc = ::Mamertes::Parser.find_command(args[0], command, args[1, args.length - 1])
305
+
306
+ if fc.present? then
307
+ rv = fc
308
+ else
309
+ args.each do |arg|
310
+ command.argument(arg)
311
+ end
312
+ end
313
+
314
+ rv
315
+ end
316
+ end
317
+ end