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.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +134 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.lock +59 -0
  5. data/MIT_LICENSE.md +21 -0
  6. data/README.md +1283 -0
  7. data/cl.gemspec +30 -0
  8. data/examples/README.md +22 -0
  9. data/examples/_src/args/cast.erb.rb +100 -0
  10. data/examples/_src/args/opts.erb.rb +100 -0
  11. data/examples/_src/args/required.erb.rb +63 -0
  12. data/examples/_src/args/splat.erb.rb +55 -0
  13. data/examples/_src/gem.erb.rb +99 -0
  14. data/examples/_src/heroku.erb.rb +47 -0
  15. data/examples/_src/rakeish.erb.rb +54 -0
  16. data/examples/_src/readme/abstract.erb.rb +27 -0
  17. data/examples/_src/readme/alias.erb.rb +22 -0
  18. data/examples/_src/readme/arg.erb.rb +21 -0
  19. data/examples/_src/readme/arg_array.erb.rb +21 -0
  20. data/examples/_src/readme/arg_type.erb.rb +23 -0
  21. data/examples/_src/readme/args_splat.erb.rb +55 -0
  22. data/examples/_src/readme/array.erb.rb +21 -0
  23. data/examples/_src/readme/basic.erb.rb +72 -0
  24. data/examples/_src/readme/default.erb.rb +21 -0
  25. data/examples/_src/readme/deprecated.erb.rb +21 -0
  26. data/examples/_src/readme/deprecated_alias.erb.rb +21 -0
  27. data/examples/_src/readme/description.erb.rb +60 -0
  28. data/examples/_src/readme/downcase.erb.rb +21 -0
  29. data/examples/_src/readme/enum.erb.rb +35 -0
  30. data/examples/_src/readme/example.erb.rb +25 -0
  31. data/examples/_src/readme/format.erb.rb +35 -0
  32. data/examples/_src/readme/internal.erb.rb +28 -0
  33. data/examples/_src/readme/negate.erb.rb +37 -0
  34. data/examples/_src/readme/note.erb.rb +25 -0
  35. data/examples/_src/readme/opts.erb.rb +33 -0
  36. data/examples/_src/readme/opts_block.erb.rb +30 -0
  37. data/examples/_src/readme/range.erb.rb +35 -0
  38. data/examples/_src/readme/registry.erb.rb +18 -0
  39. data/examples/_src/readme/required.erb.rb +35 -0
  40. data/examples/_src/readme/requireds.erb.rb +46 -0
  41. data/examples/_src/readme/requires.erb.rb +35 -0
  42. data/examples/_src/readme/runner.erb.rb +29 -0
  43. data/examples/_src/readme/runner_custom.erb.rb +25 -0
  44. data/examples/_src/readme/secret.erb.rb +22 -0
  45. data/examples/_src/readme/see.erb.rb +25 -0
  46. data/examples/_src/readme/type.erb.rb +21 -0
  47. data/examples/args/cast +98 -0
  48. data/examples/args/opts +98 -0
  49. data/examples/args/required +62 -0
  50. data/examples/args/splat +58 -0
  51. data/examples/gem +97 -0
  52. data/examples/heroku +48 -0
  53. data/examples/rakeish +50 -0
  54. data/examples/readme/abstract +28 -0
  55. data/examples/readme/alias +21 -0
  56. data/examples/readme/arg +20 -0
  57. data/examples/readme/arg_array +20 -0
  58. data/examples/readme/arg_type +22 -0
  59. data/examples/readme/args_splat +58 -0
  60. data/examples/readme/array +20 -0
  61. data/examples/readme/basic +67 -0
  62. data/examples/readme/default +20 -0
  63. data/examples/readme/deprecated +20 -0
  64. data/examples/readme/deprecated_alias +20 -0
  65. data/examples/readme/description +56 -0
  66. data/examples/readme/downcase +20 -0
  67. data/examples/readme/enum +33 -0
  68. data/examples/readme/example +21 -0
  69. data/examples/readme/format +33 -0
  70. data/examples/readme/internal +24 -0
  71. data/examples/readme/negate +44 -0
  72. data/examples/readme/note +21 -0
  73. data/examples/readme/opts +31 -0
  74. data/examples/readme/opts_block +29 -0
  75. data/examples/readme/range +33 -0
  76. data/examples/readme/registry +15 -0
  77. data/examples/readme/required +33 -0
  78. data/examples/readme/requireds +46 -0
  79. data/examples/readme/requires +33 -0
  80. data/examples/readme/runner +30 -0
  81. data/examples/readme/runner_custom +22 -0
  82. data/examples/readme/secret +21 -0
  83. data/examples/readme/see +21 -0
  84. data/examples/readme/type +20 -0
  85. data/lib/cl/arg.rb +79 -0
  86. data/lib/cl/args.rb +92 -0
  87. data/lib/cl/cast.rb +55 -0
  88. data/lib/cl/cmd.rb +74 -0
  89. data/lib/cl/config/env.rb +52 -0
  90. data/lib/cl/config/files.rb +34 -0
  91. data/lib/cl/config.rb +30 -0
  92. data/lib/cl/ctx.rb +36 -0
  93. data/lib/cl/dsl.rb +182 -0
  94. data/lib/cl/errors.rb +119 -0
  95. data/lib/cl/help/cmd.rb +118 -0
  96. data/lib/cl/help/cmds.rb +26 -0
  97. data/lib/cl/help/format.rb +69 -0
  98. data/lib/cl/help/table.rb +58 -0
  99. data/lib/cl/help/usage.rb +26 -0
  100. data/lib/cl/help.rb +37 -0
  101. data/lib/cl/helper/suggest.rb +10 -0
  102. data/lib/cl/helper.rb +47 -0
  103. data/lib/cl/opt.rb +276 -0
  104. data/lib/cl/opts/validate.rb +117 -0
  105. data/lib/cl/opts.rb +114 -0
  106. data/lib/cl/parser/format.rb +63 -0
  107. data/lib/cl/parser.rb +70 -0
  108. data/lib/cl/runner/default.rb +86 -0
  109. data/lib/cl/runner/multi.rb +34 -0
  110. data/lib/cl/runner.rb +10 -0
  111. data/lib/cl/ui.rb +146 -0
  112. data/lib/cl/version.rb +3 -0
  113. data/lib/cl.rb +62 -0
  114. 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
@@ -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
@@ -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'
@@ -0,0 +1,10 @@
1
+ class Cl
2
+ module Suggest
3
+ def suggest(dict, value)
4
+ return [] unless defined?(DidYouMean::SpellChecker)
5
+ Array(value).map do |value|
6
+ DidYouMean::SpellChecker.new(dictionary: dict.map(&:to_s)).correct(value.to_s)
7
+ end.flatten
8
+ end
9
+ end
10
+ end
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