├── requirements.pip ├── requirements-dev.pip ├── .gitignore ├── setup.py ├── data ├── foo.conf ├── nginx.new.conf └── nginx.conf ├── README.md ├── LICENSE ├── nginxparser.py └── tests.py /requirements.pip: -------------------------------------------------------------------------------- 1 | pyparsing>=1.5.5 2 | -------------------------------------------------------------------------------- /requirements-dev.pip: -------------------------------------------------------------------------------- 1 | -r requirements.pip 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.egg-info 3 | .ropeproject 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup(name='Nginxparser', 4 | version='0.3', 5 | description='Nginx Parser', 6 | author='Fatih Erikli', 7 | author_email='fatiherikli@gmail.com', 8 | url='https://github.com/fatiherikli/nginxparser', 9 | py_modules=['nginxparser'], 10 | install_requires = [ 11 | 'pyparsing>=1.5.5' 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /data/foo.conf: -------------------------------------------------------------------------------- 1 | # a test nginx conf 2 | user www-data; 3 | 4 | server { 5 | listen 80; 6 | server_name foo.com; 7 | root /home/ubuntu/sites/foo/; 8 | 9 | location /status { 10 | check_status; 11 | types { 12 | image/jpeg jpg; 13 | } 14 | } 15 | 16 | location ~ case_sensitive\.php$ { 17 | hoge hoge; 18 | } 19 | location ~* case_insensitive\.php$ {} 20 | location = exact_match\.php$ {} 21 | location ^~ ignore_regex\.php$ {} 22 | 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Nginx Configuration Parser 2 | 3 | An nginx configuration parser that uses Pyparsing. 4 | 5 | You can parse a nginx configuration file with `load` or `loads` method: 6 | 7 | ```python 8 | >>> from nginxparser import load 9 | >>> load(open("/etc/nginx/sites-enabled/foo.conf")) 10 | 11 | [['server'], [ 12 | ['listen', '80'], 13 | ['server_name', 'foo.com'], 14 | ['root', '/home/ubuntu/sites/foo/']]]] 15 | ``` 16 | 17 | Same as other serialization modules also you can export configuration with `dump` and `dumps` methods. 18 | 19 | ```python 20 | >>> from nginxparser import dumps 21 | >>> dumps([['server'], [ 22 | ['listen', '80'], 23 | ['server_name', 'foo.com'], 24 | ['root', '/home/ubuntu/sites/foo/']]]) 25 | 26 | 'server { 27 | listen 80; 28 | server_name foo.com; 29 | root /home/ubuntu/sites/foo/; 30 | }' 31 | ``` 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Fatih Erikli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /data/nginx.new.conf: -------------------------------------------------------------------------------- 1 | user nobody; 2 | worker_processes 1; 3 | error_log logs/error.log; 4 | error_log logs/error.log notice; 5 | error_log logs/error.log info; 6 | pid logs/nginx.pid; 7 | events { 8 | worker_connections 1024; 9 | } 10 | http { 11 | include mime.types; 12 | default_type application/octet-stream; 13 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 14 | '$status $body_bytes_sent "$http_referer" ' 15 | '"$http_user_agent" "$http_x_forwarded_for"'; 16 | access_log logs/access.log main; 17 | sendfile on; 18 | tcp_nopush on; 19 | keepalive_timeout 0; 20 | keepalive_timeout 65; 21 | gzip on; 22 | 23 | server { 24 | listen 8080; 25 | server_name localhost; 26 | charset koi8-r; 27 | access_log logs/host.access.log main; 28 | 29 | location / { 30 | root html; 31 | index index.html index.htm; 32 | } 33 | error_page 404 /404.html; 34 | error_page 500 502 503 504 /50x.html; 35 | 36 | location = /50x.html { 37 | root html; 38 | } 39 | 40 | location ~ \.php$ { 41 | proxy_pass http://127.0.0.1; 42 | } 43 | 44 | location ~ \.php$ { 45 | root html; 46 | fastcgi_pass 127.0.0.1:9000; 47 | fastcgi_index index.php; 48 | fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 49 | include fastcgi_params; 50 | } 51 | 52 | location ~ /\.ht { 53 | deny all; 54 | } 55 | } 56 | 57 | server { 58 | listen 8000; 59 | listen somename:8080; 60 | server_name somename alias another.alias; 61 | 62 | location / { 63 | root html; 64 | index index.html index.htm; 65 | } 66 | } 67 | 68 | server { 69 | listen 443 ssl; 70 | server_name localhost; 71 | ssl_certificate cert.pem; 72 | ssl_certificate_key cert.key; 73 | ssl_session_cache shared:SSL:1m; 74 | ssl_session_timeout 5m; 75 | ssl_ciphers HIGH:!aNULL:!MD5; 76 | 77 | location / { 78 | root html; 79 | index index.html index.htm; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /data/nginx.conf: -------------------------------------------------------------------------------- 1 | # standard default nginx config 2 | 3 | user nobody; 4 | worker_processes 1; 5 | 6 | error_log logs/error.log; 7 | error_log logs/error.log notice; 8 | error_log logs/error.log info; 9 | 10 | pid logs/nginx.pid; 11 | 12 | 13 | events { 14 | worker_connections 1024; 15 | } 16 | 17 | 18 | http { 19 | include mime.types; 20 | default_type application/octet-stream; 21 | 22 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 23 | '$status $body_bytes_sent "$http_referer" ' 24 | '"$http_user_agent" "$http_x_forwarded_for"'; 25 | 26 | access_log logs/access.log main; 27 | 28 | sendfile on; 29 | tcp_nopush on; 30 | 31 | keepalive_timeout 0; 32 | keepalive_timeout 65; 33 | 34 | gzip on; 35 | 36 | server { 37 | listen 8080; 38 | server_name localhost; 39 | 40 | charset koi8-r; 41 | 42 | access_log logs/host.access.log main; 43 | 44 | location / { 45 | root html; 46 | index index.html index.htm; 47 | } 48 | 49 | error_page 404 /404.html; 50 | 51 | # redirect server error pages to the static page /50x.html 52 | error_page 500 502 503 504 /50x.html; 53 | location = /50x.html { 54 | root html; 55 | } 56 | 57 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 58 | # 59 | location ~ \.php$ { 60 | proxy_pass http://127.0.0.1; 61 | } 62 | 63 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 64 | # 65 | location ~ \.php$ { 66 | root html; 67 | fastcgi_pass 127.0.0.1:9000; 68 | fastcgi_index index.php; 69 | fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 70 | include fastcgi_params; 71 | } 72 | 73 | # deny access to .htaccess files, if Apache's document root 74 | # concurs with nginx's one 75 | # 76 | location ~ /\.ht { 77 | deny all; 78 | } 79 | } 80 | 81 | 82 | # another virtual host using mix of IP-, name-, and port-based configuration 83 | # 84 | server { 85 | listen 8000; 86 | listen somename:8080; 87 | server_name somename alias another.alias; 88 | 89 | location / { 90 | root html; 91 | index index.html index.htm; 92 | } 93 | } 94 | 95 | 96 | # HTTPS server 97 | # 98 | #server { 99 | # listen 443 ssl; 100 | # server_name localhost; 101 | 102 | # ssl_certificate cert.pem; 103 | # ssl_certificate_key cert.key; 104 | 105 | # ssl_session_cache shared:SSL:1m; 106 | # ssl_session_timeout 5m; 107 | 108 | # ssl_ciphers HIGH:!aNULL:!MD5; 109 | # ssl_prefer_server_ciphers on; 110 | 111 | # location / { 112 | # root html; 113 | # index index.html index.htm; 114 | # } 115 | #} 116 | 117 | } 118 | -------------------------------------------------------------------------------- /nginxparser.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from pyparsing import ( 4 | Literal, White, Word, alphanums, CharsNotIn, Forward, Group, 5 | Optional, OneOrMore, ZeroOrMore, pythonStyleComment) 6 | 7 | 8 | class NginxParser(object): 9 | """ 10 | A class that parses nginx configuration with pyparsing 11 | """ 12 | 13 | # constants 14 | left_bracket = Literal("{").suppress() 15 | right_bracket = Literal("}").suppress() 16 | semicolon = Literal(";").suppress() 17 | space = White().suppress() 18 | key = Word(alphanums + "_/") 19 | value = CharsNotIn("{};,") 20 | location = CharsNotIn("{};," + string.whitespace) 21 | # modifier for location uri [ = | ~ | ~* | ^~ ] 22 | modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") 23 | 24 | # rules 25 | assignment = (key + Optional(space + value) + semicolon) 26 | block = Forward() 27 | 28 | block << Group( 29 | Group(key + Optional(space + modifier) + Optional(space + location)) 30 | + left_bracket 31 | + Group(ZeroOrMore(Group(assignment) | block)) 32 | + right_bracket) 33 | 34 | script = OneOrMore(Group(assignment) | block).ignore(pythonStyleComment) 35 | 36 | def __init__(self, source): 37 | self.source = source 38 | 39 | def parse(self): 40 | """ 41 | Returns the parsed tree. 42 | """ 43 | return self.script.parseString(self.source) 44 | 45 | def as_list(self): 46 | """ 47 | Returns the list of tree. 48 | """ 49 | return self.parse().asList() 50 | 51 | 52 | class NginxDumper(object): 53 | """ 54 | A class that dumps nginx configuration from the provided tree. 55 | """ 56 | def __init__(self, blocks, indentation=4): 57 | self.blocks = blocks 58 | self.indentation = indentation 59 | 60 | def __iter__(self, blocks=None, current_indent=0, spacer=' '): 61 | """ 62 | Iterates the dumped nginx content. 63 | """ 64 | blocks = blocks or self.blocks 65 | for key, values in blocks: 66 | if current_indent: 67 | yield spacer 68 | indentation = spacer * current_indent 69 | if isinstance(key, list): 70 | yield indentation + spacer.join(key) + ' {' 71 | for parameter in values: 72 | if isinstance(parameter[0], list): 73 | dumped = self.__iter__( 74 | [parameter], 75 | current_indent + self.indentation) 76 | for line in dumped: 77 | yield line 78 | else: 79 | dumped = spacer.join(parameter) + ';' 80 | yield spacer * ( 81 | current_indent + self.indentation) + dumped 82 | 83 | yield indentation + '}' 84 | else: 85 | yield spacer * current_indent + key + spacer + values + ';' 86 | 87 | def as_string(self): 88 | return '\n'.join(self) 89 | 90 | 91 | # Shortcut functions to respect Python's serialization interface 92 | # (like pyyaml, picker or json) 93 | 94 | def loads(source): 95 | return NginxParser(source).as_list() 96 | 97 | 98 | def load(_file): 99 | return loads(_file.read()) 100 | 101 | 102 | def dumps(blocks, indentation=4): 103 | return NginxDumper(blocks, indentation).as_string() 104 | 105 | 106 | def dump(blocks, _file, indentation=4): 107 | _file.write(dumps(blocks, indentation)) 108 | _file.close() 109 | return _file 110 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import unittest 3 | 4 | from nginxparser import NginxParser, load, dumps, dump 5 | 6 | 7 | first = operator.itemgetter(0) 8 | 9 | 10 | class TestNginxParser(unittest.TestCase): 11 | 12 | def test_assignments(self): 13 | parsed = NginxParser.assignment.parseString('root /test;').asList() 14 | self.assertEqual(parsed, ['root', '/test']) 15 | parsed = NginxParser.assignment.parseString('root /test;' 16 | 'foo bar;').asList() 17 | self.assertEqual(parsed, ['root', '/test'], ['foo', 'bar']) 18 | 19 | def test_blocks(self): 20 | parsed = NginxParser.block.parseString('foo {}').asList() 21 | self.assertEqual(parsed, [[['foo'], []]]) 22 | parsed = NginxParser.block.parseString('location /foo{}').asList() 23 | self.assertEqual(parsed, [[['location', '/foo'], []]]) 24 | parsed = NginxParser.block.parseString('foo { bar foo; }').asList() 25 | self.assertEqual(parsed, [[['foo'], [['bar', 'foo']]]]) 26 | 27 | def test_nested_blocks(self): 28 | parsed = NginxParser.block.parseString('foo { bar {} }').asList() 29 | block, content = first(parsed) 30 | self.assertEqual(first(content), [['bar'], []]) 31 | 32 | def test_dump_as_string(self): 33 | dumped = dumps([ 34 | ['user', 'www-data'], 35 | [['server'], [ 36 | ['listen', '80'], 37 | ['server_name', 'foo.com'], 38 | ['root', '/home/ubuntu/sites/foo/'], 39 | [['location', '/status'], [ 40 | ['check_status'], 41 | [['types'], [['image/jpeg', 'jpg']]], 42 | ]] 43 | ]]]) 44 | 45 | self.assertEqual(dumped, 46 | 'user www-data;\n' + 47 | 'server {\n' + 48 | ' listen 80;\n' + 49 | ' server_name foo.com;\n' + 50 | ' root /home/ubuntu/sites/foo/;\n \n' + 51 | ' location /status {\n' + 52 | ' check_status;\n \n' + 53 | ' types {\n' + 54 | ' image/jpeg jpg;\n' + 55 | ' }\n' + 56 | ' }\n' + 57 | '}') 58 | 59 | def test_parse_from_file(self): 60 | parsed = load(open('data/foo.conf')) 61 | self.assertEqual( 62 | parsed, 63 | [['user', 'www-data'], 64 | [['server'], [ 65 | ['listen', '80'], 66 | ['server_name', 'foo.com'], 67 | ['root', '/home/ubuntu/sites/foo/'], 68 | [['location', '/status'], [ 69 | ['check_status'], 70 | [['types'], [['image/jpeg', 'jpg']]], 71 | ]], 72 | [['location', '~', 'case_sensitive\.php$'], [ 73 | ['hoge', 'hoge'] 74 | ]], 75 | [['location', '~*', 'case_insensitive\.php$'], []], 76 | [['location', '=', 'exact_match\.php$'], []], 77 | [['location', '^~', 'ignore_regex\.php$'], []], 78 | ]]] 79 | ) 80 | 81 | def test_dump_as_file(self): 82 | parsed = load(open('data/nginx.conf')) 83 | parsed[-1][-1].append([['server'], 84 | [['listen', '443 ssl'], 85 | ['server_name', 'localhost'], 86 | ['ssl_certificate', 'cert.pem'], 87 | ['ssl_certificate_key', 'cert.key'], 88 | ['ssl_session_cache', 'shared:SSL:1m'], 89 | ['ssl_session_timeout', '5m'], 90 | ['ssl_ciphers', 'HIGH:!aNULL:!MD5'], 91 | [['location', '/'], 92 | [['root', 'html'], 93 | ['index', 'index.html index.htm']]]]]) 94 | f = open('data/nginx.new.conf', 'w') 95 | dump(parsed, f) 96 | parsed_new = load(open('data/nginx.new.conf')) 97 | self.assertEquals(parsed, parsed_new) 98 | 99 | 100 | if __name__ == '__main__': 101 | unittest.main() 102 | --------------------------------------------------------------------------------