clive 0.8.1 → 1.0.0
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.
- data/LICENSE +1 -1
- data/README.md +328 -227
- data/lib/clive.rb +130 -50
- data/lib/clive/argument.rb +170 -0
- data/lib/clive/arguments.rb +139 -0
- data/lib/clive/arguments/parser.rb +210 -0
- data/lib/clive/base.rb +189 -0
- data/lib/clive/command.rb +342 -444
- data/lib/clive/error.rb +66 -0
- data/lib/clive/formatter.rb +57 -141
- data/lib/clive/formatter/colour.rb +37 -0
- data/lib/clive/formatter/plain.rb +172 -0
- data/lib/clive/option.rb +185 -75
- data/lib/clive/option/runner.rb +163 -0
- data/lib/clive/output.rb +141 -16
- data/lib/clive/parser.rb +180 -87
- data/lib/clive/struct_hash.rb +109 -0
- data/lib/clive/type.rb +117 -0
- data/lib/clive/type/definitions.rb +170 -0
- data/lib/clive/type/lookup.rb +23 -0
- data/lib/clive/version.rb +3 -3
- data/spec/clive/a_cli_spec.rb +245 -0
- data/spec/clive/argument_spec.rb +148 -0
- data/spec/clive/arguments/parser_spec.rb +35 -0
- data/spec/clive/arguments_spec.rb +191 -0
- data/spec/clive/command_spec.rb +276 -209
- data/spec/clive/formatter/colour_spec.rb +129 -0
- data/spec/clive/formatter/plain_spec.rb +129 -0
- data/spec/clive/option/runner_spec.rb +92 -0
- data/spec/clive/option_spec.rb +149 -23
- data/spec/clive/output_spec.rb +86 -2
- data/spec/clive/parser_spec.rb +201 -81
- data/spec/clive/struct_hash_spec.rb +82 -0
- data/spec/clive/type/definitions_spec.rb +312 -0
- data/spec/clive/type_spec.rb +107 -0
- data/spec/clive_spec.rb +60 -0
- data/spec/extras/expectations.rb +86 -0
- data/spec/extras/focus.rb +22 -0
- data/spec/helper.rb +35 -0
- metadata +56 -36
- data/lib/clive/bool.rb +0 -67
- data/lib/clive/exceptions.rb +0 -54
- data/lib/clive/flag.rb +0 -199
- data/lib/clive/switch.rb +0 -31
- data/lib/clive/tokens.rb +0 -141
- data/spec/clive/bool_spec.rb +0 -54
- data/spec/clive/flag_spec.rb +0 -117
- data/spec/clive/formatter_spec.rb +0 -108
- data/spec/clive/switch_spec.rb +0 -14
- data/spec/clive/tokens_spec.rb +0 -38
- data/spec/shared_specs.rb +0 -16
- data/spec/spec_helper.rb +0 -12
data/lib/clive/error.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
class Clive
|
2
|
+
|
3
|
+
# For general errors with Clive. It stripts most of the backtrace which
|
4
|
+
# you don't really want, and allows you to set nice error messages
|
5
|
+
# using {.reason}. Arguments can be passed and then used in messages by
|
6
|
+
# referencing with +#n+ tokens, where +n+ is the index of the argument.
|
7
|
+
#
|
8
|
+
# A lot of this is pulled from OptionParser::ParseError see
|
9
|
+
# http://ruby-doc.org/stdlib/libdoc/optparse/rdoc/index.html.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# class MissingArgumentError < Error
|
14
|
+
# reason 'missing argument for #0'
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# raise MissingArgumentError.new(some_option)
|
18
|
+
#
|
19
|
+
class Error < StandardError
|
20
|
+
attr_accessor :args
|
21
|
+
|
22
|
+
# @param args
|
23
|
+
# Arguments that can be accessed with '#n' in {.reason}.
|
24
|
+
def initialize(*args)
|
25
|
+
@args = args
|
26
|
+
end
|
27
|
+
|
28
|
+
# Removes all references to files which are not the file being run
|
29
|
+
# unless in $DEBUG mode.
|
30
|
+
def self.filter_backtrace(array)
|
31
|
+
unless $DEBUG
|
32
|
+
array = [$0]
|
33
|
+
end
|
34
|
+
array
|
35
|
+
end
|
36
|
+
|
37
|
+
# Set the reason for the error class.
|
38
|
+
# @param str [String]
|
39
|
+
def self.reason(str)
|
40
|
+
@reason = str
|
41
|
+
end
|
42
|
+
|
43
|
+
# Accessor for the reason set with {.reason}.
|
44
|
+
def self._reason
|
45
|
+
@reason
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_backtrace(array)
|
49
|
+
super(self.class.filter_backtrace(array))
|
50
|
+
end
|
51
|
+
|
52
|
+
# Build the message by substituting the arguments into the reason.
|
53
|
+
def message
|
54
|
+
self.class._reason.gsub(/#\d/) do |i|
|
55
|
+
arg = args[i[1].to_i]
|
56
|
+
if arg.is_a?(Array)
|
57
|
+
arg.map(&:to_s).to_s
|
58
|
+
else
|
59
|
+
arg.to_s
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
alias_method :to_s, :message
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
data/lib/clive/formatter.rb
CHANGED
@@ -1,145 +1,61 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
#
|
4
|
-
#
|
1
|
+
class Clive
|
2
|
+
|
3
|
+
# @abstract Subclass and override {#to_s} (and probably {#initialize}) to
|
4
|
+
# implement a custom Formatter. {#initialize} *should* take an options
|
5
|
+
# hash.
|
6
|
+
#
|
7
|
+
# Takes care of formatting the help string. Look at {Formatter::Plain}
|
8
|
+
# for a good (if not a bit complex) reference of how to do it.
|
9
|
+
#
|
10
|
+
# Then it is just a case of passing an instance of the new formatter to
|
11
|
+
# {Clive.run}. You can use a different formatter for commands by
|
12
|
+
# passing it when creating them.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
#
|
16
|
+
# class MainFormatter < Clive::Formatter
|
17
|
+
# # ...
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# class CommandFormatter < Clive::Formatter
|
21
|
+
# # ...
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # Uses MainFormatter
|
25
|
+
# class CLI
|
26
|
+
# # ...
|
27
|
+
#
|
28
|
+
# # Uses CommandFormatter
|
29
|
+
# command :new, formatter: CommandFormatter.new do
|
30
|
+
# # ...
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# # Uses MainFormatter
|
34
|
+
# command :normal do
|
35
|
+
# # ...
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# CLI.run formatter: MainFormatter.new
|
40
|
+
#
|
5
41
|
class Formatter
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
42
|
+
|
43
|
+
attr_writer :header, :footer, :options, :commands
|
44
|
+
|
45
|
+
def initialize(opts={})
|
46
|
+
@opts = opts
|
47
|
+
|
48
|
+
@header, @footer = '', ''
|
49
|
+
@commands, @options = [], []
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
([@header] + @commands + @options + [@footer]).join("\n")
|
54
|
+
end
|
55
|
+
|
56
|
+
def inspect
|
57
|
+
"#<#{self.class.name} @opts=#@opts>"
|
18
58
|
end
|
19
|
-
|
20
|
-
# Sizes
|
21
|
-
attr_accessor :width, :prepend
|
22
|
-
|
23
|
-
def initialize(width, prepend, &block)
|
24
|
-
@width = width
|
25
|
-
@prepend = prepend
|
26
|
-
end
|
27
|
-
|
28
|
-
|
29
|
-
def format(header, footer, commands, options)
|
30
|
-
result = ""
|
31
|
-
|
32
|
-
switches = options.find_all {|i| i.class == Clive::Switch }.map(&:to_h)
|
33
|
-
bools = options.find_all {|i| i.class == Clive::Bool }.map(&:to_h).compact
|
34
|
-
flags = options.find_all {|i| i.class == Clive::Flag }.map(&:to_h)
|
35
|
-
commands = commands.map(&:to_h)
|
36
|
-
|
37
|
-
result << header << "\n" if header
|
38
|
-
|
39
|
-
unless commands.empty?
|
40
|
-
result << "\n Commands: \n"
|
41
|
-
|
42
|
-
commands.each do |hash|
|
43
|
-
hash['prepend'] = " " * @prepend
|
44
|
-
result << parse(@command, hash) << "\n"
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
|
49
|
-
unless options.empty?
|
50
|
-
result << "\n Options: \n"
|
51
|
-
|
52
|
-
switches.each do |hash|
|
53
|
-
hash['prepend'] = " " * @prepend
|
54
|
-
result << parse(@switch, hash) << "\n"
|
55
|
-
end
|
56
|
-
|
57
|
-
bools.each do |hash|
|
58
|
-
hash['prepend'] = " " * @prepend
|
59
|
-
result << parse(@bool, hash) << "\n"
|
60
|
-
end
|
61
|
-
|
62
|
-
flags.each do |hash|
|
63
|
-
hash['prepend'] = " " * @prepend
|
64
|
-
result << parse(@flag, hash) << "\n"
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
result << "\n" << footer << "\n" if footer
|
69
|
-
|
70
|
-
result
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
def switch(format)
|
75
|
-
@switch = format
|
76
|
-
end
|
77
|
-
|
78
|
-
def bool(format)
|
79
|
-
@bool = format
|
80
|
-
end
|
81
|
-
|
82
|
-
def flag(format)
|
83
|
-
@flag = format
|
84
|
-
end
|
85
|
-
|
86
|
-
def command(format)
|
87
|
-
@command = format
|
88
|
-
end
|
89
|
-
|
90
|
-
def help(format)
|
91
|
-
@help = format
|
92
|
-
end
|
93
|
-
|
94
|
-
def summary(format)
|
95
|
-
@summary = format
|
96
|
-
end
|
97
|
-
|
98
|
-
|
99
|
-
def parse(format, args)
|
100
|
-
front, back = format.split('{spaces}')
|
101
|
-
|
102
|
-
front_p = parse_format(front, args)
|
103
|
-
back_p = parse_format(back, args)
|
104
|
-
|
105
|
-
s = @width - front_p.length
|
106
|
-
s = 0 if s < 0 # can't have negative spaces!
|
107
|
-
spaces = " " * s
|
108
|
-
|
109
|
-
front_p << spaces << back_p
|
110
|
-
end
|
111
|
-
|
112
|
-
def parse_format(format, args)
|
113
|
-
if format
|
114
|
-
obj = Obj.new(args)
|
115
|
-
r = ""
|
116
|
-
Lexer.tokenise(format).each do |t,v|
|
117
|
-
case t
|
118
|
-
when :block
|
119
|
-
r << obj.evaluate(v)
|
120
|
-
when :text
|
121
|
-
r << v
|
122
|
-
end
|
123
|
-
end
|
124
|
-
r
|
125
|
-
else
|
126
|
-
""
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
class Lexer < Ast::Tokeniser
|
131
|
-
rule :text, /%(.)/ do |i|
|
132
|
-
i[1]
|
133
|
-
end
|
134
|
-
|
135
|
-
rule :block, /\{(.*?)\}/ do |i|
|
136
|
-
i[1]
|
137
|
-
end
|
138
|
-
|
139
|
-
missing do |i|
|
140
|
-
Ast::Token.new(:text, i)
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
59
|
end
|
60
|
+
|
145
61
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Clive
|
2
|
+
class Formatter
|
3
|
+
|
4
|
+
class Colour < Plain
|
5
|
+
|
6
|
+
def after_for(opt)
|
7
|
+
r = ""
|
8
|
+
after = description_for(opt).dup << " " << choices_for(opt)
|
9
|
+
unless after == " "
|
10
|
+
r << "# ".grey << Output.wrap_text(after,
|
11
|
+
left_width + padding(2).size,
|
12
|
+
@opts[:width])
|
13
|
+
end
|
14
|
+
r
|
15
|
+
end
|
16
|
+
|
17
|
+
def description_for(opt)
|
18
|
+
s = super
|
19
|
+
if s.empty?
|
20
|
+
s
|
21
|
+
else
|
22
|
+
s.grey
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def choices_for(opt)
|
27
|
+
s = super
|
28
|
+
if s.empty?
|
29
|
+
s
|
30
|
+
else
|
31
|
+
s.blue.bold
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
class Clive
|
2
|
+
class Formatter
|
3
|
+
|
4
|
+
class Plain < Formatter
|
5
|
+
|
6
|
+
DEFAULTS = {
|
7
|
+
:padding => 2,
|
8
|
+
:width => Output.terminal_width,
|
9
|
+
:min_ratio => 0.2,
|
10
|
+
:max_ratio => 0.5
|
11
|
+
}
|
12
|
+
|
13
|
+
# @param opts [Hash]
|
14
|
+
# @option opts [Integer] :width
|
15
|
+
# Total width of screen to use
|
16
|
+
# @option opts [Integer] :padding
|
17
|
+
# Amount of padding to use
|
18
|
+
# @option opts [Float] :min_ratio
|
19
|
+
# Minimum proportion of screen the left side can use
|
20
|
+
# @option opts [Float] :max_ratio
|
21
|
+
# Maximum proportion of screen the left side can use
|
22
|
+
def initialize(opts={})
|
23
|
+
@opts = DEFAULTS.merge(opts)
|
24
|
+
|
25
|
+
if @opts[:min_ratio] > @opts[:max_ratio]
|
26
|
+
@opts[:max_ratio] = @opts[:min_ratio]
|
27
|
+
end
|
28
|
+
|
29
|
+
@header, @footer = "", ""
|
30
|
+
@commands, @options = [], []
|
31
|
+
end
|
32
|
+
|
33
|
+
# Builds the help string. Formatted like:
|
34
|
+
#
|
35
|
+
# Usage: the header
|
36
|
+
#
|
37
|
+
# Commands:
|
38
|
+
# command # Description
|
39
|
+
#
|
40
|
+
# Options:
|
41
|
+
# -a, --abc <arg> # Description
|
42
|
+
#
|
43
|
+
# A footer
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
def to_s
|
47
|
+
groups = (@options + @commands).group_by {|i| i.config[:group] }
|
48
|
+
|
49
|
+
# So no groups were created, let's create some nice defaults
|
50
|
+
if groups.size == 1 && groups.keys.first == nil
|
51
|
+
# Use an array so that the order is always correct
|
52
|
+
groups = [['Commands', @commands.sort], ['Options', @options.sort]]
|
53
|
+
end
|
54
|
+
|
55
|
+
r = @header.dup << "\n\n"
|
56
|
+
|
57
|
+
groups.each do |name, group|
|
58
|
+
unless group.empty?
|
59
|
+
r << (name ? "#{padding}#{name}:\n" : '')
|
60
|
+
group.sort.sort_by {|i| i.instance_of?(Command) ? 0 : 1 }.each do |opt|
|
61
|
+
r << build_option_string(opt)
|
62
|
+
end
|
63
|
+
r << "\n"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
r << @footer
|
68
|
+
r.split("\n").map {|i| i.rstrip }.join("\n")
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
# @return [String] Default padding
|
74
|
+
def padding(n=1)
|
75
|
+
' ' * (@opts[:padding] * n)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Integer] Width of the left half, ie. up to {#after}
|
79
|
+
def left_width
|
80
|
+
w = max + padding(2).size
|
81
|
+
if w > @opts[:max_ratio] * @opts[:width]
|
82
|
+
(@opts[:max_ratio] * @opts[:width]).to_i
|
83
|
+
elsif w < @opts[:min_ratio] * @opts[:width]
|
84
|
+
(@opts[:min_ratio] * @opts[:width]).to_i
|
85
|
+
else
|
86
|
+
w.to_i
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Integer] The greatest width the left part of the screen
|
91
|
+
# can be. This allows you to use _a_ max width in calculations
|
92
|
+
# without creating a loop.
|
93
|
+
def max_left_width
|
94
|
+
(@opts[:max_ratio] * @opts[:width]).to_i
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [Integer] The length of the longest {#before}, ignoring any that break
|
98
|
+
# the line.
|
99
|
+
def max
|
100
|
+
(@options + @commands).map {|i|
|
101
|
+
before_for(i).size
|
102
|
+
}.reject {|i|
|
103
|
+
i > max_left_width
|
104
|
+
}.max
|
105
|
+
end
|
106
|
+
|
107
|
+
# Builds a single line for an Option of the form.
|
108
|
+
#
|
109
|
+
# before padding # after
|
110
|
+
#
|
111
|
+
# @param [Option]
|
112
|
+
def build_option_string(opt)
|
113
|
+
before_for(opt) << padding_for(opt) << padding << after_for(opt).rstrip << "\n"
|
114
|
+
end
|
115
|
+
|
116
|
+
# @param opt [Option]
|
117
|
+
# @return [String] Builds the first half of the help string for an Option.
|
118
|
+
def before_for(opt)
|
119
|
+
b = padding(2) << names_for(opt).dup << " " << args_for(opt)
|
120
|
+
b << "\n" if b.size > max_left_width
|
121
|
+
b
|
122
|
+
end
|
123
|
+
|
124
|
+
# @return [String] Padding for between an Option's #before and #after.
|
125
|
+
def padding_for(opt)
|
126
|
+
width = left_width - before_for(opt).clear_colours.split("\n").last.size
|
127
|
+
if width >= 0
|
128
|
+
' ' * width
|
129
|
+
else
|
130
|
+
' ' * left_width
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# @param opt [Option]
|
135
|
+
# @return [String] Builds the second half of the help string for an Option.
|
136
|
+
def after_for(opt)
|
137
|
+
r = ""
|
138
|
+
after = description_for(opt).dup << " " << choices_for(opt)
|
139
|
+
unless after == " "
|
140
|
+
r << "# "
|
141
|
+
r << Output.wrap_text(after, left_width + padding(2).size, @opts[:width])
|
142
|
+
end
|
143
|
+
r
|
144
|
+
end
|
145
|
+
|
146
|
+
def names_for(opt)
|
147
|
+
opt.to_s
|
148
|
+
end
|
149
|
+
|
150
|
+
def description_for(opt)
|
151
|
+
opt.description
|
152
|
+
end
|
153
|
+
|
154
|
+
def args_for(opt)
|
155
|
+
if opt.args != [] && !opt.config[:boolean] == true
|
156
|
+
opt.args.to_s
|
157
|
+
else
|
158
|
+
""
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def choices_for(opt)
|
163
|
+
if opt.args.size == 1 && !opt.args.first.choice_str.empty?
|
164
|
+
opt.args.first.choice_str
|
165
|
+
else
|
166
|
+
""
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|