cl 0.0.4 → 0.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.
data/lib/cl/opt.rb ADDED
@@ -0,0 +1,141 @@
1
+ require 'cl/cast'
2
+
3
+ class Cl
4
+ class Opt < Struct.new(:strs, :opts, :block)
5
+ include Cast
6
+
7
+ OPT = /^--(?:\[.*\])?(.*)$/
8
+
9
+ TYPES = {
10
+ int: :integer,
11
+ str: :string,
12
+ bool: :flag,
13
+ boolean: :flag
14
+ }
15
+
16
+ def initialize(*)
17
+ super
18
+ noize(strs) if type == :flag && default.is_a?(TrueClass)
19
+ end
20
+
21
+ def define(const)
22
+ return unless __key__ = name
23
+ const.include Module.new {
24
+ define_method (__key__) { opts[__key__] }
25
+ define_method (:"#{__key__}?") { !!opts[__key__] }
26
+ }
27
+ end
28
+
29
+ def name
30
+ 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
33
+ @name = name&.sub('-', '_')&.to_sym
34
+ end
35
+
36
+ def type
37
+ TYPES[opts[:type]] || opts[:type] || infer_type
38
+ end
39
+
40
+ def infer_type
41
+ strs.any? { |str| str.split(' ').size > 1 } ? :string : :flag
42
+ end
43
+
44
+ def int?
45
+ type == :int || type == :integer
46
+ end
47
+
48
+ def description
49
+ opts[:description]
50
+ end
51
+
52
+ def aliases?
53
+ !!opts[:alias]
54
+ end
55
+
56
+ def aliases
57
+ Array(opts[:alias])
58
+ end
59
+
60
+ def deprecated?
61
+ !!opts[:deprecated]
62
+ end
63
+
64
+ def deprecated
65
+ return [name] if opts[:deprecated].is_a?(TrueClass)
66
+ Array(opts[:deprecated]) if opts[:deprecated]
67
+ end
68
+
69
+ def default?
70
+ !!default
71
+ end
72
+
73
+ def default
74
+ opts[:default]
75
+ end
76
+
77
+ def enum?
78
+ !!opts[:enum]
79
+ end
80
+
81
+ def enum
82
+ Array(opts[:enum])
83
+ end
84
+
85
+ def known?(value)
86
+ enum.include?(value)
87
+ end
88
+
89
+ def format?
90
+ !!opts[:format]
91
+ end
92
+
93
+ def format
94
+ opts[:format].to_s.sub('(?-mix:', '').sub(/\)$/, '')
95
+ end
96
+
97
+ def formatted?(value)
98
+ opts[:format] =~ value
99
+ end
100
+
101
+ def max?
102
+ int? && !!opts[:max]
103
+ end
104
+
105
+ def max
106
+ opts[:max]
107
+ end
108
+
109
+ def required?
110
+ !!opts[:required]
111
+ end
112
+
113
+ def requires?
114
+ !!opts[:requires]
115
+ end
116
+
117
+ def requires
118
+ Array(opts[:requires])
119
+ end
120
+
121
+ def block
122
+ # raise if no block was given, and the option's name cannot be inferred
123
+ super || method(:assign)
124
+ end
125
+
126
+ def assign(opts, type, name, value)
127
+ if type == :array
128
+ opts[name] ||= []
129
+ opts[name] << value
130
+ else
131
+ opts[name] = value
132
+ end
133
+ end
134
+
135
+ def noize(strs)
136
+ strs = strs.select { |str| str.start_with?('--') }
137
+ strs = strs.reject { |str| str.include?('[no-]') }
138
+ strs.each { |str| str.replace(str.sub('--', '--[no-]')) }
139
+ end
140
+ end
141
+ end
data/lib/cl/opts.rb ADDED
@@ -0,0 +1,155 @@
1
+ require 'cl/opt'
2
+
3
+ class Cl
4
+ class Opts
5
+ include Enumerable
6
+
7
+ def define(const, *args, &block)
8
+ opts = args.last.is_a?(Hash) ? args.pop : {}
9
+ strs = args.select { |arg| arg.start_with?('-') }
10
+ opts[:description] = args.-(strs).first
11
+
12
+ opt = Opt.new(strs, opts, block)
13
+ opt.define(const)
14
+ self << opt
15
+ end
16
+
17
+ def apply(cmd, opts)
18
+ opts = with_defaults(cmd, opts)
19
+ opts = cast(opts)
20
+ validate(cmd, opts) unless opts[:help]
21
+ opts
22
+ end
23
+
24
+ def <<(opt)
25
+ # keep the --help option at the end for help output
26
+ opts.empty? ? opts << opt : opts.insert(-2, opt)
27
+ end
28
+
29
+ def [](key)
30
+ opts.detect { |opt| opt.name == key }
31
+ end
32
+
33
+ def each(&block)
34
+ opts.each(&block)
35
+ end
36
+
37
+ def to_a
38
+ opts
39
+ end
40
+
41
+ attr_writer :opts
42
+
43
+ def opts
44
+ @opts ||= []
45
+ end
46
+
47
+ def deprecated
48
+ map(&:deprecated).flatten.compact
49
+ end
50
+
51
+ def dup
52
+ super.tap { |obj| obj.opts = opts.dup }
53
+ end
54
+
55
+ private
56
+
57
+ def validate(cmd, opts)
58
+ validate_requireds(cmd, opts)
59
+ validate_required(opts)
60
+ validate_requires(opts)
61
+ validate_max(opts)
62
+ validate_format(opts)
63
+ validate_enum(opts)
64
+ end
65
+
66
+ def validate_requireds(cmd, opts)
67
+ opts = missing_requireds(cmd, opts)
68
+ raise RequiredsOpts.new(opts) if opts.any?
69
+ end
70
+
71
+ def validate_required(opts)
72
+ opts = missing_required(opts)
73
+ # make sure we do not accept unnamed required options
74
+ raise RequiredOpts.new(opts.map(&:name)) if opts.any?
75
+ end
76
+
77
+ def validate_requires(opts)
78
+ opts = missing_requires(opts)
79
+ raise RequiresOpts.new(invert(opts)) if opts.any?
80
+ end
81
+
82
+ def validate_max(opts)
83
+ opts = exceeding_max(opts)
84
+ raise ExceedingMax.new(opts) if opts.any?
85
+ end
86
+
87
+ def validate_format(opts)
88
+ opts = invalid_format(opts)
89
+ raise InvalidFormat.new(opts) if opts.any?
90
+ end
91
+
92
+ def validate_enum(opts)
93
+ opts = unknown_values(opts)
94
+ raise UnknownValues.new(opts) if opts.any?
95
+ end
96
+
97
+ def missing_requireds(cmd, opts)
98
+ opts = cmd.class.required.map do |alts|
99
+ alts if alts.none? { |alt| Array(alt).all? { |key| opts.key?(key) } }
100
+ end.compact
101
+ end
102
+
103
+ def missing_required(opts)
104
+ select(&:required?).select { |opt| !opts.key?(opt.name) }
105
+ end
106
+
107
+ def missing_requires(opts)
108
+ select(&:requires?).map do |opt|
109
+ missing = opt.requires.select { |key| !opts.key?(key) }
110
+ [opt.name, missing] if missing.any?
111
+ end.compact
112
+ end
113
+
114
+ def exceeding_max(opts)
115
+ select(&:max?).map do |opt|
116
+ [opt.name, opt.max] if opts[opt.name] > opt.max
117
+ end.compact
118
+ end
119
+
120
+ def invalid_format(opts)
121
+ select(&:format?).map do |opt|
122
+ [opt.name, opt.format] unless opt.formatted?(opts[opt.name])
123
+ end.compact
124
+ end
125
+
126
+ def unknown_values(opts)
127
+ select(&:enum?).map do |opt|
128
+ [opt.name, opts[opt.name], opt.enum] unless opt.known?(opts[opt.name])
129
+ end.compact
130
+ end
131
+
132
+ def with_defaults(cmd, opts)
133
+ select(&:default?).inject(opts) do |opts, opt|
134
+ next opts if opts.key?(opt.name)
135
+ value = opt.default
136
+ value = resolve(cmd, opts, value) if value.is_a?(Symbol)
137
+ opts.merge(opt.name => value)
138
+ end
139
+ end
140
+
141
+ def resolve(cmd, opts, key)
142
+ opts[key] || cmd.respond_to?(key) && cmd.send(key)
143
+ end
144
+
145
+ def cast(opts)
146
+ opts.map do |key, value|
147
+ [key, self[key] ? self[key].cast(value) : value]
148
+ end.to_h
149
+ end
150
+
151
+ def invert(hash)
152
+ hash.map { |key, obj| Array(obj).map { |obj| [obj, key] } }.flatten(1).to_h
153
+ end
154
+ end
155
+ end
data/lib/cl/parser.rb ADDED
@@ -0,0 +1,39 @@
1
+ require 'optparse'
2
+
3
+ class Cl
4
+ class Parser < OptionParser
5
+ attr_reader :opts
6
+
7
+ def initialize(opts, args)
8
+ @opts = {}
9
+
10
+ super do
11
+ opts.each do |opt|
12
+ on(*opt.strs) do |value|
13
+ set(opt, value)
14
+ end
15
+
16
+ opt.aliases.each do |name|
17
+ on(aliased(opt, name)) do |value|
18
+ @opts[name] = set(opt, value)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ parse!(args)
25
+ end
26
+
27
+ def aliased(opt, name)
28
+ str = opt.strs.detect { |str| str.start_with?('--') } || raise
29
+ str.sub(opt.name.to_s, name.to_s)
30
+ end
31
+
32
+ # should consider negative arities (e.g. |one, *two|)
33
+ def set(opt, value)
34
+ args = [opts, opt.type, opt.name, value]
35
+ args = args[-opt.block.arity, opt.block.arity]
36
+ instance_exec(*args, &opt.block)
37
+ end
38
+ end
39
+ end
@@ -1,41 +1,57 @@
1
- require 'cl/options'
1
+ require 'forwardable'
2
+ require 'cl/ctx'
3
+ require 'cl/parser'
4
+ require 'cl/helper'
2
5
 
3
- module Cl
6
+ class Cl
4
7
  module Runner
5
8
  class Default
6
- attr_reader :const, :args, :opts
9
+ extend Forwardable
10
+ include Merge
7
11
 
8
- def initialize(*args)
9
- args = args.flatten.map(&:to_s)
10
- @const, @args, @opts = lookup(args)
12
+ def_delegators :ctx, :abort
13
+
14
+ attr_reader :ctx, :const, :args, :opts
15
+
16
+ def initialize(ctx, args)
17
+ @ctx = ctx
18
+ @const, @args = lookup(args)
19
+ # @opts, @args = parse(args)
11
20
  end
12
21
 
13
22
  def run
14
- cmd.run
23
+ cmd.help? ? help.run : cmd.run
15
24
  end
16
25
 
17
26
  def cmd
18
- const.new(args, opts)
27
+ @cmd ||= const.new(ctx, args)
28
+ end
29
+
30
+ def help
31
+ Help.new(ctx, [cmd.registry_key])
19
32
  end
20
33
 
21
34
  private
22
35
 
23
36
  def lookup(args)
24
- cmd = keys_for(args).map { |key| Cl[key] }.compact.last
25
- cmd || abort("Unknown command: #{args.join(' ')}")
26
- opts = Options.new(cmd.opts, args).opts unless cmd == Help
27
- [cmd, args - cmds_for(cmd, args), opts]
37
+ keys = expand(args) & Cmd.registry.keys.map(&:to_s)
38
+ cmd = Cmd[keys.last] || abort("Unknown command: #{args.join(' ')}")
39
+ [cmd, args - keys(cmd)]
40
+ end
41
+
42
+ def name
43
+ const.registry_key
28
44
  end
29
45
 
30
- def cmds_for(cmd, args)
31
- name = cmd.registry_key.to_s
32
- args.take_while do |arg|
33
- name = name.sub(/#{arg}(:|$)/, '') if name =~ /#{arg}(:|$)/
34
- end
46
+ def keys(cmd)
47
+ keys = cmd.registry_key.to_s.split(':')
48
+ keys.concat(expand(keys)).uniq
35
49
  end
36
50
 
37
- def keys_for(args)
38
- args.inject([]) { |keys, key| keys << [keys.last, key].compact.join(':') }
51
+ def expand(strs)
52
+ # strs = strs.reject { |str| str.start_with?('-') }
53
+ strs = strs.take_while { |str| !str.start_with?('-') }
54
+ strs.inject([]) { |strs, str| strs << [strs.last, str].compact.join(':') }
39
55
  end
40
56
  end
41
57
  end
@@ -1,11 +1,10 @@
1
- require 'cl/options'
2
-
3
- module Cl
1
+ class Cl
4
2
  module Runner
5
3
  class Multi
6
- attr_reader :cmds
4
+ attr_reader :name, :cmds
7
5
 
8
- def initialize(*args)
6
+ def initialize(name, *args)
7
+ @name = name
9
8
  @cmds = build(group(args))
10
9
  end
11
10
 
@@ -17,7 +16,7 @@ module Cl
17
16
 
18
17
  def group(args, cmds = [])
19
18
  args.flatten.map(&:to_s).inject([[]]) do |cmds, arg|
20
- cmd = Cl[arg]
19
+ cmd = Cmd.registered?(arg) ? Cmd[arg] : nil
21
20
  cmd ? cmds << [cmd] : cmds.last << arg
22
21
  cmds.reject(&:empty?)
23
22
  end
@@ -25,7 +24,7 @@ module Cl
25
24
 
26
25
  def build(cmds)
27
26
  cmds.map do |(cmd, *args)|
28
- cmd.new(args, Options.new(cmd.opts, args).opts)
27
+ cmd.new(name, args)
29
28
  end
30
29
  end
31
30
  end
data/lib/cl/ui.rb ADDED
@@ -0,0 +1,137 @@
1
+ require 'stringio'
2
+
3
+ class Cl
4
+ module Ui
5
+ def self.new(ctx, opts)
6
+ const = Test if ctx.test?
7
+ const ||= Silent if opts[:silent]
8
+ const ||= Tty if $stdout.tty?
9
+ const ||= Pipe
10
+ const.new(opts)
11
+ end
12
+
13
+ class Base < Struct.new(:opts)
14
+ attr_writer :stdout
15
+
16
+ def stdout
17
+ @stdout ||= opts[:stdout] || $stdout
18
+ end
19
+
20
+ def puts(*str)
21
+ stdout.puts(*str)
22
+ end
23
+ end
24
+
25
+ class Silent < Base
26
+ %i(announce info notice warn error success cmd).each do |name|
27
+ define_method (name) { |*| }
28
+ end
29
+ end
30
+
31
+ class Test < Base
32
+ %i(announce info notice warn error success cmd).each do |name|
33
+ define_method (name) do |*args|
34
+ puts ["[#{name}]", *args.map(&:inspect)].join(' ')
35
+ end
36
+ end
37
+
38
+ def stdout
39
+ @stdout ||= StringIO.new
40
+ end
41
+ end
42
+
43
+ class Pipe < Base
44
+ %i(announce info notice warn error).each do |name|
45
+ define_method (name) do |msg, args = nil, _ = nil|
46
+ puts format_msg(msg, args)
47
+ end
48
+ end
49
+
50
+ %i(success cmd).each do |name|
51
+ define_method (name) { |*| }
52
+ end
53
+
54
+ private
55
+
56
+ def format_msg(msg, args)
57
+ msg = [msg, args].flatten.map(&:to_s)
58
+ msg = msg.map { |str| quote_spaced(str) }
59
+ msg.join(' ').strip
60
+ end
61
+
62
+ def quote_spaced(str)
63
+ str.include?(' ') ? %("#{str}") : str
64
+ end
65
+ end
66
+
67
+ module Colors
68
+ COLORS = {
69
+ red: "\e[31m",
70
+ green: "\e[32m",
71
+ yellow: "\e[33m",
72
+ blue: "\e[34m",
73
+ gray: "\e[37m",
74
+ reset: "\e[0m"
75
+ }
76
+
77
+ def colored(color, str)
78
+ [COLORS[color], str, COLORS[:reset]].join
79
+ end
80
+ end
81
+
82
+ class Tty < Base
83
+ include Colors
84
+
85
+ def announce(msg, args = [], msgs = [])
86
+ msg = format_msg(msg, args, msgs)
87
+ puts colored(:green, with_spacing(msg, true))
88
+ end
89
+
90
+ def info(msg, args = [], msgs = [])
91
+ msg = format_msg(msg, args, msgs)
92
+ puts colored(:blue, with_spacing(msg, true))
93
+ end
94
+
95
+ def notice(msg, args = [], msgs = [])
96
+ msg = format_msg(msg, args, msgs)
97
+ puts colored(:gray, with_spacing(msg, false))
98
+ end
99
+
100
+ def warn(msg, args = [], msgs = [])
101
+ msg = format_msg(msg, args, msgs)
102
+ puts colored(:yellow, with_spacing(msg, false))
103
+ end
104
+
105
+ def error(msg, args = [], msgs = [])
106
+ msg = format_msg(msg, args, msgs)
107
+ puts colored(:red, with_spacing(msg, true))
108
+ end
109
+
110
+ def success(msg)
111
+ announce(msg)
112
+ puts
113
+ end
114
+
115
+ def cmd(msg)
116
+ notice("$ #{msg}")
117
+ end
118
+
119
+ private
120
+
121
+ def colored(color, str)
122
+ opts[:color] ? super : str
123
+ end
124
+
125
+ def format_msg(msg, args, msgs)
126
+ msg = msgs[msg] % args if msg.is_a?(Symbol)
127
+ msg.strip
128
+ end
129
+
130
+ def with_spacing(str, space)
131
+ str = "\n#{str}" if space && !@last
132
+ @last = space
133
+ str
134
+ end
135
+ end
136
+ end
137
+ end
data/lib/cl/version.rb CHANGED
@@ -1,3 +1,3 @@
1
- module Cl
2
- VERSION = '0.0.4'
1
+ class Cl
2
+ VERSION = '0.1.0'
3
3
  end
data/lib/cl.rb CHANGED
@@ -3,12 +3,19 @@ require 'cl/help'
3
3
  require 'cl/runner/default'
4
4
  require 'cl/runner/multi'
5
5
 
6
- module Cl
6
+ class Cl
7
7
  class Error < StandardError
8
8
  MSGS = {
9
- missing_args: 'Missing arguments (given: %s, required: %s)',
10
- too_many_args: 'Too many arguments (given: %s, allowed: %s)',
11
- wrong_type: 'Wrong argument type (given: %s, expected: %s)'
9
+ missing_args: 'Missing arguments (given: %s, required: %s)',
10
+ too_many_args: 'Too many arguments (given: %s, allowed: %s)',
11
+ wrong_type: 'Wrong argument type (given: %s, expected: %s)',
12
+ exceeding_max: 'Exceeds max value: %s',
13
+ invalid_format: 'Invalid format: %s',
14
+ unknown_values: 'Unknown value: %s',
15
+ required_opt: 'Missing required option: %s',
16
+ required_opts: 'Missing required options: %s',
17
+ requires_opt: 'Missing option: %s',
18
+ requires_opts: 'Missing options: %s',
12
19
  }
13
20
 
14
21
  def initialize(msg, *args)
@@ -17,29 +24,67 @@ module Cl
17
24
  end
18
25
 
19
26
  ArgumentError = Class.new(Error)
27
+ OptionError = Class.new(Error)
28
+ RequiredOpts = Class.new(OptionError)
20
29
 
21
- def included(const)
22
- const.send(:include, Cmd)
30
+ class RequiredsOpts < OptionError
31
+ def initialize(opts)
32
+ opts = opts.map { |alts| alts.map { |alt| Array(alt).join(' and ') }.join(' or ' ) }
33
+ super(:requires_opts, opts.join(', '))
34
+ end
23
35
  end
24
36
 
25
- def run(*args)
26
- runner(*args).run
27
- rescue Error => e
28
- abort [e.message, runner(:help, *args).cmd.help].join("\n\n")
37
+ class RequiresOpts < OptionError
38
+ def initialize(opts)
39
+ msg = opts.size == 1 ? :requires_opt : :requires_opts
40
+ opts = opts.map { |one, other| "#{one} (required by #{other})" }.join(', ')
41
+ super(msg, opts)
42
+ end
43
+ end
44
+
45
+ class ExceedingMax < OptionError
46
+ def initialize(opts)
47
+ opts = opts.map { |opt, max| "#{opt} (max: #{max})" }.join(', ')
48
+ super(:exceeding_max, opts)
49
+ end
29
50
  end
30
51
 
31
- def help(*args)
32
- runner(:help, *args).run
52
+ class InvalidFormat < OptionError
53
+ def initialize(opts)
54
+ opts = opts.map { |opt, format| "#{opt} (format: #{format})" }.join(', ')
55
+ super(:invalid_format, opts)
56
+ end
33
57
  end
34
58
 
35
- attr_writer :runner
36
- @runner = :default
59
+ class UnknownValues < OptionError
60
+ def initialize(opts)
61
+ opts = opts.map { |(key, value, known)| "#{key}=#{value} (known values: #{known.join(', ')})" }.join(', ')
62
+ super(:unknown_values, opts)
63
+ end
64
+ end
65
+
66
+ attr_reader :ctx, :name, :opts
67
+
68
+ def initialize(*args)
69
+ ctx = args.shift if args.first.is_a?(Ctx)
70
+ @opts = args.last.is_a?(Hash) ? args.pop : {}
71
+ @name = args.shift || $0
72
+ @ctx = ctx || Ctx.new(name, opts)
73
+ end
74
+
75
+ def run(args)
76
+ runner(args).run
77
+ rescue Error => e
78
+ abort [e.message, runner(['help', args.first]).cmd.help].join("\n\n")
79
+ end
37
80
 
38
- def runner(*args)
39
- args = args.flatten
40
- runner = args.first.to_s == 'help' ? :default : @runner
41
- Runner.const_get(runner.to_s.capitalize).new(*args)
81
+ def runner(args)
82
+ runner = :default if args.first.to_s == 'help'
83
+ runner ||= opts[:runner] || :default
84
+ Runner.const_get(runner.to_s.capitalize).new(ctx, args)
42
85
  end
43
86
 
44
- extend self
87
+ # def help(*args)
88
+ # runner(:help, *args).run
89
+ # end
45
90
  end