ios_parser 0.3.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 +7 -0
- data/.gitignore +37 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +14 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +2 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +675 -0
- data/README.md +90 -0
- data/Rakefile +16 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/doc/state_machine.graphviz +41 -0
- data/doc/state_machine.png +0 -0
- data/ext/ios_parser/c_lexer/extconf.rb +4 -0
- data/ext/ios_parser/c_lexer/lexer.c +462 -0
- data/ios_parser.gemspec +24 -0
- data/lib/ios_parser/ios/command.rb +90 -0
- data/lib/ios_parser/ios/document.rb +53 -0
- data/lib/ios_parser/ios/queryable.rb +218 -0
- data/lib/ios_parser/ios.rb +70 -0
- data/lib/ios_parser/lexer.rb +278 -0
- data/lib/ios_parser/pure.rb +2 -0
- data/lib/ios_parser/version.rb +7 -0
- data/lib/ios_parser.rb +32 -0
- data/spec/lib/ios_parser/ios/queryable_spec.rb +153 -0
- data/spec/lib/ios_parser/ios_spec.rb +339 -0
- data/spec/lib/ios_parser/lexer_spec.rb +231 -0
- data/spec/lib/ios_parser_spec.rb +101 -0
- data/spec/spec_helper.rb +5 -0
- metadata +164 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'queryable'
|
3
|
+
|
4
|
+
module IOSParser
|
5
|
+
class IOS
|
6
|
+
class Command
|
7
|
+
include Queryable, Enumerable
|
8
|
+
attr_accessor :args, :commands, :parent, :pos, :document
|
9
|
+
|
10
|
+
def initialize(args: [], commands: [],
|
11
|
+
parent: nil, pos: nil, document: nil)
|
12
|
+
@args = args
|
13
|
+
@commands = commands
|
14
|
+
@parent = parent
|
15
|
+
@pos = pos
|
16
|
+
@document = document
|
17
|
+
end
|
18
|
+
|
19
|
+
def name
|
20
|
+
args[0]
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
args == other.args && commands == other.commands
|
25
|
+
end
|
26
|
+
|
27
|
+
def eql?(other)
|
28
|
+
self == other && self.class == other.class
|
29
|
+
end
|
30
|
+
|
31
|
+
def line
|
32
|
+
args.join(' ')
|
33
|
+
end
|
34
|
+
|
35
|
+
def path
|
36
|
+
parent ? parent.path + [parent.line] : []
|
37
|
+
end
|
38
|
+
|
39
|
+
def indentation(base: 0)
|
40
|
+
' ' * (path.length - base)
|
41
|
+
end
|
42
|
+
|
43
|
+
def each
|
44
|
+
yield self
|
45
|
+
commands.each { |command| command.each { |cmd| yield cmd } }
|
46
|
+
end
|
47
|
+
|
48
|
+
def inspect
|
49
|
+
"<IOSParser::IOS::Command:0x#{object_id.to_s(16)} "\
|
50
|
+
"@args=#{args.inspect}, "\
|
51
|
+
"@commands=#{commands.inspect}, "\
|
52
|
+
"@pos=#{pos.inspect}, "\
|
53
|
+
"@document=<IOSParser::IOS::Document:0x#{document.object_id.to_s(16)}>>"
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s(dedent: false)
|
57
|
+
indent_opts = { base: dedent ? path.length : 0 }
|
58
|
+
map { |cmd| "#{cmd.indentation(indent_opts)}#{cmd.line}\n" }.join
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_hash
|
62
|
+
{
|
63
|
+
args: args,
|
64
|
+
commands: commands.map(&:to_hash),
|
65
|
+
pos: pos
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_json
|
70
|
+
JSON.dump(to_hash)
|
71
|
+
end
|
72
|
+
|
73
|
+
class << self
|
74
|
+
def from_hash(hash, parent = nil)
|
75
|
+
hash[:parent] = parent
|
76
|
+
[:args, :commands, :pos].each do |key|
|
77
|
+
val = hash.delete(key.to_s)
|
78
|
+
hash[key] ||= val
|
79
|
+
end
|
80
|
+
|
81
|
+
hash[:commands] ||= []
|
82
|
+
hash[:commands].each_index do |i|
|
83
|
+
hash[:commands][i] = from_hash(hash[:commands][i])
|
84
|
+
end
|
85
|
+
new(hash)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end # class Command
|
89
|
+
end # class IOS
|
90
|
+
end # module IOSParser
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'queryable'
|
3
|
+
require_relative 'command'
|
4
|
+
|
5
|
+
module IOSParser
|
6
|
+
class IOS
|
7
|
+
class Document
|
8
|
+
include Queryable, Enumerable
|
9
|
+
attr_accessor :commands, :parent, :source
|
10
|
+
|
11
|
+
def initialize(source)
|
12
|
+
@commands = []
|
13
|
+
@parent = nil
|
14
|
+
@source = source
|
15
|
+
end
|
16
|
+
|
17
|
+
[:[], :push].each do |method|
|
18
|
+
define_method(method) { |*args| commands.send(method, *args) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
commands.each { |command| command.each { |cmd| yield cmd } }
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s(dedent: false)
|
26
|
+
base = dedent ? indentation : 0
|
27
|
+
map { |cmd| "#{cmd.indentation(base: base)}#{cmd.line}\n" }.join
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_hash
|
31
|
+
{ commands: commands.map(&:to_hash) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_json
|
35
|
+
JSON.dump(to_hash)
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def from_hash(hash)
|
40
|
+
hash[:parent] = parent
|
41
|
+
[:commands, :source].each do |key|
|
42
|
+
val = hash.delete(key.to_s)
|
43
|
+
hash[key] = val unless hash.key?(key)
|
44
|
+
end
|
45
|
+
|
46
|
+
new(source).tap do |doc|
|
47
|
+
doc.push(*(hash[:commands].map { |c| Command.from_hash(c) }))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end # class << self
|
51
|
+
end # class Document
|
52
|
+
end # class IOS
|
53
|
+
end # class IOSParser
|
@@ -0,0 +1,218 @@
|
|
1
|
+
module IOSParser
|
2
|
+
class IOS
|
3
|
+
module Queryable
|
4
|
+
def find_all(expr, &blk)
|
5
|
+
_find_all(MatcherReader.query_expression(expr), &blk)
|
6
|
+
end
|
7
|
+
|
8
|
+
def find(expr, &blk)
|
9
|
+
_find(MatcherReader.query_expression(expr), &blk)
|
10
|
+
end
|
11
|
+
|
12
|
+
def _find_all(expr, &blk)
|
13
|
+
[].tap do |ret|
|
14
|
+
commands.each do |command|
|
15
|
+
if match_expr(expr, command)
|
16
|
+
ret << command
|
17
|
+
yield(command) if blk
|
18
|
+
end
|
19
|
+
|
20
|
+
ret.push(*command._find_all(expr, &blk))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def match_expr(expr, command)
|
26
|
+
expr.each_pair.all? { |pred, arg| Matcher.send(pred, arg, command) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def _find(expr, &blk)
|
30
|
+
_find_all(expr) do |command|
|
31
|
+
yield(command) if blk
|
32
|
+
return command
|
33
|
+
end
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
module MatcherReader
|
38
|
+
class << self
|
39
|
+
def query_expression(raw)
|
40
|
+
case raw
|
41
|
+
when Hash then query_expression_hash(raw)
|
42
|
+
when Proc then { procedure: procedure(raw) }
|
43
|
+
when Regexp then { line: line(raw) }
|
44
|
+
when String, Array then { starts_with: starts_with(raw) }
|
45
|
+
else raise("Invalid query: #{raw.inspect}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
alias parent query_expression
|
49
|
+
alias any_child query_expression
|
50
|
+
alias no_child query_expression
|
51
|
+
|
52
|
+
def query_expression_hash(raw)
|
53
|
+
raw.each_pair { |pred, arg| raw[pred] &&= send(pred, arg) }
|
54
|
+
raw
|
55
|
+
end
|
56
|
+
|
57
|
+
def name(expr)
|
58
|
+
expr
|
59
|
+
end
|
60
|
+
|
61
|
+
def starts_with(expr)
|
62
|
+
case expr
|
63
|
+
when String then expr.split
|
64
|
+
when Array then expr
|
65
|
+
else raise("Invalid #{__method__} condition in query: #{expr}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
alias contains starts_with
|
69
|
+
alias ends_with starts_with
|
70
|
+
|
71
|
+
def procedure(expr)
|
72
|
+
unless expr.respond_to?(:call)
|
73
|
+
raise("Invalid procedure in query: #{expr}")
|
74
|
+
end
|
75
|
+
expr
|
76
|
+
end
|
77
|
+
|
78
|
+
def line(expr)
|
79
|
+
case expr
|
80
|
+
when String, Regexp then expr
|
81
|
+
when Array then expr.join(' ')
|
82
|
+
else raise("Invalid line condition in query: #{expr}")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def array_wrap_and_map(expr)
|
87
|
+
(expr.respond_to?(:map) && !expr.is_a?(Hash) ? expr : [expr])
|
88
|
+
.map { |e| query_expression(e) }
|
89
|
+
end
|
90
|
+
alias any array_wrap_and_map
|
91
|
+
alias all array_wrap_and_map
|
92
|
+
alias none array_wrap_and_map
|
93
|
+
alias not array_wrap_and_map
|
94
|
+
alias not_all array_wrap_and_map
|
95
|
+
|
96
|
+
def depth(expr)
|
97
|
+
unless expr.is_a?(Integer) || (expr.is_a?(Range) &&
|
98
|
+
expr.first.is_a?(Integer) &&
|
99
|
+
expr.last.is_a?(Integer))
|
100
|
+
raise("Invalid depth constraint in query: #{expr}")
|
101
|
+
end
|
102
|
+
expr
|
103
|
+
end
|
104
|
+
end # class << self
|
105
|
+
end # module MatcherReader
|
106
|
+
|
107
|
+
module Matcher
|
108
|
+
class << self
|
109
|
+
def name(expr, command)
|
110
|
+
expr === command.name
|
111
|
+
end
|
112
|
+
|
113
|
+
def starts_with(req_ary, command)
|
114
|
+
(0..req_ary.length - 1).all? do |i|
|
115
|
+
compare_string_or_case(req_ary[i], command.args[i])
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def contains(req_ary, command)
|
120
|
+
(0..command.args.length - req_ary.length).any? do |j|
|
121
|
+
(0..req_ary.length - 1).all? do |i|
|
122
|
+
compare_string_or_case(req_ary[i], command.args[i + j])
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def ends_with(req_ary, command)
|
128
|
+
(1..req_ary.length).all? do |i|
|
129
|
+
compare_string_or_case(req_ary[-i], command.args[-1])
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def compare_string_or_case(a, b)
|
134
|
+
case a
|
135
|
+
when String
|
136
|
+
a == b.to_s
|
137
|
+
else
|
138
|
+
a === b
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def procedure(expr, command)
|
143
|
+
expr.call(command)
|
144
|
+
end
|
145
|
+
|
146
|
+
def line(expr, command)
|
147
|
+
expr === command.line
|
148
|
+
end
|
149
|
+
|
150
|
+
def parent(expr, command)
|
151
|
+
expr.each_pair.all? do |pred, arg|
|
152
|
+
command.parent && send(pred, arg, command.parent)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def any_child(expr, command)
|
157
|
+
command.find(expr)
|
158
|
+
end
|
159
|
+
|
160
|
+
def no_child(expr, command)
|
161
|
+
!command.find(expr)
|
162
|
+
end
|
163
|
+
|
164
|
+
def any(expressions, command)
|
165
|
+
expressions.any? do |expr|
|
166
|
+
expr.each_pair.any? do |pred, arg|
|
167
|
+
send(pred, arg, command)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def all(expressions, command)
|
173
|
+
expressions.all? do |expr|
|
174
|
+
expr.each_pair.all? do |pred, arg|
|
175
|
+
send(pred, arg, command)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def not_all(expressions, command)
|
181
|
+
!expressions.all? { |expr| all([expr], command) }
|
182
|
+
end
|
183
|
+
alias not not_all
|
184
|
+
|
185
|
+
def none(expressions, command)
|
186
|
+
!expressions.any? { |expr| all([expr], command) }
|
187
|
+
end
|
188
|
+
|
189
|
+
def depth(expr, command)
|
190
|
+
case expr
|
191
|
+
when Integer then depth_exact(expr, command)
|
192
|
+
when Range then depth_range(expr, command)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def depth_exact(expr, command)
|
197
|
+
_depth(expr, command) == expr
|
198
|
+
end
|
199
|
+
|
200
|
+
def depth_range(expr, command)
|
201
|
+
_depth(expr.last, command) > expr.first
|
202
|
+
end
|
203
|
+
|
204
|
+
def _depth(max, command)
|
205
|
+
level = 0
|
206
|
+
ptr = command
|
207
|
+
while ptr.parent
|
208
|
+
ptr = ptr.parent
|
209
|
+
level += 1
|
210
|
+
return Float::MIN if level > max
|
211
|
+
end
|
212
|
+
level
|
213
|
+
end
|
214
|
+
end # class << self
|
215
|
+
end # module Matcher
|
216
|
+
end # module Queryable
|
217
|
+
end # class IOS
|
218
|
+
end # module IOSParser
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative 'ios/document'
|
2
|
+
|
3
|
+
module IOSParser
|
4
|
+
class IOS
|
5
|
+
attr_accessor :lexer, :tokens, :source, :document
|
6
|
+
|
7
|
+
def initialize(parent: nil, lexer: IOSParser::Lexer.new)
|
8
|
+
@document = Document.new(nil)
|
9
|
+
@parent = parent
|
10
|
+
@lexer = lexer
|
11
|
+
end
|
12
|
+
|
13
|
+
def tokens
|
14
|
+
@tokens ||= lexer.call(@source)
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(source)
|
18
|
+
unless source.respond_to? :each_char
|
19
|
+
raise ArgumentError, 'Provided configuration source is invalid.'
|
20
|
+
end
|
21
|
+
@source = source
|
22
|
+
@document.source = source
|
23
|
+
@document.push(*section) until tokens.empty?
|
24
|
+
@document
|
25
|
+
end
|
26
|
+
|
27
|
+
def section(parent = nil)
|
28
|
+
[].tap do |commands|
|
29
|
+
until tokens.empty? || tokens.first.last == :DEDENT
|
30
|
+
commands.push(command(parent, @document))
|
31
|
+
end
|
32
|
+
tokens.shift # discard :DEDENT
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def command(parent = nil, document = nil)
|
37
|
+
pos = tokens.first.first
|
38
|
+
opts = { args: arguments, parent: parent, document: document, pos: pos }
|
39
|
+
|
40
|
+
Command.new(opts).tap do |cmd|
|
41
|
+
cmd.commands = subsections(cmd)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def arguments_to_discard
|
46
|
+
[:INDENT, :DEDENT,
|
47
|
+
:CERTIFICATE_BEGIN, :CERTIFICATE_END,
|
48
|
+
:BANNER_BEGIN, :BANNER_END]
|
49
|
+
end
|
50
|
+
|
51
|
+
def arguments
|
52
|
+
[].tap do |args|
|
53
|
+
until tokens.empty? || tokens.first.last == :EOL
|
54
|
+
_, arg = tokens.shift
|
55
|
+
args << arg unless arguments_to_discard.include?(arg)
|
56
|
+
end
|
57
|
+
tokens.shift # discard :EOL
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def subsections(parent = nil)
|
62
|
+
if !tokens.empty? && tokens.first.last == :INDENT
|
63
|
+
tokens.shift # discard :INDENT
|
64
|
+
section(parent)
|
65
|
+
else
|
66
|
+
[]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,278 @@
|
|
1
|
+
module IOSParser
|
2
|
+
class PureLexer
|
3
|
+
attr_accessor :tokens, :token, :indents, :indent, :state, :char,
|
4
|
+
:string_terminator
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@text = ''
|
8
|
+
@token = ''
|
9
|
+
@tokens = []
|
10
|
+
@indent = 0
|
11
|
+
@indents = [0]
|
12
|
+
@state = :root
|
13
|
+
@token_char = 0
|
14
|
+
@this_char = -1
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(input_text)
|
18
|
+
initialize
|
19
|
+
|
20
|
+
input_text.each_char.with_index do |c, i|
|
21
|
+
@this_char = i
|
22
|
+
self.char = c
|
23
|
+
send(state)
|
24
|
+
end
|
25
|
+
|
26
|
+
delimit
|
27
|
+
update_indentation
|
28
|
+
scrub_banner_garbage
|
29
|
+
tokens
|
30
|
+
end
|
31
|
+
|
32
|
+
ROOT_TRANSITIONS = [
|
33
|
+
:space,
|
34
|
+
:banner_begin,
|
35
|
+
:certificate_begin,
|
36
|
+
:newline,
|
37
|
+
:comment,
|
38
|
+
:integer,
|
39
|
+
:quoted_string,
|
40
|
+
:word
|
41
|
+
].freeze
|
42
|
+
|
43
|
+
def root
|
44
|
+
@token_start ||= @this_char
|
45
|
+
|
46
|
+
ROOT_TRANSITIONS.each do |meth|
|
47
|
+
return send(meth) if send(:"#{meth}?")
|
48
|
+
end
|
49
|
+
|
50
|
+
raise LexError, "Unknown character #{char.inspect}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def make_token(value, pos: nil)
|
54
|
+
pos ||= @token_start || @this_char
|
55
|
+
@token_start = nil
|
56
|
+
[pos, value]
|
57
|
+
end
|
58
|
+
|
59
|
+
def comment
|
60
|
+
self.state = :comment
|
61
|
+
update_indentation
|
62
|
+
self.state = :root if newline?
|
63
|
+
end
|
64
|
+
|
65
|
+
def comment?
|
66
|
+
char == '#' || char == '!'
|
67
|
+
end
|
68
|
+
|
69
|
+
def banner_begin
|
70
|
+
self.state = :banner
|
71
|
+
tokens << make_token(:BANNER_BEGIN)
|
72
|
+
@token_start = @this_char + 2
|
73
|
+
@banner_delimiter = char
|
74
|
+
end
|
75
|
+
|
76
|
+
def banner_begin?
|
77
|
+
tokens[-2] && tokens[-2].last == 'banner'
|
78
|
+
end
|
79
|
+
|
80
|
+
def banner
|
81
|
+
char == @banner_delimiter ? banner_end : token << char
|
82
|
+
end
|
83
|
+
|
84
|
+
def banner_end
|
85
|
+
self.state = :root
|
86
|
+
banner_end_clean_token
|
87
|
+
tokens << make_token(token) << make_token(:BANNER_END)
|
88
|
+
self.token = ''
|
89
|
+
end
|
90
|
+
|
91
|
+
def banner_end_clean_token
|
92
|
+
token.slice!(0) if token[0] == 'C'
|
93
|
+
token.slice!(0) if ["\n", ' '].include?(token[0])
|
94
|
+
token.chomp!("\n")
|
95
|
+
end
|
96
|
+
|
97
|
+
def scrub_banner_garbage
|
98
|
+
tokens.each_index do |i|
|
99
|
+
next unless tokens[i + 1]
|
100
|
+
tokens.slice!(i + 1) if banner_garbage?(i)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def banner_garbage?(i)
|
105
|
+
tokens[i].last == :BANNER_END && tokens[i + 1].last == 'C'
|
106
|
+
end
|
107
|
+
|
108
|
+
def certificate_begin?
|
109
|
+
tokens[-6] && tokens[-6].last == :INDENT &&
|
110
|
+
tokens[-5] && tokens[-5].last == 'certificate'
|
111
|
+
end
|
112
|
+
|
113
|
+
def certificate_begin
|
114
|
+
self.state = :certificate
|
115
|
+
indents.pop
|
116
|
+
tokens[-2..-1] = [make_token(:CERTIFICATE_BEGIN, pos: tokens[-1][0])]
|
117
|
+
certificate
|
118
|
+
end
|
119
|
+
|
120
|
+
def certificate
|
121
|
+
token[-5..-1] == "quit\n" ? certificate_end : token << char
|
122
|
+
end
|
123
|
+
|
124
|
+
def certificate_end
|
125
|
+
tokens.concat certificate_end_tokens
|
126
|
+
update_indentation
|
127
|
+
@token_start = @this_char
|
128
|
+
|
129
|
+
@token = ''
|
130
|
+
self.state = :line_start
|
131
|
+
self.indent = 0
|
132
|
+
root
|
133
|
+
end
|
134
|
+
|
135
|
+
def certificate_end_tokens
|
136
|
+
[
|
137
|
+
make_token(token[0..-6].gsub!(/\s+/, ' ').strip, pos: tokens[-1][0]),
|
138
|
+
make_token(:CERTIFICATE_END, pos: @this_char),
|
139
|
+
make_token(:EOL, pos: @this_char)
|
140
|
+
]
|
141
|
+
end
|
142
|
+
|
143
|
+
def integer
|
144
|
+
self.state = :integer
|
145
|
+
case
|
146
|
+
when dot? then decimal
|
147
|
+
when digit? then token << char
|
148
|
+
when word? then word
|
149
|
+
else root
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def integer_token
|
154
|
+
token[0] == '0' ? word_token : make_token(Integer(token))
|
155
|
+
end
|
156
|
+
|
157
|
+
def digit?
|
158
|
+
('0'..'9').cover? char
|
159
|
+
end
|
160
|
+
alias integer? digit?
|
161
|
+
|
162
|
+
def dot?
|
163
|
+
char == '.'
|
164
|
+
end
|
165
|
+
|
166
|
+
def decimal
|
167
|
+
self.state = :decimal
|
168
|
+
case
|
169
|
+
when digit? then token << char
|
170
|
+
when dot? then token << char
|
171
|
+
when word? then word
|
172
|
+
else root
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def decimal_token
|
177
|
+
if token.count('.') > 1 || token[-1] == '.'
|
178
|
+
word_token
|
179
|
+
else
|
180
|
+
make_token(Float(token))
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def decimal?
|
185
|
+
dot? || digit?
|
186
|
+
end
|
187
|
+
|
188
|
+
def word
|
189
|
+
self.state = :word
|
190
|
+
word? ? token << char : root
|
191
|
+
end
|
192
|
+
|
193
|
+
def word_token
|
194
|
+
make_token(token)
|
195
|
+
end
|
196
|
+
|
197
|
+
def word?
|
198
|
+
digit? || dot? ||
|
199
|
+
('a'..'z').cover?(char) ||
|
200
|
+
('A'..'Z').cover?(char) ||
|
201
|
+
['-', '+', '$', ':', '/', ',', '(', ')', '|', '*', '#', '=', '<', '>',
|
202
|
+
'!', '"', '&', '@', ';', '%', '~', '{', '}', "'", '?', '[', ']', '_',
|
203
|
+
'^', '\\'].include?(char)
|
204
|
+
end
|
205
|
+
|
206
|
+
def space
|
207
|
+
delimit
|
208
|
+
self.indent += 1 if tokens.last && tokens.last.last == :EOL
|
209
|
+
end
|
210
|
+
|
211
|
+
def space?
|
212
|
+
char == ' ' || char == "\t" || char == "\r"
|
213
|
+
end
|
214
|
+
|
215
|
+
def quoted_string
|
216
|
+
self.state = :quoted_string
|
217
|
+
token << char
|
218
|
+
if string_terminator.nil?
|
219
|
+
self.string_terminator = char
|
220
|
+
elsif char == string_terminator
|
221
|
+
delimit
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def quoted_string_token
|
226
|
+
make_token(token)
|
227
|
+
end
|
228
|
+
|
229
|
+
def quoted_string?
|
230
|
+
char == '"' || char == "'"
|
231
|
+
end
|
232
|
+
|
233
|
+
def newline
|
234
|
+
delimit
|
235
|
+
self.state = :line_start
|
236
|
+
self.indent = 0
|
237
|
+
tokens << make_token(:EOL)
|
238
|
+
end
|
239
|
+
|
240
|
+
def newline?
|
241
|
+
char == "\n"
|
242
|
+
end
|
243
|
+
|
244
|
+
def line_start
|
245
|
+
if space?
|
246
|
+
self.indent += 1
|
247
|
+
else
|
248
|
+
update_indentation
|
249
|
+
root
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def delimit
|
254
|
+
return if token.empty?
|
255
|
+
tokens << send(:"#{state}_token")
|
256
|
+
self.state = :root
|
257
|
+
self.token = ''
|
258
|
+
end
|
259
|
+
|
260
|
+
def update_indentation
|
261
|
+
pop_dedent while 1 < indents.size && indent <= indents[-2]
|
262
|
+
push_indent if indent > indents.last
|
263
|
+
self.indent = 0
|
264
|
+
end
|
265
|
+
|
266
|
+
def pop_dedent
|
267
|
+
tokens << make_token(:DEDENT)
|
268
|
+
indents.pop
|
269
|
+
end
|
270
|
+
|
271
|
+
def push_indent
|
272
|
+
tokens << make_token(:INDENT)
|
273
|
+
indents.push(indent)
|
274
|
+
end
|
275
|
+
|
276
|
+
class LexError < StandardError; end
|
277
|
+
end # class PureLexer
|
278
|
+
end # module IOSParser
|