tty-option 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +1653 -1
  4. data/lib/tty/option.rb +63 -4
  5. data/lib/tty/option/aggregate_errors.rb +95 -0
  6. data/lib/tty/option/conversions.rb +126 -0
  7. data/lib/tty/option/converter.rb +63 -0
  8. data/lib/tty/option/deep_dup.rb +48 -0
  9. data/lib/tty/option/dsl.rb +105 -0
  10. data/lib/tty/option/dsl/arity.rb +49 -0
  11. data/lib/tty/option/dsl/conversion.rb +17 -0
  12. data/lib/tty/option/error_aggregator.rb +35 -0
  13. data/lib/tty/option/errors.rb +144 -0
  14. data/lib/tty/option/formatter.rb +389 -0
  15. data/lib/tty/option/inflection.rb +50 -0
  16. data/lib/tty/option/param_conversion.rb +34 -0
  17. data/lib/tty/option/param_permitted.rb +30 -0
  18. data/lib/tty/option/param_validation.rb +48 -0
  19. data/lib/tty/option/parameter.rb +310 -0
  20. data/lib/tty/option/parameter/argument.rb +18 -0
  21. data/lib/tty/option/parameter/environment.rb +20 -0
  22. data/lib/tty/option/parameter/keyword.rb +15 -0
  23. data/lib/tty/option/parameter/option.rb +99 -0
  24. data/lib/tty/option/parameters.rb +157 -0
  25. data/lib/tty/option/params.rb +122 -0
  26. data/lib/tty/option/parser.rb +57 -3
  27. data/lib/tty/option/parser/arguments.rb +166 -0
  28. data/lib/tty/option/parser/arity_check.rb +34 -0
  29. data/lib/tty/option/parser/environments.rb +169 -0
  30. data/lib/tty/option/parser/keywords.rb +158 -0
  31. data/lib/tty/option/parser/options.rb +273 -0
  32. data/lib/tty/option/parser/param_types.rb +51 -0
  33. data/lib/tty/option/parser/required_check.rb +36 -0
  34. data/lib/tty/option/pipeline.rb +38 -0
  35. data/lib/tty/option/result.rb +46 -0
  36. data/lib/tty/option/section.rb +26 -0
  37. data/lib/tty/option/sections.rb +56 -0
  38. data/lib/tty/option/usage.rb +166 -0
  39. data/lib/tty/option/usage_wrapper.rb +58 -0
  40. data/lib/tty/option/version.rb +3 -3
  41. 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