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/cast.rb ADDED
@@ -0,0 +1,29 @@
1
+ class Cl
2
+ module Cast
3
+ TRUE = /^(true|yes|on)$/
4
+ FALSE = /^(false|no|off)$/
5
+
6
+ def cast(value)
7
+ case type
8
+ when nil
9
+ value
10
+ when :array
11
+ Array(value).compact.flatten.map { |value| value.to_s.split(',') }.flatten
12
+ when :string, :str
13
+ value.to_s unless value.to_s.empty?
14
+ when :flag, :boolean, :bool
15
+ return true if value.to_s =~ TRUE
16
+ return false if value.to_s =~ FALSE
17
+ !!value
18
+ when :integer, :int
19
+ Integer(value)
20
+ when :float
21
+ Float(value)
22
+ else
23
+ raise ArgumentError, "Unknown type: #{type}" if value
24
+ end
25
+ rescue ::ArgumentError => e
26
+ raise ArgumentError.new(:wrong_type, value.inspect, type)
27
+ end
28
+ end
29
+ end
data/lib/cl/cmd.rb CHANGED
@@ -1,13 +1,35 @@
1
+ require 'registry'
1
2
  require 'cl/args'
2
- require 'cl/registry'
3
+ require 'cl/helper'
4
+ require 'cl/opts'
5
+ # require 'cl/registry'
3
6
 
4
- module Cl
5
- class Cmd < Struct.new(:args, :opts)
7
+ class Cl
8
+ class Cmd
6
9
  include Registry
7
10
 
8
11
  class << self
9
- def inherited(cmd)
10
- cmd.register underscore(cmd.name.split('::').last)
12
+ include Merge
13
+
14
+ inherited = ->(const) do
15
+ const.register [registry_key, underscore(const.name.split('::').last)].compact.join(':') if const.name
16
+ const.define_singleton_method(:inherited, &inherited)
17
+ end
18
+
19
+ define_method(:inherited, &inherited)
20
+
21
+ def cmds
22
+ registry.values
23
+ end
24
+
25
+ def parse(ctx, args)
26
+ opts = Parser.new(self.opts, args).opts unless self == Help
27
+ opts = merge(ctx.config[registry_key], opts) if ctx.config[registry_key]
28
+ [args, opts]
29
+ end
30
+
31
+ def abstract
32
+ unregister
11
33
  end
12
34
 
13
35
  def args(*args)
@@ -16,22 +38,35 @@ module Cl
16
38
  args.each { |arg| arg(arg, opts) }
17
39
  end
18
40
 
19
- def arg(name, opts = {})
20
- args.define(self, name, opts)
21
- end
22
-
23
- def purpose(purpose = nil)
24
- purpose ? @purpose = purpose : @purpose
41
+ def arg(*args)
42
+ self.args.define(self, *args)
25
43
  end
26
44
 
27
45
  def opt(*args, &block)
28
- opts << [args, block]
46
+ self.opts.define(self, *args, &block)
29
47
  end
30
48
 
31
49
  def opts
32
- @opts ||= superclass != Cmd && superclass.respond_to?(:opts) ? superclass.opts.dup : []
50
+ @opts ||= self == Cmd ? Opts.new : superclass.opts.dup
51
+ end
52
+
53
+ def description(description = nil)
54
+ description ? @description = description : @description
33
55
  end
34
56
 
57
+ def required?
58
+ !!@required
59
+ end
60
+
61
+ def required(*required)
62
+ required.any? ? self.required << required : @required ||= []
63
+ end
64
+
65
+ def summary(summary = nil)
66
+ summary ? @summary = summary : @summary
67
+ end
68
+ alias purpose summary
69
+
35
70
  def underscore(string)
36
71
  string.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
37
72
  gsub(/([a-z\d])([A-Z])/,'\1_\2').
@@ -39,10 +74,23 @@ module Cl
39
74
  end
40
75
  end
41
76
 
42
- def initialize(args, opts)
43
- args = self.class.args.apply(self, args)
44
- opts = self.class::OPTS.merge(opts) if self.class.const_defined?(:OPTS)
45
- super
77
+ opt '--help', 'Get help on this command'
78
+
79
+ attr_reader :ctx, :args
80
+
81
+ def initialize(ctx, args)
82
+ args, opts = self.class.parse(ctx, args)
83
+ @ctx = ctx
84
+ @opts = self.class.opts.apply(self, self.opts.merge(opts || {}))
85
+ @args = @opts[:help] ? args : self.class.args.apply(self, args)
86
+ end
87
+
88
+ def opts
89
+ @opts ||= {}
90
+ end
91
+
92
+ def deprecated_opts
93
+ opts.keys & self.class.opts.deprecated
46
94
  end
47
95
  end
48
96
  end
@@ -0,0 +1,52 @@
1
+ require 'cl/helper'
2
+
3
+ class Cl
4
+ class Config
5
+ class Env < Struct.new(:name)
6
+ include Merge
7
+
8
+ TRUE = /^(true|yes|on)$/
9
+ FALSE = /^(false|no|off)$/
10
+
11
+ def load
12
+ vars = opts.map { |cmd, opts| vars(cmd, opts) }
13
+ merge(*vars.flatten.compact)
14
+ end
15
+
16
+ private
17
+
18
+ def vars(cmd, opts)
19
+ opts.map { |opt| var(cmd, opt, key(cmd, opt)) }
20
+ end
21
+
22
+ def opts
23
+ Cmd.registry.map { |key, cmd| [key, cmd.opts.map(&:name) - [:help]] }
24
+ end
25
+
26
+ def var(cmd, opt, key)
27
+ { cmd => { opt => cast(ENV[key]) } } if ENV[key]
28
+ end
29
+
30
+ def key(*keys)
31
+ [name.upcase, *keys].join('_').upcase.sub('-', '_')
32
+ end
33
+
34
+ def only(hash, *keys)
35
+ hash.select { |key, _| keys.include?(key) }.to_h
36
+ end
37
+
38
+ def cast(value)
39
+ case value
40
+ when TRUE
41
+ true
42
+ when FALSE
43
+ false
44
+ when ''
45
+ false
46
+ else
47
+ value
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,34 @@
1
+ require 'yaml'
2
+ require 'cl/helper'
3
+
4
+ class Cl
5
+ class Config
6
+ class Files < Struct.new(:name)
7
+ include Merge
8
+
9
+ PATHS = %w(
10
+ ~/.%s.yml
11
+ ./.%s.yml
12
+ )
13
+
14
+ def load
15
+ configs.any? ? symbolize(merge(*configs)) : {}
16
+ end
17
+
18
+ private
19
+
20
+ def configs
21
+ @configs ||= paths.map { |path| YAML.load_file(path) || {} }
22
+ end
23
+
24
+ def paths
25
+ paths = PATHS.map { |path| File.expand_path(path % name) }
26
+ paths.select { |path| File.exist?(path) }
27
+ end
28
+
29
+ def symbolize(hash)
30
+ hash.map { |key, value| [key.to_sym, value] }.to_h
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/cl/config.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'cl/config/env'
2
+ require 'cl/config/files'
3
+ require 'cl/helper'
4
+
5
+ class Cl
6
+ class Config
7
+ include Merge
8
+
9
+ attr_reader :name, :opts
10
+
11
+ def initialize(name)
12
+ @name = name
13
+ @opts = load
14
+ end
15
+
16
+ def to_h
17
+ opts
18
+ end
19
+
20
+ private
21
+
22
+ def load
23
+ merge(*sources.map(&:load))
24
+ end
25
+
26
+ def sources
27
+ [Files.new(name), Env.new(name)]
28
+ end
29
+ end
30
+ end
data/lib/cl/ctx.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'forwardable'
2
+ require 'cl/config'
3
+ require 'cl/ui'
4
+
5
+ class Cl
6
+ class Ctx
7
+ extend Forwardable
8
+
9
+ def_delegators :ui, :puts, :stdout, :announce, :info, :notice, :warn,
10
+ :error, :success, :cmd
11
+
12
+ attr_accessor :config, :ui
13
+
14
+ def initialize(name, opts = {})
15
+ @config = Config.new(name).to_h
16
+ @ui = Ui.new(self, opts)
17
+ end
18
+
19
+ def abort(str)
20
+ fail(str) if test?
21
+ ui.error(str)
22
+ exit 1
23
+ end
24
+
25
+ def test?
26
+ ENV['ENV'] == 'test'
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,142 @@
1
+ require 'cl/help/table'
2
+ require 'cl/help/usage'
3
+
4
+ class Cl
5
+ class Help
6
+ class Cmd
7
+ attr_reader :cmd
8
+
9
+ def initialize(cmd)
10
+ @cmd = cmd
11
+ end
12
+
13
+ def format
14
+ [usage, arguments, options, common, summary, description].compact.join("\n\n")
15
+ end
16
+
17
+ def usage
18
+ "Usage: #{Usage.new(cmd).format}"
19
+ end
20
+
21
+ def summary
22
+ ['Summary:', indent(cmd.summary)] if cmd.summary
23
+ end
24
+
25
+ def description
26
+ ['Description:', indent(cmd.description)] if cmd.description
27
+ end
28
+
29
+ def arguments
30
+ ['Arguments:', table(:args)] if args.any?
31
+ end
32
+
33
+ def options
34
+ ['Options:', requireds, table(:opts)].compact if opts.any?
35
+ end
36
+
37
+ def common
38
+ ['Common Options:', table(:cmmn)] if common?
39
+ end
40
+
41
+ def table(name)
42
+ table = send(name)
43
+ indent(table.to_s(width - table.width + 5))
44
+ end
45
+
46
+ def args
47
+ @args ||= begin
48
+ Table.new(cmd.args.map { |arg| [arg.name, format_obj(arg)] })
49
+ end
50
+ end
51
+
52
+ def opts
53
+ @opts ||= begin
54
+ opts = cmd.opts.to_a
55
+ opts = opts - cmd.superclass.opts.to_a if common?
56
+ strs = Table.new(rjust(opts.map { |opt| [*opt.strs] }))
57
+ opts = opts.map { |opt| format_obj(opt) }
58
+ Table.new(strs.rows.zip(opts))
59
+ end
60
+ end
61
+
62
+ def cmmn
63
+ @cmmn ||= begin
64
+ opts = cmd.superclass.opts
65
+ strs = Table.new(rjust(opts.map { |opt| [*opt.strs] }))
66
+ opts = opts.map { |opt| format_obj(opt) }
67
+ Table.new(strs.rows.zip(opts))
68
+ end
69
+ end
70
+
71
+ def requireds
72
+ return unless cmd.required?
73
+ opts = cmd.required
74
+ strs = opts.map { |alts| alts.map { |alt| Array(alt).join(' and ') }.join(', or ' ) }
75
+ strs = strs.map { |str| "Either #{str} are required." }.join("\n")
76
+ indent(strs)
77
+ end
78
+
79
+ def common?
80
+ cmd.superclass < Cl::Cmd
81
+ end
82
+
83
+ def width
84
+ [args.width, opts.width, cmmn.width].max
85
+ end
86
+
87
+ def format_obj(obj)
88
+ opts = []
89
+ opts << "type: #{format_type(obj)}"
90
+ opts << 'required: true' if obj.required?
91
+ opts += format_opt(obj) if obj.is_a?(Opt)
92
+ opts = opts.join(', ')
93
+ opts = "(#{opts})" if obj.description && !opts.empty?
94
+ opts = [obj.description, opts]
95
+ opts.compact.join(' ')
96
+ end
97
+
98
+ def format_opt(opt)
99
+ opts = []
100
+ opts << "alias: #{format_aliases(opt)}" if opt.aliases?
101
+ opts << "requires: #{opt.requires.join(', ')}" if opt.requires?
102
+ opts << "default: #{format_default(opt)}" if opt.default?
103
+ opts << "known values: #{opt.enum.join(', ')}" if opt.enum?
104
+ opts << "format: #{opt.format}" if opt.format?
105
+ opts << "max: #{opt.max}" if opt.max?
106
+ opts << format_deprecated(opt) if opt.deprecated?
107
+ opts.compact
108
+ end
109
+
110
+ def format_aliases(opt)
111
+ opt.aliases.map do |name|
112
+ strs = [name]
113
+ strs << '(deprecated)' if Array(opt.deprecated).include?(name)
114
+ strs.join(' ')
115
+ end.join(', ')
116
+ end
117
+
118
+
119
+ def format_type(obj)
120
+ return obj.type unless obj.is_a?(Opt) && obj.type == :array
121
+ "array (can be given multiple times)"
122
+ end
123
+
124
+ def format_default(opt)
125
+ opt.default.is_a?(Symbol) ? opt.default.to_s.sub('_', ' ') : opt.default
126
+ end
127
+
128
+ def format_deprecated(opt)
129
+ return 'deprecated' if opt.deprecated == [opt.name]
130
+ end
131
+
132
+ def rjust(objs)
133
+ width = objs.max_by(&:size).size
134
+ objs.map { |objs| [*Array.new(width - objs.size) { '' }, *objs] }
135
+ end
136
+
137
+ def indent(str)
138
+ str.lines.map { |line| " #{line}" }.join
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,28 @@
1
+ require 'cl/help/table'
2
+ require 'cl/help/usage'
3
+
4
+ class Cl
5
+ class Help
6
+ class Cmds
7
+ HEAD = %(Type "#{$0.split('/').last} help COMMAND [SUBCOMMAND]" for more details:\n)
8
+
9
+ attr_reader :cmds
10
+
11
+ def initialize(cmds)
12
+ @cmds = cmds
13
+ end
14
+
15
+ def format
16
+ [HEAD, Table.new(list).format].join("\n")
17
+ end
18
+
19
+ def list
20
+ cmds.any? ? cmds.map { |cmd| format_cmd(cmd) } : [['[no commands]']]
21
+ end
22
+
23
+ def format_cmd(cmd)
24
+ ["#{Usage.new(cmd).format}", cmd.summary]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,45 @@
1
+ class Cl
2
+ class Help
3
+ class Table
4
+ attr_reader :data, :padding
5
+
6
+ def initialize(data)
7
+ @data = data
8
+ end
9
+
10
+ def any?
11
+ data.any?
12
+ end
13
+
14
+ def format(padding = 8)
15
+ @padding = padding
16
+ rows.join("\n")
17
+ end
18
+ alias to_s format
19
+
20
+ def rows
21
+ data.map { |row| cells(row).join(' ').rstrip }
22
+ end
23
+
24
+ def cells(row)
25
+ row.map.with_index { |cell, ix| cell.to_s.ljust(widths[ix]) }
26
+ end
27
+
28
+ def width
29
+ widths = cols[0..-2].map { |col| col.max_by(&:size).size }.inject(&:+).to_i
30
+ widths + cols.size - 1
31
+ end
32
+
33
+ def widths
34
+ cols.map.with_index do |col, ix|
35
+ width = col.compact.max_by(&:size)&.size
36
+ ix < cols.size - 2 ? width.to_i : width.to_i + padding.to_i
37
+ end
38
+ end
39
+
40
+ def cols
41
+ @cols ||= data.transpose
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,9 +1,15 @@
1
- module Cl
2
- class Format
3
- class Usage < Struct.new(:cmd)
1
+ class Cl
2
+ class Help
3
+ class Usage
4
+ attr_reader :cmd
5
+
6
+ def initialize(cmd)
7
+ @cmd = cmd
8
+ end
9
+
4
10
  def format
5
11
  usage = [$0.split('/').last, name]
6
- usage += cmd.args.map(&:to_s)
12
+ usage += cmd.args.map(&:to_s) # { |arg| "[#{arg}]" }
7
13
  usage << '[options]' if opts?
8
14
  usage.join(' ')
9
15
  end
data/lib/cl/help.rb CHANGED
@@ -1,33 +1,31 @@
1
- require 'cl/format/cmd'
2
- require 'cl/format/list'
3
-
4
- module Cl
1
+ class Cl
5
2
  class Help < Cl::Cmd
6
3
  register :help
7
4
 
8
5
  def run
9
- puts help
6
+ ctx.puts help
10
7
  end
11
8
 
12
9
  def help
13
- cmd ? Format::Cmd.new(cmd).format : Format::List.new(cmds).format
10
+ args.any? ? Cmd.new(cmd).format : Cmds.new(cmds).format
14
11
  end
15
12
 
16
13
  private
17
14
 
18
15
  def cmds
19
- cmds = Cl.cmds.reject { |cmd| cmd.registry_key == :help }
16
+ cmds = Cl::Cmd.cmds.reject { |cmd| cmd.registry_key == :help }
20
17
  key = args.join(':') if args
21
18
  cmds = cmds.select { |cmd| cmd.registry_key.to_s.start_with?(key) } if key
22
19
  cmds
23
20
  end
24
21
 
25
22
  def cmd
26
- args.inject([[], []]) do |(args, cmds), arg|
27
- args << arg
28
- cmds << Cl[args.join(':')]
29
- [args, cmds.compact]
30
- end.last.last
23
+ key = args.join(':')
24
+ return Cl::Cmd[key] if Cl::Cmd.registered?(key)
25
+ ctx.abort("Unknown command: #{key}")
31
26
  end
32
27
  end
33
28
  end
29
+
30
+ require 'cl/help/cmd'
31
+ require 'cl/help/cmds'
data/lib/cl/helper.rb ADDED
@@ -0,0 +1,11 @@
1
+ class Cl
2
+ module Merge
3
+ MERGE = ->(key, lft, rgt) do
4
+ lft.is_a?(Hash) && rgt.is_a?(Hash) ? lft.merge(rgt, &MERGE) : rgt
5
+ end
6
+
7
+ def merge(*objs)
8
+ objs.inject({}) { |lft, rgt| lft.merge(rgt, &MERGE) }
9
+ end
10
+ end
11
+ end