Skip to content

Commit 075aff5

Browse files
author
Jacob Cook
committed
Refactor conf parsing for multiline support
1 parent cb17bd9 commit 075aff5

File tree

2 files changed

+135
-77
lines changed

2 files changed

+135
-77
lines changed

nginx.py

Lines changed: 85 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def as_dict(self):
185185
def as_strings(self):
186186
"""Return the entire Server block as nginx config strings."""
187187
ret = []
188-
ret.append('server {\n')
188+
ret.append('\nserver {\n')
189189
for x in self.children:
190190
if isinstance(x, Key):
191191
ret.append(INDENT + x.as_strings)
@@ -401,6 +401,10 @@ def as_dict(self):
401401
@property
402402
def as_strings(self):
403403
"""Return key as nginx config string."""
404+
if self.value == '' or self.value is None:
405+
return '{0};\n'.format(self.name)
406+
if ';' in self.value or '#' in self.value:
407+
return '{0} "{1}";\n'.format(self.name, self.value)
404408
return '{0} {1};\n'.format(self.name, self.value)
405409

406410

@@ -413,77 +417,95 @@ def loads(data, conf=True):
413417
"""
414418
f = Conf() if conf else []
415419
lopen = []
416-
for line in data.split('\n'):
417-
line_outside_quotes = re.sub(r'"([^"]+)"|\'([^\']+)\'|\\S+', '', line)
418-
if re.match(r'\s*server\s*({.*)?$', line):
420+
index = 0
421+
422+
while True:
423+
m = re.compile(r'^\s*server\s*{', re.S).search(data[index:])
424+
if m:
419425
s = Server()
420426
lopen.insert(0, s)
421-
if re.match(r'\s*location.*', line):
422-
lpath = re.match(r'\s*location\s*(.*\S+)\s*{', line).group(1)
423-
l = Location(lpath)
427+
index += m.end()
428+
continue
429+
430+
m = re.compile(r'^\s*location\s*(.*?\S+)\s*{', re.S).search(data[index:])
431+
if m:
432+
l = Location(m.group(1))
424433
lopen.insert(0, l)
425-
if re.match(r'\s*if.*({.*)?$', line):
426-
ifs = re.match('\s*if\s*(.*\s+)\s*', line).group(1)
427-
ifs = If(ifs)
434+
index += m.end()
435+
continue
436+
437+
m = re.compile(r'^\s*if\s*(.*?\S+)\s*{', re.S).search(data[index:])
438+
if m:
439+
ifs = If(m.group(1))
428440
lopen.insert(0, ifs)
429-
if re.match(r'\s*upstream.*({.*)?$', line):
430-
ups = re.match(r'\s*upstream\s*(.*\S+)\s*[^{]', line).group().split()[1]
431-
u = Upstream(ups)
432-
lopen.insert(0, u)
433-
if re.match(r'\s*geo\s*\$.*\s{', line):
434-
geo = re.match('\s*geo\s+(\$.*)\s{', line).group(1)
435-
s = Geo(geo)
436-
lopen.insert(0, s)
437-
if re.match(r'.*;', line):
438-
cmt_regex = r'(.*)#\s*(?![^\'\"]*[\'\"])'
439-
key_regex = r'.*(?:^|^\s*|{\s*)(\S+)\s(.+);'
440-
441-
oneword_regex = r'\s*(\S+[^\s+]\S+)\s*;\s*'
442-
443-
to_eval = line
444-
if re.match(cmt_regex, line):
445-
to_eval = re.match(cmt_regex, line).group(1)
446-
if re.match(key_regex, to_eval):
447-
kname, kval = re.match(key_regex, to_eval).group(1, 2)
448-
if "#" not in kname:
449-
k = Key(kname, kval)
450-
if lopen and isinstance(lopen[0], (Container, Server)):
451-
lopen[0].add(k)
452-
else:
453-
f.add(k) if conf else f.append(k)
454-
455-
if re.match(oneword_regex, line):
456-
kname = re.match(oneword_regex, line).group(1)
457-
k = Key(kname, '')
441+
index += m.end()
442+
continue
458443

459-
if lopen and isinstance(lopen[0], (Container, Server)):
460-
lopen[0].add(k)
461-
else:
462-
f.add(k) if conf else f.append(k)
463-
464-
465-
if re.match(r'(^(?!#)([^#]*[}]{1}\s*)$)|(\s*{$)', line_outside_quotes):
466-
closenum = len(re.findall('}', line_outside_quotes))
467-
while closenum > 0:
468-
if isinstance(lopen[0], Server):
469-
f.add(lopen[0]) if conf else f.append(lopen[0])
470-
lopen.pop(0)
471-
elif isinstance(lopen[0], Container):
472-
c = lopen[0]
473-
lopen.pop(0)
474-
if lopen and isinstance(lopen[0], (Container, Server)):
475-
lopen[0].add(c)
476-
else:
477-
f.add(c) if conf else f.append(c)
478-
closenum = closenum - 1
479-
if re.match(r'.*#\s*(?![^\'\"]*[\'\"])', line):
480-
cmt_regex = r'.*#\s*(.*)(?![^\'\"]*[\'\"])'
481-
c = Comment(re.match(cmt_regex, line).group(1),
482-
inline=not re.match(r'^\s*#.*', line))
444+
m = re.compile(r'^\s*upstream\s*(.*?\S+)\s*{', re.S).search(data[index:])
445+
if m:
446+
u = Upstream(m.group(1))
447+
lopen.insert(0, u)
448+
index += m.end()
449+
continue
450+
451+
m = re.compile(r'^\s*geo\s*(.*?\S+)\s*{', re.S).search(data[index:])
452+
if m:
453+
g = Geo(m.group(1))
454+
lopen.insert(0, g)
455+
index += m.end()
456+
continue
457+
458+
m = re.compile(r'^(\s*)#\s*(.*?)\n', re.S).search(data[index:])
459+
if m:
460+
c = Comment(m.group(2), inline='\n' not in m.group(1))
483461
if lopen and isinstance(lopen[0], (Container, Server)):
484462
lopen[0].add(c)
485463
else:
486464
f.add(c) if conf else f.append(c)
465+
index += m.end() - 1
466+
continue
467+
468+
m = re.compile(r'^\s*}', re.S).search(data[index:])
469+
if m:
470+
if isinstance(lopen[0], Server):
471+
f.add(lopen[0]) if conf else f.append(lopen[0])
472+
lopen.pop(0)
473+
elif isinstance(lopen[0], Container):
474+
c = lopen[0]
475+
lopen.pop(0)
476+
if lopen and isinstance(lopen[0], (Container, Server)):
477+
lopen[0].add(c)
478+
else:
479+
f.add(c) if conf else f.append(c)
480+
index += m.end()
481+
continue
482+
483+
key_with_quoted = r'^\s*(\S*?)\s*"([^"]+)";?|\'([^\']+)\';?|\\S+;?'
484+
key_wo_quoted = r'^\s*([a-zA-Z0-9-_]+?)\s+(.+?);'
485+
m1 = re.compile(key_with_quoted, re.S).search(data[index:])
486+
m2 = re.compile(key_wo_quoted, re.S).search(data[index:])
487+
m = m1 or m2
488+
if m:
489+
k = Key(m.group(1), m.group(2))
490+
if lopen and isinstance(lopen[0], (Container, Server)):
491+
lopen[0].add(k)
492+
else:
493+
f.add(k) if conf else f.append(k)
494+
index += m.end()
495+
continue
496+
497+
m = re.compile(r'^\s*(\S+);', re.S).search(data[index:])
498+
if m:
499+
k = Key(m.group(1), '')
500+
if lopen and isinstance(lopen[0], (Container, Server)):
501+
lopen[0].add(k)
502+
else:
503+
f.add(k) if conf else f.append(k)
504+
index += m.end()
505+
continue
506+
507+
break
508+
487509
return f
488510

489511

tests.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
import unittest
1313

1414

15-
TESTBLOCK = """
15+
TESTBLOCK_CASE_1 = """
1616
include conf.d/pre/*.cfg;
1717
upstream php {
1818
server unix:/tmp/php-fcgi.socket;
1919
}
20+
2021
server {
2122
listen 80; # This comment should be present;
2223
# And this one
@@ -32,7 +33,7 @@
3233
}
3334
"""
3435

35-
SECONDTESTBLOCK = """
36+
TESTBLOCK_CASE_2 = """
3637
upstream php
3738
{
3839
server unix:/tmp/php-fcgi.socket;
@@ -65,9 +66,34 @@
6566
}
6667
"""
6768

69+
TESTBLOCK_CASE_3="""
70+
upstream test0 {
71+
ip_hash;
72+
server 127.0.0.1:8080;
73+
keepalive 16;
74+
}
75+
upstream test1{
76+
server 127.0.0.2:8080;
77+
keepalive 16;
78+
}
79+
upstream test2
80+
{
81+
server 127.0.0.3:8080;
82+
keepalive 16;
83+
}
84+
85+
server {
86+
listen 80;
87+
server_name example.com;
6888
89+
location = /
90+
{
91+
root html;
92+
}
93+
}
94+
"""
6995

70-
MESSYBLOCK = """
96+
TESTBLOCK_CASE_4 = """
7197
# This is an example of a messy config
7298
upstream php { server unix:/tmp/php-cgi.socket; }
7399
server { server_name localhost; #this is the server server_name
@@ -77,57 +103,67 @@
77103

78104
class TestPythonNginx(unittest.TestCase):
79105
def test_basic_load(self):
80-
self.assertTrue(nginx.loads(TESTBLOCK) is not None)
106+
self.assertTrue(nginx.loads(TESTBLOCK_CASE_1) is not None)
81107

82108
def test_messy_load(self):
83-
data = nginx.loads(MESSYBLOCK)
109+
data = nginx.loads(TESTBLOCK_CASE_4)
84110
self.assertTrue(data is not None)
85111
self.assertTrue(len(data.server.comments), 1)
86112
self.assertTrue(len(data.server.locations), 1)
87113

88114
def test_comment_parse(self):
89-
data = nginx.loads(TESTBLOCK)
115+
data = nginx.loads(TESTBLOCK_CASE_1)
90116
self.assertEqual(len(data.server.comments), 4)
91117
self.assertEqual(data.server.comments[2].comment, 'And also this one')
92118

93119
def test_key_parse(self):
94-
data = nginx.loads(TESTBLOCK)
120+
data = nginx.loads(TESTBLOCK_CASE_1)
95121
self.assertEqual(len(data.server.keys), 5)
96122
firstKey = data.server.keys[0]
97123
thirdKey = data.server.keys[3]
98124
self.assertEqual(firstKey.name, 'listen')
99125
self.assertEqual(firstKey.value, '80')
100126
self.assertEqual(thirdKey.name, 'mykey')
101-
self.assertEqual(thirdKey.value, '"myvalue; #notme myothervalue"')
127+
self.assertEqual(thirdKey.value, 'myvalue; #notme myothervalue')
102128

103129
def test_key_parse_complex(self):
104-
data = nginx.loads(SECONDTESTBLOCK)
130+
data = nginx.loads(TESTBLOCK_CASE_2)
105131
self.assertEqual(len(data.server.keys), 5)
106132
firstKey = data.server.keys[0]
107133
thirdKey = data.server.keys[3]
108134
self.assertEqual(firstKey.name, 'listen')
109135
self.assertEqual(firstKey.value, '80')
110136
self.assertEqual(thirdKey.name, 'mykey')
111-
self.assertEqual(thirdKey.value, '"myvalue; #notme myothervalue"')
137+
self.assertEqual(thirdKey.value, 'myvalue; #notme myothervalue')
112138
self.assertEqual(
113139
data.server.locations[-1].keys[0].value,
114140
"301 $scheme://$host:$server_port${request_uri}bitbucket/"
115141
)
116142

117143
def test_location_parse(self):
118-
data = nginx.loads(TESTBLOCK)
144+
data = nginx.loads(TESTBLOCK_CASE_1)
119145
self.assertEqual(len(data.server.locations), 1)
120146
firstLoc = data.server.locations[0]
121147
self.assertEqual(firstLoc.value, '~ \.php(?:$|/)')
122148
self.assertEqual(len(firstLoc.keys), 1)
123149

150+
def test_brace_position(self):
151+
data = nginx.loads(TESTBLOCK_CASE_3)
152+
self.assertEqual(len(data.filter('Upstream')), 3)
153+
154+
def test_single_value_keys(self):
155+
data = nginx.loads(TESTBLOCK_CASE_3)
156+
single_value_key = data.filter('Upstream')[0].keys[0]
157+
self.assertEqual(single_value_key.name, 'ip_hash')
158+
self.assertEqual(single_value_key.value, '')
159+
124160
def test_reflection(self):
125-
inp_data = nginx.loads(TESTBLOCK)
161+
inp_data = nginx.loads(TESTBLOCK_CASE_1)
126162
out_data = '\n' + nginx.dumps(inp_data)
127-
self.assertEqual(TESTBLOCK, out_data)
163+
self.assertEqual(TESTBLOCK_CASE_1, out_data)
128164

129165
def test_filtering(self):
130-
data = nginx.loads(TESTBLOCK)
166+
data = nginx.loads(TESTBLOCK_CASE_1)
131167
self.assertEqual(len(data.server.filter('Key', 'mykey')), 1)
132168
self.assertEqual(data.server.filter('Key', 'nothere'), [])
133169

0 commit comments

Comments
 (0)