rake-commander 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/.rubocop.yml +74 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +6 -0
- data/README.md +28 -0
- data/Rakefile +32 -0
- data/examples/basic.rb +30 -0
- data/lib/rake-commander/base/class_auto_loader.rb +112 -0
- data/lib/rake-commander/base/class_helpers.rb +168 -0
- data/lib/rake-commander/base.rb +29 -0
- data/lib/rake-commander/custom.rb +3 -0
- data/lib/rake-commander/option.rb +153 -0
- data/lib/rake-commander/options/arguments.rb +116 -0
- data/lib/rake-commander/options/error.rb +18 -0
- data/lib/rake-commander/options/error_rely.rb +58 -0
- data/lib/rake-commander/options/name.rb +160 -0
- data/lib/rake-commander/options/set.rb +16 -0
- data/lib/rake-commander/options.rb +171 -0
- data/lib/rake-commander/rake_context/wrapper.rb +39 -0
- data/lib/rake-commander/rake_task.rb +157 -0
- data/lib/rake-commander/version.rb +3 -0
- data/lib/rake-commander.rb +9 -0
- data/rake-commander.gemspec +29 -0
- metadata +169 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
class RakeCommander
|
2
|
+
@option_struct ||= Struct.new(:short, :name)
|
3
|
+
class Option < @option_struct
|
4
|
+
extend RakeCommander::Base::ClassHelpers
|
5
|
+
extend RakeCommander::Options::Name
|
6
|
+
|
7
|
+
attr_accessor :desc, :default
|
8
|
+
attr_writer :type_coertion, :required
|
9
|
+
attr_reader :name_full
|
10
|
+
|
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)
|
14
|
+
|
15
|
+
@name_full = name
|
16
|
+
super(short, name)
|
17
|
+
@default = kargs[:default] if kargs.key?(:default)
|
18
|
+
@desc = kargs[:desc] if kargs.key?(:desc)
|
19
|
+
@required = kargs[:required] if kargs.key?(:required)
|
20
|
+
@other_args = args
|
21
|
+
@original_block = block
|
22
|
+
yield(self) if block_given?
|
23
|
+
configure_other
|
24
|
+
end
|
25
|
+
|
26
|
+
def required?
|
27
|
+
!!@required
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Symbol]
|
31
|
+
def short
|
32
|
+
self.class.short_sym(super)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [String]
|
36
|
+
def short_hyphen
|
37
|
+
self.class.short_hyphen(short)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Symbol]
|
41
|
+
def name
|
42
|
+
self.class.name_word_sym(super)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [String]
|
46
|
+
def name_hyphen
|
47
|
+
self.class.name_hyphen(name_full)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param [Boolean] whether this option allows an argument
|
51
|
+
def argument?
|
52
|
+
self.class.name_argument?(name_full)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [String, Nil] the argument, may it exist
|
56
|
+
def argument
|
57
|
+
return nil unless argument?
|
58
|
+
self.class.name_argument(name_full)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param [Boolean] If there was an argument, whether it is required
|
62
|
+
def argument_required?
|
63
|
+
self.class.argument_required?(argument)
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Class, NilClass]
|
67
|
+
def type_coertion
|
68
|
+
@type_coertion || (default? && default.class)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Boolean]
|
72
|
+
def default?
|
73
|
+
instance_variable_defined?(:@default)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Adds this option's switch to the `OptionParser`
|
77
|
+
# @note it allows to add a `middleware` block that will be called at `parse` runtime
|
78
|
+
def add_switch(opts_parser, where: :base, &middleware)
|
79
|
+
raise "Expecting OptionParser. Given: #{opts_parser.class}" unless opts_parser.is_a?(OptionParser)
|
80
|
+
case where
|
81
|
+
when :head, :top
|
82
|
+
opts_parser.on_head(*switch_args, &option_block(&middleware))
|
83
|
+
when :tail, :end
|
84
|
+
opts_parser.on_tail(*switch_args, &option_block(&middleware))
|
85
|
+
else # :base
|
86
|
+
opts_parser.on(*switch_args, &option_block(&middleware))
|
87
|
+
end
|
88
|
+
opts_parser
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Array<Variant>]
|
92
|
+
def switch_args
|
93
|
+
configure_other
|
94
|
+
args = [short_hyphen, name_hyphen]
|
95
|
+
if str = switch_desc
|
96
|
+
args << str
|
97
|
+
end
|
98
|
+
args << type_coertion if type_coertion
|
99
|
+
args
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# Called on parse runtime
|
105
|
+
def option_block(&middleware)
|
106
|
+
block_extra_args = [default, short, name]
|
107
|
+
proc do |value|
|
108
|
+
args = block_extra_args.dup.unshift(value)
|
109
|
+
@original_block&.call(*args)
|
110
|
+
middleware&.call(*args)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def switch_desc
|
115
|
+
val = "#{desc}#{default_desc}"
|
116
|
+
return nil if val.empty?
|
117
|
+
val
|
118
|
+
end
|
119
|
+
|
120
|
+
def default_desc
|
121
|
+
return nil unless default?
|
122
|
+
str = "Default: '#{default}'"
|
123
|
+
if desc && !desc.downcase.include?('default')
|
124
|
+
str = desc.end_with?('.') ? " #{str}" : ". #{str}"
|
125
|
+
end
|
126
|
+
str
|
127
|
+
end
|
128
|
+
|
129
|
+
def other_args(*args)
|
130
|
+
@other_args ||= []
|
131
|
+
if args.empty?
|
132
|
+
@other_args
|
133
|
+
else
|
134
|
+
@other_args.push(*args)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# It consumes `other_args`, to prevent direct overrides to be overriden by it.
|
139
|
+
def configure_other
|
140
|
+
if type = other_args.find {|arg| arg.is_a?(Class)}
|
141
|
+
self.type_coertion = type
|
142
|
+
other_args.delete(type)
|
143
|
+
end
|
144
|
+
if value = other_args.find {|arg| arg.is_a?(String)}
|
145
|
+
self.desc = value
|
146
|
+
other_args.dup.each do |val|
|
147
|
+
other_args.delete(val) if val.is_a?(String)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
class RakeCommander
|
2
|
+
module Options
|
3
|
+
# Offers helpers to treat `ARGV`
|
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?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
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
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
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)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
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
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
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)
|
105
|
+
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
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'error_rely'
|
2
|
+
class RakeCommander
|
3
|
+
module Options
|
4
|
+
class MissingOption < RakeCommander::Options::ErrorRely
|
5
|
+
def initialize(value)
|
6
|
+
super("missing required option: #{to_description(value)}")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class MissingArgument < RakeCommander::Options::ErrorRely
|
11
|
+
OPTION_REGEX = /missing (?:required|) argument: (?<option>.+)/i.freeze
|
12
|
+
end
|
13
|
+
|
14
|
+
class InvalidArgument < RakeCommander::Options::ErrorRely
|
15
|
+
OPTION_REGEX = /invalid argument: (?<option>.+)/i.freeze
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
class RakeCommander
|
2
|
+
module Options
|
3
|
+
# Relies between OptionParser and RakeCommander errors
|
4
|
+
class ErrorRely < StandardError
|
5
|
+
extend RakeCommander::Options::Name
|
6
|
+
|
7
|
+
OPTION_REGEX = /(?:argument|option): (?<option>.+)/i.freeze
|
8
|
+
|
9
|
+
def initialize(value)
|
10
|
+
case value
|
11
|
+
when OptionParser::MissingArgument, OptionParser::InvalidArgument
|
12
|
+
super(value.message)
|
13
|
+
when String
|
14
|
+
super(value)
|
15
|
+
else
|
16
|
+
raise ArgumentError, "Expecting String or OptionParser error. Given: #{value.class}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def name?
|
21
|
+
option_sym.to_s.length > 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def short?
|
25
|
+
option_sym.to_s.length == 1
|
26
|
+
end
|
27
|
+
|
28
|
+
def option_sym
|
29
|
+
return @option_sym if @option_sym
|
30
|
+
return nil unless match = message.match(self.class::OPTION_REGEX)
|
31
|
+
option = match[:option]
|
32
|
+
@option_sym = \
|
33
|
+
if option.length > 1
|
34
|
+
self.class.name_word_sym(option)
|
35
|
+
else
|
36
|
+
self.class.short_sym(option)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def to_description(value)
|
43
|
+
case value
|
44
|
+
when Hash
|
45
|
+
to_description(value.values.uniq)
|
46
|
+
when Array
|
47
|
+
value.map do |v|
|
48
|
+
to_description(v)
|
49
|
+
end.join(', ')
|
50
|
+
when RakeCommander::Option
|
51
|
+
"#{value.name_hyphen} (#{value.short_hyphen})"
|
52
|
+
else
|
53
|
+
value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
class RakeCommander
|
2
|
+
module Options
|
3
|
+
module Name
|
4
|
+
# Substitions
|
5
|
+
HYPHEN_START_REGEX = /^-+/.freeze
|
6
|
+
HYPEN_REGEX = /-+/.freeze
|
7
|
+
UNDERSCORE_REGEX = /_+/.freeze
|
8
|
+
SPACE_REGEX = /\s+/.freeze
|
9
|
+
# Checkers
|
10
|
+
OPTIONAL_REGEX = /\[\w+\]$/.freeze
|
11
|
+
SINGLE_HYPHEN_REGEX = /^-(?<options>[^- ][^ ]*)/.freeze
|
12
|
+
DOUBLE_HYPHEN_REGEX = /^--(?<option>[^- ][^ ]*)/.freeze
|
13
|
+
|
14
|
+
# @return [Boolean]
|
15
|
+
def single_hyphen?(value)
|
16
|
+
return false unless value.respond_to?(:to_s)
|
17
|
+
!!value.to_s.match(SINGLE_HYPHEN_REGEX)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Boolean]
|
21
|
+
def double_hyphen?(value)
|
22
|
+
return false unless value.respond_to?(:to_s)
|
23
|
+
!!value.to_s.match(DOUBLE_HYPHEN_REGEX)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param strict [Boolean] whether hyphen is required when declaring an option `short`
|
27
|
+
# @return [Boolean]
|
28
|
+
def valid_short?(value, strict: false)
|
29
|
+
return false unless value.respond_to?(:to_s) && !value.to_s.empty?
|
30
|
+
return false unless !strict || single_hypen(value)
|
31
|
+
short_sym(value).to_s.length == 1
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param strict [Boolean] whether hyphen is required when declaring an option `name`
|
35
|
+
# @return [Boolean]
|
36
|
+
def valid_name?(value, strict: false)
|
37
|
+
return false unless value.respond_to?(:to_s) && !value.to_s.empty?
|
38
|
+
return false unless !strict || double_hyphen?(value)
|
39
|
+
name_sym(value).to_s.length > 1
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param strict [Boolean] whether hyphen is required when declaring an option `short`
|
43
|
+
# @return [Boolean] whether `value` is an hyphened option `short`
|
44
|
+
def short_hyphen?(value, strict: false)
|
45
|
+
short?(value, strict: strict) && single_hypen(value)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param strict [Boolean] whether hyphen is required when declaring an option `name`
|
49
|
+
# @return [Boolean] whether `value` is an hyphened option `name`
|
50
|
+
def name_hyphen?(value, strict: false)
|
51
|
+
name?(value, strict: strict) && double_hyphen?(value)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Converter
|
55
|
+
# @example
|
56
|
+
# * `"-d"` becomes `:d`
|
57
|
+
# @return [Symbol, NilClass]
|
58
|
+
def short_sym(value)
|
59
|
+
return nil unless value
|
60
|
+
value = value.to_s.gsub(HYPHEN_START_REGEX, '')
|
61
|
+
return nil unless value = value.chars.first
|
62
|
+
value.to_sym
|
63
|
+
end
|
64
|
+
|
65
|
+
# Converter.
|
66
|
+
# @example
|
67
|
+
# * `"--there-we-go ARGUMENT"` becomes `:"there_we_go ARGUMENT"`
|
68
|
+
# @note
|
69
|
+
# 1. It removes the double hyphen start (`--`)
|
70
|
+
# 2. Replaces any intermediate hyphen by underscore `_`
|
71
|
+
# 3. Replaces any multi-spacing by single space ` `
|
72
|
+
# @return [Symbol, NilClass]
|
73
|
+
def name_sym(value)
|
74
|
+
return nil unless value
|
75
|
+
value = value.to_s.gsub(HYPHEN_START_REGEX, '')
|
76
|
+
value = value.gsub(HYPEN_REGEX, '_')
|
77
|
+
value = value.gsub(SPACE_REGEX, ' ')
|
78
|
+
return nil if value.empty?
|
79
|
+
value.to_sym
|
80
|
+
end
|
81
|
+
|
82
|
+
# It's like `#name_sym` but it only gets the option name.
|
83
|
+
# @example
|
84
|
+
# * `"--there-we-go ARGUMENT"` becomes `:there_we_go`
|
85
|
+
# @see #name_sym
|
86
|
+
# @return [Symbol, NilClass]
|
87
|
+
def name_word_sym(value)
|
88
|
+
return nil unless value = name_sym(value)
|
89
|
+
return nil unless value = name_words(value).first
|
90
|
+
value.to_sym
|
91
|
+
end
|
92
|
+
|
93
|
+
# @return [String, NilClass] it returns the hyphened (`-`) version of a short `value`
|
94
|
+
def short_hyphen(value)
|
95
|
+
return nil unless value = short_sym(value)
|
96
|
+
"-#{value}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Gets the actual name of the option. First word.
|
100
|
+
# @example
|
101
|
+
# * `"--there-we-go ARGUMENT"` becomes `"--there-we-go"`
|
102
|
+
# * `"there-we-go"` becomes `"--there-we-go"`
|
103
|
+
# * `:there_we_go` becomes `"--there-we-go"`
|
104
|
+
# @return [String, NilClass] option `name` alone double hypened (`--`)
|
105
|
+
def name_hyphen(value)
|
106
|
+
return nil unless value = name_sym(value)
|
107
|
+
value = value.to_s.gsub(UNDERSCORE_REGEX, '-')
|
108
|
+
return nil if value.empty?
|
109
|
+
"--#{value}"
|
110
|
+
end
|
111
|
+
|
112
|
+
# @example
|
113
|
+
# * `"--there-we-go ARGUMENT"` returns `"ARGUMENT"`
|
114
|
+
# @return [String, NilClass] the argument of `value`, if present
|
115
|
+
def name_argument(value)
|
116
|
+
name_words(value)[1]
|
117
|
+
end
|
118
|
+
|
119
|
+
# @example
|
120
|
+
# * `"--there-we-go ARGUMENT"` returns `true`
|
121
|
+
# * `"--time"` returns `false`
|
122
|
+
# @return [String, NilClass] whether `value` is a name with argument
|
123
|
+
def name_argument?(value)
|
124
|
+
!!name_argument(value)
|
125
|
+
end
|
126
|
+
|
127
|
+
# @example
|
128
|
+
# * `"--there-we-go [ARGUMENT]"` returns `false`
|
129
|
+
# * `"--folder FOLDER"` returns `true`
|
130
|
+
# * `"--time"` returns `false`
|
131
|
+
# @return [Boolean] `true` if `value` does NOT end with `[String]`
|
132
|
+
def argument_required?(value)
|
133
|
+
return false unless value
|
134
|
+
!argument_optional?(value)
|
135
|
+
end
|
136
|
+
|
137
|
+
# @example
|
138
|
+
# * `"--there-we-go [ARGUMENT]"` returns `true`
|
139
|
+
# * `"--folder FOLDER"` returns `false`
|
140
|
+
# * `"--time"` returns `true`
|
141
|
+
# @note when there is NO argument it evaluates `true`
|
142
|
+
# @return [Boolean] `true` if `value` ends with `[String]`
|
143
|
+
def argument_optional?(value)
|
144
|
+
return true unless value
|
145
|
+
!!value.match(OPTIONAL_REGEX)
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
# @example
|
151
|
+
# * `"--there-we-go [ARGUMENT]"` returns `["there-we-go","[ARGUMENT]"]`
|
152
|
+
# @return [Array<String>] the words of `value` without hyphen start
|
153
|
+
def name_words(value)
|
154
|
+
return nil unless value
|
155
|
+
value = value.to_s.gsub(HYPHEN_START_REGEX, '')
|
156
|
+
value.to_s.split(SPACE_REGEX)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require_relative 'options/name'
|
2
|
+
require_relative 'options/arguments'
|
3
|
+
require_relative 'option'
|
4
|
+
|
5
|
+
class RakeCommander
|
6
|
+
module Options
|
7
|
+
class << self
|
8
|
+
def included(base)
|
9
|
+
super(base)
|
10
|
+
base.extend RakeCommander::Base::ClassHelpers
|
11
|
+
base.extend ClassMethods
|
12
|
+
base.inheritable_attrs :banner, :options_hash, :results_with_all_defaults
|
13
|
+
base.extend RakeCommander::Options::Arguments
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def options_hash
|
19
|
+
@options_hash ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def options
|
23
|
+
options_hash.values.uniq
|
24
|
+
end
|
25
|
+
|
26
|
+
def options?
|
27
|
+
!options.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def clear_options!
|
31
|
+
@options_hash = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Allows to use a set of options
|
35
|
+
# @param options [Enumerable<RakeCommander::Option>]
|
36
|
+
def use_options(options)
|
37
|
+
options = options.values if options.is_a?(Hash)
|
38
|
+
options.each do |opt|
|
39
|
+
add_to_options(opt)
|
40
|
+
end
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# Defines a new option
|
45
|
+
# @note
|
46
|
+
# - It will override with a Warning options with same `short` or `name`
|
47
|
+
def option(*args, **kargs, &block)
|
48
|
+
opt = RakeCommander::Option.new(*args, **kargs, &block)
|
49
|
+
add_to_options(opt)
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
# Overrides the auto-generated banner
|
54
|
+
def banner(desc = :not_used)
|
55
|
+
return @banner = desc unless desc == :not_used
|
56
|
+
return @banner if @banner
|
57
|
+
return task_options_banner if respond_to?(:task_options_banner, true)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Boolean] whether results should include options defined
|
61
|
+
# with a default, regarless if they are invoked
|
62
|
+
def results_with_all_defaults(value = nil)
|
63
|
+
if value.nil?
|
64
|
+
@results_with_all_defaults || false
|
65
|
+
else
|
66
|
+
@results_with_all_defaults = !!value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# It builds the `OptionParser` injecting the `middleware` block.
|
71
|
+
# @return [Hash] with `short` option as `key` and final value as `value`.
|
72
|
+
def parse_options(argv = ARGV, leftovers: [], &middleware)
|
73
|
+
left_overs = []
|
74
|
+
options_parser_with_results(middleware) do |options_parser|
|
75
|
+
argv = pre_parse_arguments(argv, options_hash)
|
76
|
+
leftovers.push(*options_parser.parse(argv))
|
77
|
+
rescue OptionParser::MissingArgument => e
|
78
|
+
raise RakeCommander::Options::MissingArgument, e, cause: nil
|
79
|
+
rescue OptionParser::InvalidArgument => e
|
80
|
+
error = RakeCommander::Options::InvalidArgument
|
81
|
+
error = error.new(e)
|
82
|
+
if (opt = options_hash[error.option_sym]) && opt.argument_required?
|
83
|
+
msg = "missing required argument: #{opt.name_hyphen} (#{opt.short_hyphen})"
|
84
|
+
raise RakeCommander::Options::MissingArgument, msg, cause: nil
|
85
|
+
else
|
86
|
+
raise error, e, cause: nil
|
87
|
+
end
|
88
|
+
end.tap do |results|
|
89
|
+
check_required_presence!(results)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
# @return [OptionParser] the built options parser.
|
96
|
+
def options_parser(&middleware)
|
97
|
+
new_options_parser do |opts|
|
98
|
+
opts.banner = banner if banner
|
99
|
+
options.each {|opt| opt.add_switch(opts, &middleware)}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def new_options_parser(&block)
|
104
|
+
require 'optparse'
|
105
|
+
OptionParser.new(&block)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
# Expects a block that should do the final call to `#parse`
|
111
|
+
def options_parser_with_results(middleware)
|
112
|
+
result_defaults.tap do |result|
|
113
|
+
results_collector = proc do |value, default, short, name|
|
114
|
+
middleware&.call(value, default, short, name)
|
115
|
+
result[short] = value.nil?? default : value
|
116
|
+
end
|
117
|
+
options_parser = options_parser(&results_collector)
|
118
|
+
yield(options_parser)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Based on `required` options, it sets the `default`
|
123
|
+
def result_defaults
|
124
|
+
{}.tap do |res_def|
|
125
|
+
options.select do |opt|
|
126
|
+
(results_with_all_defaults && opt.default?) \
|
127
|
+
|| (opt.required? && opt.default?)
|
128
|
+
end.each do |opt|
|
129
|
+
res_def[opt.short] = opt.default
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# It throws an exception if any of the required options
|
135
|
+
# is missing in results
|
136
|
+
def check_required_presence!(results)
|
137
|
+
missing = options.select do |opt|
|
138
|
+
opt.required?
|
139
|
+
end.reject do |opt|
|
140
|
+
results.key?(opt.short) || results.key?(opt.name)
|
141
|
+
end
|
142
|
+
raise RakeCommander::Options::MissingOption.new(missing) unless missing.empty?
|
143
|
+
end
|
144
|
+
|
145
|
+
def add_to_options(opt)
|
146
|
+
if prev = options_hash[opt.short]
|
147
|
+
puts "Warning: Overriding option with short '#{prev.short}' ('#{prev.name}')"
|
148
|
+
options_hash.delete(prev.short)
|
149
|
+
options_hash.delete(prev.name)
|
150
|
+
end
|
151
|
+
if prev = options_hash[opt.name]
|
152
|
+
puts "Warning: Overriding option with name '#{prev.name}' ('#{prev.short}')"
|
153
|
+
options_hash.delete(prev.short)
|
154
|
+
options_hash.delete(prev.name)
|
155
|
+
end
|
156
|
+
options_hash[opt.name] = options_hash[opt.short] = opt
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def options(argv = ARGV)
|
161
|
+
@options ||= self.class.parse_options(argv, leftovers: self.options_leftovers)
|
162
|
+
end
|
163
|
+
|
164
|
+
def options_leftovers
|
165
|
+
@options_leftovers ||= []
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
require_relative 'options/error'
|
171
|
+
require_relative 'options/set'
|