choosy 0.1.0 → 0.2.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 (45) hide show
  1. data/README.markdown +229 -221
  2. data/Rakefile +21 -3
  3. data/examples/bar.rb +44 -0
  4. data/examples/foo.rb +198 -0
  5. data/examples/superfoo.rb +125 -0
  6. data/lib/VERSION +1 -1
  7. data/lib/choosy/argument.rb +51 -0
  8. data/lib/choosy/base_command.rb +22 -7
  9. data/lib/choosy/command.rb +12 -4
  10. data/lib/choosy/dsl/argument_builder.rb +88 -0
  11. data/lib/choosy/dsl/base_command_builder.rb +71 -56
  12. data/lib/choosy/dsl/command_builder.rb +14 -2
  13. data/lib/choosy/dsl/option_builder.rb +43 -83
  14. data/lib/choosy/dsl/super_command_builder.rb +37 -9
  15. data/lib/choosy/option.rb +13 -11
  16. data/lib/choosy/parse_result.rb +8 -27
  17. data/lib/choosy/parser.rb +20 -16
  18. data/lib/choosy/printing/color.rb +39 -21
  19. data/lib/choosy/printing/erb_printer.rb +12 -3
  20. data/lib/choosy/printing/formatting_element.rb +17 -0
  21. data/lib/choosy/printing/help_printer.rb +204 -117
  22. data/lib/choosy/printing/terminal.rb +53 -0
  23. data/lib/choosy/super_command.rb +6 -6
  24. data/lib/choosy/super_parser.rb +26 -15
  25. data/lib/choosy/verifier.rb +61 -6
  26. data/spec/choosy/base_command_spec.rb +27 -2
  27. data/spec/choosy/command_spec.rb +31 -9
  28. data/spec/choosy/dsl/argument_builder_spec.rb +180 -0
  29. data/spec/choosy/dsl/base_command_builder_spec.rb +87 -44
  30. data/spec/choosy/dsl/commmand_builder_spec.rb +15 -4
  31. data/spec/choosy/dsl/option_builder_spec.rb +101 -191
  32. data/spec/choosy/dsl/super_command_builder_spec.rb +34 -9
  33. data/spec/choosy/parser_spec.rb +30 -8
  34. data/spec/choosy/printing/color_spec.rb +19 -5
  35. data/spec/choosy/printing/help_printer_spec.rb +152 -73
  36. data/spec/choosy/printing/terminal_spec.rb +27 -0
  37. data/spec/choosy/super_command_spec.rb +17 -17
  38. data/spec/choosy/super_parser_spec.rb +20 -10
  39. data/spec/choosy/verifier_spec.rb +137 -47
  40. data/spec/integration/command-A_spec.rb +6 -6
  41. data/spec/integration/command-B_spec.rb +45 -0
  42. data/spec/integration/supercommand-A_spec.rb +33 -27
  43. data/spec/integration/supercommand-B_spec.rb +32 -0
  44. data/spec/spec_helpers.rb +8 -5
  45. metadata +95 -54
@@ -0,0 +1,53 @@
1
+ require 'choosy/errors'
2
+ require 'choosy/printing/color'
3
+
4
+ module Choosy::Printing
5
+ module Terminal
6
+ DEFAULT_LINE_COUNT = 25
7
+ DEFAULT_COLUMN_COUNT = 80
8
+
9
+ def lines
10
+ @lines ||= find_terminal_size('LINES', 'lines', 0) || DEFAULT_LINE_COUNT
11
+ end
12
+
13
+ def lines=(value)
14
+ @lines = value
15
+ end
16
+
17
+ def columns
18
+ @columns ||= find_terminal_size('COLUMNS', 'cols', 1) || DEFAULT_COLUMN_COUNT
19
+ end
20
+
21
+ def columns=(value)
22
+ @columns = value
23
+ end
24
+
25
+ def color
26
+ @color ||= Color.new
27
+ end
28
+
29
+ # directly from hirb
30
+ def command_exists?(command)
31
+ ENV['PATH'].split(File::PATH_SEPARATOR).any? {|d| File.exists? File.join(d, command) }
32
+ end
33
+
34
+ private
35
+ # https://github.com/cldwalker/hirb
36
+ # modified from hirb
37
+ def find_terminal_size(env_name, tput_name, stty_index)
38
+ begin
39
+ if ENV[env_name] =~ /^\d$/
40
+ ENV[env_name].to_i
41
+ elsif (RUBY_PLATFORM =~ /java/ || (!STDIN.tty? && ENV['TERM'])) && command_exists?('tput')
42
+ `tput #{tput_name}`.to_i
43
+ elsif STDIN.tty? && command_exists?('stty')
44
+ `stty size`.scan(/\d+/).map { |s| s.to_i }[stty_index]
45
+ else
46
+ nil
47
+ end
48
+ rescue
49
+ nil
50
+ end
51
+ end
52
+ end
53
+ end
@@ -6,7 +6,7 @@ require 'choosy/dsl/super_command_builder'
6
6
 
7
7
  module Choosy
8
8
  class SuperCommand < BaseCommand
9
- attr_reader :command_builders
9
+ attr_accessor :metaname
10
10
 
11
11
  def command_builders
12
12
  @command_builders ||= {}
@@ -21,7 +21,7 @@ module Choosy
21
21
  end
22
22
 
23
23
  def parsimonious?
24
- @parsimonous ||= false
24
+ @parsimonious ||= false
25
25
  end
26
26
 
27
27
  def execute!(args)
@@ -52,12 +52,12 @@ module Choosy
52
52
  def handle_help(hc)
53
53
  command_name = hc.message
54
54
 
55
- if command_name.to_s == @name.to_s
56
- printer.print!(self)
55
+ if command_name == Choosy::DSL::SuperCommandBuilder::SUPER
56
+ puts printer.print!(self)
57
57
  else
58
58
  builder = command_builders[command_name]
59
59
  if builder
60
- printer.print!(builder.command)
60
+ puts printer.print!(builder.command)
61
61
  else
62
62
  $stdout << "#{@name}: #{format_help(command_name)}\n"
63
63
  exit 1
@@ -66,7 +66,7 @@ module Choosy
66
66
  end
67
67
 
68
68
  def format_help(command)
69
- help = if command_builders[:help]
69
+ help = if command_builders[Choosy::DSL::SuperCommandBuilder::HELP]
70
70
  "See '#{@name} help'."
71
71
  else
72
72
  ""
@@ -1,40 +1,50 @@
1
1
  require 'choosy/errors'
2
2
  require 'choosy/parser'
3
3
  require 'choosy/parse_result'
4
+ require 'choosy/verifier'
5
+ require 'choosy/dsl/super_command_builder'
4
6
 
5
7
  module Choosy
6
8
  class SuperParser
7
- attr_reader :terminals
9
+ attr_reader :terminals, :verifier
8
10
 
9
- def initialize(super_command, parsimonious=nil)
11
+ def initialize(super_command)
10
12
  @super_command = super_command
11
- @parsimonious = parsimonious || false
13
+ @verifier = Verifier.new
12
14
  generate_terminals
13
15
  end
14
16
 
15
- def parsimonious?
16
- @parsimonious
17
- end
18
-
19
17
  def parse!(args)
20
18
  result = parse_globals(args)
21
19
  unparsed = result.unparsed
22
20
 
23
21
  while unparsed.length > 0
24
22
  command_result = parse_command(unparsed, terminals)
25
- command_result.options.merge!(result.options)
26
23
  result.subresults << command_result
27
24
 
28
25
  unparsed = command_result.unparsed
29
26
  end
30
27
 
28
+ result.subresults.each do |subresult|
29
+ if subresult.command.name == Choosy::DSL::SuperCommandBuilder::HELP
30
+ verifier.verify!(subresult)
31
+ end
32
+ end
33
+
34
+ verifier.verify!(result)
35
+
36
+ result.subresults.each do |subresult|
37
+ subresult.options.merge!(result.options)
38
+ verifier.verify!(subresult)
39
+ end
40
+
31
41
  result
32
42
  end
33
43
 
34
44
  private
35
45
  def generate_terminals
36
46
  @terminals = []
37
- if parsimonious?
47
+ if @super_command.parsimonious?
38
48
  @super_command.commands.each do |c|
39
49
  @terminals << c.name.to_s
40
50
  end
@@ -43,14 +53,14 @@ module Choosy
43
53
 
44
54
  def parse_globals(args)
45
55
  result = SuperParseResult.new(@super_command)
46
- parser = Parser.new(@super_command, true)
56
+ parser = Parser.new(@super_command, true, @terminals)
47
57
  parser.parse!(args, result)
48
- result.verify!
58
+ verifier.verify_special!(result)
49
59
 
50
60
  # if we found a global action, we should have hit it by now...
51
61
  if result.unparsed.length == 0
52
- if @super_command.command_builders[:help]
53
- raise Choosy::HelpCalled.new(@super_command.name)
62
+ if @super_command.command_builders[Choosy::DSL::SuperCommandBuilder::HELP]
63
+ raise Choosy::HelpCalled.new(Choosy::DSL::SuperCommandBuilder::SUPER)
54
64
  else
55
65
  raise Choosy::SuperParseError.new("requires a command")
56
66
  end
@@ -72,9 +82,10 @@ module Choosy
72
82
 
73
83
  command = command_builder.command
74
84
  parser = Parser.new(command, false, terminals)
75
- command_result = parser.parse!(args)
85
+ command_result = Choosy::ParseResult.new(command, true)
86
+ parser.parse!(args, command_result)
76
87
 
77
- command_result.verify!
88
+ command_result
78
89
  end
79
90
  end
80
91
  end
@@ -3,29 +3,61 @@ require 'choosy/dsl/option_builder'
3
3
 
4
4
  module Choosy
5
5
  class Verifier
6
- def verify_options!(result)
6
+ def verify!(result)
7
7
  result.command.options.each do |option|
8
8
  required?(option, result)
9
9
  populate!(option, result)
10
10
  convert!(option, result)
11
11
  validate!(option, result)
12
12
  end
13
+
14
+ verify_arguments!(result)
15
+ end
16
+
17
+ def verify_special!(result)
18
+ result.command.options.each do |option|
19
+ if special?(option)
20
+ validate!(option, result)
21
+ end
22
+ end
13
23
  end
14
24
 
15
25
  def verify_arguments!(result)
16
- if result.command.respond_to?(:argument_validation) && result.command.argument_validation
17
- result.command.argument_validation.call(result.args)
26
+ if result.command.is_a?(Choosy::Command)
27
+ prefix = if result.subresult?
28
+ "#{result.command.name}: "
29
+ else
30
+ ""
31
+ end
32
+
33
+ if result.command.arguments
34
+ arguments = result.command.arguments
35
+
36
+ if result.args.length < arguments.arity.min
37
+ raise Choosy::ValidationError.new("#{prefix}too few arguments (minimum is #{arguments.arity.min})")
38
+ elsif result.args.length > arguments.arity.max
39
+ raise Choosy::ValidationError.new("#{prefix}too many arguments (max is #{arguments.arity.max}): '#{result.args[arguments.arity.max]}'")
40
+ end
41
+
42
+ if arguments.validation_step
43
+ arguments.validation_step.call(result.args, result.options)
44
+ end
45
+ else
46
+ if result.args.length > 0
47
+ raise Choosy::ValidationError.new("#{prefix}no arguments allowed: #{result.args.join(' ')}")
48
+ end
49
+ end
18
50
  end
19
51
  end
20
52
 
21
53
  def required?(option, result)
22
54
  if option.required? && result[option.name].nil?
23
- raise ValidationError.new("Required option '#{option.long_flag}' missing.")
55
+ raise ValidationError.new("required option missing: '#{option.long_flag}'")
24
56
  end
25
57
  end
26
58
 
27
59
  def populate!(option, result)
28
- return if option.name == Choosy::DSL::OptionBuilder::HELP || option.name == Choosy::DSL::OptionBuilder::VERSION
60
+ return if special?(option)
29
61
 
30
62
  if !result.options.has_key?(option.name) # Not already set
31
63
  if !option.default_value.nil? # Has default?
@@ -47,10 +79,23 @@ module Choosy
47
79
  end
48
80
  end
49
81
 
82
+ def restricted?(option, result)
83
+ return unless option.restricted?
84
+
85
+ value = result[option.name]
86
+ if option.arity.max > 1
87
+ value.each do |val|
88
+ check(option.allowable_values, val)
89
+ end
90
+ else
91
+ check(option.allowable_values, value)
92
+ end
93
+ end
94
+
50
95
  def validate!(option, result)
51
96
  value = result[option.name]
52
97
  if option.validation_step && exists?(value)
53
- option.validation_step.call(value)
98
+ option.validation_step.call(value, result.options)
54
99
  end
55
100
  end
56
101
 
@@ -58,5 +103,15 @@ module Choosy
58
103
  def exists?(value)
59
104
  value && value != []
60
105
  end
106
+
107
+ def special?(option)
108
+ option.name == Choosy::DSL::OptionBuilder::HELP || option.name == Choosy::DSL::OptionBuilder::VERSION
109
+ end
110
+
111
+ def check(allowable, value)
112
+ if !allowable.include?(value)
113
+ raise ValidationError.new("unrecognized value (only #{allowable.map{|s| "'#{s}'"}.join(', ')} allowed): '#{value}'")
114
+ end
115
+ end
61
116
  end
62
117
  end
@@ -3,9 +3,34 @@ require 'choosy/command'
3
3
 
4
4
  module Choosy
5
5
  describe BaseCommand do
6
+ before :each do
7
+ @cmd = Command.new :cmd
8
+ end
6
9
  it "should finalize the builder" do
7
- cmd = Command.new :cmd
8
- cmd.printer.should be_a(Choosy::Printing::HelpPrinter)
10
+ @cmd.printer.should be_a(Choosy::Printing::HelpPrinter)
9
11
  end
12
+
13
+ it "should order the options in dependency order" do
14
+ @cmd.alter do
15
+ integer :count, "Count" do
16
+ depends_on :bold
17
+ end
18
+
19
+ boolean :bold, "Bold" do
20
+ depends_on :font, :config
21
+ end
22
+
23
+ symbol :font, "Font" do
24
+ depends_on :config
25
+ end
26
+
27
+ file :config, "The config"
28
+ file :access, "Access code" do
29
+ depends_on :config, :count
30
+ end
31
+ end
32
+
33
+ @cmd.options.map {|o| o.name}.should eql([:config, :font, :bold, :count, :access])
34
+ end
10
35
  end
11
36
  end
@@ -5,13 +5,15 @@ require 'choosy/printing/help_printer'
5
5
  module Choosy
6
6
  describe Command do
7
7
  before :each do
8
- @c = Command.new :foo
8
+ @c = Command.new :foo do
9
+ arguments
10
+ end
9
11
  end
10
12
 
11
13
  describe :parse! do
12
14
  it "should print out the version number" do
13
- @c.alter do |c|
14
- c.version "blah"
15
+ @c.alter do
16
+ version "blah"
15
17
  end
16
18
 
17
19
  o = capture :stdout do
@@ -24,16 +26,15 @@ module Choosy
24
26
  end
25
27
 
26
28
  it "should print out the help info" do
27
- @c.alter do |c|
28
- c.summary "Summary"
29
- c.desc "this is a description"
30
- c.help
29
+ @c.alter do
30
+ summary "Summary"
31
+ help
31
32
  end
32
33
 
33
34
  o = capture :stdout do
34
- attempting {
35
+ #attempting {
35
36
  @c.parse!(['--help'])
36
- }.should raise_error(SystemExit)
37
+ #}.should raise_error(SystemExit)
37
38
  end
38
39
 
39
40
  o.should match(/-h, --help/)
@@ -46,6 +47,27 @@ module Choosy
46
47
  @c.execute!(['a', 'b'])
47
48
  }.should raise_error(Choosy::ConfigurationError, /No executor/)
48
49
  end
50
+
51
+ it "should call an proc" do
52
+ p = nil
53
+ @c.executor = Proc.new {|args, options| p = args}
54
+ @c.execute!(['a', 'b'])
55
+ p.should eql(['a', 'b'])
56
+ end
57
+
58
+ class FakeExecutor
59
+ attr_reader :called
60
+ def execute!(args, options)
61
+ @called = args
62
+ end
63
+ end
64
+
65
+ it "should call an executor if given" do
66
+ exec = FakeExecutor.new
67
+ @c.executor = exec
68
+ @c.execute!(['a'])
69
+ exec.called.should eql(['a'])
70
+ end
49
71
  end
50
72
  end
51
73
  end
@@ -0,0 +1,180 @@
1
+ require 'spec_helpers'
2
+ require 'choosy/dsl/argument_builder'
3
+
4
+ module Choosy::DSL
5
+ describe ArgumentBuilder do
6
+ before :each do
7
+ @builder = ArgumentBuilder.new
8
+ @argument = @builder.argument
9
+ end
10
+
11
+ describe :required do
12
+ it "should set the argument" do
13
+ @builder.required
14
+ @argument.required?.should be(true)
15
+ end
16
+
17
+ it "should set the argument on non-nil/non-true" do
18
+ @builder.required 1
19
+ @argument.required?.should be(false)
20
+ end
21
+
22
+ it "should set the argument on false" do
23
+ @builder.required false
24
+ @argument.required?.should be(false)
25
+ end
26
+ end#required
27
+
28
+ describe :metaname do
29
+ it "should be able to set the name of the metaname" do
30
+ @builder.metaname 'PARAM'
31
+ @argument.metaname.should eql('PARAM')
32
+ end
33
+
34
+ it "should set the arity on STD+ to 1+" do
35
+ @builder.metaname 'STD+'
36
+ @argument.arity.should eql(1..1000)
37
+ end
38
+
39
+ it "should set the arity on STD to 1" do
40
+ @builder.metaname 'STD'
41
+ @argument.arity.should eql(1..1)
42
+ end
43
+ end#metaname
44
+
45
+ describe :count do
46
+ describe "when welformed" do
47
+ it "should set :at_least the right arity" do
48
+ @builder.count :at_least => 32
49
+ @argument.arity.should eql(32..1000)
50
+ end
51
+
52
+ it "should set :at_most the right arity" do
53
+ @builder.count :at_most => 31
54
+ @argument.arity.should eql(1..31)
55
+ end
56
+
57
+ it "should set :once to the right arity" do
58
+ @builder.count :once
59
+ @argument.arity.should eql(1..1)
60
+ end
61
+
62
+ it "should set :zero to the right arity" do
63
+ @builder.count :zero
64
+ @argument.arity.should eql(0..0)
65
+ end
66
+
67
+ it "should set :none to the right arity" do
68
+ @builder.count :none
69
+ @argument.arity.should eql(0..0)
70
+ end
71
+
72
+ it "should set a number exactly" do
73
+ @builder.count 3
74
+ @argument.arity.should eql(3..3)
75
+ end
76
+
77
+ it "should allow for a range" do
78
+ @builder.count 1..2
79
+ @argument.arity.should eql(1..2)
80
+ end
81
+ end
82
+
83
+ describe "when malformed" do
84
+ it "should fail when the :exactly isn't a number" do
85
+ attempting {
86
+ @builder.count :exactly => 'p'
87
+ }.should raise_error(Choosy::ConfigurationError, /number/)
88
+ end
89
+
90
+ it "should fail when the :at_most isn't a number" do
91
+ attempting {
92
+ @builder.count :at_most => 'p'
93
+ }.should raise_error(Choosy::ConfigurationError, /number/)
94
+ end
95
+
96
+ it "should fail when the :at_least isn't a number" do
97
+ attempting {
98
+ @builder.count :at_least => 'p'
99
+ }.should raise_error(Choosy::ConfigurationError, /number/)
100
+ end
101
+
102
+ it "should fail when the :count isn't a number" do
103
+ attempting {
104
+ @builder.count 'p'
105
+ }.should raise_error(Choosy::ConfigurationError, /number/)
106
+ end
107
+
108
+ it "should fail when the :at_least is greater than :at_most" do
109
+ attempting {
110
+ @builder.count :at_least => 3, :at_most => 2
111
+ }.should raise_error(Choosy::ConfigurationError, /lower bound/)
112
+ end
113
+ end
114
+ end#count
115
+
116
+ describe :cast do
117
+ it "should allow symbol casts" do
118
+ @builder.cast :int
119
+ @argument.cast_to.should eql(:integer)
120
+ end
121
+
122
+ class CustomConverter
123
+ def convert(value)
124
+ end
125
+ end
126
+
127
+ it "should allow for custom conversions" do
128
+ conv = CustomConverter.new
129
+ @builder.cast conv
130
+ @argument.cast_to.should be(conv)
131
+ end
132
+
133
+ it "should fail if it doesn't know about a Type" do
134
+ attempting {
135
+ @builder.cast Choosy::Error
136
+ }.should raise_error(Choosy::ConfigurationError, /Unknown conversion/)
137
+ end
138
+
139
+ it "should fail if it doesn't know about a symbol" do
140
+ attempting {
141
+ @builder.cast :unknown_type
142
+ }.should raise_error(Choosy::ConfigurationError, /Unknown conversion/)
143
+ end
144
+ end#cast
145
+
146
+ describe :die do
147
+ it "should fail with a specific error" do
148
+ attempting {
149
+ @builder.die("Malformed argument")
150
+ }.should raise_error(Choosy::ValidationError, /argument error: Malformed/)
151
+ end
152
+ end
153
+
154
+ describe :validate do
155
+ it "should save the context of the validation in a Proc to call later" do
156
+ @builder.validate do
157
+ puts "Hi!"
158
+ end
159
+ @argument.validation_step.should be_a(Proc)
160
+ end
161
+
162
+ it "should have access to the larger context when called" do
163
+ value = nil
164
+ @builder.validate do
165
+ value = 'here'
166
+ end
167
+ @argument.validation_step.call
168
+ value.should eql('here')
169
+ end
170
+ end#validate
171
+
172
+ describe :finalize! do
173
+ it "should set the arity if not already set" do
174
+ @builder.finalize!
175
+ @argument.arity.should eql(0..0)
176
+ end
177
+ end#finalize!
178
+ end
179
+ end
180
+