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.
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