help_parser 4.0.0 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +42 -130
- data/lib/help_parser.rb +34 -6
- data/lib/help_parser/aliases.rb +19 -0
- data/lib/help_parser/completion.rb +135 -0
- data/lib/help_parser/constants.rb +17 -69
- data/lib/help_parser/exceptions.rb +51 -0
- data/lib/help_parser/k2t2r.rb +33 -0
- data/lib/help_parser/macros.rb +199 -0
- data/lib/help_parser/options.rb +40 -0
- data/lib/help_parser/parsea.rb +29 -0
- data/lib/help_parser/parseh.rb +29 -0
- data/lib/help_parser/parseu.rb +28 -0
- data/lib/help_parser/validations.rb +98 -0
- metadata +22 -10
- data/lib/help_parser/help_parser.rb +0 -99
- data/lib/help_parser/pattern.rb +0 -140
- data/lib/help_parser/usage.rb +0 -100
@@ -0,0 +1,51 @@
|
|
1
|
+
module HelpParser
|
2
|
+
class HelpParserException < Exception
|
3
|
+
def _init; @code = 1; end
|
4
|
+
|
5
|
+
# Must give message
|
6
|
+
def initialize(message)
|
7
|
+
_init
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def exit
|
12
|
+
if @code > 0
|
13
|
+
STDERR.puts self.message
|
14
|
+
else
|
15
|
+
puts self.message
|
16
|
+
end
|
17
|
+
Kernel.exit @code
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class VersionException < HelpParserException
|
22
|
+
def _init; @code = 0; end
|
23
|
+
end
|
24
|
+
|
25
|
+
class HelpException < HelpParserException
|
26
|
+
def _init; @code = 0; end
|
27
|
+
end
|
28
|
+
|
29
|
+
class UsageError < HelpParserException
|
30
|
+
def _init; @code = 64; end
|
31
|
+
end
|
32
|
+
|
33
|
+
class SoftwareError < HelpParserException
|
34
|
+
# Stuff that should not happen
|
35
|
+
def _init; @code = 70; end
|
36
|
+
end
|
37
|
+
|
38
|
+
class NoMatch < HelpParserException
|
39
|
+
# used to shortcircuit out
|
40
|
+
def _init; @code = 70; end
|
41
|
+
|
42
|
+
# Forces it's owm message
|
43
|
+
def initialize
|
44
|
+
super("Software Error: NoMatch was not caught by HelpParser.")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class HelpError < HelpParserException
|
49
|
+
def _init; @code = 78; end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module HelpParser
|
2
|
+
def self.k2t(specs)
|
3
|
+
k2t = NoDupHash.new
|
4
|
+
tokens = specs.select{|k,v| !(k==TYPES)}.values.flatten.select{|v|v.include?('=')}
|
5
|
+
tokens.each do |token|
|
6
|
+
if match = VARIABLE.match(token) || LONG.match(token)
|
7
|
+
name, type = match["k"], match["t"]
|
8
|
+
k2t[name] = type if !k2t.has_key?(name)
|
9
|
+
raise HelpError, "Inconsistent use of variable: #{name}" unless type==k2t[name]
|
10
|
+
else
|
11
|
+
# Expected these to be caught earlier...
|
12
|
+
raise SoftwareError, "Unexpected string in help text: #{token}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
return k2t
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.t2r(specs)
|
19
|
+
if types = specs[TYPES]
|
20
|
+
t2r = NoDupHash.new
|
21
|
+
types.each do |pair|
|
22
|
+
type, pattern = *pair
|
23
|
+
begin
|
24
|
+
t2r[type] = Regexp.new(pattern[1..-2])
|
25
|
+
rescue
|
26
|
+
raise HelpError, "Bad regex for #{type}: #{pattern}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
return t2r
|
30
|
+
end
|
31
|
+
return nil
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module HelpParser
|
2
|
+
def self.string(*names)
|
3
|
+
names.each do |name|
|
4
|
+
code = <<-CODE
|
5
|
+
class Options
|
6
|
+
def #{name}
|
7
|
+
s = @hash["#{name}"]
|
8
|
+
raise UsageError, "#{name} not a String." unless s.is_a?(String)
|
9
|
+
return s
|
10
|
+
end
|
11
|
+
end
|
12
|
+
CODE
|
13
|
+
eval code
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.string?(*names)
|
18
|
+
names.each do |name|
|
19
|
+
code = <<-CODE
|
20
|
+
class Options
|
21
|
+
def #{name}?
|
22
|
+
s = @hash["#{name}"]
|
23
|
+
raise UsageError, "#{name} not a String." unless s.nil? || s.is_a?(String)
|
24
|
+
return s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
CODE
|
28
|
+
eval code
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.strings(*names)
|
33
|
+
names.each do |name|
|
34
|
+
code = <<-CODE
|
35
|
+
class Options
|
36
|
+
def #{name}
|
37
|
+
a = @hash["#{name}"]
|
38
|
+
raise UsageError, "#{name} not Strings." unless a.is_a?(Array)
|
39
|
+
return a
|
40
|
+
end
|
41
|
+
end
|
42
|
+
CODE
|
43
|
+
eval code
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.strings?(*names)
|
48
|
+
names.each do |name|
|
49
|
+
code = <<-CODE
|
50
|
+
class Options
|
51
|
+
def #{name}?
|
52
|
+
a = @hash["#{name}"]
|
53
|
+
raise UsageError, "#{name} not Strings." unless a.nil? || a.is_a?(Array)
|
54
|
+
return a
|
55
|
+
end
|
56
|
+
end
|
57
|
+
CODE
|
58
|
+
eval code
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.float(*names)
|
63
|
+
names.each do |name|
|
64
|
+
code = <<-CODE
|
65
|
+
class Options
|
66
|
+
def #{name}
|
67
|
+
f = @hash["#{name}"]
|
68
|
+
raise if f.nil?
|
69
|
+
f.to_f
|
70
|
+
rescue
|
71
|
+
raise UsageError, "#{name} not a Float."
|
72
|
+
end
|
73
|
+
end
|
74
|
+
CODE
|
75
|
+
eval code
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.float?(*names)
|
80
|
+
names.each do |name|
|
81
|
+
code = <<-CODE
|
82
|
+
class Options
|
83
|
+
def #{name}?
|
84
|
+
f = @hash["#{name}"]
|
85
|
+
f = f.to_f if f
|
86
|
+
return f
|
87
|
+
rescue
|
88
|
+
raise UsageError, "#{name} not a Float."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
CODE
|
92
|
+
eval code
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.floats(*names)
|
97
|
+
names.each do |name|
|
98
|
+
code = <<-CODE
|
99
|
+
class Options
|
100
|
+
def #{name}
|
101
|
+
f = @hash["#{name}"]
|
102
|
+
raise unless f.is_a?(Array)
|
103
|
+
f.map{|_|_.to_f}
|
104
|
+
rescue
|
105
|
+
raise UsageError, "#{name} not Floats."
|
106
|
+
end
|
107
|
+
end
|
108
|
+
CODE
|
109
|
+
eval code
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.floats?(*names)
|
114
|
+
names.each do |name|
|
115
|
+
code = <<-CODE
|
116
|
+
class Options
|
117
|
+
def #{name}?
|
118
|
+
f = @hash["#{name}"]
|
119
|
+
return nil unless f
|
120
|
+
raise unless f.is_a?(Array)
|
121
|
+
f.map{|_|_.to_f}
|
122
|
+
rescue
|
123
|
+
raise UsageError, "#{name} not Floats."
|
124
|
+
end
|
125
|
+
end
|
126
|
+
CODE
|
127
|
+
eval code
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.int(*names)
|
132
|
+
names.each do |name|
|
133
|
+
code = <<-CODE
|
134
|
+
class Options
|
135
|
+
def #{name}
|
136
|
+
f = @hash["#{name}"]
|
137
|
+
raise if f.nil?
|
138
|
+
f.to_i
|
139
|
+
rescue
|
140
|
+
raise UsageError, "#{name} not an Integer."
|
141
|
+
end
|
142
|
+
end
|
143
|
+
CODE
|
144
|
+
eval code
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.int?(*names)
|
149
|
+
names.each do |name|
|
150
|
+
code = <<-CODE
|
151
|
+
class Options
|
152
|
+
def #{name}?
|
153
|
+
f = @hash["#{name}"]
|
154
|
+
f = f.to_i if f
|
155
|
+
return f
|
156
|
+
rescue
|
157
|
+
raise UsageError, "#{name} not an Integer."
|
158
|
+
end
|
159
|
+
end
|
160
|
+
CODE
|
161
|
+
eval code
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.ints(*names)
|
166
|
+
names.each do |name|
|
167
|
+
code = <<-CODE
|
168
|
+
class Options
|
169
|
+
def #{name}
|
170
|
+
f = @hash["#{name}"]
|
171
|
+
raise unless f.is_a?(Array)
|
172
|
+
f.map{|_|_.to_i}
|
173
|
+
rescue
|
174
|
+
raise UsageError, "#{name} not Integers."
|
175
|
+
end
|
176
|
+
end
|
177
|
+
CODE
|
178
|
+
eval code
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def self.ints?(*names)
|
183
|
+
names.each do |name|
|
184
|
+
code = <<-CODE
|
185
|
+
class Options
|
186
|
+
def #{name}?
|
187
|
+
f = @hash["#{name}"]
|
188
|
+
return nil unless f
|
189
|
+
raise unless f.is_a?(Array)
|
190
|
+
f.map{|_|_.to_i}
|
191
|
+
rescue
|
192
|
+
raise UsageError, "#{name} not Integers."
|
193
|
+
end
|
194
|
+
end
|
195
|
+
CODE
|
196
|
+
eval code
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module HelpParser
|
2
|
+
class Options
|
3
|
+
def initialize( version, help, argv)
|
4
|
+
@hash = HelpParser.parsea(argv)
|
5
|
+
if version && (@hash.has_key?('v') || @hash.has_key?("version"))
|
6
|
+
# -v or --version
|
7
|
+
raise VersionException, version
|
8
|
+
end
|
9
|
+
if help
|
10
|
+
if @hash.has_key?('h') || @hash.has_key?("help")
|
11
|
+
# -h or --help
|
12
|
+
raise HelpException, help
|
13
|
+
end
|
14
|
+
specs = HelpParser.parseh(help)
|
15
|
+
Completion.new(@hash, specs)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def _hash
|
20
|
+
@hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](k)
|
24
|
+
@hash[k]
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_missing(mthd, *args, &block)
|
28
|
+
super if block or args.length > 0
|
29
|
+
m = mthd.to_s
|
30
|
+
case m[-1]
|
31
|
+
when '?'
|
32
|
+
@hash.key? m[0..-2]
|
33
|
+
when '!'
|
34
|
+
super
|
35
|
+
else
|
36
|
+
@hash[m]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module HelpParser
|
2
|
+
def self.parsea(argv)
|
3
|
+
hsh = ArgvHash.new
|
4
|
+
n = 0
|
5
|
+
argv.each do |a|
|
6
|
+
if a[0]=='-'
|
7
|
+
break if a.size==1 # "-" quits argv processing
|
8
|
+
if a[1]=='-'
|
9
|
+
break if a.size==2 # "--" also quits argv processing
|
10
|
+
s = a[2..-1]
|
11
|
+
if s.include?('=')
|
12
|
+
k,v = s.split('=',2)
|
13
|
+
hsh[k] = v
|
14
|
+
else
|
15
|
+
hsh[s] = true
|
16
|
+
end
|
17
|
+
else
|
18
|
+
a.chars[1..-1].each do |c|
|
19
|
+
hsh[c] = true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
else
|
23
|
+
hsh[n] = a
|
24
|
+
n += 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
return hsh
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module HelpParser
|
2
|
+
def self.parseh(help)
|
3
|
+
specs,name = NoDupHash.new,""
|
4
|
+
help.each_line do |line|
|
5
|
+
line.chomp!
|
6
|
+
next if line==""
|
7
|
+
if line=~/^[A-Z]\w+:$/
|
8
|
+
name = line[0..-2].downcase
|
9
|
+
specs[name] = []
|
10
|
+
else
|
11
|
+
next if name=="" || !(line[0]==' ')
|
12
|
+
spec = (index=line.rindex("\t"))? line[0,index].strip : line.strip
|
13
|
+
HelpParser.validate_no_extraneous_spaces(spec)
|
14
|
+
if name==USAGE
|
15
|
+
HelpParser.validate_usage_spec(spec)
|
16
|
+
specs[name].push HelpParser.parseu spec
|
17
|
+
elsif name==TYPES
|
18
|
+
HelpParser.validate_type_spec(spec)
|
19
|
+
specs[name].push spec.split(CSV)
|
20
|
+
else
|
21
|
+
HelpParser.validate_option_spec(spec)
|
22
|
+
specs[name].push spec.split(CSV)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
HelpParser.validate_usage_specs(specs)
|
27
|
+
return specs
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module HelpParser
|
2
|
+
def self._parseu(chars)
|
3
|
+
tokens,token = [],""
|
4
|
+
while c = chars.shift
|
5
|
+
case c
|
6
|
+
when ' ','[',']'
|
7
|
+
unless token==""
|
8
|
+
tokens.push(token)
|
9
|
+
token = ""
|
10
|
+
end
|
11
|
+
tokens.push HelpParser._parseu(chars) if c=='['
|
12
|
+
return tokens if c==']'
|
13
|
+
else
|
14
|
+
token += c
|
15
|
+
end
|
16
|
+
end
|
17
|
+
tokens.push(token) unless token==""
|
18
|
+
return tokens
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.parseu(line)
|
22
|
+
chars = line.chars
|
23
|
+
HelpParser.validate_line_chars(chars)
|
24
|
+
tokens = HelpParser._parseu(chars)
|
25
|
+
HelpParser.validate_usage_tokens(tokens)
|
26
|
+
return tokens
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module HelpParser
|
2
|
+
def self.validate_line_chars(chars)
|
3
|
+
count = 0
|
4
|
+
chars.each do |c|
|
5
|
+
if c=='['
|
6
|
+
count += 1
|
7
|
+
elsif c==']'
|
8
|
+
count -= 1
|
9
|
+
end
|
10
|
+
break if count<0
|
11
|
+
end
|
12
|
+
raise HelpError, "Unbalance brackets: "+chars.join unless count==0
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.validate_usage_tokens(tokens)
|
16
|
+
words = []
|
17
|
+
tokens.flatten.each do |token|
|
18
|
+
match = token.match(FLAG) ||
|
19
|
+
token.match(LITERAL) ||
|
20
|
+
token.match(VARIABLE) ||
|
21
|
+
token.match(FLAG_GROUP)
|
22
|
+
raise HelpError, "Unrecognized usage token: #{token}" unless match
|
23
|
+
words.push match["k"] # key
|
24
|
+
end
|
25
|
+
words.each_with_index do |word,i|
|
26
|
+
raise HelpError, "Duplicate word: #{word}" unless i==words.rindex(word)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.validate_usage_spec(spec)
|
31
|
+
# TODO: Symmetry demands this,
|
32
|
+
# but I can't think of any help text errors I'm not already catching.
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.validate_type_spec(spec)
|
36
|
+
raise HelpError, "Unrecognized type spec: "+spec unless spec=~TYPE_DEF
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.validate_option_spec(spec)
|
40
|
+
case spec
|
41
|
+
when SHORT, LONG, SHORT_LONG, SHORT_LONG_DEFAULT
|
42
|
+
# OK
|
43
|
+
else
|
44
|
+
raise HelpError, "Unrecognized option spec: #{spec}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.validate_usage_specs(specs)
|
49
|
+
option_specs = specs.select{|a,b| !(a==USAGE || a==TYPES)}
|
50
|
+
flags = option_specs.values.flatten.select{|f|f[0]=='-'}.map{|f| HelpParser.f2k(f)}
|
51
|
+
flags.each_with_index do |flag,i|
|
52
|
+
raise HelpError, "Duplicate flag: #{flag}" unless i==flags.rindex(flag)
|
53
|
+
end
|
54
|
+
group = []
|
55
|
+
specs_usage = specs[USAGE]
|
56
|
+
unless specs_usage.nil?
|
57
|
+
specs_usage.flatten.each do |token|
|
58
|
+
if match = token.match(FLAG_GROUP)
|
59
|
+
key = match["k"]
|
60
|
+
raise HelpError, "No #{key} section given." unless specs[key]
|
61
|
+
group.push(key)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
specs.each do |key,tokens|
|
66
|
+
raise HelpError, "No #{key} cases given." unless tokens.size>0
|
67
|
+
next if specs_usage.nil? || key==USAGE || key==TYPES
|
68
|
+
raise HelpError, "No usage given for #{key}." unless group.include?(key)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.validate_k2t2r(specs, k2t, t2r)
|
73
|
+
a,b = k2t.values.uniq.sort,t2r.keys.sort
|
74
|
+
unless a==b
|
75
|
+
c = (a+b).uniq.select{|x|!(a.include?(x) && b.include?(x))}
|
76
|
+
raise HelpError, "Uncompleted types definition: #{c.join(",")}"
|
77
|
+
end
|
78
|
+
specs.each do |section,tokens|
|
79
|
+
next if section==USAGE || section==TYPES
|
80
|
+
tokens.each do |words|
|
81
|
+
next if words.size<2
|
82
|
+
default = words[-1]
|
83
|
+
next if default[0]=='-'
|
84
|
+
long_type = words[-2]
|
85
|
+
i = long_type.index('=')
|
86
|
+
next if i.nil?
|
87
|
+
long = long_type[2..(i-1)]
|
88
|
+
type = long_type[(i+1)..-1]
|
89
|
+
regex = t2r[type]
|
90
|
+
raise HelpError, "Default not #{type}: #{long}" unless regex=~default
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.validate_no_extraneous_spaces(spec)
|
96
|
+
raise HelpError, "Extraneous spaces in help." if spec == ""
|
97
|
+
end
|
98
|
+
end
|