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