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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +1653 -1
- data/lib/tty/option.rb +63 -4
- data/lib/tty/option/aggregate_errors.rb +95 -0
- data/lib/tty/option/conversions.rb +126 -0
- data/lib/tty/option/converter.rb +63 -0
- data/lib/tty/option/deep_dup.rb +48 -0
- data/lib/tty/option/dsl.rb +105 -0
- data/lib/tty/option/dsl/arity.rb +49 -0
- data/lib/tty/option/dsl/conversion.rb +17 -0
- data/lib/tty/option/error_aggregator.rb +35 -0
- data/lib/tty/option/errors.rb +144 -0
- data/lib/tty/option/formatter.rb +389 -0
- data/lib/tty/option/inflection.rb +50 -0
- data/lib/tty/option/param_conversion.rb +34 -0
- data/lib/tty/option/param_permitted.rb +30 -0
- data/lib/tty/option/param_validation.rb +48 -0
- data/lib/tty/option/parameter.rb +310 -0
- data/lib/tty/option/parameter/argument.rb +18 -0
- data/lib/tty/option/parameter/environment.rb +20 -0
- data/lib/tty/option/parameter/keyword.rb +15 -0
- data/lib/tty/option/parameter/option.rb +99 -0
- data/lib/tty/option/parameters.rb +157 -0
- data/lib/tty/option/params.rb +122 -0
- data/lib/tty/option/parser.rb +57 -3
- data/lib/tty/option/parser/arguments.rb +166 -0
- data/lib/tty/option/parser/arity_check.rb +34 -0
- data/lib/tty/option/parser/environments.rb +169 -0
- data/lib/tty/option/parser/keywords.rb +158 -0
- data/lib/tty/option/parser/options.rb +273 -0
- data/lib/tty/option/parser/param_types.rb +51 -0
- data/lib/tty/option/parser/required_check.rb +36 -0
- data/lib/tty/option/pipeline.rb +38 -0
- data/lib/tty/option/result.rb +46 -0
- data/lib/tty/option/section.rb +26 -0
- data/lib/tty/option/sections.rb +56 -0
- data/lib/tty/option/usage.rb +166 -0
- data/lib/tty/option/usage_wrapper.rb +58 -0
- data/lib/tty/option/version.rb +3 -3
- 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
|