rake-commander 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +12 -8
  4. data/CHANGELOG.md +69 -4
  5. data/LICENSE +21 -0
  6. data/README.md +94 -2
  7. data/Rakefile +11 -13
  8. data/examples/01_basic_example.rb +28 -0
  9. data/examples/02_a_chainer_example.rb +66 -0
  10. data/examples/02_a_chainer_options_set.rb +8 -0
  11. data/examples/02_b_chained_example.rb +13 -0
  12. data/examples/03_a_chainer_plus_example.rb +34 -0
  13. data/examples/03_b_chained_plus_example.rb +17 -0
  14. data/examples/Examples.rake +7 -0
  15. data/examples/README.md +79 -0
  16. data/examples/libs/shell_helpers.rb +81 -0
  17. data/lib/rake-commander/base/class_auto_loader.rb +45 -7
  18. data/lib/rake-commander/base/class_helpers.rb +16 -61
  19. data/lib/rake-commander/base/class_inheritable.rb +122 -0
  20. data/lib/rake-commander/base/custom_error.rb +52 -0
  21. data/lib/rake-commander/base/object_helpers.rb +42 -0
  22. data/lib/rake-commander/base.rb +16 -2
  23. data/lib/rake-commander/option.rb +115 -25
  24. data/lib/rake-commander/options/arguments.rb +206 -94
  25. data/lib/rake-commander/options/description.rb +17 -0
  26. data/lib/rake-commander/options/error/base.rb +86 -0
  27. data/lib/rake-commander/options/error/handling.rb +106 -0
  28. data/lib/rake-commander/options/error/invalid_argument.rb +21 -0
  29. data/lib/rake-commander/options/error/invalid_option.rb +9 -0
  30. data/lib/rake-commander/options/error/missing_argument.rb +10 -0
  31. data/lib/rake-commander/options/error/missing_option.rb +48 -0
  32. data/lib/rake-commander/options/error/unknown_argument.rb +32 -0
  33. data/lib/rake-commander/options/error.rb +75 -10
  34. data/lib/rake-commander/options/name.rb +67 -23
  35. data/lib/rake-commander/options/result.rb +107 -0
  36. data/lib/rake-commander/options/set.rb +7 -1
  37. data/lib/rake-commander/options.rb +175 -98
  38. data/lib/rake-commander/patcher/README.md +79 -0
  39. data/lib/rake-commander/patcher/application/run_method.rb +46 -0
  40. data/lib/rake-commander/patcher/application/top_level_method.rb +74 -0
  41. data/lib/rake-commander/patcher/application.rb +16 -0
  42. data/lib/rake-commander/patcher/base.rb +45 -0
  43. data/lib/rake-commander/patcher/debug.rb +32 -0
  44. data/lib/rake-commander/patcher/helpers.rb +44 -0
  45. data/lib/rake-commander/patcher.rb +26 -0
  46. data/lib/rake-commander/rake_context/wrapper.rb +2 -0
  47. data/lib/rake-commander/rake_task.rb +49 -54
  48. data/lib/rake-commander/version.rb +1 -1
  49. data/lib/rake-commander.rb +4 -0
  50. data/rake-commander.gemspec +4 -1
  51. metadata +74 -6
  52. data/examples/basic.rb +0 -30
  53. 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