travis-cl 1.2.4
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 +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
|