choosy 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,27 +1,42 @@
1
1
  require 'choosy/errors'
2
+ require 'tsort'
2
3
 
3
4
  module Choosy
5
+ class OptionBuilderHash < Hash
6
+ include TSort
7
+ alias tsort_each_node each_key
8
+
9
+ def tsort_each_child(node, &block)
10
+ deps = fetch(node).option.dependent_options
11
+ deps.each(&block) unless deps.nil?
12
+ end
13
+ end
14
+
4
15
  class BaseCommand
5
- attr_accessor :name, :summary, :description, :printer
16
+ attr_accessor :name, :summary, :printer
6
17
  attr_reader :builder, :listing, :option_builders
7
18
 
8
- def initialize(name)
19
+ def initialize(name, &block)
9
20
  @name = name
10
21
  @listing = []
11
- @option_builders = {}
22
+ @option_builders = OptionBuilderHash.new
12
23
 
13
24
  @builder = create_builder
14
- yield @builder if block_given?
25
+ if block_given?
26
+ @builder.instance_eval(&block)
27
+ end
15
28
  @builder.finalize!
16
29
  end
17
30
 
18
31
  def alter(&block)
19
- yield @builder if block_given?
32
+ if block_given?
33
+ @builder.instance_eval(&block)
34
+ end
20
35
  @builder.finalize!
21
36
  end
22
37
 
23
38
  def options
24
- @option_builders.values.map {|b| b.option}
39
+ @option_builders.tsort.map {|key| @option_builders[key].option }
25
40
  end
26
41
 
27
42
  def parse!(args, propagate=false)
@@ -30,7 +45,7 @@ module Choosy
30
45
  else
31
46
  begin
32
47
  return parse(args)
33
- rescue Choosy::ValidationError, Choosy::ConversionError, Choosy::ParseError => e
48
+ rescue Choosy::ValidationError, Choosy::ConversionError, Choosy::ParseError, Choosy::SuperParseError => e
34
49
  $stderr << "#{@name}: #{e.message}\n"
35
50
  exit 1
36
51
  rescue Choosy::HelpCalled => e
@@ -6,12 +6,16 @@ require 'choosy/verifier'
6
6
 
7
7
  module Choosy
8
8
  class Command < BaseCommand
9
- attr_accessor :executor, :argument_validation
9
+ attr_accessor :executor, :arguments
10
10
 
11
11
  def execute!(args)
12
12
  raise Choosy::ConfigurationError.new("No executor given for: #{name}") unless executor
13
13
  result = parse!(args)
14
- executor.call(result.options, result.args)
14
+ if executor.is_a?(Proc)
15
+ executor.call(result.args, result.options)
16
+ else
17
+ executor.execute!(result.args, result.options)
18
+ end
15
19
  end
16
20
 
17
21
  protected
@@ -20,13 +24,17 @@ module Choosy
20
24
  end
21
25
 
22
26
  def handle_help(hc)
23
- printer.print!(self)
27
+ puts printer.print!(self)
24
28
  end
25
29
 
26
30
  def parse(args)
27
31
  parser = Parser.new(self)
28
32
  result = parser.parse!(args)
29
- result.verify!
33
+
34
+ verifier = Verifier.new
35
+ verifier.verify!(result)
36
+
37
+ result
30
38
  end
31
39
  end
32
40
  end
@@ -0,0 +1,88 @@
1
+ require 'choosy/errors'
2
+ require 'choosy/argument'
3
+ require 'choosy/converter'
4
+
5
+ module Choosy::DSL
6
+ class ArgumentBuilder
7
+ def initialize
8
+ @count_called = false
9
+ end
10
+
11
+ def argument
12
+ @argument ||= Choosy::Argument.new
13
+ end
14
+
15
+ def required(value=nil)
16
+ argument.required = if value.nil? || value == true
17
+ true
18
+ else
19
+ false
20
+ end
21
+ end
22
+
23
+ def metaname(meta)
24
+ return if meta.nil?
25
+ argument.metaname = meta
26
+ return if @count_called
27
+
28
+ if meta =~ /\+$/
29
+ argument.multiple!
30
+ else
31
+ argument.single!
32
+ end
33
+ end
34
+
35
+ def count(restriction)
36
+ @count_called = true
37
+ if restriction.is_a?(Hash)
38
+ lower_bound = restriction[:at_least] || restriction[:exactly] || 1
39
+ upper_bound = restriction[:at_most] || restriction[:exactly] || 1000
40
+
41
+ check_count(lower_bound)
42
+ check_count(upper_bound)
43
+ if lower_bound > upper_bound
44
+ raise Choosy::ConfigurationError.new("The upper bound (#{upper_bound}) is less than the lower bound (#{lower_bound}).")
45
+ end
46
+
47
+ argument.arity = (lower_bound .. upper_bound)
48
+ elsif restriction.is_a?(Range)
49
+ argument.arity = restriction
50
+ elsif restriction == :zero || restriction == :none
51
+ argument.boolean!
52
+ elsif restriction == :once
53
+ argument.single!
54
+ else
55
+ check_count(restriction)
56
+ argument.arity = (restriction .. restriction)
57
+ end
58
+ end
59
+
60
+ def cast(ty)
61
+ argument.cast_to = Choosy::Converter.for(ty)
62
+ if argument.cast_to.nil?
63
+ raise Choosy::ConfigurationError.new("Unknown conversion cast: #{ty}")
64
+ end
65
+ end
66
+
67
+ def validate(&block)
68
+ argument.validation_step = block
69
+ end
70
+
71
+ def die(msg)
72
+ raise Choosy::ValidationError.new("argument error: #{msg}")
73
+ end
74
+
75
+ def finalize!
76
+ if argument.arity.nil?
77
+ argument.boolean!
78
+ end
79
+ end
80
+
81
+ protected
82
+ def check_count(count)
83
+ if !count.is_a?(Integer)
84
+ raise Choosy::ConfigurationError.new("Expected a number to count, got '#{count}'")
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,6 +1,7 @@
1
1
  require 'choosy/errors'
2
2
  require 'choosy/dsl/option_builder'
3
3
  require 'choosy/printing/erb_printer'
4
+ require 'choosy/printing/formatting_element'
4
5
 
5
6
  module Choosy::DSL
6
7
  class BaseCommandBuilder
@@ -10,46 +11,37 @@ module Choosy::DSL
10
11
  @command = command
11
12
  end
12
13
 
14
+ # Generic setup
15
+
13
16
  def summary(msg)
14
17
  @command.summary = msg
15
18
  end
16
19
 
17
- def printer(kind, options=nil)
18
- return if kind.nil?
19
-
20
- p = nil
21
- if kind == :standard
22
- p = Choosy::Printing::HelpPrinter.new
23
- elsif kind == :erb
24
- p = Choosy::Printing::ERBPrinter.new
25
- if options.nil? || options[:template].nil?
26
- raise Choosy::ConfigurationError.new("no template file given to ERBPrinter")
27
- elsif !File.exist?(options[:template])
28
- raise Choosy::ConfigurationError.new("the template file doesn't exist: #{options[:template]}")
29
- end
30
- p.template = options[:template]
31
- elsif kind.respond_to?(:print!)
32
- p = kind
33
- else
34
- raise Choosy::ConfigurationError.new("Unknown printing method for help: #{kind}")
35
- end
20
+ def printer(kind, options={})
21
+ @command.printer = if kind == :standard
22
+ Choosy::Printing::HelpPrinter.new(options)
23
+ elsif kind == :erb
24
+ Choosy::Printing::ERBPrinter.new(options)
25
+ elsif kind.respond_to?(:print!)
26
+ kind
27
+ else
28
+ raise Choosy::ConfigurationError.new("Unknown printing method for help: #{kind}")
29
+ end
30
+ end
36
31
 
37
- if p.respond_to?(:color) && options && options.has_key?(:color)
38
- p.color.disable! if !options[:color]
39
- end
32
+ # Formatting
40
33
 
41
- @command.printer = p
34
+ def header(msg, *styles)
35
+ @command.listing << Choosy::Printing::FormattingElement.new(:header, msg, styles)
42
36
  end
43
37
 
44
- def desc(msg)
45
- @command.description = msg
38
+ def para(msg=nil, *styles)
39
+ @command.listing << Choosy::Printing::FormattingElement.new(:para, msg, styles)
46
40
  end
47
41
 
48
- def separator(msg=nil)
49
- @command.listing << (msg.nil? ? "" : msg)
50
- end
42
+ # Options
51
43
 
52
- def option(arg)
44
+ def option(arg, &block)
53
45
  raise Choosy::ConfigurationError.new("The option name was nil") if arg.nil?
54
46
 
55
47
  builder = nil
@@ -61,7 +53,7 @@ module Choosy::DSL
61
53
 
62
54
  to_process = arg[name]
63
55
  if to_process.is_a?(Array)
64
- builder.dependencies to_process
56
+ builder.depends_on to_process
65
57
  elsif to_process.is_a?(Hash)
66
58
  builder.from_hash to_process
67
59
  else
@@ -72,33 +64,33 @@ module Choosy::DSL
72
64
  raise Choosy::ConfigurationError.new("No configuration block was given") if !block_given?
73
65
  end
74
66
 
75
- yield builder if block_given?
67
+ if block_given?
68
+ builder.instance_eval(&block)
69
+ end
76
70
  finalize_option_builder builder
77
71
  end
78
72
 
79
- # Option types
80
73
  def self.create_conversions
81
74
  Choosy::Converter::CONVERSIONS.keys.each do |method|
82
75
  next if method == :boolean || method == :bool
83
76
 
84
- define_method method do |sym, desc, config=nil, &block|
85
- simple_option(sym, desc, true, :one, method, config, &block)
86
- end
77
+ self.class_eval <<-EOF
78
+ def #{method}(sym, desc, config=nil, &block)
79
+ simple_option(sym, desc, true, :one, :#{method}, nil, config, &block)
80
+ end
87
81
 
88
- plural = "#{method}s".to_sym
89
- define_method plural do |sym, desc, config=nil, &block|
90
- simple_option(sym, desc, true, :many, method, config, &block)
91
- end
82
+ def #{method}s(sym, desc, config=nil, &block)
83
+ simple_option(sym, desc, true, :many, :#{method}, nil, config, &block)
84
+ end
92
85
 
93
- underscore = "#{method}_"
94
- define_method underscore do |sym, desc, config=nil, &block|
95
- simple_option(sym, desc, false, :one, method, config, &block)
96
- end
86
+ def #{method}_(sym, desc, config=nil, &block)
87
+ simple_option(sym, desc, false, :one, :#{method}, nil, config, &block)
88
+ end
97
89
 
98
- plural_underscore = "#{plural}_".to_sym
99
- define_method plural_underscore do |sym, desc, config=nil, &block|
100
- simple_option(sym, desc, false, :many, method, config, &block)
101
- end
90
+ def #{method}s_(sym, desc, config=nil, &block)
91
+ simple_option(sym, desc, false, :many, :#{method}, nil, config, &block)
92
+ end
93
+ EOF
102
94
  end
103
95
  end
104
96
 
@@ -110,15 +102,24 @@ module Choosy::DSL
110
102
  alias :multiple_ :strings_
111
103
 
112
104
  def boolean(sym, desc, config=nil, &block)
113
- simple_option(sym, desc, true, :zero, :boolean, config, &block)
105
+ simple_option(sym, desc, true, :zero, :boolean, nil, config, &block)
114
106
  end
115
107
  def boolean_(sym, desc, config=nil, &block)
116
- simple_option(sym, desc, false, :zero, :boolean, config, &block)
108
+ simple_option(sym, desc, false, :zero, :boolean, nil, config, &block)
117
109
  end
118
110
  alias :bool :boolean
119
111
  alias :bool_ :boolean_
120
112
 
121
- def version(msg)
113
+ def enum(sym, allowed, desc, config=nil, &block)
114
+ simple_option(sym, desc, true, :one, :symbol, allowed, config, &block)
115
+ end
116
+
117
+ def enum_(sym, allowed, desc, config=nil, &block)
118
+ simple_option(sym, desc, false, :one, :symbol, allowed, config, &block)
119
+ end
120
+ # Additional helpers
121
+
122
+ def version(msg, &block)
122
123
  v = OptionBuilder.new(OptionBuilder::VERSION)
123
124
  v.long '--version'
124
125
  v.desc "The version number"
@@ -127,7 +128,9 @@ module Choosy::DSL
127
128
  raise Choosy::VersionCalled.new(msg)
128
129
  end
129
130
 
130
- yield v if block_given?
131
+ if block_given?
132
+ v.instance_eval(&block)
133
+ end
131
134
  finalize_option_builder v
132
135
  end
133
136
 
@@ -147,21 +150,33 @@ module Choosy::DSL
147
150
  end
148
151
 
149
152
  private
150
- def simple_option(sym, desc, allow_short, param, cast, config, &block)
153
+ def simple_option(sym, desc, allow_short, meta, cast, allowed, config, &block)
151
154
  name = sym.to_s
152
155
  builder = OptionBuilder.new sym
153
156
  builder.desc desc
154
- builder.short "-#{name[0]}" if allow_short
157
+ short = case name[0]
158
+ when Fixnum
159
+ name[0].chr
160
+ else
161
+ name[0]
162
+ end
163
+
164
+ builder.short "-#{short}" if allow_short
155
165
  builder.long "--#{name.downcase.gsub(/_/, '-')}"
156
- builder.param format_param(name, param)
166
+ builder.metaname format_meta(name, meta)
157
167
  builder.cast cast
168
+ if allowed
169
+ builder.only(*allowed)
170
+ end
158
171
  builder.from_hash config if config
159
172
 
160
- yield builder if block_given?
173
+ if block_given?
174
+ builder.instance_eval(&block)
175
+ end
161
176
  finalize_option_builder builder
162
177
  end
163
178
 
164
- def format_param(name, count)
179
+ def format_meta(name, count)
165
180
  case count
166
181
  when :zero then nil
167
182
  when :one then name.upcase
@@ -1,6 +1,7 @@
1
1
  require 'choosy/errors'
2
2
  require 'choosy/dsl/base_command_builder'
3
3
  require 'choosy/dsl/option_builder'
4
+ require 'choosy/dsl/argument_builder'
4
5
  require 'choosy/printing/help_printer'
5
6
 
6
7
  module Choosy::DSL
@@ -35,9 +36,20 @@ module Choosy::DSL
35
36
  end
36
37
 
37
38
  def arguments(&block)
38
- raise Choosy::ConfigurationError.new("No block to arguments call") if !block_given?
39
+ builder = ArgumentBuilder.new
40
+ # Set multiple by default
41
+ builder.argument.multiple!
39
42
 
40
- command.argument_validation = block
43
+ if block_given?
44
+ builder.instance_eval(&block)
45
+ end
46
+
47
+ builder.finalize!
48
+ if builder.argument.metaname.nil?
49
+ builder.metaname 'ARGS+'
50
+ end
51
+
52
+ command.arguments = builder.argument
41
53
  end
42
54
  end
43
55
  end
@@ -1,37 +1,38 @@
1
1
  require 'choosy/option'
2
2
  require 'choosy/errors'
3
3
  require 'choosy/converter'
4
+ require 'choosy/dsl/argument_builder'
4
5
 
5
6
  module Choosy::DSL
6
- class OptionBuilder
7
+ class OptionBuilder < ArgumentBuilder
7
8
  HELP = :__help__
8
9
  VERSION = :__version__
9
10
 
10
- ZERO_ARITY = (0 .. 0)
11
- ONE_ARITY = (1 .. 1)
12
- MANY_ARITY = (1 .. 1000)
13
-
14
- attr_reader :option
15
-
16
11
  def initialize(name)
17
- @option = Choosy::Option.new(name)
18
- @count_called = false
12
+ super()
13
+ @name = name
14
+ end
15
+
16
+ def option
17
+ @argument ||= Choosy::Option.new(@name)
19
18
  end
20
19
 
21
- def short(flag, param=nil)
20
+ alias :argument :option
21
+
22
+ def short(flag, meta=nil)
22
23
  option.short_flag = flag
23
- param(param)
24
+ metaname(meta)
24
25
  end
25
26
 
26
- def long(flag, param=nil)
27
+ def long(flag, meta=nil)
27
28
  option.long_flag = flag
28
- param(param)
29
+ metaname(meta)
29
30
  end
30
31
 
31
- def flags(shorter, longer=nil, parameter=nil)
32
+ def flags(shorter, longer=nil, meta=nil)
32
33
  short(shorter)
33
34
  long(longer) if longer
34
- param(parameter) if parameter
35
+ metaname(meta) if meta
35
36
  end
36
37
 
37
38
  def desc(description)
@@ -42,77 +43,32 @@ module Choosy::DSL
42
43
  option.default_value = value
43
44
  end
44
45
 
45
- def required(value=nil)
46
- option.required = if value.nil? || value == true
47
- true
48
- else
49
- false
50
- end
51
- end
52
-
53
- def param(param)
54
- return if param.nil?
55
- option.flag_parameter = param
56
- return if @count_called
57
-
58
- if param =~ /\+$/
59
- option.arity = MANY_ARITY
46
+ def depends_on(*args)
47
+ if args.count == 1 && args[0].is_a?(Array)
48
+ option.dependent_options = args[0]
60
49
  else
61
- option.arity = ONE_ARITY
50
+ option.dependent_options = args
62
51
  end
63
52
  end
64
53
 
65
- def count(restriction)
66
- @count_called = true
67
- if restriction.is_a?(Hash)
68
- lower_bound = restriction[:at_least] || restriction[:exactly] || 1
69
- upper_bound = restriction[:at_most] || restriction[:exactly] || 1000
70
-
71
- check_count(lower_bound)
72
- check_count(upper_bound)
73
- if lower_bound > upper_bound
74
- raise Choosy::ConfigurationError.new("The upper bound (#{upper_bound}) is less than the lower bound (#{lower_bound}).")
75
- end
76
-
77
- option.arity = (lower_bound .. upper_bound)
78
- elsif restriction == :zero || restriction == :none
79
- option.arity = ZERO_ARITY
80
- elsif restriction == :once
81
- option.arity = ONE_ARITY
82
- else
83
- check_count(restriction)
84
- option.arity = (restriction .. restriction)
85
- end
54
+ def only(*args)
55
+ option.allowable_values = args
86
56
  end
87
57
 
88
- def cast(ty)
89
- option.cast_to = Choosy::Converter.for(ty)
90
- if option.cast_to.nil?
91
- raise Choosy::ConfigurationError.new("Unknown conversion cast: #{ty}")
92
- end
58
+ def negate(prefix=nil)
59
+ prefix ||= 'no'
60
+ option.negation = prefix
93
61
  end
94
-
95
- def validate(&block)
96
- option.validation_step = block
97
- end
98
-
99
- def fail(msg)
62
+
63
+ def die(msg)
100
64
  flag_fmt = if option.short_flag && option.long_flag
101
65
  "#{option.short_flag}/#{option.long_flag}"
102
66
  end
103
67
  flag_fmt ||= option.short_flag || option.long_flag
104
- flag_param = if option.flag_parameter
105
- " #{option.flag_parameter}"
68
+ flag_meta = if option.metaname
69
+ " #{option.metaname}"
106
70
  end
107
- raise Choosy::ValidationError.new("#{flag_fmt}#{flag_param}: #{msg}")
108
- end
109
-
110
- def dependencies(*args)
111
- if args.count == 1 && args[0].is_a?(Array)
112
- option.dependent_options = args[0]
113
- else
114
- option.dependent_options = args
115
- end
71
+ raise Choosy::ValidationError.new("#{flag_fmt}#{flag_meta}: #{msg}")
116
72
  end
117
73
 
118
74
  def from_hash(hash)
@@ -132,23 +88,27 @@ module Choosy::DSL
132
88
  end
133
89
 
134
90
  def finalize!
135
- if option.arity.nil?
136
- option.arity = ZERO_ARITY
137
- end
91
+ super
138
92
 
139
93
  if option.cast_to.nil?
140
- if option.arity == ZERO_ARITY
94
+ if option.boolean?
141
95
  option.cast_to = :boolean
142
96
  else
143
97
  option.cast_to = :string
144
98
  end
145
99
  end
146
- end
147
100
 
148
- private
149
- def check_count(count)
150
- if !count.is_a?(Integer)
151
- raise Choosy::ConfigurationError.new("Expected a number to count, got '#{count}'")
101
+ if option.boolean?
102
+ if option.restricted?
103
+ raise Choosy::ConfigurationError.new("Options cannot be both boolean and restricted to certain arguments: #{option.name}")
104
+ elsif option.negated? && option.long_flag.nil?
105
+ raise Choosy::ConfigurationError.new("The long flag is required for negation: #{option.name}")
106
+ end
107
+ option.default_value ||= false
108
+ else
109
+ if option.negated?
110
+ raise Choosy::ConfigurationError.new("Unable to negate a non-boolean option: #{option.name}")
111
+ end
152
112
  end
153
113
  end
154
114
  end