travis-cl 1.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +134 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +59 -0
- data/MIT_LICENSE.md +21 -0
- data/README.md +1283 -0
- data/cl.gemspec +30 -0
- data/examples/README.md +22 -0
- data/examples/_src/args/cast.erb.rb +100 -0
- data/examples/_src/args/opts.erb.rb +100 -0
- data/examples/_src/args/required.erb.rb +63 -0
- data/examples/_src/args/splat.erb.rb +55 -0
- data/examples/_src/gem.erb.rb +99 -0
- data/examples/_src/heroku.erb.rb +47 -0
- data/examples/_src/rakeish.erb.rb +54 -0
- data/examples/_src/readme/abstract.erb.rb +27 -0
- data/examples/_src/readme/alias.erb.rb +22 -0
- data/examples/_src/readme/arg.erb.rb +21 -0
- data/examples/_src/readme/arg_array.erb.rb +21 -0
- data/examples/_src/readme/arg_type.erb.rb +23 -0
- data/examples/_src/readme/args_splat.erb.rb +55 -0
- data/examples/_src/readme/array.erb.rb +21 -0
- data/examples/_src/readme/basic.erb.rb +72 -0
- data/examples/_src/readme/default.erb.rb +21 -0
- data/examples/_src/readme/deprecated.erb.rb +21 -0
- data/examples/_src/readme/deprecated_alias.erb.rb +21 -0
- data/examples/_src/readme/description.erb.rb +60 -0
- data/examples/_src/readme/downcase.erb.rb +21 -0
- data/examples/_src/readme/enum.erb.rb +35 -0
- data/examples/_src/readme/example.erb.rb +25 -0
- data/examples/_src/readme/format.erb.rb +35 -0
- data/examples/_src/readme/internal.erb.rb +28 -0
- data/examples/_src/readme/negate.erb.rb +37 -0
- data/examples/_src/readme/note.erb.rb +25 -0
- data/examples/_src/readme/opts.erb.rb +33 -0
- data/examples/_src/readme/opts_block.erb.rb +30 -0
- data/examples/_src/readme/range.erb.rb +35 -0
- data/examples/_src/readme/registry.erb.rb +18 -0
- data/examples/_src/readme/required.erb.rb +35 -0
- data/examples/_src/readme/requireds.erb.rb +46 -0
- data/examples/_src/readme/requires.erb.rb +35 -0
- data/examples/_src/readme/runner.erb.rb +29 -0
- data/examples/_src/readme/runner_custom.erb.rb +25 -0
- data/examples/_src/readme/secret.erb.rb +22 -0
- data/examples/_src/readme/see.erb.rb +25 -0
- data/examples/_src/readme/type.erb.rb +21 -0
- data/examples/args/cast +98 -0
- data/examples/args/opts +98 -0
- data/examples/args/required +62 -0
- data/examples/args/splat +58 -0
- data/examples/gem +97 -0
- data/examples/heroku +48 -0
- data/examples/rakeish +50 -0
- data/examples/readme/abstract +28 -0
- data/examples/readme/alias +21 -0
- data/examples/readme/arg +20 -0
- data/examples/readme/arg_array +20 -0
- data/examples/readme/arg_type +22 -0
- data/examples/readme/args_splat +58 -0
- data/examples/readme/array +20 -0
- data/examples/readme/basic +67 -0
- data/examples/readme/default +20 -0
- data/examples/readme/deprecated +20 -0
- data/examples/readme/deprecated_alias +20 -0
- data/examples/readme/description +56 -0
- data/examples/readme/downcase +20 -0
- data/examples/readme/enum +33 -0
- data/examples/readme/example +21 -0
- data/examples/readme/format +33 -0
- data/examples/readme/internal +24 -0
- data/examples/readme/negate +44 -0
- data/examples/readme/note +21 -0
- data/examples/readme/opts +31 -0
- data/examples/readme/opts_block +29 -0
- data/examples/readme/range +33 -0
- data/examples/readme/registry +15 -0
- data/examples/readme/required +33 -0
- data/examples/readme/requireds +46 -0
- data/examples/readme/requires +33 -0
- data/examples/readme/runner +30 -0
- data/examples/readme/runner_custom +22 -0
- data/examples/readme/secret +21 -0
- data/examples/readme/see +21 -0
- data/examples/readme/type +20 -0
- data/lib/cl/arg.rb +79 -0
- data/lib/cl/args.rb +92 -0
- data/lib/cl/cast.rb +55 -0
- data/lib/cl/cmd.rb +74 -0
- data/lib/cl/config/env.rb +52 -0
- data/lib/cl/config/files.rb +34 -0
- data/lib/cl/config.rb +30 -0
- data/lib/cl/ctx.rb +36 -0
- data/lib/cl/dsl.rb +182 -0
- data/lib/cl/errors.rb +119 -0
- data/lib/cl/help/cmd.rb +118 -0
- data/lib/cl/help/cmds.rb +26 -0
- data/lib/cl/help/format.rb +69 -0
- data/lib/cl/help/table.rb +58 -0
- data/lib/cl/help/usage.rb +26 -0
- data/lib/cl/help.rb +37 -0
- data/lib/cl/helper/suggest.rb +10 -0
- data/lib/cl/helper.rb +47 -0
- data/lib/cl/opt.rb +276 -0
- data/lib/cl/opts/validate.rb +117 -0
- data/lib/cl/opts.rb +114 -0
- data/lib/cl/parser/format.rb +63 -0
- data/lib/cl/parser.rb +70 -0
- data/lib/cl/runner/default.rb +86 -0
- data/lib/cl/runner/multi.rb +34 -0
- data/lib/cl/runner.rb +10 -0
- data/lib/cl/ui.rb +146 -0
- data/lib/cl/version.rb +3 -0
- data/lib/cl.rb +62 -0
- metadata +177 -0
data/lib/cl/opt.rb
ADDED
@@ -0,0 +1,276 @@
|
|
1
|
+
require 'cl/cast'
|
2
|
+
require 'cl/errors'
|
3
|
+
|
4
|
+
class Cl
|
5
|
+
class Opt < Struct.new(:strs, :opts, :block)
|
6
|
+
include Cast, Regex
|
7
|
+
|
8
|
+
OPTS = %i(
|
9
|
+
alias default deprecated description downcase eg enum example format
|
10
|
+
internal max min negate note required requires secret see sep type upcase
|
11
|
+
)
|
12
|
+
|
13
|
+
OPT = /^--(?:\[.*\])?(.*)$/
|
14
|
+
|
15
|
+
TYPES = {
|
16
|
+
int: :integer,
|
17
|
+
str: :string,
|
18
|
+
bool: :flag,
|
19
|
+
boolean: :flag
|
20
|
+
}
|
21
|
+
|
22
|
+
attr_reader :short, :long
|
23
|
+
|
24
|
+
def initialize(strs, *)
|
25
|
+
super
|
26
|
+
@short, @long = Validator.new(strs, opts).apply
|
27
|
+
end
|
28
|
+
|
29
|
+
def define(const)
|
30
|
+
return unless __key__ = name
|
31
|
+
const.send :include, Module.new {
|
32
|
+
define_method (__key__) { opts[__key__] }
|
33
|
+
define_method (:"#{__key__}?") { !!opts[__key__] }
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def name
|
38
|
+
return @name if instance_variable_defined?(:@name)
|
39
|
+
name = long.split(' ').first.match(OPT)[1] if long
|
40
|
+
@name = name.sub('-', '_').to_sym if name
|
41
|
+
end
|
42
|
+
|
43
|
+
def type
|
44
|
+
@type ||= TYPES[opts[:type]] || opts[:type] || infer_type
|
45
|
+
end
|
46
|
+
|
47
|
+
def infer_type
|
48
|
+
strs.any? { |str| str.split(' ').size > 1 } ? :string : :flag
|
49
|
+
end
|
50
|
+
|
51
|
+
def help?
|
52
|
+
name == :help
|
53
|
+
end
|
54
|
+
|
55
|
+
def flag?
|
56
|
+
type == :flag
|
57
|
+
end
|
58
|
+
|
59
|
+
def int?
|
60
|
+
type == :int || type == :integer
|
61
|
+
end
|
62
|
+
|
63
|
+
def array?
|
64
|
+
type == :array
|
65
|
+
end
|
66
|
+
|
67
|
+
def aliases?
|
68
|
+
!!opts[:alias]
|
69
|
+
end
|
70
|
+
|
71
|
+
def aliases
|
72
|
+
Array(opts[:alias])
|
73
|
+
end
|
74
|
+
|
75
|
+
def description
|
76
|
+
opts[:description]
|
77
|
+
end
|
78
|
+
|
79
|
+
def deprecated?(name = nil)
|
80
|
+
return !!opts[:deprecated] if name.nil?
|
81
|
+
names = [name.to_s.gsub('_', '-').to_sym, name.to_s.gsub('-', '_').to_sym]
|
82
|
+
deprecated? && names.include?(deprecated.first)
|
83
|
+
end
|
84
|
+
|
85
|
+
def deprecated
|
86
|
+
# If it's a string then it's a deprecation message and the option itself
|
87
|
+
# is considered deprecated. If it's a symbol it refers to a deprecated
|
88
|
+
# alias, and the option's name is the deprecation message.
|
89
|
+
return [name, opts[:deprecated]] unless opts[:deprecated].is_a?(Symbol)
|
90
|
+
opts[:deprecated] ? [opts[:deprecated], name] : []
|
91
|
+
end
|
92
|
+
|
93
|
+
def downcase?
|
94
|
+
!!opts[:downcase]
|
95
|
+
end
|
96
|
+
|
97
|
+
def default?
|
98
|
+
opts.key?(:default)
|
99
|
+
end
|
100
|
+
|
101
|
+
def default
|
102
|
+
opts[:default]
|
103
|
+
end
|
104
|
+
|
105
|
+
def enum?
|
106
|
+
!!opts[:enum]
|
107
|
+
end
|
108
|
+
|
109
|
+
def enum
|
110
|
+
Array(opts[:enum])
|
111
|
+
end
|
112
|
+
|
113
|
+
def known?(value)
|
114
|
+
return value.all? { |value| known?(value) } if value.is_a?(Array)
|
115
|
+
enum.any? { |obj| obj.is_a?(Regexp) ? obj =~ value.to_s : obj == value }
|
116
|
+
end
|
117
|
+
|
118
|
+
def unknown(value)
|
119
|
+
return value.reject { |value| known?(value) } if value.is_a?(Array)
|
120
|
+
known?(value) ? [] : Array(value)
|
121
|
+
end
|
122
|
+
|
123
|
+
def example?
|
124
|
+
!!opts[:example]
|
125
|
+
end
|
126
|
+
|
127
|
+
def example
|
128
|
+
opts[:example]
|
129
|
+
end
|
130
|
+
|
131
|
+
def format?
|
132
|
+
!!opts[:format]
|
133
|
+
end
|
134
|
+
|
135
|
+
def format
|
136
|
+
format_regex(opts[:format])
|
137
|
+
end
|
138
|
+
|
139
|
+
def formatted?(value)
|
140
|
+
return value.all? { |value| formatted?(value) } if value.is_a?(Array)
|
141
|
+
opts[:format] =~ value
|
142
|
+
end
|
143
|
+
|
144
|
+
def internal?
|
145
|
+
!!opts[:internal]
|
146
|
+
end
|
147
|
+
|
148
|
+
def min?
|
149
|
+
int? && !!opts[:min]
|
150
|
+
end
|
151
|
+
|
152
|
+
def min
|
153
|
+
opts[:min]
|
154
|
+
end
|
155
|
+
|
156
|
+
def max?
|
157
|
+
int? && !!opts[:max]
|
158
|
+
end
|
159
|
+
|
160
|
+
def max
|
161
|
+
opts[:max]
|
162
|
+
end
|
163
|
+
|
164
|
+
def negate?
|
165
|
+
!!negate
|
166
|
+
end
|
167
|
+
|
168
|
+
def negate
|
169
|
+
['no'] + Array(opts[:negate]) if flag?
|
170
|
+
end
|
171
|
+
|
172
|
+
def note?
|
173
|
+
!!opts[:note]
|
174
|
+
end
|
175
|
+
|
176
|
+
def note
|
177
|
+
opts[:note]
|
178
|
+
end
|
179
|
+
|
180
|
+
def required?
|
181
|
+
!!opts[:required]
|
182
|
+
end
|
183
|
+
|
184
|
+
def requires?
|
185
|
+
!!opts[:requires]
|
186
|
+
end
|
187
|
+
|
188
|
+
def requires
|
189
|
+
Array(opts[:requires])
|
190
|
+
end
|
191
|
+
|
192
|
+
def secret?
|
193
|
+
!!opts[:secret]
|
194
|
+
end
|
195
|
+
|
196
|
+
def see?
|
197
|
+
!!opts[:see]
|
198
|
+
end
|
199
|
+
|
200
|
+
def see
|
201
|
+
opts[:see]
|
202
|
+
end
|
203
|
+
|
204
|
+
def separator
|
205
|
+
opts[:sep]
|
206
|
+
end
|
207
|
+
|
208
|
+
def upcase?
|
209
|
+
!!opts[:upcase]
|
210
|
+
end
|
211
|
+
|
212
|
+
def block
|
213
|
+
# raise if no block was given, and the option's name cannot be inferred
|
214
|
+
super || method(:assign)
|
215
|
+
end
|
216
|
+
|
217
|
+
def assign(opts, type, _, value)
|
218
|
+
[name, *aliases].each do |name|
|
219
|
+
if array?
|
220
|
+
opts[name] ||= []
|
221
|
+
opts[name] << value
|
222
|
+
else
|
223
|
+
opts[name] = value
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def long?(str)
|
229
|
+
str.start_with?('--')
|
230
|
+
end
|
231
|
+
|
232
|
+
class Validator < Struct.new(:strs, :opts)
|
233
|
+
SHORT = /^-\w( \w+)?$/
|
234
|
+
LONG = /^--[\w\-\[\]]+( \[?\w+\]?)?$/
|
235
|
+
|
236
|
+
MSGS = {
|
237
|
+
missing_strs: 'No option strings given. Pass one short -s and/or one --long option string.',
|
238
|
+
wrong_strs: 'Wrong option strings given. Pass one short -s and/or one --long option string.',
|
239
|
+
invalid_strs: 'Invalid option strings given: %p',
|
240
|
+
unknown_opts: 'Unknown options: %s'
|
241
|
+
}
|
242
|
+
|
243
|
+
def apply
|
244
|
+
error :missing_strs if strs.empty?
|
245
|
+
error :wrong_strs if short.size > 1 || long.size > 1
|
246
|
+
error :invalid_strs, invalid unless invalid.empty?
|
247
|
+
error :unknown_opts, unknown.map(&:inspect).join(', ') unless unknown.empty?
|
248
|
+
[short.first, long.first]
|
249
|
+
end
|
250
|
+
|
251
|
+
def unknown
|
252
|
+
@unknown ||= opts.keys - Opt::OPTS
|
253
|
+
end
|
254
|
+
|
255
|
+
def invalid
|
256
|
+
@invalid ||= strs.-(valid).join(', ')
|
257
|
+
end
|
258
|
+
|
259
|
+
def valid
|
260
|
+
strs.grep(Regexp.union(SHORT, LONG))
|
261
|
+
end
|
262
|
+
|
263
|
+
def short
|
264
|
+
strs.grep(SHORT)
|
265
|
+
end
|
266
|
+
|
267
|
+
def long
|
268
|
+
strs.grep(LONG)
|
269
|
+
end
|
270
|
+
|
271
|
+
def error(key, *args)
|
272
|
+
raise Cl::Error, MSGS[key] % args
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'cl/helper'
|
2
|
+
|
3
|
+
class Cl
|
4
|
+
class Opts
|
5
|
+
module Validate
|
6
|
+
def validate(cmd, opts, values, orig)
|
7
|
+
Validate.constants.each do |name|
|
8
|
+
next if name == :Validator
|
9
|
+
const = Validate.const_get(name)
|
10
|
+
const.new(cmd, opts, values, orig).apply
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Validator < Struct.new(:cmd, :opts, :values, :orig)
|
15
|
+
include Regex
|
16
|
+
def compact(hash, *keys)
|
17
|
+
hash.reject { |_, value| value.nil? }.to_h
|
18
|
+
end
|
19
|
+
|
20
|
+
def invert(hash)
|
21
|
+
hash.map { |key, obj| Array(obj).map { |obj| [obj, key] } }.flatten(1).to_h
|
22
|
+
end
|
23
|
+
|
24
|
+
def only(hash, *keys)
|
25
|
+
hash.select { |key, _| keys.include?(key) }.to_h
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Required < Validator
|
30
|
+
def apply
|
31
|
+
# make sure we do not accept unnamed required options
|
32
|
+
raise RequiredOpts.new(missing.map(&:name)) if missing.any?
|
33
|
+
end
|
34
|
+
|
35
|
+
def missing
|
36
|
+
@missing ||= opts.select(&:required?).select { |opt| !values.key?(opt.name) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Requireds < Validator
|
41
|
+
def apply
|
42
|
+
raise RequiredsOpts.new(missing) if missing.any?
|
43
|
+
end
|
44
|
+
|
45
|
+
def missing
|
46
|
+
@missing ||= cmd.class.required.map do |alts|
|
47
|
+
alts if alts.none? { |alt| Array(alt).all? { |key| values.key?(key) } }
|
48
|
+
end.compact
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Requires < Validator
|
53
|
+
def apply
|
54
|
+
raise RequiresOpts.new(invert(missing)) if missing.any?
|
55
|
+
end
|
56
|
+
|
57
|
+
def missing
|
58
|
+
@missing ||= requires.map do |opt|
|
59
|
+
missing = opt.requires.select { |key| !values.key?(key) }
|
60
|
+
[opt.name, missing] if missing.any?
|
61
|
+
end.compact
|
62
|
+
end
|
63
|
+
|
64
|
+
def requires
|
65
|
+
opts.select(&:requires?).select { |opt| orig.key?(opt.name) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Format < Validator
|
70
|
+
def apply
|
71
|
+
raise InvalidFormat.new(invalid) if invalid.any?
|
72
|
+
end
|
73
|
+
|
74
|
+
def invalid
|
75
|
+
@invalid ||= opts.select(&:format?).map do |opt|
|
76
|
+
value = values[opt.name]
|
77
|
+
[opt.name, opt.format] if value && !opt.formatted?(value)
|
78
|
+
end.compact
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class Enum < Validator
|
83
|
+
def apply
|
84
|
+
raise UnknownValues.new(unknown) if unknown.any?
|
85
|
+
end
|
86
|
+
|
87
|
+
def unknown
|
88
|
+
@unknown ||= opts.select(&:enum?).map do |opt|
|
89
|
+
unknown = opt.unknown(values[opt.name])
|
90
|
+
next if unknown.empty?
|
91
|
+
known = opt.enum.map { |str| format_regex(str) }
|
92
|
+
[opt.name, unknown, known]
|
93
|
+
end.compact
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Range < Validator
|
98
|
+
def apply
|
99
|
+
raise OutOfRange.new(invalid) if invalid.any?
|
100
|
+
end
|
101
|
+
|
102
|
+
def invalid
|
103
|
+
@invalid ||= opts.map do |opt|
|
104
|
+
next unless value = values[opt.name]
|
105
|
+
range = only(opt.opts, :min, :max)
|
106
|
+
[opt.name, compact(range)] if invalid?(range, value)
|
107
|
+
end.compact
|
108
|
+
end
|
109
|
+
|
110
|
+
def invalid?(range, value)
|
111
|
+
min, max = range.values_at(:min, :max)
|
112
|
+
min && value < min || max && value > max
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/cl/opts.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'cl/opt'
|
2
|
+
require 'cl/opts/validate'
|
3
|
+
|
4
|
+
class Cl
|
5
|
+
class Opts
|
6
|
+
include Enumerable, Validate
|
7
|
+
|
8
|
+
def define(const, *args, &block)
|
9
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
10
|
+
strs = args.select { |arg| arg.start_with?('-') }
|
11
|
+
opts[:description] = args.-(strs).first
|
12
|
+
|
13
|
+
opt = Opt.new(strs, opts, block)
|
14
|
+
opt.define(const)
|
15
|
+
insert(opt, const)
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply(cmd, opts)
|
19
|
+
return opts if opts[:help]
|
20
|
+
orig = opts.dup
|
21
|
+
opts = defaults(cmd, opts)
|
22
|
+
opts = downcase(opts)
|
23
|
+
opts = upcase(opts)
|
24
|
+
opts = cast(opts)
|
25
|
+
opts = taint(opts)
|
26
|
+
validate(cmd, self, opts, orig)
|
27
|
+
opts
|
28
|
+
end
|
29
|
+
|
30
|
+
def insert(opt, const)
|
31
|
+
delete(opt)
|
32
|
+
return opts << opt if const == Cmd
|
33
|
+
ix = opts.index(const.superclass.opts.first)
|
34
|
+
opts.empty? ? opts << opt : opts.insert(ix.to_i, opt)
|
35
|
+
end
|
36
|
+
|
37
|
+
def [](key)
|
38
|
+
opts.detect { |opt| opt.name == key }
|
39
|
+
end
|
40
|
+
|
41
|
+
def each(&block)
|
42
|
+
opts.each(&block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete(opt)
|
46
|
+
opts.delete(opts.detect { |o| o.strs == opt.strs })
|
47
|
+
end
|
48
|
+
|
49
|
+
def first
|
50
|
+
opts.first
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_a
|
54
|
+
opts
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_writer :opts
|
58
|
+
|
59
|
+
def opts
|
60
|
+
@opts ||= []
|
61
|
+
end
|
62
|
+
|
63
|
+
def deprecated
|
64
|
+
map(&:deprecated).flatten.compact
|
65
|
+
end
|
66
|
+
|
67
|
+
def ==(other)
|
68
|
+
strs == other.strs
|
69
|
+
end
|
70
|
+
|
71
|
+
def dup
|
72
|
+
super.tap { |obj| obj.opts = opts.dup }
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def defaults(cmd, opts)
|
78
|
+
select(&:default?).inject(opts) do |opts, opt|
|
79
|
+
next opts if opts.key?(opt.name)
|
80
|
+
value = opt.default
|
81
|
+
value = resolve(cmd, opts, value) if value.is_a?(Symbol)
|
82
|
+
opts.merge(opt.name => value)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def resolve(cmd, opts, key)
|
87
|
+
opts[key] || cmd.respond_to?(key) && cmd.send(key)
|
88
|
+
end
|
89
|
+
|
90
|
+
def downcase(opts)
|
91
|
+
select(&:downcase?).inject(opts) do |opts, opt|
|
92
|
+
next opts unless value = opts[opt.name]
|
93
|
+
opts.merge(opt.name => value.to_s.downcase)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def upcase(opts)
|
98
|
+
select(&:upcase?).inject(opts) do |opts, opt|
|
99
|
+
next opts unless value = opts[opt.name]
|
100
|
+
opts.merge(opt.name => value.to_s.upcase)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def cast(opts)
|
105
|
+
opts.map do |key, value|
|
106
|
+
[key, self[key] ? self[key].cast(value) : value]
|
107
|
+
end.to_h
|
108
|
+
end
|
109
|
+
|
110
|
+
def taint(opts)
|
111
|
+
opts.to_h
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class Cl
|
2
|
+
class Parser < OptionParser
|
3
|
+
class Format < Struct.new(:opt)
|
4
|
+
NAME = /^(--(?:\[no-\])?)([^= ]+)/
|
5
|
+
|
6
|
+
def strs
|
7
|
+
strs = opt.strs + aliases
|
8
|
+
strs.map { |str| long?(str) ? long(str) : short(str) }.flatten
|
9
|
+
end
|
10
|
+
|
11
|
+
def long(str)
|
12
|
+
strs = [unnegate(str)]
|
13
|
+
strs = strs.map { |str| negated(str) }.flatten if flag?
|
14
|
+
strs = collect(strs, :dashed)
|
15
|
+
strs = collect(strs, :underscored)
|
16
|
+
strs = collect(strs, :valued) if flag? && Cl.flag_values
|
17
|
+
strs.uniq
|
18
|
+
end
|
19
|
+
|
20
|
+
def short(str)
|
21
|
+
str = "#{str} #{opt.name.upcase}" unless opt.flag? || str.include?(' ')
|
22
|
+
str
|
23
|
+
end
|
24
|
+
|
25
|
+
def unnegate(str)
|
26
|
+
str.sub('--[no-]', '--')
|
27
|
+
end
|
28
|
+
|
29
|
+
def aliases
|
30
|
+
opt.aliases.map { |name| "--#{name} #{ name.upcase unless opt.flag?}".strip }
|
31
|
+
end
|
32
|
+
|
33
|
+
def collect(strs, mod)
|
34
|
+
strs = strs + strs.map { |str| send(mod, str) }
|
35
|
+
strs.flatten.uniq
|
36
|
+
end
|
37
|
+
|
38
|
+
def negated(str)
|
39
|
+
str.dup.insert(2, '[no-]')
|
40
|
+
end
|
41
|
+
|
42
|
+
def dashed(str)
|
43
|
+
str =~ NAME && str.sub("#{$1}#{$2}", "#{$1}#{$2.tr('_', '-')}") || str
|
44
|
+
end
|
45
|
+
|
46
|
+
def underscored(str)
|
47
|
+
str =~ NAME && str.sub("#{$1}#{$2}", "#{$1}#{$2.tr('-', '_')}") || str
|
48
|
+
end
|
49
|
+
|
50
|
+
def valued(str)
|
51
|
+
"#{str} [true|false|yes|no]"
|
52
|
+
end
|
53
|
+
|
54
|
+
def long?(str)
|
55
|
+
str.start_with?('--')
|
56
|
+
end
|
57
|
+
|
58
|
+
def flag?
|
59
|
+
opt.flag? && !opt.help?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/cl/parser.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'cl/parser/format'
|
3
|
+
|
4
|
+
class Cl
|
5
|
+
class Parser < OptionParser
|
6
|
+
attr_reader :cmd, :args, :opts
|
7
|
+
|
8
|
+
def initialize(cmd, args)
|
9
|
+
@cmd = cmd
|
10
|
+
@opts = {}
|
11
|
+
opts = cmd.class.opts
|
12
|
+
|
13
|
+
super do
|
14
|
+
opts.each do |opt|
|
15
|
+
Format.new(opt).strs.each do |str|
|
16
|
+
on(str) { |value| set(opt, str, value) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
@args = parse!(normalize(opts, args))
|
22
|
+
end
|
23
|
+
|
24
|
+
# should consider negative arities (e.g. |one, *two|)
|
25
|
+
def set(opt, str, value)
|
26
|
+
name = long?(str) ? opt_name(str) : opt.name
|
27
|
+
value = true if value.nil? && opt.flag?
|
28
|
+
args = [opts, opt.type, name, value]
|
29
|
+
args = args[-opt.block.arity, opt.block.arity]
|
30
|
+
instance_exec(*args, &opt.block)
|
31
|
+
cmd.deprecations.update([opt.deprecated].to_h) if opt.deprecated?(name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def normalize(opts, args)
|
35
|
+
args = noize(opts, args)
|
36
|
+
# dasherize(args)
|
37
|
+
args
|
38
|
+
end
|
39
|
+
|
40
|
+
def noize(opts, args)
|
41
|
+
args.map do |arg|
|
42
|
+
str = negation(opts, arg)
|
43
|
+
str ? arg.sub(/^--#{str}[-_]+/, '--no-') : arg
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def negation(opts, arg)
|
48
|
+
opts.select(&:flag?).detect do |opt|
|
49
|
+
str = opt.negate.detect { |str| arg =~ /^--#{str}[-_]+#{opt.name}/ }
|
50
|
+
break str if str
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# DASHERIZE = /^--([^= ])*/
|
55
|
+
#
|
56
|
+
# def dasherize(strs)
|
57
|
+
# strs.map do |str|
|
58
|
+
# str.is_a?(String) ? str.gsub(DASHERIZE) { |opt| opt.gsub('_', '-') } : str
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
|
62
|
+
def long?(str)
|
63
|
+
str.start_with?('--')
|
64
|
+
end
|
65
|
+
|
66
|
+
def opt_name(str)
|
67
|
+
str.split(' ').first.sub(/--(\[no[_\-]\])?/, '').to_sym
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'cl/ctx'
|
3
|
+
require 'cl/helper'
|
4
|
+
|
5
|
+
class Cl
|
6
|
+
module Runner
|
7
|
+
class Default
|
8
|
+
Runner.register :default, self
|
9
|
+
|
10
|
+
singleton_class.send(:attr_accessor, :run_method)
|
11
|
+
self.run_method = :run
|
12
|
+
|
13
|
+
extend Forwardable
|
14
|
+
include Merge, Suggest
|
15
|
+
|
16
|
+
def_delegators :ctx, :abort
|
17
|
+
|
18
|
+
attr_reader :ctx, :const, :args, :opts
|
19
|
+
|
20
|
+
def initialize(ctx, args)
|
21
|
+
@ctx = ctx
|
22
|
+
@const, @args = lookup(args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
cmd.help? ? help.run : cmd.send(self.class.run_method)
|
27
|
+
rescue OptionParser::InvalidOption => e
|
28
|
+
raise UnknownOption.new(const, e.message)
|
29
|
+
end
|
30
|
+
|
31
|
+
def cmd
|
32
|
+
@cmd ||= const.new(ctx, args)
|
33
|
+
end
|
34
|
+
|
35
|
+
def help
|
36
|
+
cmd.is_a?(Help) ? cmd : Help.new(ctx, [cmd.registry_key])
|
37
|
+
end
|
38
|
+
|
39
|
+
def suggestions(args)
|
40
|
+
keys = args.inject([]) { |keys, arg| keys << [keys.last, arg].compact.join(':') }
|
41
|
+
keys.map { |key| suggest(providers.map(&:to_s), key) }.flatten
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Finds a command class to run for the given arguments.
|
47
|
+
#
|
48
|
+
# Stopping at any arg that starts with a dash, find the command
|
49
|
+
# with the key matching the most args when joined with ":", and
|
50
|
+
# remove these used args from the array
|
51
|
+
#
|
52
|
+
# For example, if there are commands registered with the keys
|
53
|
+
#
|
54
|
+
# git:pull
|
55
|
+
# git:push
|
56
|
+
#
|
57
|
+
# then for the arguments:
|
58
|
+
#
|
59
|
+
# git push master
|
60
|
+
#
|
61
|
+
# the method `lookup` will find the constant registered as `git:push`,
|
62
|
+
# remove these from the `args` array, and return both the constant, and
|
63
|
+
# the remaining args.
|
64
|
+
#
|
65
|
+
# @param args [Array<String>] arguments to run (usually ARGV)
|
66
|
+
def lookup(args)
|
67
|
+
keys = args.take_while { |key| !key.start_with?('-') }
|
68
|
+
|
69
|
+
keys = keys.inject([[], []]) do |keys, key|
|
70
|
+
keys[1] << key
|
71
|
+
keys[0] << [Cmd[keys[1].join(':')], keys[1].dup] if Cmd.registered?(keys[1].join(':'))
|
72
|
+
keys
|
73
|
+
end
|
74
|
+
|
75
|
+
cmd, keys = keys[0].last
|
76
|
+
raise UnknownCmd.new(self, args) if cmd.nil? || cmd.abstract?
|
77
|
+
keys.each { |key| args.delete_at(args.index(key)) }
|
78
|
+
[cmd, args]
|
79
|
+
end
|
80
|
+
|
81
|
+
def providers
|
82
|
+
Cmd.registry.keys
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|