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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15eed085a057dd27423f3a363e2a172cceecd664
|
4
|
+
data.tar.gz: 9159259d556060cebe4d1241d0065b6252a0280a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3b8aeb803ca69f593a861d49e574175178a57574fd42664ff853ba229be9b84118fb26c9bed3ed3029c52b1b60727fc3368ab6409a1987da527a645f46f41e4d
|
7
|
+
data.tar.gz: f40a072547ec26b18c174f3f313106132f64299c4356ba07a1b3e915558d73a8ee9674b28dad9533bdcecc21ed678460e4c21e96432fb31f83372d2e85cfcedf
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 carlosjhr64
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,151 +1,63 @@
|
|
1
|
-
#
|
2
|
-
##
|
1
|
+
# Help Parser
|
2
|
+
## V: Infamous
|
3
3
|
|
4
|
-
* [github](https://www.github.com/carlosjhr64/
|
4
|
+
* [github](https://www.github.com/carlosjhr64/Ruby-HelpParser)
|
5
5
|
* [rubygems](https://rubygems.org/gems/help_parser)
|
6
6
|
|
7
7
|
## DESCRIPTION:
|
8
|
-
|
8
|
+
All help is about to get parsed...
|
9
|
+
Again!!!
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
## LICENSE:
|
15
|
-
[MIT](https://en.wikipedia.org/wiki/MIT_License) Copyright (c) 2017 CarlosJHR64
|
16
|
-
|
17
|
-
## MINIMAL
|
18
|
-
|
19
|
-
#!/usr/bin/env ruby
|
20
|
-
require 'help_parser'
|
21
|
-
|
22
|
-
OPTIONS = HelpParser.new
|
23
|
-
p OPTIONS._hash
|
11
|
+
And this time,
|
12
|
+
the battle between complexity and simplicity
|
13
|
+
has as familiar text.
|
24
14
|
|
25
15
|
## SYNOPSIS:
|
26
16
|
|
27
|
-
|
28
|
-
require
|
29
|
-
|
30
|
-
OPTIONS = HelpParser.new('1.2.3', <<-HELP)
|
31
|
-
Usage:
|
32
|
-
synopsis [:options] <x> <y>
|
33
|
-
Options:
|
34
|
-
--verbose
|
35
|
-
HELP
|
36
|
-
|
37
|
-
# Existentials via missing "?" methods
|
38
|
-
puts 'Whats x * y?' if OPTIONS.verbose?
|
39
|
-
|
40
|
-
# Values via missing methods.
|
41
|
-
puts OPTIONS.x.to_f * OPTIONS.y.to_f
|
42
|
-
|
43
|
-
## MORE:
|
44
|
-
|
45
|
-
#!/usr/bin/env ruby
|
46
|
-
require 'help_parser'
|
47
|
-
|
48
|
-
# Any version string, does not have to be semantic versioning.
|
49
|
-
VERSION = '1.2.3'
|
17
|
+
require "pp"
|
18
|
+
require "help_parser"
|
50
19
|
|
51
20
|
HELP = <<-HELP
|
52
|
-
|
53
|
-
# Lines starting with the pound sign are comments.
|
54
|
-
# HelpPaser parses the help text by sections
|
55
|
-
# with a heading of the form `<Section Name>:`.
|
56
|
-
# The Usage section gives the allowed forms of the
|
57
|
-
# command. Keys, arguments, and selectors in
|
58
|
-
# square bracket are optional and can be nested.
|
59
|
-
# A plus sign, `+`, marks the associated token as
|
60
|
-
# taking multiple values. Note that variable
|
61
|
-
# arguments have the form `<name>`.
|
62
|
-
####################################################
|
21
|
+
The Awesome Command.
|
63
22
|
Usage:
|
64
|
-
|
65
|
-
|
66
|
-
####################################################
|
67
|
-
# Tipically you'll have an `Options:` section, but
|
68
|
-
# you can give the section any name.
|
69
|
-
# An `Options:` section should match an `:options`
|
70
|
-
# selector in at least one of your usages.
|
71
|
-
# Assigning values to keys via the command line
|
72
|
-
# is done in the form `--key=value`.
|
73
|
-
####################################################
|
23
|
+
awesome [:options+] <args>+
|
24
|
+
awesome :alternate <arg=NAME>
|
74
25
|
Options:
|
75
|
-
-
|
76
|
-
-
|
77
|
-
--
|
78
|
-
--
|
79
|
-
--
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
# your defaults, but for arguments, it's done here.
|
85
|
-
####################################################
|
86
|
-
Defaults:
|
87
|
-
name Carlos \tDefault for name
|
88
|
-
####################################################
|
89
|
-
# HelpPaser has two automatic types, `Float` and
|
90
|
-
# `Int`, which will convert values from String to
|
91
|
-
# Floats and Integers repectively.
|
92
|
-
# You can also provide you own custom validation
|
93
|
-
# with regular expressions.
|
94
|
-
####################################################
|
26
|
+
-v --version \t Give version and quit
|
27
|
+
-h --help \t Give help and quit
|
28
|
+
-s --long \t Short long synonyms
|
29
|
+
--name=NAME \t Typed
|
30
|
+
--number 5 \t Defaulted
|
31
|
+
--value=FLOAT 1.23 \t Typed and Defaulted
|
32
|
+
-a --all=YN y \t Short, long, typed, and defaulted
|
33
|
+
Alternate:
|
34
|
+
-V \t Just short
|
95
35
|
Types:
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
####################################################
|
100
|
-
# The start of a `Notes:` section tells HelpParser
|
101
|
-
# to break out of it's parsing.
|
102
|
-
# Anything else written after this heading is of
|
103
|
-
# no consequence to HelpParser.
|
104
|
-
####################################################
|
105
|
-
Notes:
|
106
|
-
Stuff the help parser will ignore.
|
36
|
+
NAME /^[A-Z][a-z]+$/
|
37
|
+
FLOAT /^\\d+\\.\\d+$/
|
38
|
+
YN /^[YNyn]$/
|
107
39
|
HELP
|
108
40
|
|
109
|
-
|
41
|
+
VERSION = "5.0.0"
|
110
42
|
|
111
|
-
#
|
112
|
-
|
113
|
-
#
|
114
|
-
#
|
115
|
-
|
116
|
-
puts "Count: #{_=OPTIONS.count}\t#{_.class}"
|
117
|
-
puts "Price: #{_=OPTIONS.price}\t#{_.class}"
|
118
|
-
puts "Name: #{_=OPTIONS.name}\t#{_.class}"
|
119
|
-
else
|
120
|
-
puts "Args: #{OPTIONS.args.join(', ')}"
|
121
|
-
end
|
122
|
-
puts "Name: #{_=OPTIONS.name}\t#{_.class}"
|
123
|
-
puts "Val: #{_=OPTIONS.val}\t#{_.class}"
|
124
|
-
puts "Number: #{_=OPTIONS.number}\t#{_.class}"
|
43
|
+
# Macros:
|
44
|
+
HelpParser.string(name) # for options.name : String
|
45
|
+
HelpParser.strings(args) # for options.args : Array(String)
|
46
|
+
HelpParser.float(value) # for options.value : Float
|
47
|
+
HelpParser.int?(number) # for options.number : Int32 | Nil
|
125
48
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
if OPTIONS.usage?
|
130
|
-
p OPTIONS._usage._hash
|
131
|
-
else
|
132
|
-
p OPTIONS._hash
|
133
|
-
end
|
49
|
+
HelpParser.run(VERSION, HELP) do |options|
|
50
|
+
hash = options._hash
|
51
|
+
pp hash # to inspect the hash
|
134
52
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
Just to have to go back and reimplement with a proper options parser
|
141
|
-
when the little script ends up being useful and grows into something bigger.
|
142
|
-
And I was doing this after I had written my own previous versions of HelpParser.
|
143
|
-
So now I should have no excuse to just use HelpParser. Always:
|
53
|
+
pp options.name if hash["name"]?
|
54
|
+
pp options.args if hash["args"]?
|
55
|
+
pp options.value if hash["value"]?
|
56
|
+
pp options.number?
|
57
|
+
end
|
144
58
|
|
145
|
-
|
146
|
-
OPTIONS = HelpParser.new # DONE!
|
59
|
+
YES!!!
|
147
60
|
|
148
|
-
|
61
|
+
## INSTALL:
|
149
62
|
|
150
|
-
|
151
|
-
no need to rework the code for not having started out with a proper options parser.
|
63
|
+
$ sudo gem install help_parser
|
data/lib/help_parser.rb
CHANGED
@@ -1,9 +1,37 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
require_relative "./help_parser/constants"
|
2
|
+
require_relative "./help_parser/exceptions"
|
3
|
+
require_relative "./help_parser/aliases"
|
4
|
+
require_relative "./help_parser/parsea"
|
5
|
+
require_relative "./help_parser/validations"
|
6
|
+
require_relative "./help_parser/parseu"
|
7
|
+
require_relative "./help_parser/parseh"
|
8
|
+
require_relative "./help_parser/k2t2r"
|
9
|
+
require_relative "./help_parser/completion"
|
10
|
+
require_relative "./help_parser/options"
|
11
|
+
require_relative "./help_parser/macros"
|
12
|
+
|
13
|
+
module HelpParser
|
14
|
+
VERSION = "5.0.0"
|
15
|
+
|
16
|
+
def self.[](
|
17
|
+
version = nil,
|
18
|
+
help = nil,
|
19
|
+
argv = [File.basename($0)]+ARGV)
|
20
|
+
Options.new(version, help, argv)
|
21
|
+
rescue HelpParserException => exception
|
22
|
+
exception.exit
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.run(
|
26
|
+
version = nil,
|
27
|
+
help = nil,
|
28
|
+
argv = [File.basename($0)]+ARGV)
|
29
|
+
options = Options.new(version, help, argv)
|
30
|
+
yield options
|
31
|
+
rescue HelpParserException => exception
|
32
|
+
exception.exit
|
33
|
+
end
|
34
|
+
end
|
7
35
|
|
8
36
|
# Requires:
|
9
37
|
#`ruby`
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module HelpParser
|
2
|
+
class NoDupHash < Hash
|
3
|
+
def []=(k,v)
|
4
|
+
raise HelpError, "Duplicate key: #{k}" if self.has_key?(k)
|
5
|
+
super
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class ArgvHash < Hash
|
10
|
+
def []=(k,v)
|
11
|
+
raise UsageError, "Duplicate key: #{k}" if self.has_key?(k)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.f2k(f)
|
17
|
+
f[1]=='-' ? f[2..((f.index('=')||0)-1)] : f[1]
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module HelpParser
|
2
|
+
class Completion
|
3
|
+
def initialize(hash, specs, cache = NoDupHash.new)
|
4
|
+
@hash,@specs,@cache = hash,specs,cache
|
5
|
+
usage if @specs.has_key?(USAGE)
|
6
|
+
pad
|
7
|
+
types
|
8
|
+
end
|
9
|
+
|
10
|
+
def usage
|
11
|
+
@specs[USAGE].each do |cmd|
|
12
|
+
begin
|
13
|
+
i = matches(cmd)
|
14
|
+
raise NoMatch unless @hash.size==i
|
15
|
+
@cache.each{|k,v|@hash[k]=v} # Variables
|
16
|
+
return
|
17
|
+
rescue NoMatch
|
18
|
+
next
|
19
|
+
ensure
|
20
|
+
@cache.clear
|
21
|
+
end
|
22
|
+
end
|
23
|
+
raise UsageError, "Please match usage."
|
24
|
+
end
|
25
|
+
|
26
|
+
def types
|
27
|
+
if t2r = HelpParser.t2r(@specs)
|
28
|
+
k2t = HelpParser.k2t(@specs)
|
29
|
+
HelpParser.validate_k2t2r(@specs, k2t, t2r)
|
30
|
+
@hash.each do |key,value|
|
31
|
+
next unless key.is_a?(String)
|
32
|
+
if type = k2t[key]
|
33
|
+
regex = t2r[type]
|
34
|
+
case value
|
35
|
+
when String
|
36
|
+
raise UsageError, "Not a #{type}: #{key}" unless value=~regex
|
37
|
+
when Array
|
38
|
+
value.each do |string|
|
39
|
+
raise UsageError, "Not a #{type}: #{key}" unless string=~regex
|
40
|
+
end
|
41
|
+
else
|
42
|
+
raise UsageError, "Need a #{type}: #{key}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def pad
|
50
|
+
# Synonyms and defaults:
|
51
|
+
@specs.each do |section,options|
|
52
|
+
next if section==USAGE || section==TYPES
|
53
|
+
options.each do |words|
|
54
|
+
next unless words.size>1
|
55
|
+
first,second,default = words[0],words[1],words[2]
|
56
|
+
if first[0]=='-'
|
57
|
+
if second[0]=='-'
|
58
|
+
i = second.index('=') || 0
|
59
|
+
short,long = first[1],second[2..(i-1)]
|
60
|
+
if @hash.has_key?(short)
|
61
|
+
if @hash.has_key?(long)
|
62
|
+
raise UsageError, "Option #{short} is a synonym for #{long}."
|
63
|
+
end
|
64
|
+
@hash[long] = (default.nil?) ? true : default
|
65
|
+
elsif value = @hash[long]
|
66
|
+
@hash[short] = true
|
67
|
+
if value==true && !default.nil?
|
68
|
+
@hash.delete(long)
|
69
|
+
@hash[long]=default
|
70
|
+
end
|
71
|
+
end
|
72
|
+
else
|
73
|
+
i = first.index('=') || 0
|
74
|
+
long,default = first[2..(i-1)],second
|
75
|
+
value = @hash[long]
|
76
|
+
if value==true
|
77
|
+
@hash.delete(long)
|
78
|
+
@hash[long] = default
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def matches(cmd, i = 0)
|
87
|
+
keys = @hash.keys
|
88
|
+
cmd.each do |token|
|
89
|
+
if token.is_a?(Array)
|
90
|
+
begin
|
91
|
+
i = matches(token, i)
|
92
|
+
rescue NoMatch
|
93
|
+
# OK, NEVERMIND!
|
94
|
+
end
|
95
|
+
next
|
96
|
+
elsif m=FLAG_GROUP.match(token)
|
97
|
+
group,plus = m["k"],m["p"]
|
98
|
+
key = keys[i]
|
99
|
+
raise NoMatch if key.nil? || key.is_a?(Integer)
|
100
|
+
list = @specs[group].flatten.select{|f|f[0]=='-'}.map{|f| HelpParser.f2k(f)}
|
101
|
+
raise NoMatch unless list.include?(key)
|
102
|
+
unless plus.nil?
|
103
|
+
loop do
|
104
|
+
key = keys[i+1]
|
105
|
+
break if key.nil? || key.is_a?(Integer) || !list.include?(key)
|
106
|
+
i+=1
|
107
|
+
end
|
108
|
+
end
|
109
|
+
elsif m=VARIABLE.match(token)
|
110
|
+
key = keys[i]
|
111
|
+
raise NoMatch unless key.is_a?(Integer)
|
112
|
+
variable,plus = m["k"],m["p"]
|
113
|
+
if plus.nil?
|
114
|
+
@cache[variable] = @hash[key]
|
115
|
+
else
|
116
|
+
strings = []
|
117
|
+
strings.push @hash[key]
|
118
|
+
loop do
|
119
|
+
key = keys[i+1]
|
120
|
+
break unless key.is_a?(Integer)
|
121
|
+
strings.push @hash[key]
|
122
|
+
i+=1
|
123
|
+
end
|
124
|
+
@cache[variable] = strings
|
125
|
+
end
|
126
|
+
else # literal
|
127
|
+
key = keys[i]
|
128
|
+
raise NoMatch if key.nil? || @hash[key]!=token
|
129
|
+
end
|
130
|
+
i += 1
|
131
|
+
end
|
132
|
+
return i
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -1,75 +1,23 @@
|
|
1
|
-
module
|
1
|
+
module HelpParser
|
2
|
+
USAGE = "usage"
|
3
|
+
TYPES = "types"
|
2
4
|
|
3
|
-
|
5
|
+
# usage
|
6
|
+
FLAG = /^[-][-]?(?<k>\w+)$/
|
7
|
+
LITERAL = /^(?<k>\w[\w.-]*:?)$/
|
8
|
+
VARIABLE = /^<(?<k>\w+)(=(?<t>[A-Z]+))?>(?<p>[+])?$/
|
9
|
+
FLAG_GROUP = /^:(?<k>\w+)(?<p>[+])?$/
|
4
10
|
|
5
|
-
|
6
|
-
|
11
|
+
# spec --?w+
|
12
|
+
SHORT = /^[-](?<s>\w)$/
|
13
|
+
LONG = /^[-][-](?<k>\w+)(=(?<t>[A-Z]+))?(,?\s+(?<d>[^-\s]\S*))?$/
|
7
14
|
|
8
|
-
|
9
|
-
|
15
|
+
# spec -w,? --w+
|
16
|
+
SHORT_LONG = /^[-](?<s>\w),?\s+[-][-](?<k>\w+)$/
|
17
|
+
SHORT_LONG_DEFAULT = /^[-](?<s>\w),?\s+[-][-](?<k>\w+)(=(?<t>[A-Z]+))?,?\s+(?<d>[^-\s]\S*)$/
|
10
18
|
|
11
|
-
|
12
|
-
|
13
|
-
NOTES,USAGE,TYPES,DEFAULTS = 'notes','usage','types','defaults'
|
14
|
-
|
15
|
-
### ERROR COLOR CODES ###
|
16
|
-
|
17
|
-
COLOR = [
|
18
|
-
"\e[0m", # no color
|
19
|
-
"\e[1;31m", # red
|
20
|
-
]
|
21
|
-
|
22
|
-
### TYPE MAPPINGS ###
|
23
|
-
|
24
|
-
FLOAT,INT = :Float, :Int
|
25
|
-
Types = {
|
26
|
-
INT => /^[+-]?\d+$/,
|
27
|
-
FLOAT => /^[+-]?\d+\.\d+$/,
|
28
|
-
}
|
29
|
-
|
30
|
-
### PATTERNS ###
|
31
|
-
|
32
|
-
S = /\s/ # space
|
33
|
-
SNS = /\s*\n\s*/
|
34
|
-
KEY = /^[-][-]?(.*)$/ #
|
35
|
-
SPS = /\s+/ # spaces
|
36
|
-
MNS = /^--?/
|
37
|
-
ERROR_KEY = /:\s+(\S+)/
|
38
|
-
COMMENTARY = /\s*\t.*$/ # tab marks start of field comment
|
39
|
-
|
40
|
-
SELECTION = /^(\w+):$/
|
41
|
-
SELECTION_P = /^:(.*?)([+]?)$/
|
42
|
-
|
43
|
-
VARIABLE = /^<(.*)>$/
|
44
|
-
VARIABLE_P = /^<(.*)>([+])?$/
|
45
|
-
|
46
|
-
### Error Messages ###
|
47
|
-
|
48
|
-
MATCH_USAGE = 'Please match usage!'
|
49
|
-
DUP_KEY = 'Duplicate Key: '
|
50
|
-
ILLEGAL_KEY = 'Illegal Key: '
|
51
|
-
SECTION_REDEFINITION = 'Section Redefinition: '
|
52
|
-
UNBALANCE_BRACKETS = 'Unbalanced Brackets in Usage: '
|
53
|
-
MISSING_MINUS = 'Missing Minus For Flag: '
|
54
|
-
NOT_A_FLAG = 'No Minus For Types Or Defaults: '
|
55
|
-
|
56
|
-
### Errors ###
|
57
|
-
|
58
|
-
class UsageError < RuntimeError
|
59
|
-
end
|
60
|
-
class HelpError < RuntimeError
|
61
|
-
end
|
62
|
-
class NoMatch < RuntimeError
|
63
|
-
end
|
64
|
-
|
65
|
-
### REFINEMENTS ###
|
66
|
-
|
67
|
-
module Refinements
|
68
|
-
refine Array do
|
69
|
-
def detect_duplicate
|
70
|
-
self.detect{|_|self.rindex(_) != self.index(_)}
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
19
|
+
# spec W+ /~/
|
20
|
+
TYPE_DEF = /^(?<t>[A-Z]+),?\s+\/(?<r>\S+)\/$/
|
74
21
|
|
22
|
+
CSV = /,?\s+/
|
75
23
|
end
|