rake-commander 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/.rubocop.yml +12 -8
- data/CHANGELOG.md +69 -4
- data/LICENSE +21 -0
- data/README.md +94 -2
- data/Rakefile +11 -13
- data/examples/01_basic_example.rb +28 -0
- data/examples/02_a_chainer_example.rb +66 -0
- data/examples/02_a_chainer_options_set.rb +8 -0
- data/examples/02_b_chained_example.rb +13 -0
- data/examples/03_a_chainer_plus_example.rb +34 -0
- data/examples/03_b_chained_plus_example.rb +17 -0
- data/examples/Examples.rake +7 -0
- data/examples/README.md +79 -0
- data/examples/libs/shell_helpers.rb +81 -0
- data/lib/rake-commander/base/class_auto_loader.rb +45 -7
- data/lib/rake-commander/base/class_helpers.rb +16 -61
- data/lib/rake-commander/base/class_inheritable.rb +122 -0
- data/lib/rake-commander/base/custom_error.rb +52 -0
- data/lib/rake-commander/base/object_helpers.rb +42 -0
- data/lib/rake-commander/base.rb +16 -2
- data/lib/rake-commander/option.rb +115 -25
- data/lib/rake-commander/options/arguments.rb +206 -94
- data/lib/rake-commander/options/description.rb +17 -0
- data/lib/rake-commander/options/error/base.rb +86 -0
- data/lib/rake-commander/options/error/handling.rb +106 -0
- data/lib/rake-commander/options/error/invalid_argument.rb +21 -0
- data/lib/rake-commander/options/error/invalid_option.rb +9 -0
- data/lib/rake-commander/options/error/missing_argument.rb +10 -0
- data/lib/rake-commander/options/error/missing_option.rb +48 -0
- data/lib/rake-commander/options/error/unknown_argument.rb +32 -0
- data/lib/rake-commander/options/error.rb +75 -10
- data/lib/rake-commander/options/name.rb +67 -23
- data/lib/rake-commander/options/result.rb +107 -0
- data/lib/rake-commander/options/set.rb +7 -1
- data/lib/rake-commander/options.rb +175 -98
- data/lib/rake-commander/patcher/README.md +79 -0
- data/lib/rake-commander/patcher/application/run_method.rb +46 -0
- data/lib/rake-commander/patcher/application/top_level_method.rb +74 -0
- data/lib/rake-commander/patcher/application.rb +16 -0
- data/lib/rake-commander/patcher/base.rb +45 -0
- data/lib/rake-commander/patcher/debug.rb +32 -0
- data/lib/rake-commander/patcher/helpers.rb +44 -0
- data/lib/rake-commander/patcher.rb +26 -0
- data/lib/rake-commander/rake_context/wrapper.rb +2 -0
- data/lib/rake-commander/rake_task.rb +49 -54
- data/lib/rake-commander/version.rb +1 -1
- data/lib/rake-commander.rb +4 -0
- data/rake-commander.gemspec +4 -1
- metadata +74 -6
- data/examples/basic.rb +0 -30
- 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
|
-
|
8
|
-
attr_writer :type_coertion, :required
|
9
|
-
attr_reader :name_full
|
8
|
+
attr_reader :name_full, :desc, :default
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
16
|
-
super(short,
|
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
|
-
|
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(*
|
113
|
+
opts_parser.on_head(*args, &block)
|
83
114
|
when :tail, :end
|
84
|
-
opts_parser.on_tail(*
|
115
|
+
opts_parser.on_tail(*args, &block)
|
85
116
|
else # :base
|
86
|
-
opts_parser.on(*
|
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
|
-
|
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
|
-
|
153
|
+
original_block&.call(*args)
|
110
154
|
middleware&.call(*args)
|
111
155
|
end
|
112
156
|
end
|
113
157
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
argv.
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|