cl 1.0.5 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: d7b66b1117dc3bb169e4d41d91123dded7479878
4
- data.tar.gz: d2fc68ec561c50b75c0d2d00ba061cbcafcdd1c3
2
+ SHA256:
3
+ metadata.gz: 05d4a5fcd74cdf55dbf91276fdd200a5041c4aabef97ffa68849ad63b81b8d30
4
+ data.tar.gz: 143667a25f93a57662f7271353815f9c6d253055aa45510f666fb37880cdda86
5
5
  SHA512:
6
- metadata.gz: dbe03ac3d14e21c1324cf477be78a0eef5e2ff430c552e2e3d3bbf69557650c845f0e71e433effd79e61c75668c2ea44ddee93240d9d83fbcb1cb87ab359f2ed
7
- data.tar.gz: f0eea40b5e24b319a384ee0fd321c1f9732cf2f5991a032efc8200d0b34afdef5046cfce121decc139732b4b91fd9d8f9338a6cfed157e44a3a05820b63b44a5
6
+ metadata.gz: b3a7e08f7c2271b0932b424d66dde47d75c758b579f0fae714bfcd42addf7f2d0cd1a4412b156757071e079b14bb32e9fbd70e43b3fb7a056bfd8efa6e0ab554
7
+ data.tar.gz: 3b32efded8658b8fce630613b3391157c9b295d8861b9000ed6fa679e8e7bb44904f34e474e6783399476c17dc4adbafd1552c050005f9df19f8323f77075247
data/CHANGELOG.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.5 (2019-08-18)
4
+
5
+ ### Fixed
6
+
7
+ * Fix an issue with alias names that contain the aliased name
8
+
3
9
  ## 1.0.4 (2019-08-15)
4
10
 
5
11
  ### Fixed
6
12
 
7
- * Fixed enum when used with `type: :array`
13
+ * Fix enum when used with `type: :array`
8
14
 
9
15
  ## 1.0.3 (2019-08-11)
10
16
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cl (1.0.4)
4
+ cl (1.0.5)
5
5
  regstry (~> 1.0.3)
6
6
 
7
7
  GEM
data/lib/cl.rb CHANGED
@@ -56,5 +56,4 @@ class Cl
56
56
  def untaint(args)
57
57
  args.map(&:dup).each(&:untaint)
58
58
  end
59
-
60
59
  end
data/lib/cl/cmd.rb CHANGED
@@ -29,8 +29,9 @@ class Cl
29
29
  registry.values
30
30
  end
31
31
 
32
- def parse(ctx, args)
33
- opts = Parser.new(self.opts, args).opts unless self == Help
32
+ def parse(ctx, cmd, args)
33
+ parser = Parser.new(cmd, args)
34
+ args, opts = parser.args, parser.opts unless self == Help
34
35
  opts = merge(ctx.config[registry_key], opts) if ctx.config[registry_key]
35
36
  [args, opts || {}]
36
37
  end
@@ -39,11 +40,10 @@ class Cl
39
40
  opt '--help', 'Get help on this command'
40
41
 
41
42
  attr_reader :ctx, :args
42
- attr_accessor :deprecations
43
43
 
44
44
  def initialize(ctx, args)
45
- args, opts = self.class.parse(ctx, args)
46
45
  @ctx = ctx
46
+ args, opts = self.class.parse(ctx, self, args)
47
47
  @opts = self.class.opts.apply(self, self.opts.merge(opts))
48
48
  @args = self.class.args.apply(self, args, opts)
49
49
  end
@@ -51,5 +51,9 @@ class Cl
51
51
  def opts
52
52
  @opts ||= {}
53
53
  end
54
+
55
+ def deprecations
56
+ @deprecations ||= {}
57
+ end
54
58
  end
55
59
  end
data/lib/cl/errors.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'cl/helper/suggest'
2
+
1
3
  class Cl
2
4
  class Error < StandardError
3
5
  MSGS = {
@@ -23,9 +25,17 @@ class Cl
23
25
  OptionError = Class.new(Error)
24
26
 
25
27
  class UnknownCmd < Error
26
- def initialize(args)
28
+ attr_reader :runner, :args
29
+
30
+ def initialize(runner, args)
31
+ @runner = runner
32
+ @args = args
27
33
  super(:unknown_cmd, args.join(' '))
28
34
  end
35
+
36
+ def suggestions
37
+ runner.suggestions(args)
38
+ end
29
39
  end
30
40
 
31
41
  class RequiredOpts < OptionError
@@ -65,9 +75,21 @@ class Cl
65
75
  end
66
76
 
67
77
  class UnknownValues < OptionError
78
+ include Suggest
79
+
80
+ attr_reader :opts
81
+
68
82
  def initialize(opts)
69
- opts = opts.map { |(key, value, known)| "#{key}=#{value} (known values: #{known.join(', ')})" }.join(', ')
70
- super(:unknown_values, opts)
83
+ @opts = opts
84
+ opts = opts.map do |(opt, values, known)|
85
+ pairs = values.map { |value| [opt, value].join('=') }.join(' ')
86
+ "#{pairs} (known values: #{known.join(', ')})"
87
+ end
88
+ super(:unknown_values, opts.join(', '))
89
+ end
90
+
91
+ def suggestions
92
+ opts.map { |_, value, known| suggest(known, value) }.flatten
71
93
  end
72
94
  end
73
95
  end
data/lib/cl/help/cmd.rb CHANGED
@@ -55,7 +55,7 @@ class Cl
55
55
  opts = cmd.opts.to_a
56
56
  opts = opts.reject(&:internal?)
57
57
  opts = opts - cmd.superclass.opts.to_a if common?
58
- strs = Table.new(rjust(opts.map { |opt| [*opt.strs] }))
58
+ strs = Table.new(rjust(opts.map { |opt| opt_strs(opt) }))
59
59
  opts = opts.map { |opt| format_obj(opt) }
60
60
  Table.new(strs.rows.zip(opts))
61
61
  end
@@ -65,12 +65,24 @@ class Cl
65
65
  @cmmn ||= begin
66
66
  opts = cmd.superclass.opts
67
67
  opts = opts.reject(&:internal?)
68
- strs = Table.new(rjust(opts.map { |opt| [*opt.strs] }))
68
+ strs = Table.new(rjust(opts.map(&:strs)))
69
69
  opts = opts.map { |opt| format_obj(opt) }
70
70
  Table.new(strs.rows.zip(opts))
71
71
  end
72
72
  end
73
73
 
74
+ def opt_strs(opt)
75
+ return opt.strs if !opt.flag? || opt.help?
76
+ opts = [opt.short]
77
+ opts << negate(opt.long, opt.negate) if opt.long && opt.negate?
78
+ opts.compact
79
+ end
80
+
81
+ def negate(opt, negations)
82
+ negations = negations.map { |str| "#{str}-" }.join('|')
83
+ opt.dup.insert(2, "[#{negations}]")
84
+ end
85
+
74
86
  def requireds
75
87
  return unless cmd.required?
76
88
  opts = cmd.required
data/lib/cl/helper.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'cl/helper/suggest'
2
+
1
3
  class Cl
2
4
  module Merge
3
5
  MERGE = ->(key, lft, rgt) do
@@ -0,0 +1,12 @@
1
+ class Cl
2
+ module Suggest
3
+ include DidYouMean if defined?(DidYouMean)
4
+
5
+ def suggest(dict, value)
6
+ return [] unless defined?(DidYouMean)
7
+ Array(value).map do |value|
8
+ SpellChecker.new(dictionary: dict.map(&:to_s)).correct(value.to_s)
9
+ end.flatten
10
+ end
11
+ end
12
+ end
data/lib/cl/opt.rb CHANGED
@@ -1,9 +1,15 @@
1
1
  require 'cl/cast'
2
+ require 'cl/errors'
2
3
 
3
4
  class Cl
4
5
  class Opt < Struct.new(:strs, :opts, :block)
5
6
  include Cast, Regex
6
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
+
7
13
  OPT = /^--(?:\[.*\])?(.*)$/
8
14
 
9
15
  TYPES = {
@@ -13,9 +19,11 @@ class Cl
13
19
  boolean: :flag
14
20
  }
15
21
 
16
- def initialize(*)
22
+ attr_reader :short, :long
23
+
24
+ def initialize(strs, *)
17
25
  super
18
- noize!(strs) if type == :flag
26
+ @short, @long = Validator.new(strs, opts).apply
19
27
  end
20
28
 
21
29
  def define(const)
@@ -28,19 +36,22 @@ class Cl
28
36
 
29
37
  def name
30
38
  return @name if instance_variable_defined?(:@name)
31
- opt = strs.detect { |str| str.start_with?('--') }
32
- name = opt.split(' ').first.match(OPT)[1] if opt
39
+ name = long.split(' ').first.match(OPT)[1] if long
33
40
  @name = name.sub('-', '_').to_sym if name
34
41
  end
35
42
 
36
43
  def type
37
- TYPES[opts[:type]] || opts[:type] || infer_type
44
+ @type ||= TYPES[opts[:type]] || opts[:type] || infer_type
38
45
  end
39
46
 
40
47
  def infer_type
41
48
  strs.any? { |str| str.split(' ').size > 1 } ? :string : :flag
42
49
  end
43
50
 
51
+ def help?
52
+ name == :help
53
+ end
54
+
44
55
  def flag?
45
56
  type == :flag
46
57
  end
@@ -65,13 +76,16 @@ class Cl
65
76
  opts[:description]
66
77
  end
67
78
 
68
- def deprecated?
69
- !!opts[:deprecated]
79
+ def deprecated?(name = nil)
80
+ name ? deprecated.first == name : !!opts[:deprecated]
70
81
  end
71
82
 
72
83
  def deprecated
84
+ # If it's a string then it's a deprecation message and the option itself
85
+ # is considered deprecated. If it's a symbol it refers to a deprecated
86
+ # alias, and the option's name is the deprecation message.
73
87
  return [name, opts[:deprecated]] unless opts[:deprecated].is_a?(Symbol)
74
- [opts[:deprecated], name] if opts[:deprecated]
88
+ opts[:deprecated] ? [opts[:deprecated], name] : []
75
89
  end
76
90
 
77
91
  def downcase?
@@ -99,6 +113,11 @@ class Cl
99
113
  enum.any? { |obj| obj.is_a?(Regexp) ? obj =~ value.to_s : obj == value }
100
114
  end
101
115
 
116
+ def unknown(value)
117
+ return value.reject { |value| known?(value) } if value.is_a?(Array)
118
+ known?(value) ? [] : Array(value)
119
+ end
120
+
102
121
  def example?
103
122
  !!opts[:example]
104
123
  end
@@ -141,11 +160,11 @@ class Cl
141
160
  end
142
161
 
143
162
  def negate?
144
- !!opts[:negate]
163
+ !!negate
145
164
  end
146
165
 
147
166
  def negate
148
- Array(opts[:negate])
167
+ ['no'] + Array(opts[:negate]) if flag?
149
168
  end
150
169
 
151
170
  def note?
@@ -193,19 +212,63 @@ class Cl
193
212
  super || method(:assign)
194
213
  end
195
214
 
196
- def assign(opts, type, name, value)
197
- if array?
198
- opts[name] ||= []
199
- opts[name] << value
200
- else
201
- opts[name] = value
215
+ def assign(opts, type, _, value)
216
+ [name, *aliases].each do |name|
217
+ if array?
218
+ opts[name] ||= []
219
+ opts[name] << value
220
+ else
221
+ opts[name] = value
222
+ end
202
223
  end
203
224
  end
204
225
 
205
- def noize!(strs)
206
- strs = strs.select { |str| str.start_with?('--') }
207
- strs = strs.reject { |str| str.include?('[no-]') }
208
- strs.each { |str| str.replace(str.sub('--', '--[no-]')) unless str == '--help' }
226
+ def long?(str)
227
+ str.start_with?('--')
228
+ end
229
+
230
+ class Validator < Struct.new(:strs, :opts)
231
+ SHORT = /^-\w( \w+)?$/
232
+ LONG = /^--[\w\-\[\]]+( \w+)?$/
233
+
234
+ MSGS = {
235
+ missing_strs: 'No option strings given. Pass one short -s and/or one --long option string.',
236
+ wrong_strs: 'Wrong option strings given. Pass one short -s and/or one --long option string.',
237
+ invalid_strs: 'Invalid option strings given: %p',
238
+ unknown_opts: 'Unknown options: %s'
239
+ }
240
+
241
+ def apply
242
+ error :missing_strs if strs.empty?
243
+ error :wrong_strs if short.size > 1 || long.size > 1
244
+ error :invalid_strs, invalid unless invalid.empty?
245
+ error :unknown_opts, unknown.map(&:inspect).join(', ') unless unknown.empty?
246
+ [short.first, long.first]
247
+ end
248
+
249
+ def unknown
250
+ @unknown ||= opts.keys - Opt::OPTS
251
+ end
252
+
253
+ def invalid
254
+ @invalid ||= strs.-(valid).join(', ')
255
+ end
256
+
257
+ def valid
258
+ strs.grep(Regexp.union(SHORT, LONG))
259
+ end
260
+
261
+ def short
262
+ strs.grep(SHORT)
263
+ end
264
+
265
+ def long
266
+ strs.grep(LONG)
267
+ end
268
+
269
+ def error(key, *args)
270
+ raise Cl::Error, MSGS[key] % args
271
+ end
209
272
  end
210
273
  end
211
274
  end
data/lib/cl/opts.rb CHANGED
@@ -18,7 +18,6 @@ class Cl
18
18
  def apply(cmd, opts)
19
19
  return opts if opts[:help]
20
20
  orig = opts.dup
21
- cmd.deprecations = deprecations(cmd, opts)
22
21
  opts = defaults(cmd, opts)
23
22
  opts = downcase(opts)
24
23
  opts = upcase(opts)
@@ -66,12 +65,6 @@ class Cl
66
65
 
67
66
  private
68
67
 
69
- def deprecations(cmd, opts)
70
- defs = cmd.class.opts.select(&:deprecated?)
71
- defs = defs.select { |opt| opts.key?(opt.deprecated[0]) }
72
- defs.map(&:deprecated).to_h
73
- end
74
-
75
68
  def defaults(cmd, opts)
76
69
  select(&:default?).inject(opts) do |opts, opt|
77
70
  next opts if opts.key?(opt.name)
@@ -86,9 +86,10 @@ class Cl
86
86
 
87
87
  def unknown
88
88
  @unknown ||= opts.select(&:enum?).map do |opt|
89
- next unless values.key?(opt.name) && !opt.known?(values[opt.name])
89
+ unknown = opt.unknown(values[opt.name])
90
+ next if unknown.empty?
90
91
  known = opt.enum.map { |str| format_regex(str) }
91
- [opt.name, values[opt.name], known]
92
+ [opt.name, unknown, known]
92
93
  end.compact
93
94
  end
94
95
  end
data/lib/cl/parser.rb CHANGED
@@ -1,47 +1,35 @@
1
1
  require 'optparse'
2
+ require 'cl/parser/format'
2
3
 
3
4
  class Cl
4
5
  class Parser < OptionParser
5
- attr_reader :opts
6
+ attr_reader :cmd, :args, :opts
6
7
 
7
- def initialize(opts, args)
8
+ def initialize(cmd, args)
9
+ @cmd = cmd
8
10
  @opts = {}
11
+ opts = cmd.class.opts
9
12
 
10
13
  super do
11
14
  opts.each do |opt|
12
- on(*args_for(opt, opt.strs)) do |value|
13
- set(opt, value)
14
- end
15
-
16
- opt.aliases.each do |name|
17
- on(*args_for(opt, [aliased(opt, name)])) do |value|
18
- @opts[name] = set(opt, value)
19
- end
15
+ Format.new(opt).strs.each do |str|
16
+ on(str) { |value| set(opt, str, value) }
20
17
  end
21
18
  end
22
19
  end
23
20
 
24
- args.replace(normalize(opts, args))
25
- parse!(args)
26
- end
27
-
28
- def args_for(opt, strs)
29
- args = dasherize(strs)
30
- args = flagerize(args) if opt.flag?
31
- args
32
- end
33
-
34
- def aliased(opt, name)
35
- str = opt.strs.detect { |str| str.start_with?('--') } || raise
36
- str.sub(/(#{opt.name}|#{opt.name.to_s.gsub('_', '-')})/, name.to_s)
21
+ orig = args.map(&:dup)
22
+ @args = parse!(normalize(opts, args))
37
23
  end
38
24
 
39
25
  # should consider negative arities (e.g. |one, *two|)
40
- def set(opt, value)
26
+ def set(opt, str, value)
27
+ name = long?(str) ? opt_name(str) : opt.name
41
28
  value = true if value.nil? && opt.flag?
42
- args = [opts, opt.type, opt.name, value]
29
+ args = [opts, opt.type, name, value]
43
30
  args = args[-opt.block.arity, opt.block.arity]
44
31
  instance_exec(*args, &opt.block)
32
+ cmd.deprecations[name] = opt.deprecated.last if opt.deprecated?(name)
45
33
  end
46
34
 
47
35
  def normalize(opts, args)
@@ -57,7 +45,7 @@ class Cl
57
45
  end
58
46
 
59
47
  def negation(opts, arg)
60
- opts.detect do |opt|
48
+ opts.select(&:flag?).detect do |opt|
61
49
  str = opt.negate.detect { |str| arg =~ /^--#{str}[-_]+#{opt.name}/ }
62
50
  break str if str
63
51
  end
@@ -71,9 +59,12 @@ class Cl
71
59
  end
72
60
  end
73
61
 
74
- def flagerize(strs)
75
- strs = strs.map { |str| str.include?(' ') ? str : "#{str} [true|false|yes|no]" }
76
- strs << TrueClass
62
+ def long?(str)
63
+ str.start_with?('--')
64
+ end
65
+
66
+ def opt_name(str)
67
+ str.split(' ').first.sub(/--(\[no[_\-]\])?/, '').to_sym
77
68
  end
78
69
  end
79
70
  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?
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
@@ -8,7 +8,7 @@ class Cl
8
8
  Runner.register :default, self
9
9
 
10
10
  extend Forwardable
11
- include Merge
11
+ include Merge, Suggest
12
12
 
13
13
  def_delegators :ctx, :abort
14
14
 
@@ -31,6 +31,11 @@ class Cl
31
31
  Help.new(ctx, [cmd.registry_key])
32
32
  end
33
33
 
34
+ def suggestions(args)
35
+ keys = args.inject([]) { |keys, arg| keys << [keys.last, arg].compact.join(':') }
36
+ keys.map { |key| suggest(providers.map(&:to_s), key) }.flatten
37
+ end
38
+
34
39
  private
35
40
 
36
41
  # Finds a command class to run for the given arguments.
@@ -63,10 +68,14 @@ class Cl
63
68
  end
64
69
 
65
70
  cmd, keys = keys[0].last
66
- cmd || raise(UnknownCmd.new(args))
71
+ raise UnknownCmd.new(self, args) unless cmd
67
72
  keys.each { |key| args.delete_at(args.index(key)) }
68
73
  [cmd, args]
69
74
  end
75
+
76
+ def providers
77
+ Cmd.registry.keys
78
+ end
70
79
  end
71
80
  end
72
81
  end
data/lib/cl/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Cl
2
- VERSION = '1.0.5'
2
+ VERSION = '1.1.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.5
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sven Fuchs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-18 00:00:00.000000000 Z
11
+ date: 2019-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: regstry
@@ -140,10 +140,12 @@ files:
140
140
  - lib/cl/help/table.rb
141
141
  - lib/cl/help/usage.rb
142
142
  - lib/cl/helper.rb
143
+ - lib/cl/helper/suggest.rb
143
144
  - lib/cl/opt.rb
144
145
  - lib/cl/opts.rb
145
146
  - lib/cl/opts/validate.rb
146
147
  - lib/cl/parser.rb
148
+ - lib/cl/parser/format.rb
147
149
  - lib/cl/runner.rb
148
150
  - lib/cl/runner/default.rb
149
151
  - lib/cl/runner/multi.rb
@@ -168,8 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
170
  - !ruby/object:Gem::Version
169
171
  version: '0'
170
172
  requirements: []
171
- rubyforge_project:
172
- rubygems_version: 2.6.13
173
+ rubygems_version: 3.0.3
173
174
  signing_key:
174
175
  specification_version: 4
175
176
  summary: Object-oriented OptionParser based CLI support