Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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__ = 'aluttik@gmail.com'
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