tty-option 0.0.0 → 0.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +1653 -1
- data/lib/tty/option.rb +63 -4
- data/lib/tty/option/aggregate_errors.rb +95 -0
- data/lib/tty/option/conversions.rb +126 -0
- data/lib/tty/option/converter.rb +63 -0
- data/lib/tty/option/deep_dup.rb +48 -0
- data/lib/tty/option/dsl.rb +105 -0
- data/lib/tty/option/dsl/arity.rb +49 -0
- data/lib/tty/option/dsl/conversion.rb +17 -0
- data/lib/tty/option/error_aggregator.rb +35 -0
- data/lib/tty/option/errors.rb +144 -0
- data/lib/tty/option/formatter.rb +389 -0
- data/lib/tty/option/inflection.rb +50 -0
- data/lib/tty/option/param_conversion.rb +34 -0
- data/lib/tty/option/param_permitted.rb +30 -0
- data/lib/tty/option/param_validation.rb +48 -0
- data/lib/tty/option/parameter.rb +310 -0
- data/lib/tty/option/parameter/argument.rb +18 -0
- data/lib/tty/option/parameter/environment.rb +20 -0
- data/lib/tty/option/parameter/keyword.rb +15 -0
- data/lib/tty/option/parameter/option.rb +99 -0
- data/lib/tty/option/parameters.rb +157 -0
- data/lib/tty/option/params.rb +122 -0
- data/lib/tty/option/parser.rb +57 -3
- data/lib/tty/option/parser/arguments.rb +166 -0
- data/lib/tty/option/parser/arity_check.rb +34 -0
- data/lib/tty/option/parser/environments.rb +169 -0
- data/lib/tty/option/parser/keywords.rb +158 -0
- data/lib/tty/option/parser/options.rb +273 -0
- data/lib/tty/option/parser/param_types.rb +51 -0
- data/lib/tty/option/parser/required_check.rb +36 -0
- data/lib/tty/option/pipeline.rb +38 -0
- data/lib/tty/option/result.rb +46 -0
- data/lib/tty/option/section.rb +26 -0
- data/lib/tty/option/sections.rb +56 -0
- data/lib/tty/option/usage.rb +166 -0
- data/lib/tty/option/usage_wrapper.rb +58 -0
- data/lib/tty/option/version.rb +3 -3
- metadata +37 -3
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "arity_check"
|
4
|
+
require_relative "param_types"
|
5
|
+
require_relative "required_check"
|
6
|
+
require_relative "../error_aggregator"
|
7
|
+
require_relative "../pipeline"
|
8
|
+
|
9
|
+
module TTY
|
10
|
+
module Option
|
11
|
+
class Parser
|
12
|
+
class Options
|
13
|
+
include ParamTypes
|
14
|
+
|
15
|
+
LONG_OPTION_RE = /^(--[^=]+)(\s+|=)?(.*)?$/.freeze
|
16
|
+
|
17
|
+
SHORT_OPTION_RE = /^(-.)(.*)$/.freeze
|
18
|
+
|
19
|
+
# Create a command line env variables parser
|
20
|
+
#
|
21
|
+
# @param [Array<Option>] options
|
22
|
+
# the list of options
|
23
|
+
# @param [Hash] config
|
24
|
+
# the configuration settings
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
def initialize(options, check_invalid_params: true,
|
28
|
+
raise_on_parse_error: false)
|
29
|
+
@options = options
|
30
|
+
@check_invalid_params = check_invalid_params
|
31
|
+
@error_aggregator =
|
32
|
+
ErrorAggregator.new(raise_on_parse_error: raise_on_parse_error)
|
33
|
+
@required_check = RequiredCheck.new(@error_aggregator)
|
34
|
+
@arity_check = ArityCheck.new(@error_aggregator)
|
35
|
+
@pipeline = Pipeline.new(@error_aggregator)
|
36
|
+
@parsed = {}
|
37
|
+
@remaining = []
|
38
|
+
@shorts = {}
|
39
|
+
@longs = {}
|
40
|
+
@arities = Hash.new(0)
|
41
|
+
|
42
|
+
setup_opts
|
43
|
+
end
|
44
|
+
|
45
|
+
# Configure list of returned options
|
46
|
+
#
|
47
|
+
# @api private
|
48
|
+
def setup_opts
|
49
|
+
@options.each do |opt|
|
50
|
+
@shorts[opt.short_name] = opt
|
51
|
+
@longs[opt.long_name] = opt
|
52
|
+
@arity_check << opt if opt.multiple?
|
53
|
+
|
54
|
+
if opt.default?
|
55
|
+
case opt.default
|
56
|
+
when Proc
|
57
|
+
assign_option(opt, opt.default.())
|
58
|
+
else
|
59
|
+
assign_option(opt, opt.default)
|
60
|
+
end
|
61
|
+
elsif !(opt.argument_optional? || opt.argument_required?)
|
62
|
+
assign_option(opt, false)
|
63
|
+
elsif opt.required?
|
64
|
+
@required_check << opt
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Read option(s) from command line
|
70
|
+
#
|
71
|
+
# @param [Array<String>] argv
|
72
|
+
#
|
73
|
+
# @api public
|
74
|
+
def parse(argv)
|
75
|
+
@argv = argv.dup
|
76
|
+
|
77
|
+
loop do
|
78
|
+
opt, value = next_option
|
79
|
+
if !opt.nil?
|
80
|
+
@required_check.delete(opt)
|
81
|
+
@arities[opt.key] += 1
|
82
|
+
|
83
|
+
if block_given?
|
84
|
+
yield(opt, value)
|
85
|
+
else
|
86
|
+
assign_option(opt, value)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
break if @argv.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
@arity_check.(@arities)
|
93
|
+
@required_check.()
|
94
|
+
|
95
|
+
[@parsed, @remaining, @error_aggregator.errors]
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Get next option
|
101
|
+
#
|
102
|
+
# @api private
|
103
|
+
def next_option
|
104
|
+
opt, value = nil, nil
|
105
|
+
|
106
|
+
while !@argv.empty? && !option?(@argv.first)
|
107
|
+
@remaining << @argv.shift
|
108
|
+
end
|
109
|
+
|
110
|
+
return if @argv.empty?
|
111
|
+
|
112
|
+
argument = @argv.shift
|
113
|
+
|
114
|
+
if (matched = argument.match(LONG_OPTION_RE))
|
115
|
+
long, sep, rest = matched[1..-1]
|
116
|
+
opt, value = *process_double_option(long, sep, rest)
|
117
|
+
elsif (matched = argument.match(SHORT_OPTION_RE))
|
118
|
+
short, other_singles = *matched[1..-1]
|
119
|
+
opt, value = *process_single_option(short, other_singles)
|
120
|
+
end
|
121
|
+
|
122
|
+
[opt, value]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Process a double option
|
126
|
+
#
|
127
|
+
# @return [Array<Option, Object>]
|
128
|
+
# a list of option and its value
|
129
|
+
#
|
130
|
+
# @api private
|
131
|
+
def process_double_option(long, sep, rest)
|
132
|
+
opt, value = nil, nil
|
133
|
+
|
134
|
+
if (opt = @longs[long])
|
135
|
+
if opt.argument_required?
|
136
|
+
if !rest.empty? || sep.to_s.include?("=")
|
137
|
+
value = rest
|
138
|
+
if opt.multi_argument? &&
|
139
|
+
!(consumed = consume_arguments).empty?
|
140
|
+
value = [rest] + consumed
|
141
|
+
end
|
142
|
+
elsif !@argv.empty?
|
143
|
+
value = opt.multi_argument? ? consume_arguments : @argv.shift
|
144
|
+
else
|
145
|
+
@error_aggregator.(MissingArgument.new(opt))
|
146
|
+
end
|
147
|
+
elsif opt.argument_optional?
|
148
|
+
if !rest.empty?
|
149
|
+
value = rest
|
150
|
+
if opt.multi_argument? &&
|
151
|
+
!(consumed = consume_arguments).empty?
|
152
|
+
value = [rest] + consumed
|
153
|
+
end
|
154
|
+
elsif !@argv.empty?
|
155
|
+
value = opt.multi_argument? ? consume_arguments : @argv.shift
|
156
|
+
end
|
157
|
+
else # boolean flag
|
158
|
+
value = true
|
159
|
+
end
|
160
|
+
else
|
161
|
+
# option stuck together with an argument or abbreviated
|
162
|
+
matching_options = 0
|
163
|
+
@longs.each_key do |key|
|
164
|
+
if !key.to_s.empty? &&
|
165
|
+
(key.to_s.start_with?(long) || long.to_s.start_with?(key))
|
166
|
+
opt = @longs[key]
|
167
|
+
matching_options += 1
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
if matching_options.zero?
|
172
|
+
if @check_invalid_params
|
173
|
+
@error_aggregator.(InvalidParameter.new("invalid option '#{long}'"))
|
174
|
+
else
|
175
|
+
@remaining << long
|
176
|
+
end
|
177
|
+
elsif matching_options == 1
|
178
|
+
value = long[opt.long_name.size..-1]
|
179
|
+
else
|
180
|
+
@error_aggregator.(AmbiguousOption.new("option '#{long}' is ambiguous"))
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
[opt, value]
|
185
|
+
end
|
186
|
+
|
187
|
+
# Process a single option
|
188
|
+
#
|
189
|
+
# @return [Array<Option, Object>]
|
190
|
+
# a list of option and its value
|
191
|
+
#
|
192
|
+
# @api private
|
193
|
+
def process_single_option(short, other_singles)
|
194
|
+
opt, value = nil, nil
|
195
|
+
|
196
|
+
if (opt = @shorts[short])
|
197
|
+
if opt.argument_required?
|
198
|
+
if !other_singles.empty?
|
199
|
+
value = other_singles
|
200
|
+
if opt.multi_argument? &&
|
201
|
+
!(consumed = consume_arguments).empty?
|
202
|
+
value = [other_singles] + consumed
|
203
|
+
end
|
204
|
+
elsif !@argv.empty?
|
205
|
+
value = opt.multi_argument? ? consume_arguments : @argv.shift
|
206
|
+
else
|
207
|
+
@error_aggregator.(MissingArgument.new(opt))
|
208
|
+
end
|
209
|
+
elsif opt.argument_optional?
|
210
|
+
if !other_singles.empty?
|
211
|
+
value = other_singles
|
212
|
+
if opt.multi_argument? &&
|
213
|
+
!(consumed = consume_arguments).empty?
|
214
|
+
value = [other_singles] + consumed
|
215
|
+
end
|
216
|
+
elsif !@argv.empty?
|
217
|
+
value = opt.multi_argument? ? consume_arguments : @argv.shift
|
218
|
+
end
|
219
|
+
else # boolean flag
|
220
|
+
if !other_singles.empty?
|
221
|
+
@argv.unshift("-#{other_singles}")
|
222
|
+
end
|
223
|
+
value = true
|
224
|
+
end
|
225
|
+
elsif @check_invalid_params
|
226
|
+
@error_aggregator.(InvalidParameter.new("invalid option '#{short}'"))
|
227
|
+
else
|
228
|
+
@remaining << short
|
229
|
+
end
|
230
|
+
|
231
|
+
[opt, value]
|
232
|
+
end
|
233
|
+
|
234
|
+
# Consume multi argument
|
235
|
+
#
|
236
|
+
# @api private
|
237
|
+
def consume_arguments(values: [])
|
238
|
+
while (value = @argv.first) && !option?(value)
|
239
|
+
val = @argv.shift
|
240
|
+
parts = val.include?("&") ? val.split(/&/) : [val]
|
241
|
+
parts.each { |part| values << part }
|
242
|
+
end
|
243
|
+
|
244
|
+
values.size == 1 ? values.first : values
|
245
|
+
end
|
246
|
+
|
247
|
+
# @api private
|
248
|
+
def assign_option(opt, val)
|
249
|
+
value = @pipeline.(opt, val)
|
250
|
+
|
251
|
+
if opt.multiple?
|
252
|
+
allowed = opt.arity < 0 || @arities[opt.key] <= opt.arity
|
253
|
+
if allowed
|
254
|
+
case value
|
255
|
+
when Hash
|
256
|
+
(@parsed[opt.key] ||= {}).merge!(value)
|
257
|
+
else
|
258
|
+
Array(value).each do |v|
|
259
|
+
(@parsed[opt.key] ||= []) << v
|
260
|
+
end
|
261
|
+
end
|
262
|
+
else
|
263
|
+
@remaining << opt.short_name
|
264
|
+
@remaining << value
|
265
|
+
end
|
266
|
+
else
|
267
|
+
@parsed[opt.key] = value
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end # Options
|
271
|
+
end # Parser
|
272
|
+
end # Option
|
273
|
+
end # TTY
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
module Option
|
5
|
+
class Parser
|
6
|
+
module ParamTypes
|
7
|
+
# Check if value looks like an argument
|
8
|
+
#
|
9
|
+
# @param [String] value
|
10
|
+
#
|
11
|
+
# @return [Boolean]
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
def argument?(value)
|
15
|
+
!value.match(/^[^-][^=]*\z/).nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
# Check if value is an environment variable
|
19
|
+
#
|
20
|
+
# @param [String] value
|
21
|
+
#
|
22
|
+
# @return [Boolean]
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
def env_var?(value)
|
26
|
+
!value.match(/^[\p{Lu}_\-\d]+=/).nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check to see if value is a keyword
|
30
|
+
#
|
31
|
+
# @return [Boolean]
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def keyword?(value)
|
35
|
+
!value.to_s.match(/^([^-=][\p{Ll}_\-\d]*)=([^=]+)/).nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
# Check if value looks like an option
|
39
|
+
#
|
40
|
+
# @param [String] value
|
41
|
+
#
|
42
|
+
# @return [Boolean]
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def option?(value)
|
46
|
+
!value.match(/^-./).nil?
|
47
|
+
end
|
48
|
+
end # ParamTypes
|
49
|
+
end # Parser
|
50
|
+
end # Option
|
51
|
+
end # TTY
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
module Option
|
5
|
+
class Parser
|
6
|
+
class RequiredCheck
|
7
|
+
def initialize(error_aggregator)
|
8
|
+
@required = []
|
9
|
+
@error_aggregator = error_aggregator
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(param)
|
13
|
+
@required << param
|
14
|
+
end
|
15
|
+
alias :<< :add
|
16
|
+
|
17
|
+
def delete(param)
|
18
|
+
@required.delete(param)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check if required options are provided
|
22
|
+
#
|
23
|
+
# @raise [MissingParameter]
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def call
|
27
|
+
return if @required.empty?
|
28
|
+
|
29
|
+
@required.each do |param|
|
30
|
+
@error_aggregator.(MissingParameter.new(param))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end # RequiredCheck
|
34
|
+
end # Parser
|
35
|
+
end # Option
|
36
|
+
end # TTY
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "param_conversion"
|
4
|
+
require_relative "param_permitted"
|
5
|
+
require_relative "param_validation"
|
6
|
+
|
7
|
+
module TTY
|
8
|
+
module Option
|
9
|
+
class Pipeline
|
10
|
+
PROCESSORS = [
|
11
|
+
ParamConversion,
|
12
|
+
ParamPermitted,
|
13
|
+
ParamValidation
|
14
|
+
]
|
15
|
+
|
16
|
+
# Create a param processing pipeline
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
def initialize(error_aggregator)
|
20
|
+
@error_aggregator = error_aggregator
|
21
|
+
end
|
22
|
+
|
23
|
+
# Process param value through conditions
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def call(param, value)
|
27
|
+
result = Result.success(value)
|
28
|
+
PROCESSORS.each do |processor|
|
29
|
+
result = processor[param, result.value]
|
30
|
+
if result.failure?
|
31
|
+
Array(result.error).each { |err| @error_aggregator.(err) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
result.value
|
35
|
+
end
|
36
|
+
end # Pipeline
|
37
|
+
end # Option
|
38
|
+
end # TTY
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
module Option
|
5
|
+
# A monad that respresents success and failure conditions
|
6
|
+
class Result
|
7
|
+
# Wrap a value in a success monad
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
def self.success(value)
|
11
|
+
Success.new(value)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Wrap a value in a failure monad
|
15
|
+
#
|
16
|
+
# @api public
|
17
|
+
def self.failure(value)
|
18
|
+
Failure.new(value)
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :value
|
22
|
+
|
23
|
+
attr_reader :error
|
24
|
+
|
25
|
+
def success?
|
26
|
+
is_a?(Success)
|
27
|
+
end
|
28
|
+
|
29
|
+
def failure?
|
30
|
+
is_a?(Failure)
|
31
|
+
end
|
32
|
+
|
33
|
+
class Success < Result
|
34
|
+
def initialize(value)
|
35
|
+
@value = value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Failure < Result
|
40
|
+
def initialize(error)
|
41
|
+
@error = error
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|