cl 1.0.5 → 1.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 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