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