travis-cl 1.2.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|