travis-cl 1.2.4
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +134 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +59 -0
- data/MIT_LICENSE.md +21 -0
- data/README.md +1283 -0
- data/cl.gemspec +30 -0
- data/examples/README.md +22 -0
- data/examples/_src/args/cast.erb.rb +100 -0
- data/examples/_src/args/opts.erb.rb +100 -0
- data/examples/_src/args/required.erb.rb +63 -0
- data/examples/_src/args/splat.erb.rb +55 -0
- data/examples/_src/gem.erb.rb +99 -0
- data/examples/_src/heroku.erb.rb +47 -0
- data/examples/_src/rakeish.erb.rb +54 -0
- data/examples/_src/readme/abstract.erb.rb +27 -0
- data/examples/_src/readme/alias.erb.rb +22 -0
- data/examples/_src/readme/arg.erb.rb +21 -0
- data/examples/_src/readme/arg_array.erb.rb +21 -0
- data/examples/_src/readme/arg_type.erb.rb +23 -0
- data/examples/_src/readme/args_splat.erb.rb +55 -0
- data/examples/_src/readme/array.erb.rb +21 -0
- data/examples/_src/readme/basic.erb.rb +72 -0
- data/examples/_src/readme/default.erb.rb +21 -0
- data/examples/_src/readme/deprecated.erb.rb +21 -0
- data/examples/_src/readme/deprecated_alias.erb.rb +21 -0
- data/examples/_src/readme/description.erb.rb +60 -0
- data/examples/_src/readme/downcase.erb.rb +21 -0
- data/examples/_src/readme/enum.erb.rb +35 -0
- data/examples/_src/readme/example.erb.rb +25 -0
- data/examples/_src/readme/format.erb.rb +35 -0
- data/examples/_src/readme/internal.erb.rb +28 -0
- data/examples/_src/readme/negate.erb.rb +37 -0
- data/examples/_src/readme/note.erb.rb +25 -0
- data/examples/_src/readme/opts.erb.rb +33 -0
- data/examples/_src/readme/opts_block.erb.rb +30 -0
- data/examples/_src/readme/range.erb.rb +35 -0
- data/examples/_src/readme/registry.erb.rb +18 -0
- data/examples/_src/readme/required.erb.rb +35 -0
- data/examples/_src/readme/requireds.erb.rb +46 -0
- data/examples/_src/readme/requires.erb.rb +35 -0
- data/examples/_src/readme/runner.erb.rb +29 -0
- data/examples/_src/readme/runner_custom.erb.rb +25 -0
- data/examples/_src/readme/secret.erb.rb +22 -0
- data/examples/_src/readme/see.erb.rb +25 -0
- data/examples/_src/readme/type.erb.rb +21 -0
- data/examples/args/cast +98 -0
- data/examples/args/opts +98 -0
- data/examples/args/required +62 -0
- data/examples/args/splat +58 -0
- data/examples/gem +97 -0
- data/examples/heroku +48 -0
- data/examples/rakeish +50 -0
- data/examples/readme/abstract +28 -0
- data/examples/readme/alias +21 -0
- data/examples/readme/arg +20 -0
- data/examples/readme/arg_array +20 -0
- data/examples/readme/arg_type +22 -0
- data/examples/readme/args_splat +58 -0
- data/examples/readme/array +20 -0
- data/examples/readme/basic +67 -0
- data/examples/readme/default +20 -0
- data/examples/readme/deprecated +20 -0
- data/examples/readme/deprecated_alias +20 -0
- data/examples/readme/description +56 -0
- data/examples/readme/downcase +20 -0
- data/examples/readme/enum +33 -0
- data/examples/readme/example +21 -0
- data/examples/readme/format +33 -0
- data/examples/readme/internal +24 -0
- data/examples/readme/negate +44 -0
- data/examples/readme/note +21 -0
- data/examples/readme/opts +31 -0
- data/examples/readme/opts_block +29 -0
- data/examples/readme/range +33 -0
- data/examples/readme/registry +15 -0
- data/examples/readme/required +33 -0
- data/examples/readme/requireds +46 -0
- data/examples/readme/requires +33 -0
- data/examples/readme/runner +30 -0
- data/examples/readme/runner_custom +22 -0
- data/examples/readme/secret +21 -0
- data/examples/readme/see +21 -0
- data/examples/readme/type +20 -0
- data/lib/cl/arg.rb +79 -0
- data/lib/cl/args.rb +92 -0
- data/lib/cl/cast.rb +55 -0
- data/lib/cl/cmd.rb +74 -0
- data/lib/cl/config/env.rb +52 -0
- data/lib/cl/config/files.rb +34 -0
- data/lib/cl/config.rb +30 -0
- data/lib/cl/ctx.rb +36 -0
- data/lib/cl/dsl.rb +182 -0
- data/lib/cl/errors.rb +119 -0
- data/lib/cl/help/cmd.rb +118 -0
- data/lib/cl/help/cmds.rb +26 -0
- data/lib/cl/help/format.rb +69 -0
- data/lib/cl/help/table.rb +58 -0
- data/lib/cl/help/usage.rb +26 -0
- data/lib/cl/help.rb +37 -0
- data/lib/cl/helper/suggest.rb +10 -0
- data/lib/cl/helper.rb +47 -0
- data/lib/cl/opt.rb +276 -0
- data/lib/cl/opts/validate.rb +117 -0
- data/lib/cl/opts.rb +114 -0
- data/lib/cl/parser/format.rb +63 -0
- data/lib/cl/parser.rb +70 -0
- data/lib/cl/runner/default.rb +86 -0
- data/lib/cl/runner/multi.rb +34 -0
- data/lib/cl/runner.rb +10 -0
- data/lib/cl/ui.rb +146 -0
- data/lib/cl/version.rb +3 -0
- data/lib/cl.rb +62 -0
- metadata +177 -0
data/lib/cl/dsl.rb
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
@abstract = true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def abstract?
|
|
14
|
+
!!@abstract
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Declare multiple arguments at once
|
|
18
|
+
#
|
|
19
|
+
# See {Cl::Cmd::Dsl#arg} for more details.
|
|
20
|
+
def args(*args)
|
|
21
|
+
return @args ||= superclass.respond_to?(:args) ? superclass.args.dup : Args.new unless args.any?
|
|
22
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
23
|
+
args.each { |arg| arg(arg, opts) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Declares an argument
|
|
27
|
+
#
|
|
28
|
+
# Use this method to declare arguments the command accepts.
|
|
29
|
+
#
|
|
30
|
+
# For example:
|
|
31
|
+
#
|
|
32
|
+
# ```ruby
|
|
33
|
+
# class GitPush < Cl::Cmd
|
|
34
|
+
# arg remote, 'The Git remote to push to.', type: :string
|
|
35
|
+
# end
|
|
36
|
+
# ```
|
|
37
|
+
#
|
|
38
|
+
# Arguments do not need to be declared, in order to be passed to the Cmd
|
|
39
|
+
# instance, but it is useful to do so for more explicit help output, and
|
|
40
|
+
# in order to define extra properties on the arguments (e.g. their type).
|
|
41
|
+
#
|
|
42
|
+
# @overload arg(name, description, opts)
|
|
43
|
+
# @param name [String] the argument name
|
|
44
|
+
# @param description [String] description for the argument, shown in the help output
|
|
45
|
+
# @param opts [Hash] argument options
|
|
46
|
+
# @option opts [Symbol] :type the argument type (`:array`, `:string`, `:integer`, `:float`, `:boolean`)
|
|
47
|
+
# @option opts [Boolean] :required whether the argument is required
|
|
48
|
+
# @option opts [String] :sep separator to split strings by, if the argument is an array
|
|
49
|
+
# @option opts [Boolean] :splat whether to splat the argument, if the argument is an array
|
|
50
|
+
def arg(*args)
|
|
51
|
+
self.args.define(self, *args)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Declare a description for this command
|
|
55
|
+
#
|
|
56
|
+
# This is the description that will be shown in the command details help output.
|
|
57
|
+
#
|
|
58
|
+
# For example:
|
|
59
|
+
#
|
|
60
|
+
# ```ruby
|
|
61
|
+
# class Api::Login < Cl::Cmd
|
|
62
|
+
# description <<~str
|
|
63
|
+
# Use this command to login to our API.
|
|
64
|
+
# [...]
|
|
65
|
+
# str
|
|
66
|
+
# end
|
|
67
|
+
# ```
|
|
68
|
+
#
|
|
69
|
+
# @return [String] the description if no argument was given
|
|
70
|
+
def description(description = nil)
|
|
71
|
+
description ? @description = description : @description
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Declare an example text for this command
|
|
75
|
+
#
|
|
76
|
+
# This is the example text that will be shown in the command details help output.
|
|
77
|
+
#
|
|
78
|
+
# For example:
|
|
79
|
+
#
|
|
80
|
+
# ```ruby
|
|
81
|
+
# class Api::Login < Cl::Cmd
|
|
82
|
+
# example <<~str
|
|
83
|
+
# For example, in order to login to our API with your username and
|
|
84
|
+
# password, you can use:
|
|
85
|
+
#
|
|
86
|
+
# ./api --username [username] --password [password]
|
|
87
|
+
# str
|
|
88
|
+
# end
|
|
89
|
+
# ```
|
|
90
|
+
#
|
|
91
|
+
# @return [String] the description if no argument was given
|
|
92
|
+
def examples(examples = nil)
|
|
93
|
+
examples ? @examples = examples : @examples
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Declares an option
|
|
97
|
+
#
|
|
98
|
+
# Use this method to declare options a command accepts.
|
|
99
|
+
#
|
|
100
|
+
# See [this section](/#Options) for a full explanation on each feature supported by command options.
|
|
101
|
+
#
|
|
102
|
+
# @overload opt(name, description, opts)
|
|
103
|
+
# @param name [String] the option name
|
|
104
|
+
# @param description [String] description for the option, shown in the help output
|
|
105
|
+
# @param opts [Hash] option options
|
|
106
|
+
# @option opts [Symbol or Array<Symbol>] :alias alias name(s) for the option
|
|
107
|
+
# @option opts [Object] :default default value for the option
|
|
108
|
+
# @option opts [String or Symbol] :deprecated deprecation message for the option, or if given a Symbol, deprecated alias name
|
|
109
|
+
# @option opts [Boolean] :downcase whether to downcase the option value
|
|
110
|
+
# @option opts [Boolean] :upcase whether to upcase the option value
|
|
111
|
+
# @option opts [Array<Object>] :enum list of acceptable option values
|
|
112
|
+
# @option opts [String] :example example(s) for the option, shown in help output
|
|
113
|
+
# @option opts [Regexp] :format acceptable option value format
|
|
114
|
+
# @option opts [Boolean] :internal whether to hide the option from help output
|
|
115
|
+
# @option opts [Numeric] :min minimum acceptable value
|
|
116
|
+
# @option opts [Numeric] :max maximum acceptable value
|
|
117
|
+
# @option opts [String] :see see also reference (e.g. documentation URL)
|
|
118
|
+
# @option opts [Symbol] :type the option value type (`:array`, `:string`, `:integer`, `:float`, `:boolean`)
|
|
119
|
+
# @option opts [Boolean] :required whether the option is required
|
|
120
|
+
# @option opts [Array<Symbol> or Symbol] :requires (an)other options required this option depends on
|
|
121
|
+
def opt(*args, &block)
|
|
122
|
+
self.opts.define(self, *args, &block)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Collection of options supported by this command
|
|
126
|
+
#
|
|
127
|
+
# This collection is being inherited from super classes.
|
|
128
|
+
def opts
|
|
129
|
+
@opts ||= self == Cmd ? Opts.new : superclass.opts.dup
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Whether any alternative option requirements have been declared.
|
|
133
|
+
#
|
|
134
|
+
# See [this section](/#Required_Options) for a full explanation of how
|
|
135
|
+
# alternative option requirements can be used.
|
|
136
|
+
def required?
|
|
137
|
+
!!@required
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Declare alternative option requirements.
|
|
141
|
+
#
|
|
142
|
+
# Alternative (combinations of) options can be required. These need to be declared on the class body.
|
|
143
|
+
#
|
|
144
|
+
# For example,
|
|
145
|
+
#
|
|
146
|
+
# ```ruby
|
|
147
|
+
# class Api::Login < Cl::Cmd
|
|
148
|
+
# # DNF, read as: api_key OR username AND password
|
|
149
|
+
# required :api_key, [:username, :password]
|
|
150
|
+
#
|
|
151
|
+
# opt '--api_key KEY'
|
|
152
|
+
# opt '--username NAME'
|
|
153
|
+
# opt '--password PASS'
|
|
154
|
+
# end
|
|
155
|
+
# ```
|
|
156
|
+
# Will require either the option `api_key`, or both the options `username` and `password`.
|
|
157
|
+
#
|
|
158
|
+
# See [this section](/#Required_Options) for a full explanation of how
|
|
159
|
+
# alternative option requirements can be used.
|
|
160
|
+
def required(*required)
|
|
161
|
+
required.any? ? self.required << required : @required ||= []
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Declare a summary for this command
|
|
165
|
+
#
|
|
166
|
+
# This is the summary that will be shown in both the command list, and command details help output.
|
|
167
|
+
#
|
|
168
|
+
# For example:
|
|
169
|
+
#
|
|
170
|
+
# ```ruby
|
|
171
|
+
# class Api::Login < Cl::Cmd
|
|
172
|
+
# summary 'Login to the API'
|
|
173
|
+
# end
|
|
174
|
+
# ```
|
|
175
|
+
#
|
|
176
|
+
# @return [String] the summary if no argument was given
|
|
177
|
+
def summary(summary = nil)
|
|
178
|
+
summary ? @summary = summary : @summary
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
data/lib/cl/errors.rb
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require 'cl/helper/suggest'
|
|
2
|
+
|
|
3
|
+
class Cl
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
MSGS = {
|
|
6
|
+
unknown_cmd: 'Unknown command: %s',
|
|
7
|
+
unknown_option: 'Unknown option: %s',
|
|
8
|
+
unknown_arg: 'Unknown argument value: %s (known: %s)',
|
|
9
|
+
missing_args: 'Missing arguments (given: %s, required: %s)',
|
|
10
|
+
too_many_args: 'Too many arguments: %s (given: %s, allowed: %s)',
|
|
11
|
+
wrong_type: 'Wrong argument type (given: %s, expected: %s)',
|
|
12
|
+
out_of_range: 'Out of range: %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
|
+
|
|
29
|
+
class UnknownCmd < Error
|
|
30
|
+
attr_reader :runner, :args
|
|
31
|
+
|
|
32
|
+
def initialize(runner, args)
|
|
33
|
+
@runner = runner
|
|
34
|
+
@args = args
|
|
35
|
+
super(:unknown_cmd, args.join(' '))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def suggestions
|
|
39
|
+
runner.suggestions(args)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class UnknownOption < Error
|
|
44
|
+
attr_reader :cmd, :opt
|
|
45
|
+
|
|
46
|
+
VALUE = /=[^ ]*/
|
|
47
|
+
|
|
48
|
+
def initialize(cmd, str)
|
|
49
|
+
@cmd = cmd
|
|
50
|
+
@opt = str.sub('invalid option: ', '').sub(VALUE, '')
|
|
51
|
+
super(:unknown_option, opt)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def suggestions
|
|
55
|
+
cmd.suggestions(opt)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class RequiredOpts < OptionError
|
|
60
|
+
def initialize(opts)
|
|
61
|
+
msg = opts.size == 1 ? :required_opt : :required_opts
|
|
62
|
+
super(msg, opts.join(', '))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class RequiredsOpts < OptionError
|
|
67
|
+
def initialize(opts)
|
|
68
|
+
opts = opts.map { |alts| alts.map { |alt| Array(alt).join(' and ') }.join(', or ' ) }
|
|
69
|
+
super(:requires_opts, opts.join('; '))
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class RequiresOpts < OptionError
|
|
74
|
+
def initialize(opts)
|
|
75
|
+
msg = opts.size == 1 ? :requires_opt : :requires_opts
|
|
76
|
+
opts = opts.map { |one, other| "#{one} (required by #{other})" }.join(', ')
|
|
77
|
+
super(msg, opts)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class OutOfRange < OptionError
|
|
82
|
+
def initialize(opts)
|
|
83
|
+
opts = opts.map { |opt, opts| "#{opt} (#{opts.map { |pair| pair.join(': ') }.join(', ')})" }.join(', ')
|
|
84
|
+
super(:out_of_range, opts)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class InvalidFormat < OptionError
|
|
89
|
+
def initialize(opts)
|
|
90
|
+
opts = opts.map { |opt, format| "#{opt} (format: #{format})" }.join(', ')
|
|
91
|
+
super(:invalid_format, opts)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class UnknownArgumentValue < OptionError
|
|
96
|
+
def initialize(value, known)
|
|
97
|
+
super(:unknown_arg, value, known)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class UnknownValues < OptionError
|
|
102
|
+
include Suggest
|
|
103
|
+
|
|
104
|
+
attr_reader :opts
|
|
105
|
+
|
|
106
|
+
def initialize(opts)
|
|
107
|
+
@opts = opts
|
|
108
|
+
opts = opts.map do |(opt, values, known)|
|
|
109
|
+
pairs = values.map { |value| [opt, value].join('=') }.join(' ')
|
|
110
|
+
"#{pairs} (known values: #{known.join(', ')})"
|
|
111
|
+
end
|
|
112
|
+
super(:unknown_values, opts.join(', '))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def suggestions
|
|
116
|
+
opts.map { |_, value, known| suggest(known, value) }.flatten
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/cl/help/cmd.rb
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require 'cl/help/format'
|
|
2
|
+
require 'cl/help/table'
|
|
3
|
+
require 'cl/help/usage'
|
|
4
|
+
|
|
5
|
+
class Cl
|
|
6
|
+
class Help
|
|
7
|
+
class Cmd < Struct.new(:ctx, :cmd)
|
|
8
|
+
include Format
|
|
9
|
+
|
|
10
|
+
def format
|
|
11
|
+
[usage, summary, description, arguments, options, common, examples].compact.join("\n\n")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def usage
|
|
15
|
+
"Usage: #{Usage.new(ctx, cmd).format.join("\n or: ")}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def summary
|
|
19
|
+
['Summary:', indent(cmd.summary)] if cmd.summary
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def description
|
|
23
|
+
['Description:', indent(cmd.description)] if cmd.description
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def arguments
|
|
27
|
+
['Arguments:', table(:args)] if args.any?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def options
|
|
31
|
+
['Options:', requireds, table(:opts)].compact if opts.any?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def common
|
|
35
|
+
['Common Options:', table(:cmmn)] if common?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def examples
|
|
39
|
+
['Examples:', indent(cmd.examples)] if cmd.examples
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def table(name)
|
|
43
|
+
table = send(name)
|
|
44
|
+
indent(table.to_s(width - table.width + 5))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def args
|
|
48
|
+
@args ||= begin
|
|
49
|
+
Table.new(cmd.args.map { |arg| [arg.name, format_obj(arg)] })
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def opts
|
|
54
|
+
@opts ||= begin
|
|
55
|
+
opts = cmd.opts.to_a
|
|
56
|
+
opts = opts.reject(&:internal?)
|
|
57
|
+
opts = opts - cmd.superclass.opts.to_a if common?
|
|
58
|
+
strs = Table.new(rjust(opts.map { |opt| opt_strs(opt) }))
|
|
59
|
+
opts = opts.map { |opt| format_obj(opt) }
|
|
60
|
+
Table.new(strs.rows.zip(opts))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def cmmn
|
|
65
|
+
@cmmn ||= begin
|
|
66
|
+
opts = cmd.superclass.opts
|
|
67
|
+
opts = opts.reject(&:internal?)
|
|
68
|
+
strs = Table.new(rjust(opts.map(&:strs)))
|
|
69
|
+
opts = opts.map { |opt| format_obj(opt) }
|
|
70
|
+
Table.new(strs.rows.zip(opts))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def opt_strs(opt)
|
|
75
|
+
return opt.strs if !opt.flag? || opt.help?
|
|
76
|
+
opts = [opt.short]
|
|
77
|
+
opts.push(negate?(opt) ? negate(opt) : opt.long)
|
|
78
|
+
opts.compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def negate?(opt)
|
|
82
|
+
negations = opt.negate.map { |str| "#{str}-" }.join('|')
|
|
83
|
+
opt.long && opt.negate? && opt.long !~ /\[#{negations}\]/
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def negate(opt)
|
|
87
|
+
negations = opt.negate.map { |str| "#{str}-" }.join('|')
|
|
88
|
+
opt.long.dup.insert(2, "[#{negations}]")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def requireds
|
|
92
|
+
return unless cmd.required?
|
|
93
|
+
opts = cmd.required
|
|
94
|
+
strs = opts.map { |alts| alts.map { |alt| Array(alt).join(' and ') }.join(', or ' ) }
|
|
95
|
+
strs = strs.map { |str| "Either #{str} are required." }.join("\n")
|
|
96
|
+
indent(strs) unless strs.empty?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def common?
|
|
100
|
+
cmd.superclass < Cl::Cmd
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def width
|
|
104
|
+
[args.width, opts.width, cmmn.width].max
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def rjust(objs)
|
|
108
|
+
return objs unless objs.any?
|
|
109
|
+
width = objs.max_by(&:size).size
|
|
110
|
+
objs.map { |objs| [*Array.new(width - objs.size) { '' }, *objs] }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def indent(str)
|
|
114
|
+
str.lines.map { |line| " #{line}".rstrip }.join("\n")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/cl/help/cmds.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'cl/help/table'
|
|
2
|
+
require 'cl/help/usage'
|
|
3
|
+
|
|
4
|
+
class Cl
|
|
5
|
+
class Help
|
|
6
|
+
class Cmds < Struct.new(:ctx, :cmds)
|
|
7
|
+
HEAD = %(Type "%s help COMMAND [SUBCOMMAND]" for more details:\n)
|
|
8
|
+
|
|
9
|
+
def format
|
|
10
|
+
[head, Table.new(list).format].join("\n")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def head
|
|
14
|
+
HEAD % ctx.name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def list
|
|
18
|
+
cmds.any? ? cmds.map { |cmd| format_cmd(cmd) } : [['[no commands]']]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def format_cmd(cmd)
|
|
22
|
+
["#{Usage.new(ctx, cmd).format.first}", cmd.summary]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
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' 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 << "downcases" if opt.downcase?
|
|
37
|
+
opts << "upcases" 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
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
class Cl
|
|
2
|
+
class Help
|
|
3
|
+
class Table
|
|
4
|
+
include Wrap
|
|
5
|
+
|
|
6
|
+
attr_reader :data, :padding
|
|
7
|
+
|
|
8
|
+
def initialize(data)
|
|
9
|
+
@data = data
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def any?
|
|
13
|
+
data.any?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def format(padding = 8)
|
|
17
|
+
@padding = padding
|
|
18
|
+
rows.join("\n")
|
|
19
|
+
end
|
|
20
|
+
alias to_s format
|
|
21
|
+
|
|
22
|
+
def rows
|
|
23
|
+
data.map { |row| cells(row).join(' ').rstrip }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cells(row)
|
|
27
|
+
row.map.with_index do |cell, ix|
|
|
28
|
+
indent(wrap(cell.to_s), widths[ix - 1]).ljust(widths[ix])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def indent(str, width)
|
|
33
|
+
return str if str.empty? || !width
|
|
34
|
+
[str.lines[0], *str.lines[1..-1].map { |str| ' ' * (width + 1) + str }].join.rstrip
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def width
|
|
38
|
+
widths = cols[0..-2].map { |col| col.max_by(&:size).size }.inject(&:+).to_i
|
|
39
|
+
widths + cols.size - 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def widths
|
|
43
|
+
cols.map.with_index do |col, ix|
|
|
44
|
+
max = col.compact.max_by(&:size)
|
|
45
|
+
pad(max ? max.size : 0, ix)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def pad(width, ix)
|
|
50
|
+
ix < cols.size - 2 ? width : width + padding.to_i
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def cols
|
|
54
|
+
@cols ||= data.transpose
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class Cl
|
|
2
|
+
class Help
|
|
3
|
+
class Usage < Struct.new(:ctx, :cmd)
|
|
4
|
+
def format
|
|
5
|
+
cmd.registry_keys.map do |key|
|
|
6
|
+
line(key)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def line(key)
|
|
11
|
+
usage = [executable, key.to_s.gsub(':', ' ')]
|
|
12
|
+
usage += cmd.args.map(&:to_s) # { |arg| "[#{arg}]" }
|
|
13
|
+
usage << '[options]' if opts?
|
|
14
|
+
usage.join(' ')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def executable
|
|
18
|
+
ctx.name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def opts?
|
|
22
|
+
cmd.opts.any?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/cl/help.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class Cl
|
|
2
|
+
class Help < Cl::Cmd
|
|
3
|
+
register :help
|
|
4
|
+
|
|
5
|
+
arg :args, splat: true
|
|
6
|
+
|
|
7
|
+
def run
|
|
8
|
+
ctx.puts help
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def help
|
|
12
|
+
Array(args).any? ? Cmd.new(ctx, cmd).format : Cmds.new(ctx, cmds).format
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def help?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def cmds
|
|
22
|
+
cmds = Cl::Cmd.cmds.reject { |cmd| cmd.registry_key == :help }
|
|
23
|
+
key = args.join(':') if args
|
|
24
|
+
cmds = cmds.select { |cmd| cmd.registry_key.to_s.start_with?(key) } if key
|
|
25
|
+
cmds
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cmd
|
|
29
|
+
key = args.join(':')
|
|
30
|
+
return Cl::Cmd[key] if Cl::Cmd.registered?(key)
|
|
31
|
+
ctx.abort("Unknown command: #{key}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
require 'cl/help/cmd'
|
|
37
|
+
require 'cl/help/cmds'
|
data/lib/cl/helper.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'cl/helper/suggest'
|
|
2
|
+
|
|
3
|
+
class Cl
|
|
4
|
+
module Merge
|
|
5
|
+
MERGE = ->(key, lft, rgt) do
|
|
6
|
+
lft.is_a?(Hash) && rgt.is_a?(Hash) ? lft.merge(rgt, &MERGE) : rgt
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def merge(*objs)
|
|
10
|
+
objs.inject({}) { |lft, rgt| lft.merge(rgt, &MERGE) }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Regex
|
|
15
|
+
def format_regex(str)
|
|
16
|
+
return str unless str.is_a?(Regexp)
|
|
17
|
+
"/#{str.to_s.sub('(?-mix:', '').sub(/\)$/, '')}/"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module Wrap
|
|
22
|
+
def wrap(str, opts = {})
|
|
23
|
+
width = opts[:width] || 80
|
|
24
|
+
str.lines.map do |line|
|
|
25
|
+
line.size > width ? line.gsub(/(.{1,#{width}})(\s+|$)/, "\\1\n").strip : line
|
|
26
|
+
end.join("\n")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module Underscore
|
|
31
|
+
def underscore(string)
|
|
32
|
+
string.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
|
33
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
|
34
|
+
downcase
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
extend Merge, Regex, Wrap
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if RUBY_VERSION == '2.0.0'
|
|
42
|
+
Array.class_eval do
|
|
43
|
+
def to_h
|
|
44
|
+
Hash[self]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|