1
1
#!/usr/bin/env python
2
2
# -*- coding: utf-8 -*-
3
+ import os
3
4
import sys
4
5
5
6
from argparse import ArgumentParser , FileType , RawDescriptionHelpFormatter
8
9
from . import __version__
9
10
from .lexer import lex as lex_file
10
11
from .parser import parse as parse_file
12
+ from .builder import build as build_file , _enquote , DELIMITERS
11
13
from .errors import NgxParserBaseException
12
- from .compat import PY2 , json
14
+ from .compat import PY2 , json , input
13
15
14
- DELIMITERS = ('{' , '}' , ';' )
15
16
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 )
65
22
66
23
67
24
def _dump_payload (obj , fp , indent ):
@@ -86,6 +43,66 @@ def callback(e):
86
43
_dump_payload (payload , out , indent = indent )
87
44
88
45
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
+
89
106
def lex (filename , out , indent = None , line_numbers = False ):
90
107
payload = list (lex_file (filename ))
91
108
if not line_numbers :
@@ -105,36 +122,11 @@ def minify(filename, out):
105
122
106
123
107
124
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
-
132
125
payload = parse_file (filename )
133
-
126
+ parsed = payload [ 'config' ][ 0 ][ 'parsed' ]
134
127
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 )
138
130
else :
139
131
e = payload ['errors' ][0 ]
140
132
raise NgxParserBaseException (e ['error' ], e ['file' ], e ['line' ])
@@ -179,6 +171,17 @@ def create_subparser(function, help):
179
171
p .add_argument ('--tb-onerror' , action = 'store_true' , help = 'include tracebacks in config errors' )
180
172
p .add_argument ('--single-file' , action = 'store_true' , dest = 'single' , help = 'do not include other config files' )
181
173
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
+
182
185
p = create_subparser (lex , 'lexes tokens from an nginx config file' )
183
186
p .add_argument ('filename' , help = 'the nginx config file' )
184
187
p .add_argument ('-o' , '--out' , type = FileType ('w' ), default = '-' , help = 'write output to a file' )
@@ -197,10 +200,10 @@ def create_subparser(function, help):
197
200
g .add_argument ('-t' , '--tabs' , action = 'store_true' , help = 'indent with tabs instead of spaces' )
198
201
199
202
def help (command ):
200
- if command not in parser ._actions [1 ].choices :
203
+ if command not in parser ._actions [- 1 ].choices :
201
204
parser .error ('unknown command %r' % command )
202
205
else :
203
- parser ._actions [1 ].choices [command ].print_help ()
206
+ parser ._actions [- 1 ].choices [command ].print_help ()
204
207
205
208
p = create_subparser (help , 'show help for commands' )
206
209
p .add_argument ('command' , help = 'command to show help for' )
0 commit comments