cl 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +1 -0
  3. data/Gemfile.lock +1 -1
  4. data/NOTES.md +4 -0
  5. data/README.md +131 -74
  6. data/examples/README.md +22 -0
  7. data/examples/{src → _src}/args/cast.erb.rb +0 -0
  8. data/examples/{src → _src}/args/opts.erb.rb +0 -0
  9. data/examples/{src → _src}/args/required.erb.rb +0 -0
  10. data/examples/{src → _src}/args/splat.erb.rb +0 -0
  11. data/examples/{src → _src}/gem.erb.rb +0 -0
  12. data/examples/{src → _src}/heroku.erb.rb +0 -0
  13. data/examples/{src → _src}/rakeish.erb.rb +0 -0
  14. data/examples/{src → _src}/readme/abstract.erb.rb +0 -0
  15. data/examples/{src → _src}/readme/alias.erb.rb +0 -0
  16. data/examples/{src → _src}/readme/arg.erb.rb +0 -0
  17. data/examples/{src → _src}/readme/arg_array.erb.rb +0 -0
  18. data/examples/{src → _src}/readme/arg_type.erb.rb +0 -0
  19. data/examples/{src → _src}/readme/args_splat.erb.rb +0 -0
  20. data/examples/{src → _src}/readme/array.erb.rb +0 -0
  21. data/examples/_src/readme/basic.erb.rb +70 -0
  22. data/examples/{src → _src}/readme/default.erb.rb +0 -0
  23. data/examples/{src → _src}/readme/deprecated.erb.rb +0 -0
  24. data/examples/{src → _src}/readme/deprecated_alias.erb.rb +0 -0
  25. data/examples/_src/readme/description.erb.rb +58 -0
  26. data/examples/{src → _src}/readme/downcase.erb.rb +0 -0
  27. data/examples/{src → _src}/readme/enum.erb.rb +0 -0
  28. data/examples/{src → _src}/readme/example.erb.rb +0 -0
  29. data/examples/{src → _src}/readme/format.erb.rb +0 -0
  30. data/examples/{src → _src}/readme/internal.erb.rb +0 -0
  31. data/examples/{src → _src}/readme/negate.erb.rb +0 -0
  32. data/examples/{src/readme/node.erb.rb → _src/readme/note.erb.rb} +0 -0
  33. data/examples/{src → _src}/readme/opts.erb.rb +0 -0
  34. data/examples/{src → _src}/readme/opts_block.erb.rb +0 -0
  35. data/examples/{src → _src}/readme/range.erb.rb +0 -0
  36. data/examples/_src/readme/registry.erb.rb +16 -0
  37. data/examples/{src → _src}/readme/required.erb.rb +0 -0
  38. data/examples/{src → _src}/readme/requireds.erb.rb +0 -0
  39. data/examples/{src → _src}/readme/requires.erb.rb +0 -0
  40. data/examples/_src/readme/runner.erb.rb +27 -0
  41. data/examples/_src/readme/runner_custom.erb.rb +25 -0
  42. data/examples/{src → _src}/readme/secret.erb.rb +0 -0
  43. data/examples/{src → _src}/readme/see.erb.rb +0 -0
  44. data/examples/{src → _src}/readme/type.erb.rb +0 -0
  45. data/examples/readme/basic +65 -0
  46. data/examples/readme/description +54 -0
  47. data/examples/readme/note +6 -4
  48. data/examples/readme/registry +13 -0
  49. data/examples/readme/runner +28 -0
  50. data/examples/readme/runner_custom +22 -0
  51. data/lib/cl/args.rb +10 -18
  52. data/lib/cl/cast.rb +46 -24
  53. data/lib/cl/help/cmd.rb +3 -55
  54. data/lib/cl/help/format.rb +69 -0
  55. data/lib/cl/help/table.rb +5 -2
  56. data/lib/cl/opt.rb +4 -0
  57. data/lib/cl/opts/validate.rb +117 -0
  58. data/lib/cl/opts.rb +11 -101
  59. data/lib/cl/version.rb +1 -1
  60. metadata +49 -36
  61. data/examples/readme/node +0 -19
@@ -0,0 +1,65 @@
1
+ require 'cl'
2
+
3
+ # e.g. lib/owners/add.rb
4
+ module Owners
5
+ class Add < Cl::Cmd
6
+ summary 'Add one or more owners to an existing owner group'
7
+
8
+ description <<~str
9
+ Use this command to add one or more owners to an existing
10
+ owner group.
11
+
12
+ [...]
13
+ str
14
+
15
+ args :owner
16
+
17
+ opt '-t', '--to TO', 'An existing owner group'
18
+
19
+ def run
20
+ # implement adding the owner as given in `owner` (as well as `args`)
21
+ # to the group given in `to` (as well as `opts[:to]`).
22
+ p owner: owner, to: to, to?: to?, args: args, opts: opts
23
+ end
24
+ end
25
+ end
26
+
27
+ # Running this, e.g. using `bin/owners add one,two --to group` will instantiate the
28
+ # class `Owners::Add`, and call the method `run` on it.
29
+
30
+ # e.g. bin/owners
31
+ #
32
+ # args normally would be ARGV
33
+ args = %w(add one --to group)
34
+
35
+ Cl.new('owners').run(args)
36
+
37
+ # Output:
38
+ #
39
+ # {:owner=>"one", :to=>"group", :to?=>true, :args=>["one"], :opts=>{:to=>"group"}}
40
+
41
+ Cl.new('owners').run(%w(add --help))
42
+
43
+ # Output:
44
+ #
45
+ # Usage: owners add [owner] [options]
46
+ #
47
+ # Summary:
48
+ #
49
+ # Add one or more owners to an existing owner group
50
+ #
51
+ # Description:
52
+ #
53
+ # Use this command to add one or more owners to an existing
54
+ # owner group.
55
+ #
56
+ # [...]
57
+ #
58
+ # Arguments:
59
+ #
60
+ # owner type: string
61
+ #
62
+ # Options:
63
+ #
64
+ # -t --to TO An existing owner group (type: string)
65
+ # --help Get help on this command
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path('lib')
3
+
4
+ require 'cl'
5
+
6
+ module Owners
7
+ class Add < Cl::Cmd
8
+ summary 'Add one or more owners to an existing owner group'
9
+
10
+ description <<~str
11
+ Use this command to add one or more owners to an existing
12
+ owner group.
13
+ str
14
+
15
+ examples <<~str
16
+ Adding a single user to the group admins:
17
+
18
+ owners add user --to admins
19
+
20
+ Adding a several users at once:
21
+
22
+ owners add one two three --to admins
23
+ str
24
+ end
25
+ end
26
+
27
+ Cl.new('owners').run(%w(add --help))
28
+
29
+ # Output:
30
+ #
31
+ # Usage: owners add [options]
32
+ #
33
+ # Summary:
34
+ #
35
+ # Add one or more owners to an existing owner group
36
+ #
37
+ # Description:
38
+ #
39
+ # Use this command to add one or more owners to an existing
40
+ # owner group.
41
+ #
42
+ # Options:
43
+ #
44
+ # --help Get help on this command
45
+ #
46
+ # Examples:
47
+ #
48
+ # Adding a single user to the group admins:
49
+ #
50
+ # owners add user --to admins
51
+ #
52
+ # Adding a several users at once:
53
+ #
54
+ # owners add one two three --to admins
data/examples/readme/note CHANGED
@@ -9,9 +9,11 @@ end
9
9
 
10
10
  Cl.new('owners').run(%w(add --help))
11
11
 
12
- # Usage: owners add [options]
12
+ # Output:
13
13
  #
14
- # Options:
14
+ # Usage: owners add [options]
15
15
  #
16
- # --to GROUP type: string, note: needs to be a group
17
- # --help Get help on this command
16
+ # Options:
17
+ #
18
+ # --to GROUP type: string, note: needs to be a group
19
+ # --help Get help on this command
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path('lib')
3
+
4
+ module Cmd
5
+ class One < Cl::Cmd
6
+ end
7
+
8
+ class Two < Cl::Cmd
9
+ end
10
+ end
11
+
12
+ p Cl::Cmd[:one] # => Cmd::One
13
+ p Cl::Cmd[:two] # => Cmd::Two
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path('lib')
3
+
4
+ module Git
5
+ class Pull < Cl::Cmd
6
+ register :'git:pull'
7
+
8
+ def run
9
+ p cmd: registry_key, args: args
10
+ end
11
+ end
12
+ end
13
+
14
+ # With this class registered (and assuming the executable that calls `Cl` is
15
+ # `bin/run`) the default runner would recognize and run it:
16
+ #
17
+ # $ bin/run git:pull master # instantiates Git::Pull, and passes ["master"] as args
18
+ # $ bin/run git pull master # does the same
19
+
20
+ Cl.new('run').run(%w(git:pull master))
21
+ # Output:
22
+ #
23
+ # {:cmd=>:"git:pull", :args=>["master"]}
24
+
25
+ Cl.new('run').run(%w(git pull master))
26
+ # Output:
27
+ #
28
+ # {:cmd=>:"git:pull", :args=>["master"]}
@@ -0,0 +1,22 @@
1
+ # anywhere in your library
2
+
3
+ require 'cl'
4
+
5
+ class Runner
6
+ Cl::Runner.register :custom, self
7
+
8
+ def initialize(ctx, args)
9
+ # ...
10
+ end
11
+
12
+ def run
13
+ const = identify_cmd_class_from_args
14
+ const.new(ctx, args).run
15
+ end
16
+ end
17
+
18
+ # in bin/run
19
+ Cl.new('run', runner: :custom).run(ARGV)
20
+
21
+
22
+
data/lib/cl/args.rb CHANGED
@@ -13,11 +13,11 @@ class Cl
13
13
  self.args << arg
14
14
  end
15
15
 
16
- def apply(cmd, args, opts)
17
- return args if self.args.empty? || opts[:help]
18
- args = grouped(args)
19
- validate(args)
20
- args.map { |(arg, value)| arg.set(cmd, value) }.flatten(1)
16
+ def apply(cmd, values, opts)
17
+ return values if args.empty? || opts[:help]
18
+ values = splat(values) if splat?
19
+ validate(values)
20
+ args.zip(values).map { |(arg, value)| arg.set(cmd, value) }.flatten(1).compact
21
21
  end
22
22
 
23
23
  def each(&block)
@@ -51,19 +51,11 @@ class Cl
51
51
  select(&:required?).size
52
52
  end
53
53
 
54
- def grouped(values)
55
- values.inject([0, {}]) do |(ix, group), value|
56
- arg = args[ix]
57
- if arg && arg.splat?
58
- group[arg] ||= []
59
- group[arg] << value
60
- ix += 1 if args.size + group[arg].size > values.size
61
- else
62
- group[arg] = value
63
- ix += 1
64
- end
65
- [ix, group]
66
- end.last
54
+ def splat(values)
55
+ args.each.with_index.inject([]) do |group, (arg, ix)|
56
+ count = arg && arg.splat? ? [values.size - args.size + ix + 1] : []
57
+ group << values.shift(*count)
58
+ end
67
59
  end
68
60
  end
69
61
  end
data/lib/cl/cast.rb CHANGED
@@ -1,33 +1,55 @@
1
1
  class Cl
2
2
  module Cast
3
- TRUE = /^(true|yes|on)$/
4
- FALSE = /^(false|no|off)$/
3
+ class Cast < Struct.new(:type, :value, :opts)
4
+ TRUE = /^(true|yes|on)$/
5
+ FALSE = /^(false|no|off)$/
5
6
 
6
- def cast(value)
7
- case type
8
- when nil
9
- value
10
- when :array
11
- Array(value).compact.flatten.map { |value| split(value) }.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
7
+ def apply
8
+ return send(type) if respond_to?(type, true)
9
+ raise ArgumentError, "Unknown type: #{type}"
10
+ rescue ::ArgumentError => e
11
+ raise ArgumentError.new(:wrong_type, value.inspect, type)
24
12
  end
25
- rescue ::ArgumentError => e
26
- raise ArgumentError.new(:wrong_type, value.inspect, type)
13
+
14
+ private
15
+
16
+ def array
17
+ Array(value).compact.flatten.map { |value| split(value) }.flatten.compact
18
+ end
19
+
20
+ def string
21
+ value.to_s unless value.to_s.empty?
22
+ end
23
+ alias str string
24
+
25
+ def boolean
26
+ return true if value.to_s =~ TRUE
27
+ return false if value.to_s =~ FALSE
28
+ !!value
29
+ end
30
+ alias bool boolean
31
+ alias flag boolean
32
+
33
+ def int
34
+ Integer(value)
35
+ end
36
+ alias integer int
37
+
38
+ def float
39
+ Float(value)
40
+ end
41
+
42
+ def split(value)
43
+ separator ? value.to_s.split(separator) : value
44
+ end
45
+
46
+ def separator
47
+ opts[:separator]
48
+ end
27
49
  end
28
50
 
29
- def split(value)
30
- separator ? value.to_s.split(separator) : value
51
+ def cast(value)
52
+ type ? Cast.new(type, value, separator: separator).apply : value
31
53
  end
32
54
  end
33
55
  end
data/lib/cl/help/cmd.rb CHANGED
@@ -1,10 +1,11 @@
1
+ require 'cl/help/format'
1
2
  require 'cl/help/table'
2
3
  require 'cl/help/usage'
3
4
 
4
5
  class Cl
5
6
  class Help
6
7
  class Cmd < Struct.new(:ctx, :cmd)
7
- include Regex
8
+ include Format
8
9
 
9
10
  def format
10
11
  [usage, summary, description, arguments, options, common, examples].compact.join("\n\n")
@@ -86,59 +87,6 @@ class Cl
86
87
  [args.width, opts.width, cmmn.width].max
87
88
  end
88
89
 
89
- def format_obj(obj)
90
- opts = []
91
- opts << "type: #{format_type(obj)}" unless obj.type == :flag
92
- opts << 'required: true' if obj.required?
93
- opts += format_opt(obj) if obj.is_a?(Opt)
94
- opts = opts.join(', ')
95
- opts = "(#{opts})" if obj.description && !opts.empty?
96
- opts = [obj.description, opts]
97
- opts.compact.map(&:strip).join(' ')
98
- end
99
-
100
- def format_opt(opt)
101
- opts = []
102
- opts << "alias: #{format_aliases(opt)}" if opt.aliases?
103
- opts << "requires: #{opt.requires.join(', ')}" if opt.requires?
104
- opts << "default: #{format_default(opt)}" if opt.default?
105
- opts << "known values: #{format_enum(opt)}" if opt.enum?
106
- opts << "format: #{opt.format}" if opt.format?
107
- opts << "downcase: true" if opt.downcase?
108
- opts << "min: #{opt.min}" if opt.min?
109
- opts << "max: #{opt.max}" if opt.max?
110
- opts << "e.g.: #{opt.example}" if opt.example?
111
- opts << "note: #{opt.note}" if opt.note?
112
- opts << "see: #{opt.see}" if opt.see?
113
- opts << format_deprecated(opt) if opt.deprecated?
114
- opts.compact
115
- end
116
-
117
- def format_aliases(opt)
118
- opt.aliases.map do |name|
119
- strs = [name]
120
- strs << "(deprecated, please use #{opt.name})" if opt.deprecated[0] == name
121
- strs.join(' ')
122
- end.join(', ')
123
- end
124
-
125
- def format_enum(opt)
126
- opt.enum.map { |value| format_regex(value) }.join(', ')
127
- end
128
-
129
- def format_type(obj)
130
- return obj.type unless obj.is_a?(Opt) && obj.type == :array
131
- "array (string, can be given multiple times)"
132
- end
133
-
134
- def format_default(opt)
135
- opt.default.is_a?(Symbol) ? opt.default.to_s.sub('_', ' ') : opt.default
136
- end
137
-
138
- def format_deprecated(opt)
139
- return "deprecated (#{opt.deprecated[1]})" if opt.deprecated[0] == opt.name
140
- end
141
-
142
90
  def rjust(objs)
143
91
  return objs unless objs.any?
144
92
  width = objs.max_by(&:size).size
@@ -146,7 +94,7 @@ class Cl
146
94
  end
147
95
 
148
96
  def indent(str)
149
- str.lines.map { |line| " #{line}" }.join
97
+ str.lines.map { |line| " #{line}".rstrip }.join("\n")
150
98
  end
151
99
  end
152
100
  end
@@ -0,0 +1,69 @@
1
+ class Cl
2
+ class Help
3
+ module Format
4
+ def format_obj(obj)
5
+ Obj.new(obj).format
6
+ end
7
+
8
+ class Obj < Struct.new(:obj)
9
+ def format
10
+ opts = []
11
+ opts << "type: #{type(obj)}" unless obj.type == :flag
12
+ opts << 'required: true' if obj.required?
13
+ opts += Opt.new(obj).format if obj.is_a?(Cl::Opt)
14
+ opts = opts.join(', ')
15
+ opts = "(#{opts})" if obj.description && !opts.empty?
16
+ opts = [obj.description, opts]
17
+ opts.compact.map(&:strip).join(' ')
18
+ end
19
+
20
+ def type(obj)
21
+ return obj.type unless obj.is_a?(Cl::Opt) && obj.type == :array
22
+ "array (string, can be given multiple times)"
23
+ end
24
+ end
25
+
26
+ class Opt < Struct.new(:opt)
27
+ include Regex
28
+
29
+ def format
30
+ opts = []
31
+ opts << "alias: #{format_aliases(opt)}" if opt.aliases?
32
+ opts << "requires: #{opt.requires.join(', ')}" if opt.requires?
33
+ opts << "default: #{format_default(opt)}" if opt.default?
34
+ opts << "known values: #{format_enum(opt)}" if opt.enum?
35
+ opts << "format: #{opt.format}" if opt.format?
36
+ opts << "downcase: true" if opt.downcase?
37
+ opts << "upcase: true" if opt.upcase?
38
+ opts << "min: #{opt.min}" if opt.min?
39
+ opts << "max: #{opt.max}" if opt.max?
40
+ opts << "e.g.: #{opt.example}" if opt.example?
41
+ opts << "note: #{opt.note}" if opt.note?
42
+ opts << "see: #{opt.see}" if opt.see?
43
+ opts << format_deprecated(opt) if opt.deprecated?
44
+ opts.compact
45
+ end
46
+
47
+ def format_aliases(opt)
48
+ opt.aliases.map do |name|
49
+ strs = [name]
50
+ strs << "(deprecated, please use #{opt.name})" if opt.deprecated[0] == name
51
+ strs.join(' ')
52
+ end.join(', ')
53
+ end
54
+
55
+ def format_enum(opt)
56
+ opt.enum.map { |value| format_regex(value) }.join(', ')
57
+ end
58
+
59
+ def format_default(opt)
60
+ opt.default.is_a?(Symbol) ? opt.default.to_s.sub('_', ' ') : opt.default
61
+ end
62
+
63
+ def format_deprecated(opt)
64
+ return "deprecated (#{opt.deprecated[1]})" if opt.deprecated[0] == opt.name
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
data/lib/cl/help/table.rb CHANGED
@@ -42,11 +42,14 @@ class Cl
42
42
  def widths
43
43
  cols.map.with_index do |col, ix|
44
44
  max = col.compact.max_by(&:size)
45
- width = max ? max.size : 0
46
- ix < cols.size - 2 ? width : width + padding.to_i
45
+ pad(max ? max.size : 0, ix)
47
46
  end
48
47
  end
49
48
 
49
+ def pad(width, ix)
50
+ ix < cols.size - 2 ? width : width + padding.to_i
51
+ end
52
+
50
53
  def cols
51
54
  @cols ||= data.transpose
52
55
  end
data/lib/cl/opt.rb CHANGED
@@ -179,6 +179,10 @@ class Cl
179
179
  opts[:sep]
180
180
  end
181
181
 
182
+ def upcase?
183
+ !!opts[:upcase]
184
+ end
185
+
182
186
  def block
183
187
  # raise if no block was given, and the option's name cannot be inferred
184
188
  super || method(:assign)
@@ -0,0 +1,117 @@
1
+ require 'cl/helper'
2
+
3
+ class Cl
4
+ class Opts
5
+ module Validate
6
+ def validate(cmd, opts, values)
7
+ Validate.constants.each do |name|
8
+ next if name == :Validator
9
+ const = Validate.const_get(name)
10
+ const.new(cmd, opts, values).apply
11
+ end
12
+ end
13
+
14
+ class Validator < Struct.new(:cmd, :opts, :values)
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| values.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
+ value = values[opt.name]
90
+ next unless value && !opt.known?(value)
91
+ known = opt.enum.map { |str| format_regex(str) }
92
+ [opt.name, value, 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