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.
- data/Gemfile +7 -0
- data/Gemfile.lock +25 -0
- data/LICENSE +23 -0
- data/README.markdown +393 -0
- data/Rakefile +57 -0
- data/lib/VERSION +1 -0
- data/lib/choosy/base_command.rb +59 -0
- data/lib/choosy/command.rb +32 -0
- data/lib/choosy/converter.rb +115 -0
- data/lib/choosy/dsl/base_command_builder.rb +172 -0
- data/lib/choosy/dsl/command_builder.rb +43 -0
- data/lib/choosy/dsl/option_builder.rb +155 -0
- data/lib/choosy/dsl/super_command_builder.rb +41 -0
- data/lib/choosy/errors.rb +11 -0
- data/lib/choosy/option.rb +22 -0
- data/lib/choosy/parse_result.rb +64 -0
- data/lib/choosy/parser.rb +184 -0
- data/lib/choosy/printing/color.rb +101 -0
- data/lib/choosy/printing/erb_printer.rb +23 -0
- data/lib/choosy/printing/help_printer.rb +174 -0
- data/lib/choosy/super_command.rb +77 -0
- data/lib/choosy/super_parser.rb +81 -0
- data/lib/choosy/verifier.rb +62 -0
- data/lib/choosy/version.rb +12 -0
- data/lib/choosy.rb +16 -0
- data/spec/choosy/base_command_spec.rb +11 -0
- data/spec/choosy/command_spec.rb +51 -0
- data/spec/choosy/converter_spec.rb +145 -0
- data/spec/choosy/dsl/base_command_builder_spec.rb +328 -0
- data/spec/choosy/dsl/commmand_builder_spec.rb +80 -0
- data/spec/choosy/dsl/option_builder_spec.rb +386 -0
- data/spec/choosy/dsl/super_command_builder_spec.rb +83 -0
- data/spec/choosy/parser_spec.rb +275 -0
- data/spec/choosy/printing/color_spec.rb +74 -0
- data/spec/choosy/printing/help_printer_spec.rb +117 -0
- data/spec/choosy/super_command_spec.rb +80 -0
- data/spec/choosy/super_parser_spec.rb +106 -0
- data/spec/choosy/verifier_spec.rb +180 -0
- data/spec/integration/command-A_spec.rb +37 -0
- data/spec/integration/supercommand-A_spec.rb +61 -0
- data/spec/spec_helpers.rb +30 -0
- metadata +150 -0
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'choosy/errors'
|
2
|
+
require 'choosy/parse_result'
|
3
|
+
require 'choosy/dsl/option_builder'
|
4
|
+
|
5
|
+
module Choosy
|
6
|
+
class Parser
|
7
|
+
attr_reader :flags, :terminals, :command
|
8
|
+
|
9
|
+
def initialize(command, lazy=nil, terminals=nil)
|
10
|
+
@command = command
|
11
|
+
@lazy = lazy || false
|
12
|
+
@terminals = terminals || []
|
13
|
+
|
14
|
+
@flags = {}
|
15
|
+
return if command.options.nil?
|
16
|
+
command.options.each do |o|
|
17
|
+
verify_option(o)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse!(argv, result=nil)
|
22
|
+
index = 0
|
23
|
+
result ||= ParseResult.new(@command)
|
24
|
+
|
25
|
+
while index < argv.length
|
26
|
+
case argv[index]
|
27
|
+
when '-'
|
28
|
+
if lazy?
|
29
|
+
result.unparsed << '-'
|
30
|
+
index += 1
|
31
|
+
else
|
32
|
+
raise Choosy::ParseError.new("Unfinished option '-'")
|
33
|
+
end
|
34
|
+
when '--'
|
35
|
+
result.unparsed << '--' if lazy?
|
36
|
+
index = parse_rest(argv, index, result)
|
37
|
+
when /^-/
|
38
|
+
index = parse_option(argv, index, result)
|
39
|
+
else
|
40
|
+
index = parse_arg(argv, index, result)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
def lazy?
|
48
|
+
@lazy
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def verify_option(option)
|
53
|
+
verify_flag(option, option.short_flag)
|
54
|
+
verify_flag(option, option.long_flag)
|
55
|
+
end
|
56
|
+
|
57
|
+
def verify_flag(option, flag)
|
58
|
+
return nil if flag.nil?
|
59
|
+
if @flags[flag]
|
60
|
+
raise Choosy::ConfigurationError.new("Duplicate option: '#{flag}'")
|
61
|
+
end
|
62
|
+
@flags[flag] = option
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_option(argv, index, result)
|
66
|
+
current = argv[index]
|
67
|
+
flag, arg = current.split("=", 2)
|
68
|
+
option = @flags[flag]
|
69
|
+
|
70
|
+
if option.nil?
|
71
|
+
if lazy?
|
72
|
+
result.unparsed << current
|
73
|
+
return index + 1
|
74
|
+
else
|
75
|
+
raise Choosy::ParseError.new("Unrecognized option: '#{flag}'")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
if option.arity == Choosy::DSL::OptionBuilder::ZERO_ARITY
|
80
|
+
parse_boolean_option(result, option, index, arg, current)
|
81
|
+
elsif option.arity == Choosy::DSL::OptionBuilder::ONE_ARITY
|
82
|
+
parse_single_option(result, option, index, argv, flag, arg)
|
83
|
+
else # Vararg
|
84
|
+
parse_multiple_option(result, option, index, argv, flag, arg)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse_boolean_option(result, option, index, arg, current)
|
89
|
+
raise Choosy::ParseError.new("Argument given to boolean flag: '#{current}'") if arg
|
90
|
+
result.options[option.name] = !option.default_value
|
91
|
+
index + 1
|
92
|
+
end
|
93
|
+
|
94
|
+
def parse_single_option(result, option, index, argv, flag, arg)
|
95
|
+
if arg
|
96
|
+
result.options[option.name] = arg
|
97
|
+
index += 1
|
98
|
+
else
|
99
|
+
current, index = read_arg(argv, index + 1, result)
|
100
|
+
if current.nil?
|
101
|
+
raise Choosy::ParseError.new("Argument missing for option: '#{flag}'")
|
102
|
+
else
|
103
|
+
result.options[option.name] = current
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
index
|
108
|
+
end
|
109
|
+
|
110
|
+
def parse_multiple_option(result, option, index, argv, flag, arg)
|
111
|
+
if arg
|
112
|
+
if option.arity.min > 1
|
113
|
+
raise Choosy::ParseError.new("The '#{flag}' flag requires at least #{option.arity.min} arguments")
|
114
|
+
end
|
115
|
+
result.options[option.name] = arg
|
116
|
+
return index + 1
|
117
|
+
end
|
118
|
+
|
119
|
+
index += 1
|
120
|
+
min = index + option.arity.min
|
121
|
+
max = index + option.arity.max
|
122
|
+
args = []
|
123
|
+
|
124
|
+
while index < min
|
125
|
+
current, index = read_arg(argv, index, result)
|
126
|
+
if current.nil?
|
127
|
+
raise Choosy::ParseError.new("The '#{flag}' flag requires at least #{option.arity.min} arguments")
|
128
|
+
end
|
129
|
+
args << current
|
130
|
+
end
|
131
|
+
|
132
|
+
while index < max && index < argv.length
|
133
|
+
current, index = read_arg(argv, index, result)
|
134
|
+
break if current.nil?
|
135
|
+
args << current
|
136
|
+
end
|
137
|
+
|
138
|
+
if index < argv.length && argv[index] == '-'
|
139
|
+
index += 1
|
140
|
+
end
|
141
|
+
|
142
|
+
result.options[option.name] = args
|
143
|
+
index
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_arg(argv, index, result)
|
147
|
+
while index < argv.length
|
148
|
+
current, index = read_arg(argv, index, result)
|
149
|
+
break if current.nil?
|
150
|
+
if lazy?
|
151
|
+
result.unparsed << current
|
152
|
+
else
|
153
|
+
result.args << current
|
154
|
+
end
|
155
|
+
end
|
156
|
+
index
|
157
|
+
end
|
158
|
+
|
159
|
+
def read_arg(argv, index, result)
|
160
|
+
return [nil, index] if index >= argv.length
|
161
|
+
|
162
|
+
current = argv[index]
|
163
|
+
return [nil, index] if current[0] == '-'
|
164
|
+
if @terminals.include? current
|
165
|
+
result.unparsed.push(*argv[index, argv.length])
|
166
|
+
return [nil, argv.length]
|
167
|
+
end
|
168
|
+
[current, index + 1]
|
169
|
+
end
|
170
|
+
|
171
|
+
def parse_rest(argv, index, result)
|
172
|
+
index += 1
|
173
|
+
while index < argv.length
|
174
|
+
if lazy?
|
175
|
+
result.unparsed << argv[index]
|
176
|
+
else
|
177
|
+
result.args << argv[index]
|
178
|
+
end
|
179
|
+
index += 1
|
180
|
+
end
|
181
|
+
index
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'choosy/errors'
|
2
|
+
|
3
|
+
module Choosy::Printing
|
4
|
+
class Color
|
5
|
+
# Extrapolated from:
|
6
|
+
# http://kpumuk.info/ruby-on-rails/colorizing-console-ruby-script-output/
|
7
|
+
COLORS = {
|
8
|
+
:black => 0,
|
9
|
+
:red => 1,
|
10
|
+
:green => 2,
|
11
|
+
:yellow => 3,
|
12
|
+
:blue => 4,
|
13
|
+
:magenta => 5,
|
14
|
+
:cyan => 6,
|
15
|
+
:white => 7,
|
16
|
+
}
|
17
|
+
|
18
|
+
EFFECTS = {
|
19
|
+
:reset => 0,
|
20
|
+
:bright => 1,
|
21
|
+
:underline => 4,
|
22
|
+
:blink => 5,
|
23
|
+
:exchange => 7,
|
24
|
+
:hide => 8
|
25
|
+
}
|
26
|
+
|
27
|
+
FOREGROUND = 30
|
28
|
+
BACKGROUND = 40
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
begin
|
32
|
+
require 'Win32/Console/ANSI' if RUBY_PLATFORM =~ /win32/
|
33
|
+
@disabled = false
|
34
|
+
rescue LoadError
|
35
|
+
# STDERR.puts "You must gem install win32console to use color on Windows"
|
36
|
+
disable!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def disabled?
|
41
|
+
@disabled
|
42
|
+
end
|
43
|
+
|
44
|
+
def disable!
|
45
|
+
@disabled = true
|
46
|
+
end
|
47
|
+
|
48
|
+
def color?(color)
|
49
|
+
COLORS.has_key?(color.to_sym)
|
50
|
+
end
|
51
|
+
|
52
|
+
def effect?(effect)
|
53
|
+
EFFECTS.has_key?(effect.to_sym)
|
54
|
+
end
|
55
|
+
|
56
|
+
def respond_to?(method)
|
57
|
+
color?(method) || effect?(method)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Dynamically handle colors and effects
|
61
|
+
def method_missing(method, *args, &block)
|
62
|
+
str, offset = unpack_args(method, args)
|
63
|
+
return str || "" if disabled?
|
64
|
+
|
65
|
+
if color?(method)
|
66
|
+
bedazzle(COLORS[method] + offset, str)
|
67
|
+
elsif effect?(method)
|
68
|
+
bedazzle(EFFECTS[method], str)
|
69
|
+
else
|
70
|
+
raise NoMethodError.new("undefined method '#{method}' for Color")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def unpack_args(method, args)
|
76
|
+
case args.length
|
77
|
+
when 0
|
78
|
+
[nil, FOREGROUND]
|
79
|
+
when 1
|
80
|
+
[args[0], FOREGROUND]
|
81
|
+
when 2
|
82
|
+
case args[1]
|
83
|
+
when :foreground then [args[0], FOREGROUND]
|
84
|
+
when :background then [args[0], BACKGROUND]
|
85
|
+
else raise ArgumentError.new("unrecognized state for Color##{method}, :foreground or :background only")
|
86
|
+
end
|
87
|
+
else
|
88
|
+
raise ArgumentError.new("too many arguments to Color##{method} (max 2)")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def bedazzle(number, str)
|
93
|
+
prefix = "e#{number}[m"
|
94
|
+
if str.nil?
|
95
|
+
prefix
|
96
|
+
else
|
97
|
+
"#{prefix}#{str}e0[m"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'choosy/errors'
|
2
|
+
require 'choosy/printing/help_printer'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module Choosy::Printing
|
6
|
+
class ERBPrinter < HelpPrinter
|
7
|
+
attr_reader :command
|
8
|
+
attr_accessor :template
|
9
|
+
|
10
|
+
def print!(command)
|
11
|
+
@command = command
|
12
|
+
contents = nil
|
13
|
+
File.open(template, 'r') {|f| contents = f.read }
|
14
|
+
erb = ERB.new contents
|
15
|
+
|
16
|
+
erb.run(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def erb_binding
|
20
|
+
binding
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'choosy/errors'
|
2
|
+
require 'choosy/printing/color'
|
3
|
+
|
4
|
+
module Choosy::Printing
|
5
|
+
class HelpPrinter
|
6
|
+
DEFAULT_LINE_COUNT = 25
|
7
|
+
DEFAULT_COLUMN_COUNT = 80
|
8
|
+
|
9
|
+
attr_reader :color
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@color = Color.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def lines
|
16
|
+
@lines ||= find_terminal_size('LINES', 'lines', 0) || DEFAULT_LINE_COUNT
|
17
|
+
end
|
18
|
+
|
19
|
+
def lines=(value)
|
20
|
+
@lines = value
|
21
|
+
end
|
22
|
+
|
23
|
+
def columns
|
24
|
+
@columns ||= find_terminal_size('COLUMNS', 'cols', 1) || DEFAULT_COLUMN_COUNT
|
25
|
+
end
|
26
|
+
|
27
|
+
def columns=(value)
|
28
|
+
@columns = value
|
29
|
+
end
|
30
|
+
|
31
|
+
def colored=(val)
|
32
|
+
@color.disable! unless val
|
33
|
+
end
|
34
|
+
|
35
|
+
def print!(command)
|
36
|
+
print_usage(command)
|
37
|
+
print_summary(command.summary) if command.summary
|
38
|
+
print_description(command.description) if command.description
|
39
|
+
command.listing.each do |l|
|
40
|
+
if l.is_a?(String)
|
41
|
+
print_separator(l)
|
42
|
+
elsif l.is_a?(Choosy::Option)
|
43
|
+
print_option(l)
|
44
|
+
else
|
45
|
+
print_command(l)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# FIXME: hideously ugly
|
51
|
+
def print_usage(command)
|
52
|
+
args = if command.respond_to?(:argument_validation) && command.argument_validation
|
53
|
+
" [ARGS]"
|
54
|
+
else
|
55
|
+
""
|
56
|
+
end
|
57
|
+
cmds = if command.respond_to?(:commands)
|
58
|
+
" [COMMANDS]"
|
59
|
+
else
|
60
|
+
""
|
61
|
+
end
|
62
|
+
options = if command.option_builders.length == 0
|
63
|
+
""
|
64
|
+
else
|
65
|
+
" [OPTIONS]"
|
66
|
+
end
|
67
|
+
$stdout << "USAGE: #{command.name}#{cmds}#{options}#{args}\n"
|
68
|
+
end
|
69
|
+
|
70
|
+
def print_summary(summary)
|
71
|
+
write_lines(summary, ' ')
|
72
|
+
end
|
73
|
+
|
74
|
+
def print_description(desc)
|
75
|
+
print_separator("DESCRIPTION")
|
76
|
+
write_lines(desc, " ")
|
77
|
+
end
|
78
|
+
|
79
|
+
def print_separator(sep)
|
80
|
+
$stdout << "\n#{sep}\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
def print_option(option)
|
84
|
+
$stdout << " "
|
85
|
+
if option.short_flag
|
86
|
+
$stdout << option.short_flag
|
87
|
+
if option.long_flag
|
88
|
+
$stdout << ", "
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
if option.long_flag
|
93
|
+
$stdout << option.long_flag
|
94
|
+
end
|
95
|
+
|
96
|
+
if option.flag_parameter
|
97
|
+
$stdout << " "
|
98
|
+
$stdout << option.flag_parameter
|
99
|
+
end
|
100
|
+
|
101
|
+
$stdout << "\n"
|
102
|
+
write_lines(option.description, " ")
|
103
|
+
end
|
104
|
+
|
105
|
+
def print_command(command)
|
106
|
+
write_lines("#{command.name}\t#{command.summary}", " ")
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
def write_lines(str, prefix)
|
111
|
+
str.split("\n").each do |line|
|
112
|
+
if line.length == 0
|
113
|
+
$stdout << "\n"
|
114
|
+
else
|
115
|
+
wrap_long_lines(line, prefix)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# FIXME: not exactly pretty, but it works, mostly
|
121
|
+
MAX_BACKTRACK = 25
|
122
|
+
def wrap_long_lines(line, prefix)
|
123
|
+
index = 0
|
124
|
+
while index < line.length
|
125
|
+
segment_size = line.length - index
|
126
|
+
if segment_size >= columns
|
127
|
+
i = columns + index - prefix.length
|
128
|
+
while i > columns - MAX_BACKTRACK
|
129
|
+
if line[i] == ' '
|
130
|
+
indent_line(line[index, i - index], prefix)
|
131
|
+
index = i + 1
|
132
|
+
break
|
133
|
+
else
|
134
|
+
i -= 1
|
135
|
+
end
|
136
|
+
end
|
137
|
+
else
|
138
|
+
indent_line(line[index, line.length], prefix)
|
139
|
+
index += segment_size
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def indent_line(line, prefix)
|
145
|
+
$stdout << prefix
|
146
|
+
$stdout << line
|
147
|
+
$stdout << "\n"
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
# https://github.com/cldwalker/hirb
|
152
|
+
# modified from hirb
|
153
|
+
def find_terminal_size(env_name, tput_name, stty_index)
|
154
|
+
begin
|
155
|
+
if ENV[env_name] =~ /^\d$/
|
156
|
+
ENV[env_name].to_i
|
157
|
+
elsif (RUBY_PLATFORM =~ /java/ || (!STDIN.tty? && ENV['TERM'])) && command_exists?('tput')
|
158
|
+
`tput #{tput_name}`.to_i
|
159
|
+
elsif STDIN.tty? && command_exists?('stty')
|
160
|
+
`stty size`.scan(/\d+/).map { |s| s.to_i }[stty_index]
|
161
|
+
else
|
162
|
+
nil
|
163
|
+
end
|
164
|
+
rescue
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# directly from hirb
|
170
|
+
def command_exists?(command)
|
171
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).any? {|d| File.exists? File.join(d, command) }
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'choosy/errors'
|
2
|
+
require 'choosy/parser'
|
3
|
+
require 'choosy/base_command'
|
4
|
+
require 'choosy/super_parser'
|
5
|
+
require 'choosy/dsl/super_command_builder'
|
6
|
+
|
7
|
+
module Choosy
|
8
|
+
class SuperCommand < BaseCommand
|
9
|
+
attr_reader :command_builders
|
10
|
+
|
11
|
+
def command_builders
|
12
|
+
@command_builders ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def commands
|
16
|
+
@command_builders.values.map {|b| b.command }
|
17
|
+
end
|
18
|
+
|
19
|
+
def parsimonious=(value)
|
20
|
+
@parsimonious = value
|
21
|
+
end
|
22
|
+
|
23
|
+
def parsimonious?
|
24
|
+
@parsimonous ||= false
|
25
|
+
end
|
26
|
+
|
27
|
+
def execute!(args)
|
28
|
+
super_result = parse!(args)
|
29
|
+
super_result.subresults.each do |result|
|
30
|
+
cmd = result.command
|
31
|
+
if cmd.executor.nil?
|
32
|
+
raise Choosy::ConfigurationError.new("No executor given for: #{cmd.name}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
super_result.subresults.each do |result|
|
37
|
+
cmd = result.command
|
38
|
+
cmd.executor.call(result.options, result.args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
def create_builder
|
44
|
+
Choosy::DSL::SuperCommandBuilder.new(self)
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse(args)
|
48
|
+
parser = SuperParser.new(self)
|
49
|
+
parser.parse!(args)
|
50
|
+
end
|
51
|
+
|
52
|
+
def handle_help(hc)
|
53
|
+
command_name = hc.message
|
54
|
+
|
55
|
+
if command_name.to_s == @name.to_s
|
56
|
+
printer.print!(self)
|
57
|
+
else
|
58
|
+
builder = command_builders[command_name]
|
59
|
+
if builder
|
60
|
+
printer.print!(builder.command)
|
61
|
+
else
|
62
|
+
$stdout << "#{@name}: #{format_help(command_name)}\n"
|
63
|
+
exit 1
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def format_help(command)
|
69
|
+
help = if command_builders[:help]
|
70
|
+
"See '#{@name} help'."
|
71
|
+
else
|
72
|
+
""
|
73
|
+
end
|
74
|
+
"'#{command}' is not a standard command. #{help}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'choosy/errors'
|
2
|
+
require 'choosy/parser'
|
3
|
+
require 'choosy/parse_result'
|
4
|
+
|
5
|
+
module Choosy
|
6
|
+
class SuperParser
|
7
|
+
attr_reader :terminals
|
8
|
+
|
9
|
+
def initialize(super_command, parsimonious=nil)
|
10
|
+
@super_command = super_command
|
11
|
+
@parsimonious = parsimonious || false
|
12
|
+
generate_terminals
|
13
|
+
end
|
14
|
+
|
15
|
+
def parsimonious?
|
16
|
+
@parsimonious
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse!(args)
|
20
|
+
result = parse_globals(args)
|
21
|
+
unparsed = result.unparsed
|
22
|
+
|
23
|
+
while unparsed.length > 0
|
24
|
+
command_result = parse_command(unparsed, terminals)
|
25
|
+
command_result.options.merge!(result.options)
|
26
|
+
result.subresults << command_result
|
27
|
+
|
28
|
+
unparsed = command_result.unparsed
|
29
|
+
end
|
30
|
+
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def generate_terminals
|
36
|
+
@terminals = []
|
37
|
+
if parsimonious?
|
38
|
+
@super_command.commands.each do |c|
|
39
|
+
@terminals << c.name.to_s
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_globals(args)
|
45
|
+
result = SuperParseResult.new(@super_command)
|
46
|
+
parser = Parser.new(@super_command, true)
|
47
|
+
parser.parse!(args, result)
|
48
|
+
result.verify!
|
49
|
+
|
50
|
+
# if we found a global action, we should have hit it by now...
|
51
|
+
if result.unparsed.length == 0
|
52
|
+
if @super_command.command_builders[:help]
|
53
|
+
raise Choosy::HelpCalled.new(@super_command.name)
|
54
|
+
else
|
55
|
+
raise Choosy::SuperParseError.new("requires a command")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_command(args, terminals)
|
63
|
+
command_name = args.shift
|
64
|
+
command_builder = @super_command.command_builders[command_name.to_sym]
|
65
|
+
if command_builder.nil?
|
66
|
+
if command_name =~ /^-/
|
67
|
+
raise Choosy::SuperParseError.new("unrecognized option: '#{command_name}'")
|
68
|
+
else
|
69
|
+
raise Choosy::SuperParseError.new("unrecognized command: '#{command_name}'")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
command = command_builder.command
|
74
|
+
parser = Parser.new(command, false, terminals)
|
75
|
+
command_result = parser.parse!(args)
|
76
|
+
|
77
|
+
command_result.verify!
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'choosy/errors'
|
2
|
+
require 'choosy/dsl/option_builder'
|
3
|
+
|
4
|
+
module Choosy
|
5
|
+
class Verifier
|
6
|
+
def verify_options!(result)
|
7
|
+
result.command.options.each do |option|
|
8
|
+
required?(option, result)
|
9
|
+
populate!(option, result)
|
10
|
+
convert!(option, result)
|
11
|
+
validate!(option, result)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def verify_arguments!(result)
|
16
|
+
if result.command.respond_to?(:argument_validation) && result.command.argument_validation
|
17
|
+
result.command.argument_validation.call(result.args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def required?(option, result)
|
22
|
+
if option.required? && result[option.name].nil?
|
23
|
+
raise ValidationError.new("Required option '#{option.long_flag}' missing.")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def populate!(option, result)
|
28
|
+
return if option.name == Choosy::DSL::OptionBuilder::HELP || option.name == Choosy::DSL::OptionBuilder::VERSION
|
29
|
+
|
30
|
+
if !result.options.has_key?(option.name) # Not already set
|
31
|
+
if !option.default_value.nil? # Has default?
|
32
|
+
result[option.name] = option.default_value
|
33
|
+
elsif option.cast_to == :boolean
|
34
|
+
result[option.name] = false
|
35
|
+
elsif option.arity.max > 1
|
36
|
+
result[option.name] = []
|
37
|
+
else
|
38
|
+
result[option.name] = nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def convert!(option, result)
|
44
|
+
value = result[option.name]
|
45
|
+
if exists? value
|
46
|
+
result[option.name] = Converter.convert(option.cast_to, value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate!(option, result)
|
51
|
+
value = result[option.name]
|
52
|
+
if option.validation_step && exists?(value)
|
53
|
+
option.validation_step.call(value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def exists?(value)
|
59
|
+
value && value != []
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|