cl 0.1.12 → 0.1.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path('lib')
3
+
4
+ require 'cl'
5
+
6
+ class Add < Cl::Cmd
7
+ opt '--to GROUP'
8
+ opt '--retries INT', requires: :to
9
+
10
+ def run
11
+ p to, retries
12
+ end
13
+ end
14
+
15
+ Cl.new('owners').run(%w(add --to one --retries 1))
16
+
17
+ # Output:
18
+ #
19
+ # "one"
20
+ # 1
21
+
22
+ Cl.new('owners').run(%w(add --retries 1))
23
+
24
+ # Missing option: to (required by retries)
25
+ #
26
+ # Usage: requires add [options]
27
+ #
28
+ # Options:
29
+ #
30
+ # --to GROUP type: string
31
+ # --retries INT type: string, requires: to
32
+ # --help Get help on this command
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path('lib')
3
+
4
+ require 'cl'
5
+
6
+ class Add < Cl::Cmd
7
+ opt '--to GROUP', see: 'https://docs.io/cli/owners/add'
8
+
9
+ def run
10
+ p retries
11
+ end
12
+ end
13
+
14
+ Cl.new('owners').run(%w(add --help))
15
+
16
+ # Usage: see add [options]
17
+ #
18
+ # Options:
19
+ #
20
+ # --to GROUP type: string, see: https://docs.io/cli/owners/add
21
+ # --help Get help on this command
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path('lib')
3
+
4
+ require 'cl'
5
+
6
+ class Add < Cl::Cmd
7
+ opt '--active BOOL', type: :boolean
8
+ opt '--retries INT', type: :integer
9
+ opt '--sleep FLOAT', type: :float
10
+
11
+ def run
12
+ p active.class, retries.class, sleep.class
13
+ end
14
+ end
15
+
16
+ Cl.new('owners').run(%w(add --active yes --retries 1 --sleep 0.1))
17
+
18
+ # Output:
19
+ #
20
+ # TrueClass
21
+ # Integer
22
+ # Float
data/lib/cl.rb CHANGED
@@ -1,70 +1,17 @@
1
1
  require 'cl/cmd'
2
2
  require 'cl/help'
3
- require 'cl/runner/default'
4
- require 'cl/runner/multi'
3
+ require 'cl/runner'
4
+ require 'cl/errors'
5
5
 
6
6
  class Cl
7
- class Error < StandardError
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)',
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',
19
- }
20
-
21
- def initialize(msg, *args)
22
- super(MSGS[msg] ? MSGS[msg] % args : msg)
23
- end
24
- end
25
-
26
- ArgumentError = Class.new(Error)
27
- OptionError = Class.new(Error)
28
- RequiredOpts = Class.new(OptionError)
29
-
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
35
- end
36
-
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
50
- end
51
-
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
57
- end
58
-
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
7
  attr_reader :ctx, :name, :opts
67
8
 
9
+ # @overload initialize(ctx, name, opts)
10
+ # @param ctx [Cl::Ctx] the current execution context (optional)
11
+ # @param name [String] the program (executable) name (optional, defaults to $0)
12
+ # @param opts [Hash] options (optional)
13
+ # @option opts [Cl::Runner] :runner registry key for a runner (optional, defaults to :default)
14
+ # @option opts [Cl::Ui] :ui the ui for handling user interaction
68
15
  def initialize(*args)
69
16
  ctx = args.shift if args.first.is_a?(Ctx)
70
17
  @opts = args.last.is_a?(Hash) ? args.pop : {}
@@ -72,19 +19,32 @@ class Cl
72
19
  @ctx = ctx || Ctx.new(name, opts)
73
20
  end
74
21
 
22
+ # Runs the command.
23
+ #
24
+ # Instantiates a runner with the given arguments, and runs it.
25
+ #
26
+ # If the command fails (raises a Cl::Error) then the exception is caught, and
27
+ # the process aborted with the error message and help output for the given
28
+ # command.
29
+ #
30
+ # @param args [Array<String>] arguments (usually ARGV)
75
31
  def run(args)
76
32
  runner(args.map(&:dup)).run
33
+ rescue UnknownCmd => e
34
+ ctx.abort e
77
35
  rescue Error => e
78
- abort [e.message, runner(['help', args.first]).cmd.help].join("\n\n")
36
+ ctx.abort e, help(args.first)
79
37
  end
80
38
 
39
+ # Returns a runner instance for the given arguments.
81
40
  def runner(args)
82
41
  runner = :default if args.first.to_s == 'help'
83
42
  runner ||= opts[:runner] || :default
84
- Runner.const_get(runner.to_s.capitalize).new(ctx, args)
43
+ Runner[runner].new(ctx, args)
85
44
  end
86
45
 
87
- # def help(*args)
88
- # runner(:help, *args).run
89
- # end
46
+ # Returns help output for the given command
47
+ def help(*args)
48
+ runner(['help', *args]).cmd.help
49
+ end
90
50
  end
@@ -20,20 +20,24 @@ class Cl
20
20
  opts[:description]
21
21
  end
22
22
 
23
- def splat?
24
- type == :array
25
- end
26
-
27
23
  def required?
28
24
  !!opts[:required]
29
25
  end
30
26
 
27
+ def separator
28
+ opts[:sep]
29
+ end
30
+
31
+ def splat?
32
+ opts[:splat] && type == :array
33
+ end
34
+
31
35
  def to_s
32
36
  str = name
33
37
  case type
34
38
  when :array then str = "#{str}.."
35
- when :integer, :int then str = "#{str}:int"
36
39
  when :boolean, :bool then str = "#{str}:bool"
40
+ when :integer, :int then str = "#{str}:int"
37
41
  when :float then str = "#{str}:float"
38
42
  end
39
43
  required? ? str : "[#{str}]"
@@ -8,7 +8,7 @@ class Cl
8
8
  when nil
9
9
  value
10
10
  when :array
11
- Array(value).compact.flatten.map { |value| value.to_s.split(',') }.flatten
11
+ Array(value).compact.flatten.map { |value| split(value) }.flatten
12
12
  when :string, :str
13
13
  value.to_s unless value.to_s.empty?
14
14
  when :flag, :boolean, :bool
@@ -25,5 +25,9 @@ class Cl
25
25
  rescue ::ArgumentError => e
26
26
  raise ArgumentError.new(:wrong_type, value.inspect, type)
27
27
  end
28
+
29
+ def split(value)
30
+ separator ? value.to_s.split(separator) : value
31
+ end
28
32
  end
29
33
  end
@@ -1,21 +1,28 @@
1
1
  require 'registry'
2
2
  require 'cl/args'
3
- require 'cl/helper'
3
+ require 'cl/dsl'
4
4
  require 'cl/opts'
5
5
  require 'cl/parser'
6
6
 
7
7
  class Cl
8
+ # Base class for all command classes that can be run.
9
+ #
10
+ # Inherit your command classes from this class, use the {Cl::Cmd::Dsl} to
11
+ # declare arguments, options, summary, description, examples etc., and
12
+ # implement the method #run.
13
+ #
14
+ # See {Cl::Cmd::Dsl} for details on the DSL methods.
8
15
  class Cmd
9
16
  include Registry
17
+ extend Dsl
10
18
 
11
19
  class << self
12
- include Merge
20
+ include Merge, Underscore
13
21
 
14
22
  inherited = ->(const) do
15
23
  const.register [registry_key, underscore(const.name.split('::').last)].compact.join(':') if const.name
16
24
  const.define_singleton_method(:inherited, &inherited)
17
25
  end
18
-
19
26
  define_method(:inherited, &inherited)
20
27
 
21
28
  def cmds
@@ -27,51 +34,6 @@ class Cl
27
34
  opts = merge(ctx.config[registry_key], opts) if ctx.config[registry_key]
28
35
  [args, opts || {}]
29
36
  end
30
-
31
- def abstract
32
- unregister
33
- end
34
-
35
- def args(*args)
36
- return @args ||= Args.new unless args.any?
37
- opts = args.last.is_a?(Hash) ? args.pop : {}
38
- args.each { |arg| arg(arg, opts) }
39
- end
40
-
41
- def arg(*args)
42
- self.args.define(self, *args)
43
- end
44
-
45
- def opt(*args, &block)
46
- self.opts.define(self, *args, &block)
47
- end
48
-
49
- def opts
50
- @opts ||= self == Cmd ? Opts.new : superclass.opts.dup
51
- end
52
-
53
- def description(description = nil)
54
- description ? @description = description : @description
55
- end
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
-
70
- def underscore(string)
71
- string.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
72
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
73
- downcase
74
- end
75
37
  end
76
38
 
77
39
  opt '--help', 'Get help on this command'
@@ -92,7 +54,7 @@ class Cl
92
54
  def deprecated_opts
93
55
  opts = self.class.opts.select(&:deprecated?)
94
56
  opts = opts.select { |opt| self.opts.key?(opt.deprecated[0]) }
95
- opts.map(&:deprecated)
57
+ opts.map(&:deprecated).to_h
96
58
  end
97
59
  end
98
60
  end
@@ -13,13 +13,11 @@ class Cl
13
13
 
14
14
  def initialize(name, opts = {})
15
15
  @config = Config.new(name).to_h
16
- @ui = Ui.new(self, opts)
16
+ @ui = opts[:ui] || Ui.new(self, opts)
17
17
  end
18
18
 
19
- def abort(str)
20
- fail(str) if test?
21
- ui.error(str)
22
- exit 1
19
+ def abort(error, *strs)
20
+ ui.abort(error, *strs)
23
21
  end
24
22
 
25
23
  def test?
@@ -0,0 +1,176 @@
1
+ require 'cl/helper'
2
+
3
+ class Cl
4
+ class Cmd
5
+ module Dsl
6
+ include Merge, Underscore
7
+
8
+ def abstract
9
+ unregister
10
+ end
11
+
12
+ # Declare multiple arguments at once
13
+ #
14
+ # See {Cl::Cmd::Dsl#arg} for more details.
15
+ def args(*args)
16
+ return @args ||= Args.new unless args.any?
17
+ opts = args.last.is_a?(Hash) ? args.pop : {}
18
+ args.each { |arg| arg(arg, opts) }
19
+ end
20
+
21
+ # Declares an argument
22
+ #
23
+ # Use this method to declare arguments the command accepts.
24
+ #
25
+ # For example:
26
+ #
27
+ # ```ruby
28
+ # class GitPush < Cl::Cmd
29
+ # arg remote, 'The Git remote to push to.', type: :string
30
+ # end
31
+ # ```
32
+ #
33
+ # Arguments do not need to be declared, in order to be passed to the Cmd
34
+ # instance, but it is useful to do so for more explicit help output, and
35
+ # in order to define extra properties on the arguments (e.g. their type).
36
+ #
37
+ # @overload arg(name, description, opts)
38
+ # @param name [String] the argument name
39
+ # @param description [String] description for the argument, shown in the help output
40
+ # @param opts [Hash] argument options
41
+ # @option opts [Symbol] :type the argument type (`:array`, `:string`, `:integer`, `:float`, `:boolean`)
42
+ # @option opts [Boolean] :required whether the argument is required
43
+ # @option opts [String] :sep separator to split strings by, if the argument is an array
44
+ # @option opts [Boolean] :splat whether to splat the argument, if the argument is an array
45
+ def arg(*args)
46
+ self.args.define(self, *args)
47
+ end
48
+
49
+ # Declare a description for this command
50
+ #
51
+ # This is the description that will be shown in the command details help output.
52
+ #
53
+ # For example:
54
+ #
55
+ # ```ruby
56
+ # class Api::Login < Cl::Cmd
57
+ # description <<~str
58
+ # Use this command to login to our API.
59
+ # [...]
60
+ # str
61
+ # end
62
+ # ```
63
+ #
64
+ # @return [String] the description if no argument was given
65
+ def description(description = nil)
66
+ description ? @description = description : @description
67
+ end
68
+
69
+ # Declare an example text for this command
70
+ #
71
+ # This is the example text that will be shown in the command details help output.
72
+ #
73
+ # For example:
74
+ #
75
+ # ```ruby
76
+ # class Api::Login < Cl::Cmd
77
+ # example <<~str
78
+ # For example, in order to login to our API with your username and
79
+ # password, you can use:
80
+ #
81
+ # ./api --username [username] --password [password]
82
+ # str
83
+ # end
84
+ # ```
85
+ #
86
+ # @return [String] the description if no argument was given
87
+ def examples(examples = nil)
88
+ examples ? @examples = examples : @examples
89
+ end
90
+
91
+ # Declares an option
92
+ #
93
+ # Use this method to declare options a command accepts.
94
+ #
95
+ # See [this section](/#Options) for a full explanation on each feature supported by command options.
96
+ #
97
+ # @overload opt(name, description, opts)
98
+ # @param name [String] the option name
99
+ # @param description [String] description for the option, shown in the help output
100
+ # @param opts [Hash] option options
101
+ # @option opts [Symbol or Array<Symbol>] :alias alias name(s) for the option
102
+ # @option opts [Object] :default default value for the option
103
+ # @option opts [String or Symbol] :deprecated deprecation message for the option, or if given a Symbol, deprecated alias name
104
+ # @option opts [Boolean] :downcase whether to downcase the option value
105
+ # @option opts [Array<Object>] :enum list of acceptable option values
106
+ # @option opts [String] :example example(s) for the option, shown in help output
107
+ # @option opts [Regexp] :format acceptable option value format
108
+ # @option opts [Boolean] :internal whether to hide the option from help output
109
+ # @option opts [Numeric] :min minimum acceptable value
110
+ # @option opts [Numeric] :max maximum acceptable value
111
+ # @option opts [String] :see see also reference (e.g. documentation URL)
112
+ # @option opts [Symbol] :type the option value type (`:array`, `:string`, `:integer`, `:float`, `:boolean`)
113
+ # @option opts [Boolean] :required whether the option is required
114
+ # @option opts [Array<Symbol> or Symbol] :requires (an)other options required this option depends on
115
+ def opt(*args, &block)
116
+ self.opts.define(self, *args, &block)
117
+ end
118
+
119
+ # Collection of options supported by this command
120
+ #
121
+ # This collection is being inherited from super classes.
122
+ def opts
123
+ @opts ||= self == Cmd ? Opts.new : superclass.opts.dup
124
+ end
125
+
126
+ # Whether any alternative option requirements have been declared.
127
+ #
128
+ # See [this section](/#Required_Options) for a full explanation of how
129
+ # alternative option requirements can be used.
130
+ def required?
131
+ !!@required
132
+ end
133
+
134
+ # Declare alternative option requirements.
135
+ #
136
+ # Alternative (combinations of) options can be required. These need to be declared on the class body.
137
+ #
138
+ # For example,
139
+ #
140
+ # ```ruby
141
+ # class Api::Login < Cl::Cmd
142
+ # # DNF, read as: api_key OR username AND password
143
+ # required :api_key, [:username, :password]
144
+ #
145
+ # opt '--api_key KEY'
146
+ # opt '--username NAME'
147
+ # opt '--password PASS'
148
+ # end
149
+ # ```
150
+ # Will require either the option `api_key`, or both the options `username` and `password`.
151
+ #
152
+ # See [this section](/#Required_Options) for a full explanation of how
153
+ # alternative option requirements can be used.
154
+ def required(*required)
155
+ required.any? ? self.required << required : @required ||= []
156
+ end
157
+
158
+ # Declare a summary for this command
159
+ #
160
+ # This is the summary that will be shown in both the command list, and command details help output.
161
+ #
162
+ # For example:
163
+ #
164
+ # ```ruby
165
+ # class Api::Login < Cl::Cmd
166
+ # summary 'Login to the API'
167
+ # end
168
+ # ```
169
+ #
170
+ # @return [String] the summary if no argument was given
171
+ def summary(summary = nil)
172
+ summary ? @summary = summary : @summary
173
+ end
174
+ end
175
+ end
176
+ end