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,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../error_aggregator"
4
+ require_relative "../pipeline"
5
+ require_relative "param_types"
6
+ require_relative "required_check"
7
+
8
+ module TTY
9
+ module Option
10
+ class Parser
11
+ class Arguments
12
+ include ParamTypes
13
+
14
+ # Create a command line arguments parser
15
+ #
16
+ # @param [Array<Argument>] arguments
17
+ # the list of arguments
18
+ # @param [Hash] config
19
+ # the configuration settings
20
+ #
21
+ # @api public
22
+ def initialize(arguments, check_invalid_params: true,
23
+ raise_on_parse_error: false)
24
+ @arguments = arguments
25
+ @error_aggregator =
26
+ ErrorAggregator.new(raise_on_parse_error: raise_on_parse_error)
27
+ @required_check = RequiredCheck.new(@error_aggregator)
28
+ @pipeline = Pipeline.new(@error_aggregator)
29
+ @parsed = {}
30
+ @remaining = []
31
+
32
+ @defaults = {}
33
+ @arguments.each do |arg|
34
+ if arg.default?
35
+ case arg.default
36
+ when Proc
37
+ @defaults[arg.key] = arg.default.()
38
+ else
39
+ @defaults[arg.key] = arg.default
40
+ end
41
+ elsif arg.required?
42
+ @required_check << arg
43
+ end
44
+ end
45
+ end
46
+
47
+ # Read positional arguments from the command line
48
+ #
49
+ # @param [Array<String>] argv
50
+ #
51
+ # @return [Array<Hash, Array, Hash>]
52
+ # a list of parsed and unparsed arguments and errors
53
+ #
54
+ # @api private
55
+ def parse(argv)
56
+ @argv = argv.dup
57
+
58
+ @arguments.each do |arg|
59
+ values = next_argument(arg)
60
+ @required_check.delete(arg) unless values.empty?
61
+
62
+ assign_argument(arg, values)
63
+ end
64
+
65
+ while (val = @argv.shift)
66
+ @remaining << val
67
+ end
68
+
69
+ @required_check.()
70
+
71
+ [@parsed, @remaining, @error_aggregator.errors]
72
+ end
73
+
74
+ private
75
+
76
+ # @api private
77
+ def next_argument(arg)
78
+ if arg.arity >= 0
79
+ process_exact_arity(arg)
80
+ else
81
+ process_infinite_arity(arg)
82
+ end
83
+ end
84
+
85
+ def process_exact_arity(arg)
86
+ values = []
87
+ arity = arg.arity
88
+
89
+ while arity > 0
90
+ break if @argv.empty?
91
+ value = @argv.shift
92
+ if argument?(value)
93
+ values << value
94
+ arity -= 1
95
+ else
96
+ @remaining << value
97
+ end
98
+ end
99
+
100
+ if 0 < values.size && values.size < arg.arity &&
101
+ Array(@defaults[arg.key]).size < arg.arity
102
+ @error_aggregator.(InvalidArity.new(arg, values.size))
103
+ end
104
+
105
+ values
106
+ end
107
+
108
+ def process_infinite_arity(arg)
109
+ values = []
110
+ arity = arg.arity.abs - 1
111
+
112
+ arity.times do |i|
113
+ break if @argv.empty?
114
+ value = @argv.shift
115
+ if argument?(value)
116
+ values << value
117
+ else
118
+ @remaining << value
119
+ end
120
+ end
121
+
122
+ # consume remaining
123
+ while (value = @argv.shift)
124
+ if argument?(value)
125
+ values << value
126
+ else
127
+ @remaining << value
128
+ end
129
+ end
130
+
131
+ if values.size < arity && Array(@defaults[arg.key]).size < arity
132
+ @error_aggregator.(InvalidArity.new(arg, values.size))
133
+ end
134
+
135
+ values
136
+ end
137
+
138
+ # Assign argument to the parsed
139
+ #
140
+ # @param [Argument] arg
141
+ # @param [Array] values
142
+ #
143
+ # @api private
144
+ def assign_argument(arg, values)
145
+ val = case values.size
146
+ when 0
147
+ if arg.default?
148
+ case arg.default
149
+ when Proc
150
+ @defaults[arg.key]
151
+ else
152
+ @defaults[arg.key]
153
+ end
154
+ end
155
+ when 1
156
+ values.first
157
+ else
158
+ values
159
+ end
160
+
161
+ @parsed[arg.key] = @pipeline.(arg, val)
162
+ end
163
+ end # Arguments
164
+ end # Parser
165
+ end # Option
166
+ end # TTY
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ module Option
5
+ class Parser
6
+ class ArityCheck
7
+ def initialize(error_aggregator)
8
+ @multiplies = []
9
+ @error_aggregator = error_aggregator
10
+ end
11
+
12
+ def add(param)
13
+ @multiplies << param
14
+ end
15
+ alias :<< :add
16
+
17
+ # Check if parameter matches arity
18
+ #
19
+ # @raise [InvalidArity]
20
+ #
21
+ # @api private
22
+ def call(arities)
23
+ @multiplies.each do |param|
24
+ arity = arities[param.key]
25
+
26
+ if arity < param.min_arity
27
+ @error_aggregator.(InvalidArity.new(param, arity))
28
+ end
29
+ end
30
+ end
31
+ end # ArityCheck
32
+ end # Parser
33
+ end # Option
34
+ end # TTY
@@ -0,0 +1,169 @@
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 Environments
13
+ include ParamTypes
14
+
15
+ ENV_VAR_RE = /([\p{Lu}_\-\d]+)=([^=]+)/.freeze
16
+
17
+ # Create a command line env variables parser
18
+ #
19
+ # @param [Array<Environment>] environments
20
+ # the list of environment variables
21
+ # @param [Hash] config
22
+ # the configuration settings
23
+ #
24
+ # @api public
25
+ def initialize(environments, check_invalid_params: true,
26
+ raise_on_parse_error: false)
27
+ @environments = environments
28
+ @check_invalid_params = check_invalid_params
29
+ @error_aggregator =
30
+ ErrorAggregator.new(raise_on_parse_error: raise_on_parse_error)
31
+ @required_check = RequiredCheck.new(@error_aggregator)
32
+ @arity_check = ArityCheck.new(@error_aggregator)
33
+ @pipeline = Pipeline.new(@error_aggregator)
34
+ @parsed = {}
35
+ @remaining = []
36
+ @names = {}
37
+ @arities = Hash.new(0)
38
+
39
+ @environments.each do |env_arg|
40
+ @names[env_arg.name] = env_arg
41
+ @arity_check << env_arg if env_arg.multiple?
42
+
43
+ if env_arg.default?
44
+ case env_arg.default
45
+ when Proc
46
+ assign_envvar(env_arg, env_arg.default.())
47
+ else
48
+ assign_envvar(env_arg, env_arg.default)
49
+ end
50
+ elsif env_arg.required?
51
+ @required_check << env_arg
52
+ end
53
+ end
54
+ end
55
+
56
+ # Read environment variable(s) from command line or ENV hash
57
+ #
58
+ # @param [Array<String>] argv
59
+ # @param [Hash<String,Object>] env
60
+ #
61
+ # @api public
62
+ def parse(argv, env)
63
+ @argv = argv.dup
64
+ @env = env
65
+
66
+ loop do
67
+ env_var, value = next_envvar
68
+ if !env_var.nil?
69
+ @required_check.delete(env_var)
70
+ @arities[env_var.key] += 1
71
+
72
+ if block_given?
73
+ yield(env_var, value)
74
+ end
75
+ assign_envvar(env_var, value)
76
+ end
77
+ break if @argv.empty?
78
+ end
79
+
80
+ @environments.each do |env_arg|
81
+ if (value = env[env_arg.name])
82
+ @required_check.delete(env_arg)
83
+ @arities[env_arg.key] += 1
84
+ assign_envvar(env_arg, value)
85
+ end
86
+ end
87
+
88
+ @arity_check.(@arities)
89
+ @required_check.()
90
+
91
+ [@parsed, @remaining, @error_aggregator.errors]
92
+ end
93
+
94
+ private
95
+
96
+ def next_envvar
97
+ env_var, value = nil, nil
98
+
99
+ while !@argv.empty? && !env_var?(@argv.first)
100
+ @remaining << @argv.shift
101
+ end
102
+
103
+ if @argv.empty?
104
+ return
105
+ else
106
+ environment = @argv.shift
107
+ end
108
+
109
+ if (match = environment.match(ENV_VAR_RE))
110
+ _, name, val = *match.to_a
111
+
112
+ if (env_var = @names[name])
113
+ if env_var.multi_argument? &&
114
+ !(consumed = consume_arguments).empty?
115
+ value = [val] + consumed
116
+ else
117
+ value = val
118
+ end
119
+ elsif @check_invalid_params
120
+ @error_aggregator.(InvalidParameter.new("invalid environment #{match}"))
121
+ else
122
+ @remaining << match.to_s
123
+ end
124
+ end
125
+
126
+ [env_var, value]
127
+ end
128
+
129
+ # Consume multi argument
130
+ #
131
+ # @api private
132
+ def consume_arguments(values: [])
133
+ while (value = @argv.first) &&
134
+ !option?(value) && !keyword?(value) && !env_var?(value)
135
+
136
+ val = @argv.shift
137
+ parts = val.include?("&") ? val.split(/&/) : [val]
138
+ parts.each { |part| values << part }
139
+ end
140
+
141
+ values
142
+ end
143
+
144
+ # @api private
145
+ def assign_envvar(env_arg, val)
146
+ value = @pipeline.(env_arg, val)
147
+
148
+ if env_arg.multiple?
149
+ allowed = env_arg.arity < 0 || @arities[env_arg.key] <= env_arg.arity
150
+ if allowed
151
+ case value
152
+ when Hash
153
+ (@parsed[env_arg.key] ||= {}).merge!(value)
154
+ else
155
+ Array(value).each do |v|
156
+ (@parsed[env_arg.key] ||= []) << v
157
+ end
158
+ end
159
+ else
160
+ @remaining << "#{env_arg.name}=#{value}"
161
+ end
162
+ else
163
+ @parsed[env_arg.key] = value
164
+ end
165
+ end
166
+ end # Environments
167
+ end # Parser
168
+ end # Option
169
+ end # TTY
@@ -0,0 +1,158 @@
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 Keywords
13
+ include ParamTypes
14
+
15
+ KEYWORD_ARG_RE = /([^=-].*?)=([^=]+)/.freeze
16
+
17
+ # Create a command line keywords parser
18
+ #
19
+ # @param [Array<Keyword>] keywords
20
+ # the list of keywords
21
+ # @param [Hash] config
22
+ # the configuration settings
23
+ #
24
+ # @api public
25
+ def initialize(keywords, check_invalid_params: true,
26
+ raise_on_parse_error: false)
27
+ @keywords = keywords
28
+ @check_invalid_params = check_invalid_params
29
+ @error_aggregator =
30
+ ErrorAggregator.new(raise_on_parse_error: raise_on_parse_error)
31
+ @required_check = RequiredCheck.new(@error_aggregator)
32
+ @arity_check = ArityCheck.new(@error_aggregator)
33
+ @pipeline = Pipeline.new(@error_aggregator)
34
+ @parsed = {}
35
+ @remaining = []
36
+ @names = {}
37
+ @arities = Hash.new(0)
38
+
39
+ @keywords.each do |kwarg|
40
+ @names[kwarg.name] = kwarg
41
+ @arity_check << kwarg if kwarg.multiple?
42
+
43
+ if kwarg.default?
44
+ case kwarg.default
45
+ when Proc
46
+ assign_keyword(kwarg, kwarg.default.())
47
+ else
48
+ assign_keyword(kwarg, kwarg.default)
49
+ end
50
+ elsif kwarg.required?
51
+ @required_check << kwarg
52
+ end
53
+ end
54
+ end
55
+
56
+ # Read keyword arguments from the command line
57
+ #
58
+ # @api public
59
+ def parse(argv)
60
+ @argv = argv.dup
61
+
62
+ loop do
63
+ kwarg, value = next_keyword
64
+ if !kwarg.nil?
65
+ @required_check.delete(kwarg)
66
+ @arities[kwarg.key] += 1
67
+
68
+ if block_given?
69
+ yield(kwarg, value)
70
+ end
71
+ assign_keyword(kwarg, value)
72
+ end
73
+ break if @argv.empty?
74
+ end
75
+
76
+ @arity_check.(@arities)
77
+ @required_check.()
78
+
79
+ [@parsed, @remaining, @error_aggregator.errors]
80
+ end
81
+
82
+ private
83
+
84
+ # Get next keyword
85
+ #
86
+ # @api private
87
+ def next_keyword
88
+ kwarg, value = nil, nil
89
+
90
+ while !@argv.empty? && !keyword?(@argv.first)
91
+ @remaining << @argv.shift
92
+ end
93
+
94
+ if @argv.empty?
95
+ return
96
+ else
97
+ keyword = @argv.shift
98
+ end
99
+
100
+ if (match = keyword.match(KEYWORD_ARG_RE))
101
+ _, name, val = *match.to_a
102
+
103
+ if (kwarg = @names[name])
104
+ if kwarg.multi_argument? &&
105
+ !(consumed = consume_arguments).empty?
106
+ value = [val] + consumed
107
+ else
108
+ value = val
109
+ end
110
+ elsif @check_invalid_params
111
+ @error_aggregator.(InvalidParameter.new("invalid keyword #{match}"))
112
+ else
113
+ @remaining << match.to_s
114
+ end
115
+ end
116
+
117
+ [kwarg, value]
118
+ end
119
+
120
+ # Consume multi argument
121
+ #
122
+ # @api private
123
+ def consume_arguments(values: [])
124
+ while (value = @argv.first) && !option?(value) && !keyword?(value)
125
+ val = @argv.shift
126
+ parts = val.include?("&") ? val.split(/&/) : [val]
127
+ parts.each { |part| values << part }
128
+ end
129
+
130
+ values
131
+ end
132
+
133
+ # @api private
134
+ def assign_keyword(kwarg, val)
135
+ value = @pipeline.(kwarg, val)
136
+
137
+ if kwarg.multiple?
138
+ allowed = kwarg.arity < 0 || @arities[kwarg.key] <= kwarg.arity
139
+ if allowed
140
+ case value
141
+ when Hash
142
+ (@parsed[kwarg.key] ||= {}).merge!(value)
143
+ else
144
+ Array(value).each do |v|
145
+ (@parsed[kwarg.key] ||= []) << v
146
+ end
147
+ end
148
+ else
149
+ @remaining << "#{kwarg.name}=#{value}"
150
+ end
151
+ else
152
+ @parsed[kwarg.key] = value
153
+ end
154
+ end
155
+ end # Keywords
156
+ end # Parser
157
+ end # Option
158
+ end # TTY