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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ module Option
5
+ module DSL
6
+ module Arity
7
+ # @api public
8
+ def one
9
+ 1
10
+ end
11
+
12
+ # @api public
13
+ def two
14
+ 2
15
+ end
16
+
17
+ # Zero or more arity
18
+ #
19
+ # @api public
20
+ def zero_or_more
21
+ -1
22
+ end
23
+ alias any zero_or_more
24
+ alias any_args zero_or_more
25
+
26
+ # One or more arity
27
+ #
28
+ # @api public
29
+ def one_or_more
30
+ -2
31
+ end
32
+
33
+ # Two or more arity
34
+ #
35
+ # @api public
36
+ def two_or_more
37
+ -3
38
+ end
39
+
40
+ # At last number values for arity
41
+ #
42
+ # @api public
43
+ def at_least(number)
44
+ -number.to_i - 1
45
+ end
46
+ end # Arity
47
+ end # DSL
48
+ end # Option
49
+ end # TTY
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ module Option
5
+ module DSL
6
+ module Conversion
7
+ def map_of(type)
8
+ :"#{type}_map"
9
+ end
10
+
11
+ def list_of(type)
12
+ :"#{type}_list"
13
+ end
14
+ end # Convert
15
+ end # DSL
16
+ end # Option
17
+ end # TTY
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "inflection"
4
+
5
+ module TTY
6
+ module Option
7
+ class ErrorAggregator
8
+ include Inflection
9
+
10
+ # Collected errors
11
+ attr_reader :errors
12
+
13
+ def initialize(errors = [], raise_on_parse_error: false)
14
+ @errors = errors
15
+ @raise_on_parse_error = raise_on_parse_error
16
+ end
17
+
18
+ # Record or raise an error
19
+ #
20
+ # @param [TTY::Option::Error] error
21
+ # @param [String] message
22
+ #
23
+ # @api public
24
+ def call(error, message = nil)
25
+ if error.is_a?(Class)
26
+ error = message.nil? ? error.new : error.new(message)
27
+ end
28
+
29
+ raise(error) if @raise_on_parse_error
30
+
31
+ @errors << error
32
+ end
33
+ end # ErrorAggregator
34
+ end # Option
35
+ end # TTY
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ module Option
5
+ Error = Class.new(StandardError)
6
+
7
+ # Raised when a parameter invariant is invalid
8
+ ConfigurationError = Class.new(Error)
9
+
10
+ # Raised when attempting to register already registered parameter
11
+ ParameterConflict = Class.new(Error)
12
+
13
+ # Raised when overriding already defined conversion
14
+ ConversionAlreadyDefined = Class.new(Error)
15
+
16
+ # Raised when conversion cannot be performed
17
+ ConversionError = Class.new(Error)
18
+
19
+ # Raised when conversion type isn't registered
20
+ UnsupportedConversion = Class.new(Error)
21
+
22
+ # Raised during command line input parsing
23
+ class ParseError < Error
24
+ attr_accessor :param
25
+ end
26
+
27
+ # Raised when found unrecognized parameter
28
+ InvalidParameter = Class.new(ParseError)
29
+
30
+ # Raised when an option matches more than one parameter option
31
+ AmbiguousOption = Class.new(ParseError)
32
+
33
+ # Raised when parameter argument doesn't match expected value
34
+ class InvalidArgument < ParseError
35
+ MESSAGE = "value of `%<value>s` fails validation for '%<name>s' %<type>s"
36
+
37
+ def initialize(param_or_message, value = nil)
38
+ if param_or_message.is_a?(Parameter)
39
+ @param = param_or_message
40
+
41
+ message = format(MESSAGE,
42
+ value: value,
43
+ name: param.name,
44
+ type: param.to_sym)
45
+ else
46
+ message = param_or_message
47
+ end
48
+
49
+ super(message)
50
+ end
51
+ end
52
+
53
+ # Raised when number of parameter arguments doesn't match
54
+ class InvalidArity < ParseError
55
+ MESSAGE = "%<type>s '%<name>s' should appear %<expect>s but appeared %<actual>s"
56
+
57
+ def initialize(param_or_message, arity = nil)
58
+ if param_or_message.is_a?(Parameter)
59
+ @param = param_or_message
60
+ prefix = param.arity < 0 ? "at least " : ""
61
+ expected_arity = param.arity < 0 ? param.arity.abs - 1 : param.arity
62
+
63
+ message = format(MESSAGE,
64
+ type: param.to_sym,
65
+ name: param.name,
66
+ expect: prefix + pluralize("time", expected_arity),
67
+ actual: pluralize("time", arity))
68
+ else
69
+ message = param_or_message
70
+ end
71
+
72
+ super(message)
73
+ end
74
+
75
+ # Pluralize a noun
76
+ #
77
+ # @api private
78
+ def pluralize(noun, count = 1)
79
+ "#{count} #{noun}#{'s' unless count == 1}"
80
+ end
81
+ end
82
+
83
+ # Raised when conversion provided with unexpected argument
84
+ class InvalidConversionArgument < ParseError
85
+ MESSAGE = "cannot convert value of `%<value>s` into '%<cast>s' type " \
86
+ "for '%<name>s' %<type>s"
87
+
88
+ def initialize(param, value)
89
+ @param = param
90
+ message = format(MESSAGE, value: value, cast: param.convert,
91
+ name: param.name, type: param.to_sym)
92
+ super(message)
93
+ end
94
+ end
95
+
96
+ # Raised when option requires an argument
97
+ class MissingArgument < ParseError
98
+ MESSAGE = "%<type>s %<name>s requires an argument"
99
+
100
+ def initialize(param)
101
+ @param = param
102
+ message = format(MESSAGE, type: param.to_sym, name: param.name)
103
+ super(message)
104
+ end
105
+ end
106
+
107
+ # Raised when a parameter is required but not present
108
+ class MissingParameter < ParseError
109
+ MESSAGE = "%<type>s '%<name>s' must be provided"
110
+
111
+ def initialize(param_or_message)
112
+ if param_or_message.is_a?(Parameter)
113
+ @param = param_or_message
114
+ message = format(MESSAGE, name: param.name, type: param.to_sym)
115
+ else
116
+ message = param_or_message
117
+ end
118
+
119
+ super(message)
120
+ end
121
+ end
122
+
123
+ # Raised when argument value isn't permitted
124
+ class UnpermittedArgument < ParseError
125
+ MESSAGE = "unpermitted value `%<value>s` for '%<name>s' %<type>s: " \
126
+ "choose from %<choices>s"
127
+
128
+ def initialize(param_or_message, value = nil)
129
+ if param_or_message.is_a?(Parameter)
130
+ @param = param_or_message
131
+ message = format(MESSAGE,
132
+ value: value,
133
+ name: param.name,
134
+ type: param.to_sym,
135
+ choices: param.permit.join(", "))
136
+ else
137
+ message = param_or_message
138
+ end
139
+
140
+ super(message)
141
+ end
142
+ end
143
+ end # Option
144
+ end # TTY
@@ -0,0 +1,389 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sections"
4
+ require_relative "usage_wrapper"
5
+
6
+ module TTY
7
+ module Option
8
+ class Formatter
9
+ include UsageWrapper
10
+
11
+ SHORT_OPT_LENGTH = 4
12
+ DEFAULT_WIDTH = 80
13
+ NEWLINE = "\n"
14
+ ELLIPSIS = "..."
15
+ SPACE = " "
16
+
17
+ DEFAULT_PARAM_DISPLAY = ->(str) { str.to_s.upcase }
18
+ DEFAULT_ORDER = ->(params) { params.sort }
19
+ NOOP_PROC = ->(param) { param }
20
+ DEFAULT_NAME_SELECTOR = ->(param) { param.name }
21
+
22
+ # @api public
23
+ def self.help(parameters, usage, **config, &block)
24
+ new(parameters, usage, **config).help(&block)
25
+ end
26
+
27
+ attr_reader :width
28
+
29
+ # Create a help formatter
30
+ #
31
+ # @param [Parameters]
32
+ #
33
+ # @api public
34
+ def initialize(parameters, usage, param_display: DEFAULT_PARAM_DISPLAY,
35
+ width: DEFAULT_WIDTH, order: DEFAULT_ORDER, indent: 0)
36
+ @parameters = parameters
37
+ @usage = usage
38
+ @param_display = param_display
39
+ @order = order
40
+ @width = width
41
+ @indent = indent
42
+ @space_indent = SPACE * indent
43
+ @param_indent = indent + 2
44
+ @section_names = {
45
+ usage: "Usage:",
46
+ arguments: "Arguments:",
47
+ keywords: "Keywords:",
48
+ options: "Options:",
49
+ env: "Environment:",
50
+ examples: "Examples:"
51
+ }
52
+ end
53
+
54
+ # A formatted help usage information
55
+ #
56
+ # @return [String]
57
+ #
58
+ # @api public
59
+ def help(&block)
60
+ sections = Sections.new
61
+
62
+ sections.add(:header, help_header) if @usage.header?
63
+ sections.add(:banner, help_banner)
64
+ sections.add(:description, help_description) if @usage.desc?
65
+
66
+ if @parameters.arguments.any?(&:display?)
67
+ sections.add(:arguments, help_arguments)
68
+ end
69
+
70
+ if @parameters.keywords.any?(&:display?)
71
+ sections.add(:keywords, help_keywords)
72
+ end
73
+
74
+ if @parameters.options?
75
+ sections.add(:options, help_options)
76
+ end
77
+
78
+ if @parameters.environments.any?(&:display?)
79
+ sections.add(:environments, help_environments)
80
+ end
81
+
82
+ sections.add(:examples, help_examples) if @usage.example?
83
+ sections.add(:footer, help_footer) if @usage.footer?
84
+
85
+ if block_given?
86
+ yield(sections)
87
+ end
88
+
89
+ formatted = sections.reject(&:empty?).join(NEWLINE)
90
+ formatted.end_with?(NEWLINE) ? formatted : formatted + NEWLINE
91
+ end
92
+
93
+ def help_header
94
+ "#{format_multiline(@usage.header, @indent)}#{NEWLINE}"
95
+ end
96
+
97
+ def help_banner
98
+ (@usage.banner? ? @usage.banner : format_usage)
99
+ end
100
+
101
+ def help_description
102
+ "#{NEWLINE}#{format_description}"
103
+ end
104
+
105
+ def help_arguments
106
+ "#{NEWLINE}#{@space_indent}#{@section_names[:arguments]}#{NEWLINE}" +
107
+ format_section(@parameters.arguments, ->(param) do
108
+ @param_display.(param.name)
109
+ end)
110
+ end
111
+
112
+ def help_keywords
113
+ "#{NEWLINE}#{@space_indent}#{@section_names[:keywords]}#{NEWLINE}" +
114
+ format_section(@parameters.keywords, ->(param) do
115
+ kwarg_param_display(param).split("=").map(&@param_display).join("=")
116
+ end)
117
+ end
118
+
119
+ def help_options
120
+ "#{NEWLINE}#{@space_indent}#{@section_names[:options]}#{NEWLINE}" +
121
+ format_options
122
+ end
123
+
124
+ def help_environments
125
+ "#{NEWLINE}#{@space_indent}#{@section_names[:env]}#{NEWLINE}" +
126
+ format_section(@order.(@parameters.environments))
127
+ end
128
+
129
+ def help_examples
130
+ "#{NEWLINE}#{@space_indent}#{@section_names[:examples]}#{NEWLINE}" +
131
+ format_examples
132
+ end
133
+
134
+ def help_footer
135
+ "#{NEWLINE}#{format_multiline(@usage.footer, @indent)}"
136
+ end
137
+
138
+ private
139
+
140
+ # Provide a default usage banner
141
+ #
142
+ # @api private
143
+ def format_usage
144
+ usage = @space_indent + @section_names[:usage] + SPACE
145
+ output = []
146
+ output << @usage.program
147
+ output << " #{@usage.commands.join(" ")}" if @usage.command?
148
+ output << " [#{@param_display.("options")}]" if @parameters.options?
149
+ output << " [#{@param_display.("environment")}]" if @parameters.environments?
150
+ output << " #{format_arguments_usage}" if @parameters.arguments?
151
+ output << " #{format_keywords_usage}" if @parameters.keywords?
152
+ usage + wrap(output.join, indent: usage.length, width: width)
153
+ end
154
+
155
+ # Format arguments
156
+ #
157
+ # @api private
158
+ def format_arguments_usage
159
+ return "" unless @parameters.arguments?
160
+
161
+ @parameters.arguments.reduce([]) do |acc, arg|
162
+ next acc if arg.hidden?
163
+
164
+ acc << format_argument_usage(arg)
165
+ end.join(SPACE)
166
+ end
167
+
168
+ # Provide an argument summary
169
+ #
170
+ # @api private
171
+ def format_argument_usage(arg)
172
+ arg_name = @param_display.(arg.name)
173
+ format_parameter_usage(arg, arg_name)
174
+ end
175
+
176
+ # Format parameter usage
177
+ #
178
+ # @api private
179
+ def format_parameter_usage(param, param_name)
180
+ args = []
181
+ if 0 < param.arity
182
+ args << "[" if param.optional?
183
+ args << param_name
184
+ (param.arity - 1).times { args << " #{param_name}" }
185
+ args. << "]" if param.optional?
186
+ args.join
187
+ else
188
+ (param.arity.abs - 1).times { args << param_name }
189
+ args << "[#{param_name}#{ELLIPSIS}]"
190
+ args.join(SPACE)
191
+ end
192
+ end
193
+
194
+ # Format keywords usage
195
+ #
196
+ # @api private
197
+ def format_keywords_usage
198
+ return "" unless @parameters.keywords?
199
+
200
+ @parameters.keywords.reduce([]) do |acc, kwarg|
201
+ next acc if kwarg.hidden?
202
+
203
+ acc << format_keyword_usage(kwarg)
204
+ end.join(SPACE)
205
+ end
206
+
207
+ # Provide a keyword summary
208
+ #
209
+ # @api private
210
+ def format_keyword_usage(kwarg)
211
+ param_name = kwarg_param_display(kwarg, @param_display)
212
+ format_parameter_usage(kwarg, param_name)
213
+ end
214
+
215
+ # Provide a keyword argument display format
216
+ #
217
+ # @api private
218
+ def kwarg_param_display(kwarg, param_display = NOOP_PROC)
219
+ kwarg_name = param_display.(kwarg.name)
220
+ conv_name = case kwarg.convert
221
+ when Proc, NilClass
222
+ kwarg_name
223
+ else
224
+ param_display.(kwarg.convert)
225
+ end
226
+
227
+ "#{kwarg_name}=#{conv_name}"
228
+ end
229
+
230
+ # Format a parameter section in the help display
231
+ #
232
+ # @param [String] parameters_name
233
+ # the name of parameter type
234
+ #
235
+ # @param [Proc] name_selector
236
+ # selects a name from the parameter, by defeault the name
237
+ #
238
+ # @return [String]
239
+ #
240
+ # @api private
241
+ def format_section(params, name_selector = DEFAULT_NAME_SELECTOR)
242
+ longest_param = params.map(&name_selector).compact.max_by(&:length).length
243
+
244
+ params.reduce([]) do |acc, param|
245
+ next acc if param.hidden?
246
+
247
+ acc << format_section_parameter(param, longest_param, name_selector)
248
+ end.join(NEWLINE)
249
+ end
250
+
251
+ # Format a section parameter line
252
+ #
253
+ # @return [String]
254
+ #
255
+ # @api private
256
+ def format_section_parameter(param, longest_param, name_selector)
257
+ line = []
258
+ desc = []
259
+ indent = @param_indent + longest_param + 2
260
+ param_name = name_selector.(param)
261
+
262
+ if param.desc?
263
+ line << format("%s%-#{longest_param}s", SPACE * @param_indent, param_name)
264
+ desc << " #{param.desc}"
265
+ else
266
+ line << format("%s%s", SPACE * @param_indent, param_name)
267
+ end
268
+
269
+ if param.permit?
270
+ desc << format(" (permitted: %s)", param.permit.join(", "))
271
+ end
272
+
273
+ if (default = format_default(param))
274
+ desc << default
275
+ end
276
+
277
+ line << wrap(desc.join, indent: indent, width: width)
278
+ line.join
279
+ end
280
+
281
+ # Format multiline description
282
+ #
283
+ # @api private
284
+ def format_description
285
+ format_multiline(@usage.desc, @indent)
286
+ end
287
+
288
+ # Returns all the options formatted to fit 80 columns
289
+ #
290
+ # @return [String]
291
+ #
292
+ # @api private
293
+ def format_options
294
+ return "" if @parameters.options.empty?
295
+
296
+ longest_option = @parameters.options.map(&:long)
297
+ .compact.max_by(&:length).length
298
+ any_short = @parameters.options.map(&:short).compact.any?
299
+ ordered_options = @order.(@parameters.options)
300
+
301
+ ordered_options.reduce([]) do |acc, option|
302
+ next acc if option.hidden?
303
+ acc << format_option(option, longest_option, any_short)
304
+ end.join(NEWLINE)
305
+ end
306
+
307
+ # Format an option
308
+ #
309
+ # @api private
310
+ def format_option(option, longest_length, any_short)
311
+ line = [@space_indent]
312
+ desc = []
313
+ indent = @indent
314
+
315
+ if any_short
316
+ short_option = option.short? ? option.short_name : SPACE
317
+ line << format("%#{SHORT_OPT_LENGTH}s", short_option)
318
+ indent += SHORT_OPT_LENGTH
319
+ end
320
+
321
+ # short & long option separator
322
+ line << ((option.short? && option.long?) ? ", " : " ")
323
+ indent += 2
324
+
325
+ if option.long?
326
+ if option.desc?
327
+ line << format("%-#{longest_length}s", option.long)
328
+ else
329
+ line << option.long
330
+ end
331
+ else
332
+ line << format("%-#{longest_length}s", SPACE)
333
+ end
334
+ indent += longest_length
335
+
336
+ if option.desc?
337
+ desc << " #{option.desc}"
338
+ end
339
+ indent += 2
340
+
341
+ if option.permit?
342
+ desc << format(" (permitted: %s)", option.permit.join(","))
343
+ end
344
+
345
+ if (default = format_default(option))
346
+ desc << default
347
+ end
348
+
349
+ line << wrap(desc.join, indent: indent, width: width)
350
+
351
+ line.join
352
+ end
353
+
354
+ # Format default value
355
+ #
356
+ # @api private
357
+ def format_default(param)
358
+ return if !param.default? || [true, false].include?(param.default)
359
+
360
+ if param.default.is_a?(String)
361
+ format(" (default %p)", param.default)
362
+ else
363
+ format(" (default %s)", param.default)
364
+ end
365
+ end
366
+
367
+ # Format examples section
368
+ #
369
+ # @api private
370
+ def format_examples
371
+ format_multiline(@usage.example, @param_indent)
372
+ end
373
+
374
+ # Format multiline content
375
+ #
376
+ # @api private
377
+ def format_multiline(lines, indent)
378
+ last_index = lines.size - 1
379
+ lines.map.with_index do |line, i|
380
+ line.map do |part|
381
+ part.split(NEWLINE).map do |p|
382
+ wrap(p, indent: indent, width: width, indent_first: true)
383
+ end.join(NEWLINE)
384
+ end.join(NEWLINE) + (last_index != i ? NEWLINE : "")
385
+ end.join(NEWLINE)
386
+ end
387
+ end # Formatter
388
+ end # Option
389
+ end # TTY