clive 0.8.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/LICENSE +1 -1
  2. data/README.md +328 -227
  3. data/lib/clive.rb +130 -50
  4. data/lib/clive/argument.rb +170 -0
  5. data/lib/clive/arguments.rb +139 -0
  6. data/lib/clive/arguments/parser.rb +210 -0
  7. data/lib/clive/base.rb +189 -0
  8. data/lib/clive/command.rb +342 -444
  9. data/lib/clive/error.rb +66 -0
  10. data/lib/clive/formatter.rb +57 -141
  11. data/lib/clive/formatter/colour.rb +37 -0
  12. data/lib/clive/formatter/plain.rb +172 -0
  13. data/lib/clive/option.rb +185 -75
  14. data/lib/clive/option/runner.rb +163 -0
  15. data/lib/clive/output.rb +141 -16
  16. data/lib/clive/parser.rb +180 -87
  17. data/lib/clive/struct_hash.rb +109 -0
  18. data/lib/clive/type.rb +117 -0
  19. data/lib/clive/type/definitions.rb +170 -0
  20. data/lib/clive/type/lookup.rb +23 -0
  21. data/lib/clive/version.rb +3 -3
  22. data/spec/clive/a_cli_spec.rb +245 -0
  23. data/spec/clive/argument_spec.rb +148 -0
  24. data/spec/clive/arguments/parser_spec.rb +35 -0
  25. data/spec/clive/arguments_spec.rb +191 -0
  26. data/spec/clive/command_spec.rb +276 -209
  27. data/spec/clive/formatter/colour_spec.rb +129 -0
  28. data/spec/clive/formatter/plain_spec.rb +129 -0
  29. data/spec/clive/option/runner_spec.rb +92 -0
  30. data/spec/clive/option_spec.rb +149 -23
  31. data/spec/clive/output_spec.rb +86 -2
  32. data/spec/clive/parser_spec.rb +201 -81
  33. data/spec/clive/struct_hash_spec.rb +82 -0
  34. data/spec/clive/type/definitions_spec.rb +312 -0
  35. data/spec/clive/type_spec.rb +107 -0
  36. data/spec/clive_spec.rb +60 -0
  37. data/spec/extras/expectations.rb +86 -0
  38. data/spec/extras/focus.rb +22 -0
  39. data/spec/helper.rb +35 -0
  40. metadata +56 -36
  41. data/lib/clive/bool.rb +0 -67
  42. data/lib/clive/exceptions.rb +0 -54
  43. data/lib/clive/flag.rb +0 -199
  44. data/lib/clive/switch.rb +0 -31
  45. data/lib/clive/tokens.rb +0 -141
  46. data/spec/clive/bool_spec.rb +0 -54
  47. data/spec/clive/flag_spec.rb +0 -117
  48. data/spec/clive/formatter_spec.rb +0 -108
  49. data/spec/clive/switch_spec.rb +0 -14
  50. data/spec/clive/tokens_spec.rb +0 -38
  51. data/spec/shared_specs.rb +0 -16
  52. data/spec/spec_helper.rb +0 -12
@@ -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
@@ -1,145 +1,61 @@
1
- module Clive
2
-
3
- # The formatter controls formatting of help. It can be configured
4
- # using Clive::Command#help_formatter.
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
- class Obj
8
- def initialize(args)
9
- args.each do |k, v|
10
- self.class.send(:define_method, k) { v }
11
- end
12
- end
13
-
14
- # Evaluate the code given within the Obj created.
15
- def evaluate(code)
16
- eval(code)
17
- end
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