rake-commander 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +14 -8
  4. data/CHANGELOG.md +84 -4
  5. data/Gemfile +1 -1
  6. data/LICENSE +21 -0
  7. data/README.md +95 -3
  8. data/Rakefile +11 -13
  9. data/examples/01_basic_example.rb +28 -0
  10. data/examples/02_a_chainer_example.rb +66 -0
  11. data/examples/02_a_chainer_options_set.rb +8 -0
  12. data/examples/02_b_chained_example.rb +13 -0
  13. data/examples/03_a_chainer_plus_example.rb +34 -0
  14. data/examples/03_b_chained_plus_example.rb +17 -0
  15. data/examples/Examples.rake +7 -0
  16. data/examples/README.md +79 -0
  17. data/examples/libs/shell_helpers.rb +81 -0
  18. data/lib/rake-commander/base/class_auto_loader.rb +45 -7
  19. data/lib/rake-commander/base/class_helpers.rb +16 -61
  20. data/lib/rake-commander/base/class_inheritable.rb +122 -0
  21. data/lib/rake-commander/base/custom_error.rb +52 -0
  22. data/lib/rake-commander/base/object_helpers.rb +42 -0
  23. data/lib/rake-commander/base.rb +16 -2
  24. data/lib/rake-commander/option.rb +115 -25
  25. data/lib/rake-commander/options/arguments.rb +206 -94
  26. data/lib/rake-commander/options/description.rb +17 -0
  27. data/lib/rake-commander/options/error/base.rb +86 -0
  28. data/lib/rake-commander/options/error/handling.rb +106 -0
  29. data/lib/rake-commander/options/error/invalid_argument.rb +21 -0
  30. data/lib/rake-commander/options/error/invalid_option.rb +9 -0
  31. data/lib/rake-commander/options/error/missing_argument.rb +10 -0
  32. data/lib/rake-commander/options/error/missing_option.rb +48 -0
  33. data/lib/rake-commander/options/error/unknown_argument.rb +32 -0
  34. data/lib/rake-commander/options/error.rb +75 -10
  35. data/lib/rake-commander/options/name.rb +67 -23
  36. data/lib/rake-commander/options/result.rb +107 -0
  37. data/lib/rake-commander/options/set.rb +7 -1
  38. data/lib/rake-commander/options.rb +175 -102
  39. data/lib/rake-commander/patcher/README.md +79 -0
  40. data/lib/rake-commander/patcher/application/run_method.rb +46 -0
  41. data/lib/rake-commander/patcher/application/top_level_method.rb +74 -0
  42. data/lib/rake-commander/patcher/application.rb +16 -0
  43. data/lib/rake-commander/patcher/base.rb +45 -0
  44. data/lib/rake-commander/patcher/debug.rb +32 -0
  45. data/lib/rake-commander/patcher/helpers.rb +44 -0
  46. data/lib/rake-commander/patcher.rb +26 -0
  47. data/lib/rake-commander/rake_context/wrapper.rb +2 -0
  48. data/lib/rake-commander/rake_task.rb +50 -50
  49. data/lib/rake-commander/version.rb +1 -1
  50. data/lib/rake-commander.rb +4 -0
  51. data/rake-commander.gemspec +5 -2
  52. metadata +75 -7
  53. data/examples/basic.rb +0 -30
  54. data/lib/rake-commander/options/error_rely.rb +0 -58
@@ -3,26 +3,41 @@ class RakeCommander
3
3
  class Option < @option_struct
4
4
  extend RakeCommander::Base::ClassHelpers
5
5
  extend RakeCommander::Options::Name
6
+ include RakeCommander::Options::Description
6
7
 
7
- attr_accessor :desc, :default
8
- attr_writer :type_coertion, :required
9
- attr_reader :name_full
8
+ attr_reader :name_full, :desc, :default
10
9
 
11
- def initialize(short, name, *args, **kargs, &block)
12
- raise ArgumentError, "A short of one letter should be provided. Given: #{short}" unless self.class.valid_short?(short)
13
- raise ArgumentError, "A name should be provided. Given: #{name}" unless self.class.valid_name?(name)
10
+ # @param sample [Boolean] allows to skip the `short` and `name` validations
11
+ def initialize(*args, sample: false, **kargs, &block)
12
+ short, name = capture_arguments_short_n_name!(args, kargs, sample: sample)
14
13
 
15
- @name_full = name
16
- super(short, name)
14
+ @name_full = name.freeze
15
+ super(short.freeze, @name_full)
17
16
  @default = kargs[:default] if kargs.key?(:default)
18
17
  @desc = kargs[:desc] if kargs.key?(:desc)
19
18
  @required = kargs[:required] if kargs.key?(:required)
19
+ @type_coertion = kargs[:type] if kargs.key?(:type)
20
20
  @other_args = args
21
21
  @original_block = block
22
- yield(self) if block_given?
23
22
  configure_other
24
23
  end
25
24
 
25
+ # Makes a copy of this option
26
+ # @return [RakeCommander::Option]
27
+ def dup(**kargs, &block)
28
+ block ||= original_block
29
+ self.class.new(**dup_key_arguments.merge(kargs), &block)
30
+ end
31
+ alias_method :deep_dup, :dup
32
+
33
+ # Creates a new option, result of merging this `opt` with this option,
34
+ # @return [RakeCommander::Option] where opt has been merged
35
+ def merge(opt)
36
+ raise "Expecting RakeCommander::Option. Given: #{opt.class}" unless opt.is_a?(RakeCommander::Option)
37
+ dup(**opt.dup_key_arguments, &opt.original_block)
38
+ end
39
+
40
+ # @return [Boolean] whether this option is required.
26
41
  def required?
27
42
  !!@required
28
43
  end
@@ -32,6 +47,13 @@ class RakeCommander
32
47
  self.class.short_sym(super)
33
48
  end
34
49
 
50
+ # `OptionParser` interprets free shorts that match the first letter of an option name
51
+ # as an invocation of that option. This method allows to identify this.
52
+ # return [Symbol]
53
+ def short_implicit
54
+ self.class.short_sym(@name_full)
55
+ end
56
+
35
57
  # @return [String]
36
58
  def short_hyphen
37
59
  self.class.short_hyphen(short)
@@ -47,6 +69,11 @@ class RakeCommander
47
69
  self.class.name_hyphen(name_full)
48
70
  end
49
71
 
72
+ # @return [Boolean]
73
+ def boolean_name?
74
+ self.class.boolean_name?(name_full)
75
+ end
76
+
50
77
  # @param [Boolean] whether this option allows an argument
51
78
  def argument?
52
79
  self.class.name_argument?(name_full)
@@ -75,26 +102,43 @@ class RakeCommander
75
102
 
76
103
  # Adds this option's switch to the `OptionParser`
77
104
  # @note it allows to add a `middleware` block that will be called at `parse` runtime
78
- def add_switch(opts_parser, where: :base, &middleware)
105
+ # @param opt_parser [OptionParser] the option parser to add this option's switch.
106
+ # @param implicit_short [Boolean] whether the implicit short of this option is active in the opts_parser.
107
+ def add_switch(opts_parser, where: :base, implicit_short: false, &middleware)
79
108
  raise "Expecting OptionParser. Given: #{opts_parser.class}" unless opts_parser.is_a?(OptionParser)
109
+ args = switch_args(implicit_short: implicit_short)
110
+ block = option_block(&middleware)
80
111
  case where
81
112
  when :head, :top
82
- opts_parser.on_head(*switch_args, &option_block(&middleware))
113
+ opts_parser.on_head(*args, &block)
83
114
  when :tail, :end
84
- opts_parser.on_tail(*switch_args, &option_block(&middleware))
115
+ opts_parser.on_tail(*args, &block)
85
116
  else # :base
86
- opts_parser.on(*switch_args, &option_block(&middleware))
117
+ opts_parser.on(*args, &block)
87
118
  end
88
119
  opts_parser
89
120
  end
90
121
 
122
+ protected
123
+
124
+ attr_reader :original_block
125
+
126
+ # @return [Hash] keyed arguments to create a new object
127
+ def dup_key_arguments
128
+ {}.tap do |kargs|
129
+ kargs.merge!(short: short.dup.freeze) if short
130
+ kargs.merge!(name: name_full.dup.freeze) if name_full
131
+ kargs.merge!(desc: desc.dup) if desc
132
+ kargs.merge!(default: default.dup) if default?
133
+ kargs.merge!(required: required?)
134
+ end
135
+ end
136
+
91
137
  # @return [Array<Variant>]
92
- def switch_args
138
+ def switch_args(implicit_short: false)
93
139
  configure_other
94
140
  args = [short_hyphen, name_hyphen]
95
- if str = switch_desc
96
- args << str
97
- end
141
+ args.push(*switch_desc(implicit_short: implicit_short))
98
142
  args << type_coertion if type_coertion
99
143
  args
100
144
  end
@@ -106,26 +150,72 @@ class RakeCommander
106
150
  block_extra_args = [default, short, name]
107
151
  proc do |value|
108
152
  args = block_extra_args.dup.unshift(value)
109
- @original_block&.call(*args)
153
+ original_block&.call(*args)
110
154
  middleware&.call(*args)
111
155
  end
112
156
  end
113
157
 
114
- def switch_desc
115
- val = "#{desc}#{default_desc}"
116
- return nil if val.empty?
117
- val
158
+ # @note in `OptionParser` you can multiline the description with alignment
159
+ # by providing multiple strings.
160
+ # @return [Array<String>]
161
+ def switch_desc(implicit_short: false, line_width: DESC_MAX_LENGTH)
162
+ ishort = implicit_short ? "( -#{short_implicit} ) " : ''
163
+ str = "#{required_desc}#{ishort}#{desc}#{default_desc}"
164
+ return [] if str.empty?
165
+ string_to_lines(str, max: line_width)
166
+ end
167
+
168
+ def required_desc
169
+ required?? "< REQ > " : "[ opt ] "
118
170
  end
119
171
 
120
172
  def default_desc
121
173
  return nil unless default?
122
- str = "Default: '#{default}'"
174
+ str = "{ Default: '#{default}' }"
123
175
  if desc && !desc.downcase.include?('default')
124
176
  str = desc.end_with?('.') ? " #{str}" : ". #{str}"
125
177
  end
126
178
  str
127
179
  end
128
180
 
181
+ # Helper to simplify `short` and `name` capture from arguments and keyed arguments.
182
+ # @return [Array<Symbol, String>] the pair `[short, name]`
183
+ def capture_arguments_short_n_name!(args, kargs, sample: false)
184
+ name, short = kargs.values_at(:name, :short)
185
+ short ||= capture_arguments_short!(args)
186
+ name ||= capture_arguments_name!(args, sample_n_short: sample && short)
187
+
188
+ unless sample
189
+ raise ArgumentError, "A short of one letter should be provided. Given: #{short}" unless self.class.valid_short?(short)
190
+ raise ArgumentError, "A name should be provided. Given: #{name}" unless self.class.valid_name?(name)
191
+ end
192
+
193
+ [short, name]
194
+ end
195
+
196
+ # Helper to figure out the option short from args
197
+ # @note if found it removes it from args.
198
+ # @return [String, Symbol, NilClass]
199
+ def capture_arguments_short!(args)
200
+ short = nil
201
+ short ||= self.class.capture_arguments_short!(args, symbol: true)
202
+ short ||= self.class.capture_arguments_short!(args, symbol: true, strict: false)
203
+ short ||= self.class.capture_arguments_short!(args)
204
+ short || self.class.capture_arguments_short!(args, strict: false)
205
+ end
206
+
207
+ # Helper to figure out the option name from args
208
+ # @note if found it removes it from args.
209
+ # @return [String, Symbol, NilClass]
210
+ def capture_arguments_name!(args, sample_n_short: false)
211
+ name = nil
212
+ name ||= self.class.capture_arguments_name!(args, symbol: true)
213
+ name ||= self.class.capture_arguments_name!(args, symbol: true, strict: false)
214
+ name ||= self.class.capture_arguments_name!(args)
215
+ name || self.class.capture_arguments_name!(args, strict: false) unless sample_n_short
216
+ end
217
+
218
+ # The remaining `args` received in the initialization
129
219
  def other_args(*args)
130
220
  @other_args ||= []
131
221
  if args.empty?
@@ -138,11 +228,11 @@ class RakeCommander
138
228
  # It consumes `other_args`, to prevent direct overrides to be overriden by it.
139
229
  def configure_other
140
230
  if type = other_args.find {|arg| arg.is_a?(Class)}
141
- self.type_coertion = type
231
+ @type_coertion = type
142
232
  other_args.delete(type)
143
233
  end
144
234
  if value = other_args.find {|arg| arg.is_a?(String)}
145
- self.desc = value
235
+ @desc = value
146
236
  other_args.dup.each do |val|
147
237
  other_args.delete(val) if val.is_a?(String)
148
238
  end
@@ -2,112 +2,224 @@ class RakeCommander
2
2
  module Options
3
3
  # Offers helpers to treat `ARGV`
4
4
  module Arguments
5
- include RakeCommander::Options::Name
6
-
7
- # Options with arguments should not take another option as value.
8
- # `OptionParser` can do this even if the the argument is optional.
9
- # This method re-arranges the arguments based on options that receive parameters,
10
- # provided that an option is not taken as a value of a previous option that accepts arguments.
11
- # If an option with argument is missing the argument, but has a `default` value,
12
- # that `default` value will be inserted after the option in the array
13
- # to prevent the `OptionParser::MissingArgument` error to stop the parsing process.
14
- # @note
15
- # 1. Any word or letter with _hypen_ -`` or _double hypen_ `--` is interpreted as option(s)
16
- # 2. To overcome this limitation, you may enclose in double quotes and argument with
17
- # that start (i,e, `"--argument"`).
18
- # @example
19
- # 1. `-abc ARGUMENT` where only `c` receives the argument becomes `-ab -c ARGUMENT`
20
- # 3. `-abc ARGUMENT` where `b` and `c` are argument receivers becomes `-a -b nil -c ARGUMENT`
21
- # 2. `-acb ARGUMENT` where only `c` receives the argument becomes `-a -c nil -b ARGUMENT`
22
- # 4. `-c --some-option ARGUMENT` where both options receive argument, becomes `-c nil --some-option ARGUMENT`
23
- # 5. `-c --some-option -d ARGUMENT` where both options receive argument, becomes `-c nil --some-option nil -d ARGUMENT`
24
- # 6. `-cd ARGUMENT` where `c` default is `"yeah"`, becomes `-c yeah -d ARGUMENT`
25
- # @param argv [Array<String>]
26
- # @param options [Hash] the defined `RakeCommander::Option` to re-arrange `argv` with.
27
- # @return [Array<String>] the re-arranged `argv`
28
- def pre_parse_arguments(argv = ARGV, options)
29
- pre_parsed = explicit_argument_options(argv, options)
30
- compact_short = ''
31
- pre_parsed.each_with_object([]) do |(opt_ref, args), out|
32
- next out.push(*args) unless opt_ref.is_a?(Symbol)
33
- is_short = opt_ref.to_s.length == 1
34
- next compact_short << opt_ref.to_s if is_short && args.empty?
35
- out.push("-#{compact_short}") unless compact_short.empty?
36
- compact_short = ''
37
- opt_str = is_short ? "-#{opt_ref}" : name_hyphen(opt_ref)
38
- out.push(opt_str, *args)
39
- end.tap do |out|
40
- out.push("-#{compact_short}") unless compact_short.empty?
5
+ RAKE_COMMAND_EXTENDED_OPTIONS_START = '--'.freeze
6
+ NAME_ARGUMENT = /^--(?<option>[\w_-]*).*?$/.freeze
7
+ BOOLEAN_ARGUMENT = /(?:^|--)no-(?<option>[\w_-]*).*?$/.freeze
8
+
9
+ class << self
10
+ def included(base)
11
+ super(base)
12
+ base.extend ClassMethods
41
13
  end
42
14
  end
43
15
 
44
- private
45
-
46
- # @example the output is actually a Hash, keyed by the Symbol of the option (short or name)
47
- # 1. `-abc ARGUMENT` where only `c` receives the argument becomes `:a :b :c ARGUMENT`
48
- # 3. `-abc ARGUMENT` where `b` and `c` are argument receivers becomes `:a :b nil :c ARGUMENT`
49
- # 2. `-acb ARGUMENT` where only `c` receives the argument becomes `:a :c nil :b ARGUMENT`
50
- # 4. `-c --some-option ARGUMENT` where both options receive argument, becomes `:c nil :some_option ARGUMENT`
51
- # 5. `-c --some-option -d ARGUMENT` where first two options receive argument, becomes `:c nil :some_option nil :d ARGUMENT`
52
- # 6. `-cd ARGUMENT` where `c` default is `"yeah"`, becomes `:c yeah :d ARGUMENT`
53
- # @return [Hash<Symbol, Array>]
54
- def explicit_argument_options(argv, options)
55
- decoupled = decluster_shorts_n_names_to_sym(argv)
56
- grouped = group_symbols_with_strings(decoupled)
57
- normalized = insert_missing_argument_to_groups(grouped, options)
58
- normalized.each_with_object({}) do |group, pre_parsed|
59
- opt_ref = group.first.is_a?(Symbol)? group.shift : nil
60
- pre_parsed[opt_ref] = group
16
+ module ClassMethods
17
+ include RakeCommander::Options::Name
18
+
19
+ # @note it assumes `ARGV` has been left unaltered.
20
+ # @return [Boolean] whether enhanced parsing should be switched on or off.
21
+ def argv_with_enhanced_syntax?(argv = ARGV)
22
+ return false unless argv.is_a?(Array)
23
+ argv.include?(RAKE_COMMAND_EXTENDED_OPTIONS_START)
61
24
  end
62
- end
63
25
 
64
- # It adds the missing argument to options that expect it.
65
- # @note it uses `default` if present, and `nil` otherwise.
66
- # @param groups [@see #pair_symbols_with_strings]
67
- def insert_missing_argument_to_groups(groups, options)
68
- groups.each do |group|
69
- args = group.dup
70
- opt_ref = args.shift
71
- next unless args.empty?
72
- next unless opt_ref.is_a?(Symbol)
73
- next unless opt = options[opt_ref]
74
- next unless opt.argument?
75
- next group.push(opt.default) if opt.default?
76
- group.push(nil)
26
+ # Configuration setting
27
+ # Whether the additional arguments (extended options) managed by this gem
28
+ # should be removed/consumed from `ARGV` before `Rake` processes option arguments.
29
+ # @note
30
+ # 1. When `true` it **will enable**
31
+ # * A **patch** on `Rake::Application`**, provided that `ARGV` is cropped
32
+ # before `Rake` identifies **tasks** and rake native **options**.
33
+ # Note that this specific patch only works if rake commander was loaded
34
+ # BEFORE `Rake::Application#run` is invoked.
35
+ # 2. When `false`, an implicit `exit(0)` is added at the end of a rake task
36
+ # defined via `RakeCommander`, as a work-around that prevents `Rake` from
37
+ # chaining option arguments as if they were actual tasks.
38
+ # @note
39
+ # 1. This only refers to what comes after `RAKE_COMMAND_EXTENDED_OPTIONS_START` (`--`)
40
+ # @return [Boolean]
41
+ def argv_cropping_for_rake(value = :not_used)
42
+ @argv_cropping_for_rake = true if @argv_cropping_for_rake.nil?
43
+ return @argv_cropping_for_rake if value == :not_used
44
+ @argv_cropping_for_rake = !!value
45
+ end
46
+
47
+ # It returns the part of `ARGV` that are arguments of `RakeCommander::Options` parsing.
48
+ # @note please observe that `Rake` has it's own options. For this reason using
49
+ # a delimiter (`RAKE_COMMAND_EXTENDED_OPTIONS_START`) shows up to be necessary to
50
+ # create some sort of command line argument namespacing.
51
+ # @param argv [Array<String>] the command line arguments array.
52
+ # @return [Array<String>] the target arguments to be parsed by `RakeCommander::Options`
53
+ def argv_extended_options(argv = ARGV.dup)
54
+ if idx = argv.index(RAKE_COMMAND_EXTENDED_OPTIONS_START)
55
+ argv[idx+1..-1]
56
+ else
57
+ []
58
+ end
77
59
  end
78
- end
79
60
 
80
- # @return [Array<Array>] where the first element of each `Array` is a symbol
81
- # followed by one or more `String`.
82
- def group_symbols_with_strings(argv)
83
- [].tap do |out|
84
- curr_ary = nil
85
- argv.each do |arg|
86
- if arg.is_a?(Symbol)
87
- out << (curr_ary = [arg])
88
- else # must be `String`
89
- out << (curr_ary = []) unless curr_ary
90
- curr_ary << arg
61
+ # It slices from the original `ARGV` the extended_options of this gem.
62
+ # @note this is necessary to prevent `Rake` to interpret them.
63
+ # @return [Array<String>] the argv without the extended options of this gem.
64
+ def argv_rake_native_arguments(argv = ARGV.dup)
65
+ return argv unless argv_cropping_for_rake
66
+ if idx = argv.index(RAKE_COMMAND_EXTENDED_OPTIONS_START)
67
+ argv = argv[0..idx]
68
+ end
69
+ argv
70
+ end
71
+
72
+ # **Re-open** `parse_options` method, provided that we slice `ARGV`
73
+ # to only include the extended options of this gem, which start at
74
+ # `RAKE_COMMAND_EXTENDED_OPTIONS_START`.
75
+ # @note
76
+ # 1. Without this `ARGV` cut, it will throw `OptionParser::InvalidOption` error
77
+ # - So some tidy up is necessary and the head of the command (i.e. `rake some:task --`)
78
+ # should be excluded from arguments to input to the options parser.
79
+ # @see `RakeCommander::Options#parse_options`
80
+ def parse_options(argv = ARGV, *args, **kargs, &block)
81
+ argv = argv_extended_options(argv)
82
+ argv = argv_pre_parsed(argv, options: options_hash(with_implicit: true))
83
+ super(argv, *args, **kargs, &block)
84
+ end
85
+
86
+ # Options with arguments should not take another option as value.
87
+ # `OptionParser` can do this even if the the argument is optional.
88
+ # This method re-arranges the arguments based on options that receive parameters,
89
+ # provided that an option is not taken as a value of a previous option that accepts arguments.
90
+ # If an option with argument is missing the argument, but has a `default` value,
91
+ # that `default` value will be inserted after the option in the array
92
+ # to prevent the `OptionParser::MissingArgument` error to stop the parsing process.
93
+ # @note
94
+ # 1. Any word or letter with _hypen_ -`` or _double hypen_ `--` is interpreted as option(s)
95
+ # 2. To overcome this limitation, you may enclose in double quotes and argument with
96
+ # that start (i,e, `"--argument"`).
97
+ # @example
98
+ # 1. `-abc ARGUMENT` where only `c` receives the argument becomes `-ab -c ARGUMENT`
99
+ # 3. `-abc ARGUMENT` where `b` and `c` are argument receivers becomes `-a -b nil -c ARGUMENT`
100
+ # 2. `-acb ARGUMENT` where only `c` receives the argument becomes `-a -c nil -b ARGUMENT`
101
+ # 4. `-c --some-option ARGUMENT` where both options receive argument, becomes `-c nil --some-option ARGUMENT`
102
+ # 5. `-c --some-option -d ARGUMENT` where both options receive argument, becomes `-c nil --some-option nil -d ARGUMENT`
103
+ # 6. `-cd ARGUMENT` where `c` default is `"yeah"`, becomes `-c yeah -d ARGUMENT`
104
+ # @param argv [Array<String>]
105
+ # @param options [Hash] the defined `RakeCommander::Option` to re-arrange `argv` with.
106
+ # @return [Array<String>] the re-arranged `argv`
107
+ def argv_pre_parsed(argv = ARGV, options:)
108
+ pre_parsed = explicit_argument_options(argv, options)
109
+ compact_short = ''
110
+ pre_parsed.each_with_object([]) do |(opt_ref, args), out|
111
+ next out.push(*args) unless opt_ref.is_a?(Symbol)
112
+ is_short = opt_ref.to_s.length == 1
113
+ next compact_short << opt_ref.to_s if is_short && args.empty?
114
+ out.push("-#{compact_short}") unless compact_short.empty?
115
+ compact_short = ''
116
+ opt_str = is_short ? "-#{opt_ref}" : name_hyphen(opt_ref)
117
+ out.push(opt_str, *args)
118
+ end.tap do |out|
119
+ out.push("-#{compact_short}") unless compact_short.empty?
120
+ end
121
+ end
122
+
123
+ protected
124
+
125
+ # It wraps the `task_method` to check if the patch to crop `ARGV` is active.
126
+ # If it's not active it will call `exit(0)` at the end of the task run, to prevent
127
+ # `Rake` from interpreting option arguments as rake tasks.
128
+ # @note **reopens** `RakeCommander::RakeTask` method
129
+ # * If `argv_cropping_for_rake` is `false` it calls `exit(0)` right at the end of the task.
130
+ # * This relates on whether the patch to `Rake::Application` has been applied.
131
+ # @return [Proc] the wrapped block.
132
+ def task_context(&task_method)
133
+ proc do |*task_args|
134
+ super(&task_method).call(*task_args)
135
+ exit(0) unless argv_cropping_for_rake
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ # @example the output is actually a Hash, keyed by the Symbol of the option (short or name)
142
+ # 1. `-abc ARGUMENT` where only `c` receives the argument becomes `:a :b :c ARGUMENT`
143
+ # 3. `-abc ARGUMENT` where `b` and `c` are argument receivers becomes `:a :b nil :c ARGUMENT`
144
+ # 2. `-acb ARGUMENT` where only `c` receives the argument becomes `:a :c nil :b ARGUMENT`
145
+ # 4. `-c --some-option ARGUMENT` where both options receive argument, becomes `:c nil :some_option ARGUMENT`
146
+ # 5. `-c --some-option -d ARGUMENT` where first two options receive argument, becomes `:c nil :some_option nil :d ARGUMENT`
147
+ # 6. `-cd ARGUMENT` where `c` default is `"yeah"`, becomes `:c yeah :d ARGUMENT`
148
+ # @return [Hash<Symbol, Array>]
149
+ def explicit_argument_options(argv, options)
150
+ decoupled = decluster_shorts_n_names_to_sym(argv)
151
+ grouped = group_symbols_with_strings(decoupled)
152
+ normalized = insert_missing_argument_to_groups(grouped, options)
153
+ normalized.each_with_object({}) do |group, pre_parsed|
154
+ opt_ref = group.first.is_a?(Symbol)? group.shift : nil
155
+ pre_parsed[opt_ref] = group
156
+ end
157
+ end
158
+
159
+ # It ADDS the missing argument to options that expect it.
160
+ # @note
161
+ # 1. It uses `default` if present
162
+ # 2. Otherwise it uses `nil`, but only if required (not when optional).
163
+ # @param groups [@see #pair_symbols_with_strings]
164
+ def insert_missing_argument_to_groups(groups, options)
165
+ groups.each do |group|
166
+ args = group.dup
167
+ opt_ref = args.shift
168
+ next unless args.empty?
169
+ next unless opt_ref.is_a?(Symbol)
170
+ next unless opt = _retrieve_option_ref(opt_ref, options)
171
+ next unless opt.argument?
172
+ next group.push(opt.default) if opt.default?
173
+ next unless opt.argument_required?
174
+ group.push(nil)
175
+ end
176
+ end
177
+
178
+ # Retrieve the option based on `ref`
179
+ # @note It might be `--no-option-name`
180
+ # @return [RakeCommander::Option, NilClass]
181
+ def _retrieve_option_ref(opt_ref, options)
182
+ opt = options[opt_ref]
183
+ return opt if opt
184
+ return nil unless match = opt_ref.to_s.match(BOOLEAN_ARGUMENT)
185
+ return nil unless opt_ref = match[:option]
186
+ return nil unless opt = options[opt_ref.to_sym]
187
+ return nil unless opt.boolean_name?
188
+ opt
189
+ end
190
+
191
+ # @return [Array<Array>] where the first element of each `Array` is a symbol
192
+ # followed by one or more `String`.
193
+ def group_symbols_with_strings(argv)
194
+ [].tap do |out|
195
+ curr_ary = nil
196
+ argv.each do |arg|
197
+ if arg.is_a?(Symbol)
198
+ out << (curr_ary = [arg])
199
+ else # must be `String`
200
+ out << (curr_ary = []) unless curr_ary
201
+ curr_ary << arg
202
+ end
91
203
  end
92
204
  end
93
205
  end
94
- end
95
206
 
96
- # It splits `argv` compacted shorts into their `Symbol` version.
97
- # Symbolizes option `names` (long version).
98
- # @return [Array<String, Symbol>] where symbols are options and strings arguments.
99
- def decluster_shorts_n_names_to_sym(argv)
100
- argv.each_with_object([]) do |arg, out|
101
- if single_hyphen?(arg) # short option(s)
102
- options = arg.match(SINGLE_HYPHEN_REGEX)[:options]
103
- options.split('').each do |short|
104
- out << short_sym(short)
207
+ # It splits `argv` compacted shorts into their `Symbol` version.
208
+ # Symbolizes option `names` (long version).
209
+ # @return [Array<String, Symbol>] where symbols are options and strings arguments.
210
+ def decluster_shorts_n_names_to_sym(argv)
211
+ argv.each_with_object([]) do |arg, out|
212
+ if single_hyphen?(arg) # short option(s)
213
+ options = arg.match(SINGLE_HYPHEN_REGEX)[:options]
214
+ options.split('').each do |short|
215
+ out << short_sym(short)
216
+ end
217
+ elsif double_hyphen?(arg) # name option
218
+ name = arg.match(NAME_ARGUMENT)[:option]
219
+ out << name_sym(name)
220
+ else # argument
221
+ out << arg
105
222
  end
106
- elsif double_hyphen?(arg) # name option
107
- name = arg.match(DOUBLE_HYPHEN_REGEX)[:option]
108
- out << name_sym(name)
109
- else # argument
110
- out << arg
111
223
  end
112
224
  end
113
225
  end
@@ -0,0 +1,17 @@
1
+ class RakeCommander
2
+ module Options
3
+ module Description
4
+ DESC_MAX_LENGTH = 80
5
+
6
+ private
7
+
8
+ def string_to_lines(str, max: DESC_MAX_LENGTH)
9
+ str.scan(liner_regex(max)).map(&:strip)
10
+ end
11
+
12
+ def liner_regex(len = DESC_MAX_LENGTH)
13
+ /.{0,#{len}}[^ ](?:\s|$)/mi
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,86 @@
1
+ class RakeCommander
2
+ module Options
3
+ module Error
4
+ # Base error class that does a rely between OptionParser and RakeCommander errors
5
+ class Base < RakeCommander::Base::CustomError
6
+ extend RakeCommander::Options::Name
7
+ OPTION_REGEX = /(?:argument|option): (?<option>.+)/i.freeze
8
+
9
+ class << self
10
+ # Helper to check if `error` is this class or any children class
11
+ # @raise ArgumentError if it does not meet this condition.
12
+ def require_argument!(error, arg_name, accept_children: true)
13
+ msg = "Expecting #{arg_name} to be #{self}"
14
+ msg << "or child thereof." if accept_children
15
+ msg << ". Given: #{error.is_a?(Class)? error : error.class}"
16
+ raise ArgumentError, msg unless error <= self
17
+ end
18
+
19
+ # To (re)define the RegExp used to identify the option of an error message.
20
+ def option_regex(value = :not_used)
21
+ @option_regex ||= OPTION_REGEX
22
+ return @option_regex if value == :not_used
23
+ @option_regex = value
24
+ end
25
+
26
+ # Identifies the option `Symbol` (short or name) for a given message
27
+ def option_sym(message)
28
+ return nil unless match = message.match(option_regex)
29
+ option = match[:option]
30
+ return name_word_sym(option) if option.length > 1
31
+ short_sym(option)
32
+ end
33
+ end
34
+
35
+ attr_reader :from, :option
36
+
37
+ def initialize(value = nil, from: nil, option: nil)
38
+ @from = from
39
+ @option = option
40
+ super(value)
41
+ end
42
+
43
+ # Options that are related to the error. There may not be any.
44
+ def options
45
+ [option].compact.flatten
46
+ end
47
+
48
+ def name?
49
+ option_sym.to_s.length > 1
50
+ end
51
+
52
+ def short?
53
+ option_sym.to_s.length == 1
54
+ end
55
+
56
+ def option_sym
57
+ @option_sym ||= self.class.option_sym(message)
58
+ end
59
+
60
+ def from_desc
61
+ return '' unless from
62
+ if from.respond_to?(:name)
63
+ "(#{from.name}) "
64
+ elsif from.respond_to?(:to_s)
65
+ "(#{from}) "
66
+ else
67
+ ''
68
+ end
69
+ end
70
+
71
+ protected
72
+
73
+ def to_message(value)
74
+ case value
75
+ when Array
76
+ to_message(value.map {|v| "'#{v}'"}.join(', '))
77
+ when String
78
+ "#{from_desc}#{super}"
79
+ else
80
+ super
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end