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.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +134 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.lock +59 -0
  5. data/MIT_LICENSE.md +21 -0
  6. data/README.md +1283 -0
  7. data/cl.gemspec +30 -0
  8. data/examples/README.md +22 -0
  9. data/examples/_src/args/cast.erb.rb +100 -0
  10. data/examples/_src/args/opts.erb.rb +100 -0
  11. data/examples/_src/args/required.erb.rb +63 -0
  12. data/examples/_src/args/splat.erb.rb +55 -0
  13. data/examples/_src/gem.erb.rb +99 -0
  14. data/examples/_src/heroku.erb.rb +47 -0
  15. data/examples/_src/rakeish.erb.rb +54 -0
  16. data/examples/_src/readme/abstract.erb.rb +27 -0
  17. data/examples/_src/readme/alias.erb.rb +22 -0
  18. data/examples/_src/readme/arg.erb.rb +21 -0
  19. data/examples/_src/readme/arg_array.erb.rb +21 -0
  20. data/examples/_src/readme/arg_type.erb.rb +23 -0
  21. data/examples/_src/readme/args_splat.erb.rb +55 -0
  22. data/examples/_src/readme/array.erb.rb +21 -0
  23. data/examples/_src/readme/basic.erb.rb +72 -0
  24. data/examples/_src/readme/default.erb.rb +21 -0
  25. data/examples/_src/readme/deprecated.erb.rb +21 -0
  26. data/examples/_src/readme/deprecated_alias.erb.rb +21 -0
  27. data/examples/_src/readme/description.erb.rb +60 -0
  28. data/examples/_src/readme/downcase.erb.rb +21 -0
  29. data/examples/_src/readme/enum.erb.rb +35 -0
  30. data/examples/_src/readme/example.erb.rb +25 -0
  31. data/examples/_src/readme/format.erb.rb +35 -0
  32. data/examples/_src/readme/internal.erb.rb +28 -0
  33. data/examples/_src/readme/negate.erb.rb +37 -0
  34. data/examples/_src/readme/note.erb.rb +25 -0
  35. data/examples/_src/readme/opts.erb.rb +33 -0
  36. data/examples/_src/readme/opts_block.erb.rb +30 -0
  37. data/examples/_src/readme/range.erb.rb +35 -0
  38. data/examples/_src/readme/registry.erb.rb +18 -0
  39. data/examples/_src/readme/required.erb.rb +35 -0
  40. data/examples/_src/readme/requireds.erb.rb +46 -0
  41. data/examples/_src/readme/requires.erb.rb +35 -0
  42. data/examples/_src/readme/runner.erb.rb +29 -0
  43. data/examples/_src/readme/runner_custom.erb.rb +25 -0
  44. data/examples/_src/readme/secret.erb.rb +22 -0
  45. data/examples/_src/readme/see.erb.rb +25 -0
  46. data/examples/_src/readme/type.erb.rb +21 -0
  47. data/examples/args/cast +98 -0
  48. data/examples/args/opts +98 -0
  49. data/examples/args/required +62 -0
  50. data/examples/args/splat +58 -0
  51. data/examples/gem +97 -0
  52. data/examples/heroku +48 -0
  53. data/examples/rakeish +50 -0
  54. data/examples/readme/abstract +28 -0
  55. data/examples/readme/alias +21 -0
  56. data/examples/readme/arg +20 -0
  57. data/examples/readme/arg_array +20 -0
  58. data/examples/readme/arg_type +22 -0
  59. data/examples/readme/args_splat +58 -0
  60. data/examples/readme/array +20 -0
  61. data/examples/readme/basic +67 -0
  62. data/examples/readme/default +20 -0
  63. data/examples/readme/deprecated +20 -0
  64. data/examples/readme/deprecated_alias +20 -0
  65. data/examples/readme/description +56 -0
  66. data/examples/readme/downcase +20 -0
  67. data/examples/readme/enum +33 -0
  68. data/examples/readme/example +21 -0
  69. data/examples/readme/format +33 -0
  70. data/examples/readme/internal +24 -0
  71. data/examples/readme/negate +44 -0
  72. data/examples/readme/note +21 -0
  73. data/examples/readme/opts +31 -0
  74. data/examples/readme/opts_block +29 -0
  75. data/examples/readme/range +33 -0
  76. data/examples/readme/registry +15 -0
  77. data/examples/readme/required +33 -0
  78. data/examples/readme/requireds +46 -0
  79. data/examples/readme/requires +33 -0
  80. data/examples/readme/runner +30 -0
  81. data/examples/readme/runner_custom +22 -0
  82. data/examples/readme/secret +21 -0
  83. data/examples/readme/see +21 -0
  84. data/examples/readme/type +20 -0
  85. data/lib/cl/arg.rb +79 -0
  86. data/lib/cl/args.rb +92 -0
  87. data/lib/cl/cast.rb +55 -0
  88. data/lib/cl/cmd.rb +74 -0
  89. data/lib/cl/config/env.rb +52 -0
  90. data/lib/cl/config/files.rb +34 -0
  91. data/lib/cl/config.rb +30 -0
  92. data/lib/cl/ctx.rb +36 -0
  93. data/lib/cl/dsl.rb +182 -0
  94. data/lib/cl/errors.rb +119 -0
  95. data/lib/cl/help/cmd.rb +118 -0
  96. data/lib/cl/help/cmds.rb +26 -0
  97. data/lib/cl/help/format.rb +69 -0
  98. data/lib/cl/help/table.rb +58 -0
  99. data/lib/cl/help/usage.rb +26 -0
  100. data/lib/cl/help.rb +37 -0
  101. data/lib/cl/helper/suggest.rb +10 -0
  102. data/lib/cl/helper.rb +47 -0
  103. data/lib/cl/opt.rb +276 -0
  104. data/lib/cl/opts/validate.rb +117 -0
  105. data/lib/cl/opts.rb +114 -0
  106. data/lib/cl/parser/format.rb +63 -0
  107. data/lib/cl/parser.rb +70 -0
  108. data/lib/cl/runner/default.rb +86 -0
  109. data/lib/cl/runner/multi.rb +34 -0
  110. data/lib/cl/runner.rb +10 -0
  111. data/lib/cl/ui.rb +146 -0
  112. data/lib/cl/version.rb +3 -0
  113. data/lib/cl.rb +62 -0
  114. 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