travis-cl 1.2.4

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