crossplane 0.1.5
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/bin/crossplane +7 -0
- data/lib/crossplane/analyzer.rb +1964 -0
- data/lib/crossplane/builder.rb +203 -0
- data/lib/crossplane/cli.rb +139 -0
- data/lib/crossplane/config.rb +56 -0
- data/lib/crossplane/errors.rb +64 -0
- data/lib/crossplane/globals.rb +42 -0
- data/lib/crossplane/lexer.rb +155 -0
- data/lib/crossplane/parser.rb +270 -0
- data/lib/crossplane/utils.rb +128 -0
- data/lib/crossplane/version.rb +3 -0
- metadata +111 -0
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'crossplane/globals'
|
2
|
+
require 'json'
|
3
|
+
require 'pathname'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
#require_relative 'globals.rb'
|
7
|
+
|
8
|
+
module CrossPlane
|
9
|
+
class Builder
|
10
|
+
DELIMITERS = ['{', '}', ';']
|
11
|
+
EXTERNAL_BUILDERS = {}
|
12
|
+
NEWLINE = "\n"
|
13
|
+
TAB = "\t"
|
14
|
+
|
15
|
+
attr_accessor :header
|
16
|
+
attr_accessor :indent
|
17
|
+
attr_accessor :padding
|
18
|
+
attr_accessor :payload
|
19
|
+
attr_accessor :state
|
20
|
+
attr_accessor :tabs
|
21
|
+
|
22
|
+
def initialize(*args)
|
23
|
+
args = args[0] || {}
|
24
|
+
|
25
|
+
required = ['payload']
|
26
|
+
conflicts = []
|
27
|
+
requires = {}
|
28
|
+
valid = {
|
29
|
+
'params' => [
|
30
|
+
'header',
|
31
|
+
'indent',
|
32
|
+
'payload',
|
33
|
+
'tabs',
|
34
|
+
]
|
35
|
+
}
|
36
|
+
|
37
|
+
content = CrossPlane.utils.validate_constructor(client: self, args: args, required: required, conflicts: conflicts, requires: requires, valid: valid)
|
38
|
+
self.header = (content[:header] && content[:header] == true) ? true : false
|
39
|
+
self.indent = content[:indent] ? content[:indent] : 4
|
40
|
+
self.payload = content[:payload] ? content[:payload] : nil
|
41
|
+
self.tabs = (content[:tabs] && content[:tabs] == true) ? true : false
|
42
|
+
end
|
43
|
+
|
44
|
+
def build(*args)
|
45
|
+
self.padding = self.tabs ? TAB : ' ' * self.indent
|
46
|
+
self.state = {
|
47
|
+
'prev_obj' => nil,
|
48
|
+
'depth' => -1,
|
49
|
+
}
|
50
|
+
|
51
|
+
if self.header
|
52
|
+
lines = [
|
53
|
+
"# This config was built from JSON using NGINX crossplane.\n",
|
54
|
+
"# If you encounter any bugs please report them here:\n",
|
55
|
+
"# https://github.com/gdanko/crossplane/issues\n",
|
56
|
+
"\n"
|
57
|
+
]
|
58
|
+
else
|
59
|
+
lines = []
|
60
|
+
end
|
61
|
+
|
62
|
+
lines += _build_lines(payload)
|
63
|
+
puts lines.join('')
|
64
|
+
exit
|
65
|
+
return lines.join('')
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def _put_line(line, obj)
|
70
|
+
margin = self.padding * self.state['depth']
|
71
|
+
|
72
|
+
# don't need put \n on first line and after comment
|
73
|
+
if self.state['prev_obj'].nil?
|
74
|
+
return margin + line
|
75
|
+
end
|
76
|
+
|
77
|
+
# trailing comments have to be without \n
|
78
|
+
if obj['directive'] == '#' and obj['line'] == self.state['prev_obj']['line']
|
79
|
+
return ' ' + line
|
80
|
+
end
|
81
|
+
|
82
|
+
return NEWLINE + margin + line
|
83
|
+
end
|
84
|
+
|
85
|
+
def _build_lines(objs)
|
86
|
+
lines = Enumerator.new do |y|
|
87
|
+
self.state['depth'] = self.state['depth'] + 1
|
88
|
+
|
89
|
+
objs.each do |obj|
|
90
|
+
directive = obj['directive']
|
91
|
+
if EXTERNAL_BUILDERS[directive]
|
92
|
+
#built = external_builder(obj, padding, state)
|
93
|
+
#y.yield(_put_line(built_obj))
|
94
|
+
#next
|
95
|
+
end
|
96
|
+
|
97
|
+
if directive == '#'
|
98
|
+
y.yield(_put_line(
|
99
|
+
'#' + obj['comment'],
|
100
|
+
obj
|
101
|
+
))
|
102
|
+
next
|
103
|
+
end
|
104
|
+
|
105
|
+
args = obj['args'].map{|arg| _enquote(arg)}
|
106
|
+
|
107
|
+
if directive == 'if'
|
108
|
+
line = format('if (%s)', args.join(' '))
|
109
|
+
elsif args
|
110
|
+
line = format('%s %s', directive, args.join(' '))
|
111
|
+
else
|
112
|
+
line = directive
|
113
|
+
end
|
114
|
+
|
115
|
+
if not obj.key?('block')
|
116
|
+
y.yield(_put_line(line + ';', obj))
|
117
|
+
else
|
118
|
+
y.yield(_put_line(line + ' {', obj))
|
119
|
+
|
120
|
+
# set prev_obj to propper indentation in block
|
121
|
+
self.state['prev_obj'] = obj
|
122
|
+
_build_lines(obj['block']).each do |line|
|
123
|
+
y.yield(line)
|
124
|
+
end
|
125
|
+
y.yield(_put_line('}', obj))
|
126
|
+
end
|
127
|
+
self.state['prev_obj'] = obj
|
128
|
+
end
|
129
|
+
self.state['depth'] = self.state['depth'] - 1
|
130
|
+
end
|
131
|
+
lines.to_a
|
132
|
+
end
|
133
|
+
|
134
|
+
def _escape(string)
|
135
|
+
chars = Enumerator.new do |y|
|
136
|
+
prev, char = '', ''
|
137
|
+
string.split('').each do |char|
|
138
|
+
if prev == '\\' or prev + char == '${'
|
139
|
+
prev += char
|
140
|
+
y.yield char
|
141
|
+
next
|
142
|
+
end
|
143
|
+
|
144
|
+
if prev == '$'
|
145
|
+
y.yield prev
|
146
|
+
end
|
147
|
+
|
148
|
+
if not ['\\', '$'].include?(char)
|
149
|
+
y.yield char
|
150
|
+
end
|
151
|
+
prev = char
|
152
|
+
end
|
153
|
+
|
154
|
+
if ['\\', '$'].include?(char)
|
155
|
+
y.yield char
|
156
|
+
end
|
157
|
+
end
|
158
|
+
chars
|
159
|
+
end
|
160
|
+
|
161
|
+
def _needs_quotes(string)
|
162
|
+
if string == ''
|
163
|
+
return true
|
164
|
+
elsif DELIMITERS.include?(string)
|
165
|
+
return false
|
166
|
+
end
|
167
|
+
|
168
|
+
# lexer should throw an error when variable expansion syntax
|
169
|
+
# is messed up, but just wrap it in quotes for now I guess
|
170
|
+
chars = _escape(string)
|
171
|
+
|
172
|
+
begin
|
173
|
+
while char = chars.next
|
174
|
+
|
175
|
+
|
176
|
+
# arguments can't start with variable expansion syntax
|
177
|
+
if CrossPlane.utils.isspace(char) or ['{', ';', '"', "'", '${'].include?(char)
|
178
|
+
return true
|
179
|
+
end
|
180
|
+
|
181
|
+
expanding = false
|
182
|
+
#chars.each do |char|
|
183
|
+
# if CrossPlane.utils.isspace(char) or ['{', ';', '"', "'"].include(char)
|
184
|
+
# return true
|
185
|
+
# elsif char ==
|
186
|
+
# char in ('\\', '$') or expanding
|
187
|
+
return expanding
|
188
|
+
end
|
189
|
+
rescue StopIteration
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def _enquote(arg)
|
194
|
+
if _needs_quotes(arg)
|
195
|
+
#arg = repr(codecs.decode(arg, 'raw_unicode_escape'))
|
196
|
+
arg = arg.gsub('\\\\', '\\')
|
197
|
+
end
|
198
|
+
return arg
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'crossplane/builder'
|
2
|
+
require 'crossplane/config'
|
3
|
+
require 'crossplane/parser'
|
4
|
+
require 'json'
|
5
|
+
require 'logger'
|
6
|
+
require 'pp'
|
7
|
+
require 'thor'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
#require_relative 'builder.rb'
|
11
|
+
#require_relative 'config.rb'
|
12
|
+
#require_relative 'parser.rb'
|
13
|
+
|
14
|
+
$script = File.basename($0)
|
15
|
+
$config = CrossPlane::Config.new()
|
16
|
+
|
17
|
+
trap('SIGINT') {
|
18
|
+
puts("\nControl-C received.")
|
19
|
+
exit(0)
|
20
|
+
}
|
21
|
+
|
22
|
+
def configure_options(thor, opt_type, opts)
|
23
|
+
opts = opts.sort_by { |k| k[:name].to_s }
|
24
|
+
opts.each do |opt|
|
25
|
+
required = opt.key?(:required) ? opt[:required] : false
|
26
|
+
aliases = opt.key?(:aliases) ? opt[:aliases] : []
|
27
|
+
if opt_type == "class"
|
28
|
+
thor.class_option(opt[:name], :banner => opt[:banner], :desc => opt[:desc], :aliases => aliases, :required => required, :type => opt[:type])
|
29
|
+
elsif opt_type == "method"
|
30
|
+
thor.method_option(opt[:name], :banner => opt[:banner], :desc => opt[:desc], :aliases => aliases, :required => required, :type => opt[:type])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class CLI < Thor
|
36
|
+
desc 'parse <filename>', 'parses an nginx config file and returns a json payload'
|
37
|
+
configure_options(self, 'method', $config.parse_options)
|
38
|
+
def parse(filename)
|
39
|
+
payload = CrossPlane::Parser.new(
|
40
|
+
filename: filename,
|
41
|
+
combine: options['combine'] || false,
|
42
|
+
strict: options['strict'] || false,
|
43
|
+
catch_errors: options['no_catch'] ? false : true,
|
44
|
+
comments: options['include_comments'] || false,
|
45
|
+
ignore: options['ignore'] ? options['ignore'].split(/\s*,\s*/) : [],
|
46
|
+
single: options['single'] || false,
|
47
|
+
).parse()
|
48
|
+
|
49
|
+
if options['out']
|
50
|
+
File.open(options['out'], 'w') do |f|
|
51
|
+
f.write(JSON.pretty_generate(payload))
|
52
|
+
end
|
53
|
+
else
|
54
|
+
puts options['pretty'] ? JSON.pretty_generate(payload) : payload.to_json
|
55
|
+
end
|
56
|
+
exit 0
|
57
|
+
end
|
58
|
+
|
59
|
+
desc 'build <filename>', 'builds an nginx config from a json payload'
|
60
|
+
configure_options(self, 'method', $config.build_options)
|
61
|
+
def build(filename)
|
62
|
+
dirname = Dir.pwd unless dirname
|
63
|
+
|
64
|
+
# read the json payload from the specified file
|
65
|
+
payload = JSON.parse(File.read(filename))
|
66
|
+
builder = CrossPlane::Builder.new(
|
67
|
+
payload: payload['config'][0]['parsed']
|
68
|
+
)
|
69
|
+
|
70
|
+
if not options['force'] and not options['stdout']
|
71
|
+
existing = []
|
72
|
+
payload['config'].each do |config|
|
73
|
+
path = config['file']
|
74
|
+
p = Pathname.new(path)
|
75
|
+
path = p.absolute? ? path: File.join(dirname, path)
|
76
|
+
if File.exist?(path)
|
77
|
+
existing.push(path)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# ask the user if it's okay to overwrite existing files
|
83
|
+
if existing.length > 0
|
84
|
+
puts(format('building %s would overwrite these files:', filename))
|
85
|
+
puts existing.join("\n")
|
86
|
+
# if not _prompt_yes():
|
87
|
+
# print('not overwritten')
|
88
|
+
# return
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# if stdout is set then just print each file after another like nginx -T
|
93
|
+
#if options['stdout']
|
94
|
+
payload['config'].each do |config|
|
95
|
+
path = config['file']
|
96
|
+
p = Pathname.new(path)
|
97
|
+
path = p.absolute? ? path: File.join(dirname, path)
|
98
|
+
parsed = config['parsed']
|
99
|
+
output = builder.build(
|
100
|
+
parsed,
|
101
|
+
indent: options['indent'] || 4, # fix default option in config.rb
|
102
|
+
tabs: options['tabs'],
|
103
|
+
header: options['header']
|
104
|
+
)
|
105
|
+
output = output.rstrip + "\n"
|
106
|
+
end
|
107
|
+
return
|
108
|
+
#end
|
109
|
+
end
|
110
|
+
|
111
|
+
desc 'lex <filename>', 'lexes tokens from an nginx config file'
|
112
|
+
configure_options(self, 'method', $config.lex_options)
|
113
|
+
def lex(filename)
|
114
|
+
payload = CrossPlane::Lexer.new(
|
115
|
+
filename: filename,
|
116
|
+
).lex()
|
117
|
+
lex = (not options['line_numbers'].nil? and options['line_numbers'] == true) ? payload : payload.map{|e| e[0]}
|
118
|
+
|
119
|
+
if options['out']
|
120
|
+
File.open(options['out'], 'w') do |f|
|
121
|
+
f.write(JSON.pretty_generate(lex))
|
122
|
+
end
|
123
|
+
else
|
124
|
+
puts options['pretty'] ? JSON.pretty_generate(lex) : lex.to_json
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
desc 'minify', 'removes all whitespace from an nginx config'
|
129
|
+
def minify(filename)
|
130
|
+
puts 'minifiy'
|
131
|
+
exit
|
132
|
+
end
|
133
|
+
|
134
|
+
#desc 'format', 'formats an nginx config file'
|
135
|
+
#def format(filename)
|
136
|
+
# puts 'format'
|
137
|
+
# exit
|
138
|
+
#end
|
139
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module CrossPlane
|
2
|
+
class Config
|
3
|
+
attr_accessor :opts
|
4
|
+
attr_accessor :common_opts
|
5
|
+
attr_accessor :parse_opts
|
6
|
+
attr_accessor :build_opts
|
7
|
+
attr_accessor :lex_opts
|
8
|
+
attr_accessor :minify_opts
|
9
|
+
attr_accessor :format_opts
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
self.common_opts = {
|
13
|
+
|
14
|
+
}
|
15
|
+
|
16
|
+
self.parse_opts = {
|
17
|
+
'out' => {:name => :out, :aliases => ['-o'], :banner => '<string>', :desc => 'write output to a file', :type => :string, :required => false},
|
18
|
+
'pretty' => {:name => :pretty, :desc => 'pretty print the json output', :type => :boolean, :required => false, :default => :false},
|
19
|
+
'ignore' => {:name => :ignore, :banner => '<str>', :desc => 'ignore directives (comma-separated)', :type => :string, :required => false, :default => []},
|
20
|
+
'no-catch' => {:name => :no_catch, :desc => 'only collect first error in file', :type => :boolean, :required => false, :default => :false},
|
21
|
+
#'tb-onerror' => {:name => :tb_onerror, :desc => 'include tracebacks in config errors', :type => :boolean, :required => false},
|
22
|
+
#'combine' => {:name => :combine, :desc => 'use includes to create one single file', :type => :boolean, :required => false},
|
23
|
+
#'single' => {:name => :single, :desc => 'do not include other config files', :type => :boolean, :required => false},
|
24
|
+
'include-comments' => {:name => :include_comments, :desc => 'include comments in json', :type => :boolean, :required => false, :default => :false},
|
25
|
+
'strict' => {:name => :strict, :desc => 'raise errors for unknown directives', :type => :boolean, :required => false, :default => :false},
|
26
|
+
}
|
27
|
+
|
28
|
+
self.build_opts = {
|
29
|
+
'dir' => {:name => :dir, :banner => '<string>', :desc => 'the base directory to build in', :type => :string, :required => false},
|
30
|
+
'force' => {:name => :force, :desc => 'overwrite existing files', :type => :boolean, :required => false},
|
31
|
+
'indent' => {:name => :indent, :banner => '<string>', :desc => 'number of spaces to indent output', :type => :numeric, :required => false, :default => 4},
|
32
|
+
'tabs' => {:name => :tabs, :desc => 'indent with tabs instead of spaces', :type => :boolean, :required => false},
|
33
|
+
#'no-headers' => {:name => :no_header2, :desc => 'do not write header to configsd', :type => :boolean, :required => false},
|
34
|
+
'stdout' => {:name => :stdout, :desc => 'write configs to stdout instead', :type => :boolean, :required => false},
|
35
|
+
}
|
36
|
+
|
37
|
+
self.lex_opts = {
|
38
|
+
'out' => {:name => :out, :aliases => ['-o'], :banner => '<string>', :desc => 'write output to a file', :type => :string, :required => false},
|
39
|
+
'indent' => {:name => :indent, :aliases => ['-i'], :banner => '<int>', :desc => 'number of spaces to indent output', :type => :numeric, :required => false, :default => 4},
|
40
|
+
'numbers' => {:name => :line_numbers, :aliases => ['-n'], :desc => 'include line numbers in json payload', :type => :boolean, :required => false, :default => true},
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_options()
|
45
|
+
return self.common_opts.merge(self.parse_opts).map { |_k, v| v }
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_options()
|
49
|
+
return self.common_opts.merge(self.build_opts).map { |_k, v| v }
|
50
|
+
end
|
51
|
+
|
52
|
+
def lex_options()
|
53
|
+
return self.common_opts.merge(self.lex_opts).map { |_k, v| v }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module CrossPlane
|
2
|
+
class ConstructorError < StandardError
|
3
|
+
def initialize(errors: nil)
|
4
|
+
@errors = errors
|
5
|
+
@error = @errors.join('; ')
|
6
|
+
super(@error)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class NgxParserBaseException < StandardError
|
11
|
+
attr_reader :filename, :lineno, :strerror
|
12
|
+
def initialize(filename, lineno, strerror)
|
13
|
+
@filename = filename
|
14
|
+
@lineno = lineno
|
15
|
+
@strerror = strerror
|
16
|
+
if @lineno.nil?
|
17
|
+
@error = format('%s in %s', @strerror, @filename)
|
18
|
+
else
|
19
|
+
@error = format('%s in %s:%s', @strerror, @filename, @lineno)
|
20
|
+
end
|
21
|
+
super(@error)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class NgxParserDirectiveError < StandardError
|
26
|
+
def initialize(reason, filename, lineno)
|
27
|
+
@reason = reason
|
28
|
+
@filename = filename
|
29
|
+
@lineno = lineno
|
30
|
+
@error = (format('%s in %s:%s', @reason, @filename, @lineno))
|
31
|
+
super(@error)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class NgxParserSyntaxError < NgxParserBaseException
|
36
|
+
def initialize(filename, lineno, strerror)
|
37
|
+
super(filename, lineno, strerror)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class NgxParserDirectiveArgumentsError < NgxParserBaseException
|
42
|
+
def initialize(filename, lineno, strerror)
|
43
|
+
super(filename, lineno, strerror)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class NgxParserDirectiveContextError < NgxParserBaseException
|
48
|
+
def initialize(filename, lineno, strerror)
|
49
|
+
super(filename, lineno, strerror)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class NgxParserDirectiveUnknownError < NgxParserBaseException
|
54
|
+
def initialize(filename, lineno, strerror)
|
55
|
+
super(filename, lineno, strerror)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class NgxParserIncludeError < NgxParserBaseException
|
60
|
+
def initialize(filename, lineno, strerror)
|
61
|
+
super(filename, lineno, strerror)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'crossplane/config'
|
2
|
+
require 'crossplane/utils'
|
3
|
+
|
4
|
+
#require_relative 'config.rb'
|
5
|
+
#require_relative 'utils.rb'
|
6
|
+
|
7
|
+
module CrossPlane
|
8
|
+
def self.utils=(utils)
|
9
|
+
@utils = utils
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.utils
|
13
|
+
@utils
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.config=(config)
|
17
|
+
@config = config
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.config
|
21
|
+
@config
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.debug=(debug)
|
25
|
+
@debug = debug
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.debug
|
29
|
+
@debug
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.logger=(logger)
|
33
|
+
@logger = logger
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.logger
|
37
|
+
@logger || CrossPlane.utils.configure_logger(debug: true)
|
38
|
+
end
|
39
|
+
|
40
|
+
CrossPlane.config = CrossPlane::Config.new()
|
41
|
+
CrossPlane.utils = CrossPlane::Utils.new()
|
42
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'crossplane/errors'
|
2
|
+
require 'crossplane/globals'
|
3
|
+
|
4
|
+
#require_relative 'errors.rb'
|
5
|
+
#require_relative 'globals.rb'
|
6
|
+
|
7
|
+
module CrossPlane
|
8
|
+
class Lexer
|
9
|
+
EXTERNAL_LEXERS = {}
|
10
|
+
NEWLINE = "\n"
|
11
|
+
|
12
|
+
attr_accessor :filename
|
13
|
+
def initialize(*args)
|
14
|
+
args = args[0] || {}
|
15
|
+
|
16
|
+
required = ['filename']
|
17
|
+
conflicts = []
|
18
|
+
requires = {}
|
19
|
+
valid = {
|
20
|
+
'params' => [
|
21
|
+
'filename',
|
22
|
+
]
|
23
|
+
}
|
24
|
+
|
25
|
+
content = CrossPlane.utils.validate_constructor(client: self, args: args, required: required, conflicts: conflicts, requires: requires, valid: valid)
|
26
|
+
self.filename = content[:filename]
|
27
|
+
end
|
28
|
+
|
29
|
+
def lex(*args)
|
30
|
+
tokens = _lex_file()
|
31
|
+
_balance_braces(tokens)
|
32
|
+
tokens
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def _lex_file(*args)
|
37
|
+
token = '' # the token buffer
|
38
|
+
next_token_is_directive = true
|
39
|
+
|
40
|
+
enum = Enumerator.new do |y|
|
41
|
+
File.open(self.filename, 'r') { |f|
|
42
|
+
f.each do |line|
|
43
|
+
lineno = $.
|
44
|
+
line.split('').each do |char|
|
45
|
+
y.yield [char, lineno]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
tokens = []
|
52
|
+
begin
|
53
|
+
while tuple = enum.next
|
54
|
+
char, line = tuple
|
55
|
+
if CrossPlane.utils.isspace(char)
|
56
|
+
if not token.empty?
|
57
|
+
tokens.push([token, line])
|
58
|
+
if next_token_is_directive and EXTERNAL_LEXERS[token]
|
59
|
+
next_token_is_directive = true
|
60
|
+
else
|
61
|
+
next_token_is_directive = false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
while CrossPlane.utils.isspace(char)
|
66
|
+
char, line = enum.next
|
67
|
+
end
|
68
|
+
|
69
|
+
token = ''
|
70
|
+
end
|
71
|
+
|
72
|
+
# if starting comment
|
73
|
+
if token.empty? and char == '#'
|
74
|
+
while not char.end_with?(NEWLINE)
|
75
|
+
token = token + char
|
76
|
+
char, _ = enum.next
|
77
|
+
end
|
78
|
+
tokens.push([token, line])
|
79
|
+
token = ''
|
80
|
+
next
|
81
|
+
end
|
82
|
+
|
83
|
+
# handle parameter expansion syntax (ex: "${var[@]}")
|
84
|
+
if token and token[-1] == '$' and char == '{'
|
85
|
+
next_token_is_directive = false
|
86
|
+
while token[-1] != '}' and not CrossPlane.utils.isspace(char)
|
87
|
+
token += char
|
88
|
+
char, line = enum.next
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# if a quote is found, add the whole string to the token buffer
|
93
|
+
if ['"', "'"].include?(char)
|
94
|
+
if not token.empty?
|
95
|
+
token = token + char
|
96
|
+
next
|
97
|
+
end
|
98
|
+
quote = char
|
99
|
+
char, line = enum.next
|
100
|
+
while char != quote
|
101
|
+
if char == '\\' + quote
|
102
|
+
token = token + quote
|
103
|
+
else
|
104
|
+
token = token + char
|
105
|
+
end
|
106
|
+
char, line = enum.next
|
107
|
+
end
|
108
|
+
|
109
|
+
tokens.push([token, line])
|
110
|
+
token = ''
|
111
|
+
next
|
112
|
+
end
|
113
|
+
|
114
|
+
if ['{', '}', ';'].include?(char)
|
115
|
+
if not token.empty?
|
116
|
+
tokens.push([token, line])
|
117
|
+
token = ''
|
118
|
+
end
|
119
|
+
tokens.push([char, line]) if char.length > 0
|
120
|
+
next_token_is_directive = true
|
121
|
+
next
|
122
|
+
end
|
123
|
+
token = token + char
|
124
|
+
end
|
125
|
+
rescue StopIteration
|
126
|
+
end
|
127
|
+
tokens
|
128
|
+
end
|
129
|
+
|
130
|
+
def _balance_braces(tokens)
|
131
|
+
depth = 0
|
132
|
+
|
133
|
+
for token, line in tokens
|
134
|
+
if token == '}'
|
135
|
+
depth = depth -1
|
136
|
+
elsif token == '{'
|
137
|
+
depth = depth + 1
|
138
|
+
end
|
139
|
+
|
140
|
+
if depth < 0
|
141
|
+
reason = 'unexpected "}"'
|
142
|
+
raise CrossPlane::NgxParserSyntaxError.new(self.filename, line, reason)
|
143
|
+
else
|
144
|
+
yield token, line if block_given?
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
if depth > 0
|
149
|
+
reason = 'unexpected end of file, expecting "}"'
|
150
|
+
raise CrossPlane::NgxParserSyntaxError.new(self.filename, line, reason)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|