Skip to content

Commit 56db904

Browse files
authored
Merge pull request #24 from nginxinc/add-config-builder
Added basic config builder
2 parents 349f0c2 + 12def06 commit 56db904

File tree

9 files changed

+351
-88
lines changed

9 files changed

+351
-88
lines changed

README.rst

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ Reliable and fast NGINX configuration file parser.
1010
- `Command Line Tool`_
1111

1212
- `crossplane parse`_
13+
- `crossplane build`_
1314
- `crossplane lex`_
1415
- `crossplane format`_
1516
- `crossplane minify`_
1617

1718
- `Python Module`_
1819

1920
- `crossplane.parse()`_
21+
- `crossplane.build()`_
2022
- `crossplane.lex()`_
2123

2224
- `Contributing`_
@@ -313,6 +315,31 @@ The second, ``--tb-onerror``, will add a ``"callback"`` key to all error objects
313315
a string representation of the traceback that would have been raised by the parser if the exception had not been caught.
314316
This can be useful for logging purposes.
315317

318+
crossplane build
319+
----------------
320+
321+
.. code-block::
322+
323+
usage: crossplane build [-h] [-d PATH] [-f] [-i NUM | -t] [--no-headers]
324+
[--stdout] [-v]
325+
filename
326+
327+
builds an nginx config from a json payload
328+
329+
positional arguments:
330+
filename the file with the config payload
331+
332+
optional arguments:
333+
-h, --help show this help message and exit
334+
-v, --verbose verbose output
335+
-d PATH, --dir PATH the base directory to build in
336+
-f, --force overwrite existing files
337+
-i NUM, --indent NUM number of spaces to indent output
338+
-t, --tabs indent with tabs instead of spaces
339+
--no-headers do not write header to configs
340+
--stdout write configs to stdout instead
341+
342+
316343
crossplane lex
317344
--------------
318345

@@ -419,18 +446,35 @@ crossplane.parse()
419446
.. code-block:: python
420447
421448
import crossplane
422-
crossplane.parse('/etc/nginx/nginx.conf')
449+
payload = crossplane.parse('/etc/nginx/nginx.conf')
423450
424451
This will return the same payload as described in the `crossplane parse`_ section, except it will be
425452
Python dicts and not one giant JSON string.
426453

454+
crossplane.build()
455+
------------------
456+
457+
.. code-block:: python
458+
459+
import crossplane
460+
config = crossplane.build(
461+
[{
462+
"directive": "events",
463+
"args": [],
464+
"block": [{
465+
"directive": "worker_connections",
466+
"args": ["1024"]
467+
}]
468+
}]
469+
)
470+
427471
crossplane.lex()
428472
----------------
429473

430474
.. code-block:: python
431475
432476
import crossplane
433-
crossplane.lex('/etc/nginx/nginx.conf')
477+
tokens = crossplane.lex('/etc/nginx/nginx.conf')
434478
435479
``crossplane.lex`` generates 2-tuples. Inserting these pairs into a list will result in a long list similar
436480
to what you can see in the `crossplane lex`_ section when the ``--line-numbers`` flag is used, except it

crossplane/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
# -*- coding: utf-8 -*-
22
from .parser import parse
33
from .lexer import lex
4+
from .builder import build
45

5-
__all__ = ['parse', 'lex']
6+
__all__ = ['parse', 'lex', 'build']
67

78
__title__ = 'crossplane'
89
__summary__ = 'Reliable and fast NGINX configuration file parser.'
910
__url__ = 'https://github.com/nginxinc/crossplane'
1011

11-
__version__ = '0.1.3'
12+
__version__ = '0.2.0'
1213

1314
__author__ = 'Arie van Luttikhuizen'
1415
__email__ = '[email protected]'

crossplane/__main__.py

Lines changed: 84 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
3+
import os
34
import sys
45

56
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
@@ -8,60 +9,16 @@
89
from . import __version__
910
from .lexer import lex as lex_file
1011
from .parser import parse as parse_file
12+
from .builder import build as build_file, _enquote, DELIMITERS
1113
from .errors import NgxParserBaseException
12-
from .compat import PY2, json
14+
from .compat import PY2, json, input
1315

14-
DELIMITERS = ('{', '}', ';')
1516

16-
17-
def _escape(string):
18-
prev, char = '', ''
19-
for char in string:
20-
if prev == '\\' or prev + char == '${':
21-
prev += char
22-
yield prev
23-
continue
24-
if prev == '$':
25-
yield prev
26-
if char not in ('\\', '$'):
27-
yield char
28-
prev = char
29-
if char in ('\\', '$'):
30-
yield char
31-
32-
33-
def _needs_quotes(string):
34-
if string == '':
35-
return True
36-
elif string in DELIMITERS:
37-
return False
38-
39-
# lexer should throw an error when variable expansion syntax
40-
# is messed up, but just wrap it in quotes for now I guess
41-
chars = _escape(string)
42-
43-
# arguments can't start with variable expansion syntax
44-
char = next(chars)
45-
if char.isspace() or char in ('{', ';', '"', "'", '${'):
46-
return True
47-
48-
expanding = False
49-
for char in chars:
50-
if char.isspace() or char in ('{', ';', '"', "'"):
51-
return True
52-
elif char == ('${' if expanding else '}'):
53-
return True
54-
elif char == ('}' if expanding else '${'):
55-
expanding = not expanding
56-
57-
return char in ('\\', '$') or expanding
58-
59-
60-
def _enquote(arg):
61-
arg = str(arg.encode('utf-8') if PY2 else arg)
62-
if _needs_quotes(arg):
63-
arg = repr(arg.decode('string_escape') if PY2 else arg)
64-
return arg
17+
def _prompt_yes():
18+
try:
19+
return input('overwrite? (y/n [n]) ').lower().startswith('y')
20+
except (KeyboardInterrupt, EOFError):
21+
sys.exit(1)
6522

6623

6724
def _dump_payload(obj, fp, indent):
@@ -86,6 +43,66 @@ def callback(e):
8643
_dump_payload(payload, out, indent=indent)
8744

8845

46+
def build(filename, dirname, force, indent, tabs, header, stdout, verbose):
47+
with open(filename, 'r') as fp:
48+
payload = json.load(fp)
49+
50+
if dirname is None:
51+
dirname = os.getcwd()
52+
53+
existing = []
54+
dirs_to_make = []
55+
56+
# find which files from the json payload will overwrite existing files and
57+
# which directories need to be created in order for the config to be built
58+
for config in payload['config']:
59+
path = config['file']
60+
if not os.path.isabs(path):
61+
path = os.path.join(dirname, path)
62+
dirpath = os.path.dirname(path)
63+
if os.path.exists(path):
64+
existing.append(path)
65+
elif not os.path.exists(dirpath) and dirpath not in dirs_to_make:
66+
dirs_to_make.append(dirpath)
67+
68+
# ask the user if it's okay to overwrite existing files
69+
if existing and not force and not stdout:
70+
print('building {} would overwrite these files:'.format(filename))
71+
print('\n'.join(existing))
72+
if not _prompt_yes():
73+
print('not overwritten')
74+
return
75+
76+
# make directories necessary for the config to be built
77+
for dirpath in dirs_to_make:
78+
os.makedirs(dirpath)
79+
80+
# build the nginx configuration file from the json payload
81+
for config in payload['config']:
82+
path = os.path.join(dirname, config['file'])
83+
84+
if header:
85+
output = (
86+
'# This config was built from JSON using NGINX crossplane.\n'
87+
'# If you encounter any bugs please report them here:\n'
88+
'# https://github.com/nginxinc/crossplane/issues\n'
89+
'\n'
90+
)
91+
else:
92+
output = ''
93+
94+
parsed = config['parsed']
95+
output += build_file(parsed, indent, tabs) + '\n'
96+
97+
if stdout:
98+
print('# ' + path + '\n' + output)
99+
else:
100+
with open(path, 'w') as fp:
101+
fp.write(output)
102+
if verbose:
103+
print('wrote to ' + path)
104+
105+
89106
def lex(filename, out, indent=None, line_numbers=False):
90107
payload = list(lex_file(filename))
91108
if not line_numbers:
@@ -105,36 +122,11 @@ def minify(filename, out):
105122

106123

107124
def format(filename, out, indent=None, tabs=False):
108-
padding = '\t' if tabs else ' ' * indent
109-
110-
def _format(objs, depth):
111-
margin = padding * depth
112-
113-
for obj in objs:
114-
directive = obj['directive']
115-
args = [_enquote(arg) for arg in obj['args']]
116-
117-
if directive == 'if':
118-
line = 'if (' + ' '.join(args) + ')'
119-
elif args:
120-
line = directive + ' ' + ' '.join(args)
121-
else:
122-
line = directive
123-
124-
if obj.get('block') is None:
125-
yield margin + line + ';'
126-
else:
127-
yield margin + line + ' {'
128-
for line in _format(obj['block'], depth=depth+1):
129-
yield line
130-
yield margin + '}'
131-
132125
payload = parse_file(filename)
133-
126+
parsed = payload['config'][0]['parsed']
134127
if payload['status'] == 'ok':
135-
config = payload['config'][0]['parsed']
136-
lines = _format(config, depth=0)
137-
out.write('\n'.join(lines) + '\n')
128+
output = build_file(parsed, indent, tabs) + '\n'
129+
out.write(output)
138130
else:
139131
e = payload['errors'][0]
140132
raise NgxParserBaseException(e['error'], e['file'], e['line'])
@@ -179,6 +171,17 @@ def create_subparser(function, help):
179171
p.add_argument('--tb-onerror', action='store_true', help='include tracebacks in config errors')
180172
p.add_argument('--single-file', action='store_true', dest='single', help='do not include other config files')
181173

174+
p = create_subparser(build, 'builds an nginx config from a json payload')
175+
p.add_argument('filename', help='the file with the config payload')
176+
p.add_argument('-v', '--verbose', action='store_true', help='verbose output')
177+
p.add_argument('-d', '--dir', metavar='PATH', default=None, dest='dirname', help='the base directory to build in')
178+
p.add_argument('-f', '--force', action='store_true', help='overwrite existing files')
179+
g = p.add_mutually_exclusive_group()
180+
g.add_argument('-i', '--indent', type=int, metavar='NUM', help='number of spaces to indent output', default=4)
181+
g.add_argument('-t', '--tabs', action='store_true', help='indent with tabs instead of spaces')
182+
p.add_argument('--no-headers', action='store_false', dest='header', help='do not write header to configs')
183+
p.add_argument('--stdout', action='store_true', help='write configs to stdout instead')
184+
182185
p = create_subparser(lex, 'lexes tokens from an nginx config file')
183186
p.add_argument('filename', help='the nginx config file')
184187
p.add_argument('-o', '--out', type=FileType('w'), default='-', help='write output to a file')
@@ -197,10 +200,10 @@ def create_subparser(function, help):
197200
g.add_argument('-t', '--tabs', action='store_true', help='indent with tabs instead of spaces')
198201

199202
def help(command):
200-
if command not in parser._actions[1].choices:
203+
if command not in parser._actions[-1].choices:
201204
parser.error('unknown command %r' % command)
202205
else:
203-
parser._actions[1].choices[command].print_help()
206+
parser._actions[-1].choices[command].print_help()
204207

205208
p = create_subparser(help, 'show help for commands')
206209
p.add_argument('command', help='command to show help for')

0 commit comments

Comments
 (0)