Skip to content

Commit

Permalink
Merge pull request #24 from nginxinc/add-config-builder
Browse files Browse the repository at this point in the history
Added basic config builder
  • Loading branch information
aluttik authored Jan 23, 2018
2 parents 349f0c2 + 12def06 commit 56db904
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 88 deletions.
48 changes: 46 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ Reliable and fast NGINX configuration file parser.
- `Command Line Tool`_

- `crossplane parse`_
- `crossplane build`_
- `crossplane lex`_
- `crossplane format`_
- `crossplane minify`_

- `Python Module`_

- `crossplane.parse()`_
- `crossplane.build()`_
- `crossplane.lex()`_

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

crossplane build
----------------

.. code-block::
usage: crossplane build [-h] [-d PATH] [-f] [-i NUM | -t] [--no-headers]
[--stdout] [-v]
filename
builds an nginx config from a json payload
positional arguments:
filename the file with the config payload
optional arguments:
-h, --help show this help message and exit
-v, --verbose verbose output
-d PATH, --dir PATH the base directory to build in
-f, --force overwrite existing files
-i NUM, --indent NUM number of spaces to indent output
-t, --tabs indent with tabs instead of spaces
--no-headers do not write header to configs
--stdout write configs to stdout instead
crossplane lex
--------------

Expand Down Expand Up @@ -419,18 +446,35 @@ crossplane.parse()
.. code-block:: python
import crossplane
crossplane.parse('/etc/nginx/nginx.conf')
payload = crossplane.parse('/etc/nginx/nginx.conf')
This will return the same payload as described in the `crossplane parse`_ section, except it will be
Python dicts and not one giant JSON string.

crossplane.build()
------------------

.. code-block:: python
import crossplane
config = crossplane.build(
[{
"directive": "events",
"args": [],
"block": [{
"directive": "worker_connections",
"args": ["1024"]
}]
}]
)
crossplane.lex()
----------------

.. code-block:: python
import crossplane
crossplane.lex('/etc/nginx/nginx.conf')
tokens = crossplane.lex('/etc/nginx/nginx.conf')
``crossplane.lex`` generates 2-tuples. Inserting these pairs into a list will result in a long list similar
to what you can see in the `crossplane lex`_ section when the ``--line-numbers`` flag is used, except it
Expand Down
5 changes: 3 additions & 2 deletions crossplane/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# -*- coding: utf-8 -*-
from .parser import parse
from .lexer import lex
from .builder import build

__all__ = ['parse', 'lex']
__all__ = ['parse', 'lex', 'build']

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

__version__ = '0.1.3'
__version__ = '0.2.0'

__author__ = 'Arie van Luttikhuizen'
__email__ = '[email protected]'
Expand Down
165 changes: 84 additions & 81 deletions crossplane/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys

from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
Expand All @@ -8,60 +9,16 @@
from . import __version__
from .lexer import lex as lex_file
from .parser import parse as parse_file
from .builder import build as build_file, _enquote, DELIMITERS
from .errors import NgxParserBaseException
from .compat import PY2, json
from .compat import PY2, json, input

DELIMITERS = ('{', '}', ';')


def _escape(string):
prev, char = '', ''
for char in string:
if prev == '\\' or prev + char == '${':
prev += char
yield prev
continue
if prev == '$':
yield prev
if char not in ('\\', '$'):
yield char
prev = char
if char in ('\\', '$'):
yield char


def _needs_quotes(string):
if string == '':
return True
elif string in DELIMITERS:
return False

# lexer should throw an error when variable expansion syntax
# is messed up, but just wrap it in quotes for now I guess
chars = _escape(string)

# arguments can't start with variable expansion syntax
char = next(chars)
if char.isspace() or char in ('{', ';', '"', "'", '${'):
return True

expanding = False
for char in chars:
if char.isspace() or char in ('{', ';', '"', "'"):
return True
elif char == ('${' if expanding else '}'):
return True
elif char == ('}' if expanding else '${'):
expanding = not expanding

return char in ('\\', '$') or expanding


def _enquote(arg):
arg = str(arg.encode('utf-8') if PY2 else arg)
if _needs_quotes(arg):
arg = repr(arg.decode('string_escape') if PY2 else arg)
return arg
def _prompt_yes():
try:
return input('overwrite? (y/n [n]) ').lower().startswith('y')
except (KeyboardInterrupt, EOFError):
sys.exit(1)


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


def build(filename, dirname, force, indent, tabs, header, stdout, verbose):
with open(filename, 'r') as fp:
payload = json.load(fp)

if dirname is None:
dirname = os.getcwd()

existing = []
dirs_to_make = []

# find which files from the json payload will overwrite existing files and
# which directories need to be created in order for the config to be built
for config in payload['config']:
path = config['file']
if not os.path.isabs(path):
path = os.path.join(dirname, path)
dirpath = os.path.dirname(path)
if os.path.exists(path):
existing.append(path)
elif not os.path.exists(dirpath) and dirpath not in dirs_to_make:
dirs_to_make.append(dirpath)

# ask the user if it's okay to overwrite existing files
if existing and not force and not stdout:
print('building {} would overwrite these files:'.format(filename))
print('\n'.join(existing))
if not _prompt_yes():
print('not overwritten')
return

# make directories necessary for the config to be built
for dirpath in dirs_to_make:
os.makedirs(dirpath)

# build the nginx configuration file from the json payload
for config in payload['config']:
path = os.path.join(dirname, config['file'])

if header:
output = (
'# This config was built from JSON using NGINX crossplane.\n'
'# If you encounter any bugs please report them here:\n'
'# https://github.com/nginxinc/crossplane/issues\n'
'\n'
)
else:
output = ''

parsed = config['parsed']
output += build_file(parsed, indent, tabs) + '\n'

if stdout:
print('# ' + path + '\n' + output)
else:
with open(path, 'w') as fp:
fp.write(output)
if verbose:
print('wrote to ' + path)


def lex(filename, out, indent=None, line_numbers=False):
payload = list(lex_file(filename))
if not line_numbers:
Expand All @@ -105,36 +122,11 @@ def minify(filename, out):


def format(filename, out, indent=None, tabs=False):
padding = '\t' if tabs else ' ' * indent

def _format(objs, depth):
margin = padding * depth

for obj in objs:
directive = obj['directive']
args = [_enquote(arg) for arg in obj['args']]

if directive == 'if':
line = 'if (' + ' '.join(args) + ')'
elif args:
line = directive + ' ' + ' '.join(args)
else:
line = directive

if obj.get('block') is None:
yield margin + line + ';'
else:
yield margin + line + ' {'
for line in _format(obj['block'], depth=depth+1):
yield line
yield margin + '}'

payload = parse_file(filename)

parsed = payload['config'][0]['parsed']
if payload['status'] == 'ok':
config = payload['config'][0]['parsed']
lines = _format(config, depth=0)
out.write('\n'.join(lines) + '\n')
output = build_file(parsed, indent, tabs) + '\n'
out.write(output)
else:
e = payload['errors'][0]
raise NgxParserBaseException(e['error'], e['file'], e['line'])
Expand Down Expand Up @@ -179,6 +171,17 @@ def create_subparser(function, help):
p.add_argument('--tb-onerror', action='store_true', help='include tracebacks in config errors')
p.add_argument('--single-file', action='store_true', dest='single', help='do not include other config files')

p = create_subparser(build, 'builds an nginx config from a json payload')
p.add_argument('filename', help='the file with the config payload')
p.add_argument('-v', '--verbose', action='store_true', help='verbose output')
p.add_argument('-d', '--dir', metavar='PATH', default=None, dest='dirname', help='the base directory to build in')
p.add_argument('-f', '--force', action='store_true', help='overwrite existing files')
g = p.add_mutually_exclusive_group()
g.add_argument('-i', '--indent', type=int, metavar='NUM', help='number of spaces to indent output', default=4)
g.add_argument('-t', '--tabs', action='store_true', help='indent with tabs instead of spaces')
p.add_argument('--no-headers', action='store_false', dest='header', help='do not write header to configs')
p.add_argument('--stdout', action='store_true', help='write configs to stdout instead')

p = create_subparser(lex, 'lexes tokens from an nginx config file')
p.add_argument('filename', help='the nginx config file')
p.add_argument('-o', '--out', type=FileType('w'), default='-', help='write output to a file')
Expand All @@ -197,10 +200,10 @@ def create_subparser(function, help):
g.add_argument('-t', '--tabs', action='store_true', help='indent with tabs instead of spaces')

def help(command):
if command not in parser._actions[1].choices:
if command not in parser._actions[-1].choices:
parser.error('unknown command %r' % command)
else:
parser._actions[1].choices[command].print_help()
parser._actions[-1].choices[command].print_help()

p = create_subparser(help, 'show help for commands')
p.add_argument('command', help='command to show help for')
Expand Down
Loading

0 comments on commit 56db904

Please sign in to comment.