choosy 0.1.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 (42) hide show
  1. data/Gemfile +7 -0
  2. data/Gemfile.lock +25 -0
  3. data/LICENSE +23 -0
  4. data/README.markdown +393 -0
  5. data/Rakefile +57 -0
  6. data/lib/VERSION +1 -0
  7. data/lib/choosy/base_command.rb +59 -0
  8. data/lib/choosy/command.rb +32 -0
  9. data/lib/choosy/converter.rb +115 -0
  10. data/lib/choosy/dsl/base_command_builder.rb +172 -0
  11. data/lib/choosy/dsl/command_builder.rb +43 -0
  12. data/lib/choosy/dsl/option_builder.rb +155 -0
  13. data/lib/choosy/dsl/super_command_builder.rb +41 -0
  14. data/lib/choosy/errors.rb +11 -0
  15. data/lib/choosy/option.rb +22 -0
  16. data/lib/choosy/parse_result.rb +64 -0
  17. data/lib/choosy/parser.rb +184 -0
  18. data/lib/choosy/printing/color.rb +101 -0
  19. data/lib/choosy/printing/erb_printer.rb +23 -0
  20. data/lib/choosy/printing/help_printer.rb +174 -0
  21. data/lib/choosy/super_command.rb +77 -0
  22. data/lib/choosy/super_parser.rb +81 -0
  23. data/lib/choosy/verifier.rb +62 -0
  24. data/lib/choosy/version.rb +12 -0
  25. data/lib/choosy.rb +16 -0
  26. data/spec/choosy/base_command_spec.rb +11 -0
  27. data/spec/choosy/command_spec.rb +51 -0
  28. data/spec/choosy/converter_spec.rb +145 -0
  29. data/spec/choosy/dsl/base_command_builder_spec.rb +328 -0
  30. data/spec/choosy/dsl/commmand_builder_spec.rb +80 -0
  31. data/spec/choosy/dsl/option_builder_spec.rb +386 -0
  32. data/spec/choosy/dsl/super_command_builder_spec.rb +83 -0
  33. data/spec/choosy/parser_spec.rb +275 -0
  34. data/spec/choosy/printing/color_spec.rb +74 -0
  35. data/spec/choosy/printing/help_printer_spec.rb +117 -0
  36. data/spec/choosy/super_command_spec.rb +80 -0
  37. data/spec/choosy/super_parser_spec.rb +106 -0
  38. data/spec/choosy/verifier_spec.rb +180 -0
  39. data/spec/integration/command-A_spec.rb +37 -0
  40. data/spec/integration/supercommand-A_spec.rb +61 -0
  41. data/spec/spec_helpers.rb +30 -0
  42. metadata +150 -0
@@ -0,0 +1,275 @@
1
+ require 'spec_helpers'
2
+ require 'choosy/parser'
3
+ require 'choosy/command'
4
+ require 'choosy/dsl/command_builder'
5
+
6
+ module Choosy
7
+ class ParserBuilder
8
+ def initialize
9
+ @terminals = nil
10
+ @lazy = false
11
+ @command = Choosy::Command.new(:parser)
12
+ end
13
+
14
+ def lazy!
15
+ @lazy = true
16
+ self
17
+ end
18
+
19
+ def parse!(*args)
20
+ parser = build
21
+ parser.parse!(args)
22
+ end
23
+
24
+ def terminals(*terms)
25
+ @terminals = terms
26
+ self
27
+ end
28
+
29
+ def boolean(sym, default=nil)
30
+ default ||= false
31
+ @command.alter do |c|
32
+ c.boolean(sym, sym.to_s, :default => default)
33
+ end
34
+ self
35
+ end
36
+
37
+ def single(sym)
38
+ @command.alter do |c|
39
+ c.single(sym, sym.to_s)
40
+ end
41
+ self
42
+ end
43
+
44
+ def multiple(sym, min=nil, max=nil)
45
+ min ||= 1
46
+ max ||= 1000
47
+ @command.alter do |c|
48
+ c.multiple(sym, sym.to_s) do |m|
49
+ m.count :at_least => min, :at_most => max
50
+ end
51
+ end
52
+ self
53
+ end
54
+
55
+ def build
56
+ Parser.new(@command, @lazy, @terminals)
57
+ end
58
+ end
59
+
60
+ describe Parser do
61
+ before :each do
62
+ @pb = ParserBuilder.new
63
+ end
64
+
65
+ describe "with no options" do
66
+ it "should handle everyting after the '--'" do
67
+ @pb.parse!('--', '-a', 'not an option').args.should have(2).items
68
+ end
69
+
70
+ it "should be able to parse a list of unremarvable objects" do
71
+ @pb.parse!('a', 'b', 'c').args.should have(3).items
72
+ end
73
+
74
+ it "should stop when it encounters a terminal" do
75
+ @pb.terminals('a', 'b')
76
+ res = @pb.parse!('c', 'n', 'b', 'q')
77
+ res.args.should eql(['c', 'n'])
78
+ res.unparsed.should eql(['b', 'q'])
79
+ end
80
+
81
+ it "should capture even if the first item is a terminal" do
82
+ res = @pb.terminals('a').parse!('a', 'b')
83
+ res.unparsed.should eql(['a', 'b'])
84
+ end
85
+ end
86
+
87
+ describe "when being built" do
88
+ it "should fail when duplicate short options are given" do
89
+ @pb.boolean :opt
90
+ @pb.boolean :opt2
91
+ attempting {
92
+ @pb.build
93
+ }.should raise_error(Choosy::ConfigurationError, /Duplicate option: '-o'/)
94
+ end
95
+
96
+ it "should fail when duplicate long options are given" do
97
+ @pb.boolean :opt
98
+ @pb.boolean :Opt
99
+ attempting {
100
+ @pb.build
101
+ }.should raise_error(Choosy::ConfigurationError, /Duplicate option: '--opt'/)
102
+ end
103
+ end
104
+
105
+ describe "while unsuccessfully parsing arguments" do
106
+ it "should fail with an unassociated '-'" do
107
+ attempting {
108
+ @pb.parse!('a', '-')
109
+ }.should raise_error(Choosy::ParseError, /'-'/)
110
+ end
111
+
112
+ it "should fail on unrecognized option" do
113
+ attempting {
114
+ @pb.parse!('a', '-l')
115
+ }.should raise_error(Choosy::ParseError, "Unrecognized option: '-l'")
116
+ end
117
+
118
+ it "should fail when a boolean argument attaches an argument" do
119
+ attempting {
120
+ @pb.boolean(:o).parse!('-o=blah')
121
+ }.should raise_error(Choosy::ParseError, "Argument given to boolean flag: '-o=blah'")
122
+ end
123
+
124
+ it "should fail when a required argument is missing" do
125
+ attempting {
126
+ @pb.single(:option).parse!('-o')
127
+ }.should raise_error(Choosy::ParseError, "Argument missing for option: '-o'")
128
+ end
129
+
130
+ describe "of multiple arity" do
131
+ it "should fail when not enough arguments are provided after the '='" do
132
+ attempting {
133
+ @pb.multiple(:option, 2).parse!('-o=Opt')
134
+ }.should raise_error(Choosy::ParseError, "The '-o' flag requires at least 2 arguments")
135
+ end
136
+
137
+ it "should fail when not enough arguments are provided" do
138
+ attempting {
139
+ puts @pb.multiple(:option, 3).parse!('-o', 'Opt', 'OO').options
140
+ }.should raise_error(Choosy::ParseError, "The '-o' flag requires at least 3 arguments")
141
+ end
142
+ end
143
+ end
144
+
145
+ describe "while successfully parsing options" do
146
+ it "should handle a single short boolean argument" do
147
+ res = @pb.boolean(:o).parse!('-o')
148
+ res.args.should be_empty
149
+ res.options.should eql({:o => true})
150
+ end
151
+
152
+ it "should handle a boolean flag whose default is true" do
153
+ res = @pb.boolean(:opt, true).parse!('-o')
154
+ res.options.should eql({:opt => false})
155
+ end
156
+
157
+ it "should handle a flag with an arity of 1" do
158
+ res = @pb.single(:option).parse!('-o', 'Opt')
159
+ res.args.should be_empty
160
+ res.options.should eql({:option => 'Opt'})
161
+ end
162
+
163
+ it "should handle a flag of arity 1 with an '=' sign" do
164
+ res = @pb.single(:option).parse!('-o=Opt')
165
+ res.args.should be_empty
166
+ res.options.should eql({:option => 'Opt'})
167
+ end
168
+
169
+ describe "of multiple arity" do
170
+ it "should parse a single argument" do
171
+ res = @pb.multiple(:option).parse!('-o', 'Opt')
172
+ res.args.should be_empty
173
+ res.options.should eql({:option => ['Opt']})
174
+ end
175
+
176
+ it "should leave args at the end if limited in size" do
177
+ res = @pb.multiple(:option, 1, 2).parse!('-o', '1', '2', '3')
178
+ res.args.should eql(['3'])
179
+ res.options.should eql({:option => ['1', '2']})
180
+ end
181
+
182
+ it "should handle the '-' part to end argument parsing to options" do
183
+ res = @pb.multiple(:option).parse!('-o', '1', '2', '-', '3')
184
+ res.args.should eql(['3'])
185
+ res.options.should eql({:option => ['1', '2']})
186
+ end
187
+ end
188
+
189
+ describe "in combination" do
190
+ it "should handle multiple boolean flags" do
191
+ @pb.boolean(:abs)
192
+ @pb.boolean(:not)
193
+ res = @pb.parse!('-a', '-n', 'q')
194
+ res.args.should eql(['q'])
195
+ res.options.should eql({:abs => true, :not => true})
196
+ end
197
+
198
+ it "should be able to hantld multi-arg options and booleans" do
199
+ @pb.boolean(:abs)
200
+ @pb.multiple(:mult)
201
+ res = @pb.parse!('a', '-m', 'b', 'c', '-a', 'c')
202
+ res.args.should eql(['a', 'c'])
203
+ res.options.should eql({:abs => true, :mult => ['b', 'c']})
204
+ end
205
+
206
+ it "should be able to handle multiple single arg options" do
207
+ @pb.single(:sub)
208
+ @pb.single(:add)
209
+ res = @pb.parse!('-s', '1', '3', '-a', '2', '4')
210
+ res.args.should eql(['3', '4'])
211
+ res.options.should eql({:sub => '1', :add => '2'})
212
+ end
213
+ end
214
+ end
215
+
216
+ describe "while being lazy" do
217
+ it "should keep extra arguments as unparsed" do
218
+ res = @pb.lazy!.parse!('a', 'b')
219
+ res.unparsed.should eql(['a', 'b'])
220
+ end
221
+
222
+ it "should simply retain the '-' character" do
223
+ res = @pb.lazy!.parse!('a', '-', 'b')
224
+ res.unparsed.should eql(['a', '-', 'b'])
225
+ end
226
+
227
+ it "should feed args after '--'" do
228
+ res = @pb.lazy!.parse!('a', '--', 'z')
229
+ res.unparsed.should eql(['a', '--', 'z'])
230
+ end
231
+
232
+ it "should skip options it doesn't capture" do
233
+ res = @pb.lazy!.parse!('-a', 'b')
234
+ res.unparsed.should eql(['-a', 'b'])
235
+ res.options.should eql({})
236
+ end
237
+
238
+ it "should skip irrelevant boolean args" do
239
+ res = @pb.lazy!.boolean(:opt).parse!('a', '-b', '--opt', 'c')
240
+ res.unparsed.should eql(['a', '-b', 'c'])
241
+ end
242
+
243
+ it "should capture relevant boolean args" do
244
+ res = @pb.lazy!.boolean(:opt).parse!('a', '-b', '--opt', 'c')
245
+ res.options.should eql({:opt => true})
246
+ end
247
+
248
+ it "should capture relevant 1 arity args" do
249
+ res = @pb.lazy!.single(:opt).parse!('-a', 'n', '-o', 'i', 'q')
250
+ res.options.should eql({:opt => 'i'})
251
+ end
252
+
253
+ it "should skip uncaptured 1 arity args" do
254
+ res = @pb.lazy!.single(:opt).parse!('-a', 'n', '-o', 'i', 'q')
255
+ res.unparsed.should eql(['-a', 'n', 'q'])
256
+ end
257
+
258
+ it "should capture multipe argument items" do
259
+ res = @pb.lazy!.multiple(:opt).parse!('-a', '-o', 'r', 's', 't', '-', 'q')
260
+ res.options.should eql({:opt => ['r', 's', 't']})
261
+ end
262
+
263
+ it "should skip anything stopped by the '-' in a multi-arg parse" do
264
+ res = @pb.lazy!.multiple(:opt).parse!('-a', '-o', 'r', 's', 't', '-', 'q')
265
+ res.unparsed.should eql(['-a', 'q'])
266
+ end
267
+
268
+ it "should include the terminals in the unparsed part" do
269
+ res = @pb.lazy!.boolean(:a).terminals('b', 'c').parse!('q', '-a', 'b', 'c')
270
+ res.unparsed.should eql(['q', 'b', 'c'])
271
+ end
272
+ end#lazy
273
+ end
274
+ end
275
+
@@ -0,0 +1,74 @@
1
+ require 'spec_helpers'
2
+ require 'choosy/printing/color'
3
+
4
+ module Choosy::Printing
5
+ describe Color do
6
+ before :each do
7
+ @c = Color.new
8
+ end
9
+
10
+ it "should be enabled when run from the command line" do
11
+ @c.disabled?.should be(false)
12
+ end
13
+
14
+ it "should not respond to random colors" do
15
+ attempting {
16
+ @c.non_color
17
+ }.should raise_error(NoMethodError)
18
+ end
19
+
20
+ it "should respond to colors" do
21
+ Color::COLORS.each_key do |k|
22
+ @c.respond_to?(k).should be(true)
23
+ end
24
+ end
25
+
26
+ it "should respond to effects" do
27
+ Color::EFFECTS.each_key do |k|
28
+ @c.respond_to?(k).should be(true)
29
+ end
30
+ end
31
+
32
+ it "should return the string itself if not enabled" do
33
+ @c.disable!
34
+ @c.red("this").should eql("this")
35
+ end
36
+
37
+ it "should return an empty string if not enabled" do
38
+ @c.disable!
39
+ @c.red.should eql("")
40
+ end
41
+
42
+ it "should fail on too many arguments" do
43
+ attempting {
44
+ @c.red("string", :foreground, "extra")
45
+ }.should raise_error(ArgumentError, /Color#red/)
46
+ end
47
+
48
+ it "should fail when setting a style not foreground or background" do
49
+ attempting {
50
+ @c.red("string", :no_method)
51
+ }.should raise_error(ArgumentError, /:foreground or :background/)
52
+ end
53
+
54
+ it "should return a formatted string for just the color code when no args are given" do
55
+ @c.red.should eql("e31[m")
56
+ end
57
+
58
+ it "should return a formatted string with a reset if a full string is supplied" do
59
+ @c.red("this").should eql("e31[mthise0[m")
60
+ end
61
+
62
+ it "should recognize when the foreground is being set" do
63
+ @c.red("this", :foreground).should eql("e31[mthise0[m")
64
+ end
65
+
66
+ it "should recognize when the background is being set" do
67
+ @c.red("this", :background).should eql("e41[mthise0[m")
68
+ end
69
+
70
+ it "should also handle effects" do
71
+ @c.blink("this").should eql("e5[mthise0[m")
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,117 @@
1
+ require 'spec_helpers'
2
+ require 'choosy/command'
3
+ require 'choosy/printing/help_printer'
4
+
5
+ module Choosy::Printing
6
+ describe HelpPrinter do
7
+ before :each do
8
+ @h = HelpPrinter.new
9
+ @c = Choosy::Command.new :foo do |foo|
10
+ foo.summary "This is a summary of a command."
11
+ foo.desc <<EOF
12
+ This is a description of this command that should span
13
+
14
+ multiple lines and carry itself beyond the average line length when actually called out from the unit tests itself so that we can correctly guage the line wrapping.
15
+ EOF
16
+ foo.separator
17
+ foo.boolean :evaluate, "The evaluation"
18
+ foo.integer :count, "The count"
19
+ foo.separator
20
+
21
+ foo.separator 'Indifferent options:'
22
+ foo.boolean :debug, "Debug output"
23
+ foo.version "1.2"
24
+ foo.help
25
+ end
26
+ end
27
+
28
+ it "should know the width of the screen, if possible, or set a default [COULD BREAK ON YOUR MACHINE]" do
29
+ @h.columns.should satisfy {|c| c >= HelpPrinter::DEFAULT_COLUMN_COUNT }
30
+ end
31
+
32
+ it "should know the lenght the screen, if possible, or set a default [COULD BREAK ON YOUR MACHINE]" do
33
+ @h.lines.should satisfy {|l| l >= HelpPrinter::DEFAULT_LINE_COUNT }
34
+ end
35
+
36
+ it "should now how to format a usage string" do
37
+ o = capture :stdout do
38
+ @h.print_usage(@c)
39
+ end
40
+
41
+ o.should eql("USAGE: foo [OPTIONS]\n")
42
+ end
43
+
44
+ it "should show the usage string if it accepts extra arguments" do
45
+ @c.alter do |foo|
46
+ foo.arguments do |args|
47
+ end
48
+ end
49
+
50
+ o = capture :stdout do
51
+ @h.print_usage(@c)
52
+ end
53
+
54
+ o.should eql("USAGE: foo [OPTIONS] [ARGS]\n")
55
+ end
56
+
57
+ it "should print a newline on an empty separator" do
58
+ o = capture :stdout do
59
+ @h.print_separator("")
60
+ end
61
+
62
+ o.should eql("\n\n")
63
+ end
64
+
65
+ it "should print a line with separator text" do
66
+ o = capture :stdout do
67
+ @h.print_separator("this line")
68
+ end
69
+
70
+ o.should eql("\nthis line\n")
71
+ end
72
+
73
+ it "should print out a command" do
74
+ o = capture :stdout do
75
+ @h.print_command(@c)
76
+ end
77
+
78
+ o.should eql(" foo\tThis is a summary of a command.\n")
79
+ end
80
+
81
+ it "should print the summary, if present" do
82
+ o = capture :stdout do
83
+ @h.print_summary(@c.summary)
84
+ end
85
+
86
+ o.should eql(" This is a summary of a command.\n")
87
+ end
88
+
89
+ it "should print out an option on 2 lines." do
90
+ o = capture :stdout do
91
+ @h.print_option(@c.listing[2])
92
+ end
93
+
94
+ o.should eql(" -c, --count COUNT\n The count\n")
95
+ end
96
+
97
+ it "should print out any commands that are present"
98
+
99
+ it "should print out the description" do
100
+ @h.columns = 80
101
+ o = capture :stdout do
102
+ @h.print_description(@c.description)
103
+ end
104
+
105
+ o.should eql(<<EOF
106
+
107
+ DESCRIPTION
108
+ This is a description of this command that should span
109
+
110
+ multiple lines and carry itself beyond the average line length when actually
111
+ called out from the unit tests itself so that we can correctly guage the line
112
+ wrapping.
113
+ EOF
114
+ )
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helpers'
2
+ require 'choosy/super_command'
3
+
4
+ module Choosy
5
+ describe SuperCommand do
6
+ before :each do
7
+ @c = SuperCommand.new :superfoo
8
+ end
9
+
10
+ describe :parse! do
11
+ it "should be able to print out the version number" do
12
+ @c.alter do |c|
13
+ c.version "superblah"
14
+ end
15
+
16
+ o = capture :stdout do
17
+ @c.parse!(['--version'])
18
+ end
19
+
20
+ o.should eql("superblah\n")
21
+ end
22
+
23
+ it "should print out the supercommand help message" do
24
+ @c.alter do |c|
25
+ c.help
26
+ end
27
+
28
+ o = capture :stdout do
29
+ @c.parse!([])
30
+ end
31
+
32
+ o.should match(/USAGE:/)
33
+ end
34
+
35
+ it "should print out a subcommand help message" do
36
+ @c.alter do |c|
37
+ c.help
38
+ c.command :bar do |bar|
39
+ bar.boolean :count, "The count"
40
+ end
41
+ end
42
+
43
+ o = capture :stdout do
44
+ @c.parse!(['help', 'bar'])
45
+ end
46
+
47
+ o.should match(/--count/)
48
+ end
49
+ end#parse!
50
+
51
+ describe :execute! do
52
+ it "should fail when no executor is present" do
53
+ @c.alter do |c|
54
+ c.command :bar do |bar|
55
+ bar.boolean :count, "The count"
56
+ end
57
+ end
58
+
59
+ attempting {
60
+ @c.execute!(['bar', '--count'])
61
+ }.should raise_error(Choosy::ConfigurationError, /No executor/)
62
+ end
63
+
64
+ it "should call the executors" do
65
+ count = 0
66
+ @c.alter do |c|
67
+ c.command :bar do |bar|
68
+ bar.integer :count, "The count"
69
+ bar.executor do |opts, args|
70
+ count = opts[:count]
71
+ end
72
+ end
73
+ end
74
+
75
+ @c.execute!(['bar', '--count', '5'])
76
+ count.should eql(5)
77
+ end
78
+ end#execute!
79
+ end
80
+ end
@@ -0,0 +1,106 @@
1
+ require 'spec_helpers'
2
+ require 'choosy/super_parser'
3
+ require 'choosy/super_command'
4
+
5
+ module Choosy
6
+ class SuperParserBuilder
7
+ attr_reader :super
8
+
9
+ def initialize
10
+ @super = Choosy::SuperCommand.new :super
11
+ @parsimonious = false
12
+ end
13
+
14
+ def command(name)
15
+ @super.alter do |a|
16
+ a.command name do |c|
17
+ yield c if block_given?
18
+ end
19
+ end
20
+ self
21
+ end
22
+
23
+ def single(name)
24
+ @super.alter do |a|
25
+ a.single name, name.to_s
26
+ end
27
+ self
28
+ end
29
+
30
+ def parse!(*args)
31
+ parser = build
32
+ parser.parse!(args)
33
+ end
34
+
35
+ def parsimonious!
36
+ @parsimonious = true
37
+ end
38
+
39
+ def build
40
+ SuperParser.new(@super, @parsimonious)
41
+ end
42
+ end
43
+
44
+ describe SuperParser do
45
+ before :each do
46
+ @p = SuperParserBuilder.new
47
+ end
48
+
49
+ describe "without any subcommands" do
50
+ it "should fail with a regular global options" do
51
+ attempting {
52
+ @p.single(:count).parse!('--count', '5')
53
+ }.should raise_error(Choosy::SuperParseError, /requires a command/)
54
+ end
55
+
56
+ it "should fail on unrecognized subcommands" do
57
+ attempting {
58
+ @p.single(:count).parse!('baz')
59
+ }.should raise_error(Choosy::SuperParseError, /unrecognized command: 'baz'/)
60
+ end
61
+
62
+ it "should fail on unrecognized options" do
63
+ attempting {
64
+ @p.parse!('--here')
65
+ }.should raise_error(Choosy::SuperParseError, /unrecognized option: '--here'/)
66
+ end
67
+
68
+ it "should raise a HelpCalled error when a help command is defined" do
69
+ @p.super.builder.help
70
+ attempting {
71
+ @p.parse!()
72
+ }.should raise_error(Choosy::HelpCalled, :super)
73
+ end
74
+ end
75
+
76
+ describe "with subcommands" do
77
+ describe "being liberal" do
78
+ it "should be able to parse a single subcommand" do
79
+ @p.command(:bar).parse!('bar').subresults.should have(1).item
80
+ end
81
+
82
+ it "should merge parent option with child commands" do
83
+ @p.single(:count).command(:bar).parse!('bar').subresults[0].options.should eql({:count => nil})
84
+ end
85
+
86
+ it "should merge parent option value with child" do
87
+ @p.single(:count).command(:bar).parse!('bar', '--count', '3').subresults[0].options.should eql({:count => '3'})
88
+ end
89
+
90
+ it "should collect other names of commands as arguments" do
91
+ @p.command(:bar).command(:baz).parse!('bar', 'baz').subresults.should have(1).item
92
+ end
93
+ end
94
+
95
+ describe "being parsimonious" do
96
+ before :each do
97
+ @p.parsimonious!
98
+ end
99
+
100
+ it "should parse separate commands" do
101
+ @p.command(:bar).command(:baz).parse!('bar', 'baz').subresults.should have(2).items
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end