help_parser 4.0.0 → 5.0.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.
- 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
|