cl 0.1.12 → 0.1.13

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