cl 1.0.0 → 1.0.1

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.
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