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.
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