blob: 457757b510c9c3f2567ea59dafe3ffbac5ec2aa8 [file] [log] [blame]
Chris Luke54ccf222016-07-25 16:38:11 -04001#!/usr/bin/env python
2# Copyright (c) 2016 Comcast Cable Communications Management, LLC.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at:
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16# Looks for preprocessor macros with struct initializers and siphons them
17# off into another file for later parsing; ostensibly to generate
18# documentation from struct initializer data.
19
20import os, sys, re, argparse, json
21
22DEFAULT_OUTPUT = "build-root/docs/siphons"
23DEFAULT_PREFIX = os.getcwd()
24
25ap = argparse.ArgumentParser()
26ap.add_argument("--output", '-o', metavar="directory", default=DEFAULT_OUTPUT,
27 help="Output directory for .siphon files [%s]" % DEFAULT_OUTPUT)
28ap.add_argument("--input-prefix", metavar="path", default=DEFAULT_PREFIX,
29 help="Prefix to strip from input pathnames [%s]" % DEFAULT_PREFIX)
30ap.add_argument("input", nargs='+', metavar="input_file",
31 help="Input C source files")
32args = ap.parse_args()
33
34"""Patterns that match the start of code blocks we want to siphon"""
35siphon_patterns = [
36 ( re.compile("(?P<m>VLIB_CLI_COMMAND)\s*[(](?P<name>[a-zA-Z0-9_]+)(,[^)]*)?[)]"), "clicmd" ),
37]
38
39"""Matches a siphon comment block start"""
40siphon_block_start = re.compile("^\s*/\*\?\s*(.*)$")
41
42"""Matches a siphon comment block stop"""
43siphon_block_stop = re.compile("^(.*)\s*\?\*/\s*$")
44
45"""Siphon block directive delimiter"""
46siphon_block_delimiter = "%%"
47
48"""Matches a siphon block directive such as '%clicmd:group_label Debug CLI%'"""
49siphon_block_directive = re.compile("(%s)\s*([a-zA-Z0-9_:]+)\s+(.*)\s*(%s)" % \
50 (siphon_block_delimiter, siphon_block_delimiter))
51
52"""Matches the start of an initializer block"""
53siphon_initializer = re.compile("\s*=")
54
55"""
56count open and close braces in str
57return (0, index) when braces were found and count becomes 0.
58index indicates the position at which the last closing brace was
59found.
60return (-1, -1) if a closing brace is found before any opening one.
61return (count, -1) if not all opening braces are closed, count is the
62current depth
63"""
64def count_braces(str, count=0, found=False):
65 for index in range(0, len(str)):
66 if str[index] == '{':
67 count += 1;
68 found = True
69 elif str[index] == '}':
70 if count == 0:
71 # means we never found an open brace
72 return (-1, -1)
73 count -= 1;
74
75 if count == 0 and found:
76 return (count, index)
77
78 return (count, -1)
79
80# Collated output for each siphon
81output = {}
82
83# Pre-process file names in case they indicate a file with
84# a list of files
85files = []
86for filename in args.input:
87 if filename.startswith('@'):
88 with open(filename[1:], 'r') as fp:
89 lines = fp.readlines()
90 for line in lines:
91 files.append(line.strip())
92 lines = None
93 else:
94 files.append(filename)
95
96# Iterate all the input files we've been given
97for filename in files:
98 # Strip the current directory off the start of the
99 # filename for brevity
100 if filename[0:len(args.input_prefix)] == args.input_prefix:
101 filename = filename[len(args.input_prefix):]
102 if filename[0] == "/":
103 filename = filename[1:]
104
105 # Work out the abbreviated directory name
106 directory = os.path.dirname(filename)
107 if directory[0:2] == "./":
108 directory = directory[2:]
109 elif directory[0:len(args.input_prefix)] == args.input_prefix:
110 directory = directory[len(args.input_prefix):]
111 if directory[0] == "/":
112 directory = directory[1:]
113
114 # Open the file and explore its contents...
115 sys.stderr.write("Siphoning from %s...\n" % filename)
116 directives = {}
117 with open(filename) as fd:
118 siphon = None
119 close_siphon = None
120 siphon_block = ""
121 in_block = False
122 line_num = 0
123 siphon_line = 0
124
125 for line in fd:
126 line_num += 1
127 str = line[:-1] # filter \n
128
129 """See if there is a block directive and if so extract it"""
130 def process_block_directive(str, directives):
131 m = siphon_block_directive.search(str)
132 if m is not None:
133 k = m.group(2)
134 v = m.group(3).strip()
135 directives[k] = v
136 # Return only the parts we did not match
137 return str[0:m.start(1)] + str[m.end(4):]
138
139 return str
140
141 def process_block_prefix(str):
142 if str.startswith(" * "):
143 str = str[3:]
144 elif str == " *":
145 str = ""
146 return str
147
148 if not in_block:
149 # See if the line contains the start of a siphon doc block
150 m = siphon_block_start.search(str)
151 if m is not None:
152 in_block = True
153 t = m.group(1)
154
155 # Now check if the block closes on the same line
156 m = siphon_block_stop.search(t)
157 if m is not None:
158 t = m.group(1)
159 in_block = False
160
161 # Check for directives
162 t = process_block_directive(t, directives)
163
164 # Filter for normal comment prefixes
165 t = process_block_prefix(t)
166
167 # Add what is left
168 siphon_block += t
169
170 # Skip to next line
171 continue
172
173 else:
174 # Check to see if we have an end block marker
175 m = siphon_block_stop.search(str)
176 if m is not None:
177 in_block = False
178 t = m.group(1)
179 else:
180 t = str
181
182 # Check for directives
183 t = process_block_directive(t, directives)
184
185 # Filter for normal comment prefixes
186 t = process_block_prefix(t)
187
188 # Add what is left
189 siphon_block += t + "\n"
190
191 # Skip to next line
192 continue
193
194
195 if siphon is None:
196 # Look for blocks we need to siphon
197 for p in siphon_patterns:
198 if p[0].match(str):
199 siphon = [ p[1], str + "\n", 0 ]
200 siphon_line = line_num
201
202 # see if we have an initializer
203 m = siphon_initializer.search(str)
204 if m is not None:
205 # count the braces on this line
206 (count, index) = count_braces(str[m.start():])
207 siphon[2] = count
208 # TODO - it's possible we have the initializer all on the first line
209 # we should check for it, but also account for the possibility that
210 # the open brace is on the next line
211 #if count == 0:
212 # # braces balanced
213 # close_siphon = siphon
214 # siphon = None
215 else:
216 # no initializer: close the siphon right now
217 close_siphon = siphon
218 siphon = None
219 else:
220 # See if we should end the siphon here - do we have balanced
221 # braces?
222 (count, index) = count_braces(str, count=siphon[2], found=True)
223 if count == 0:
224 # braces balanced - add the substring and close the siphon
225 siphon[1] += str[:index+1] + ";\n"
226 close_siphon = siphon
227 siphon = None
228 else:
229 # add the whole string, move on
230 siphon[2] = count
231 siphon[1] += str + "\n"
232
233 if close_siphon is not None:
234 # Write the siphoned contents to the right place
235 siphon_name = close_siphon[0]
236 if siphon_name not in output:
237 output[siphon_name] = {
238 "global": {},
239 "items": [],
240 "file": "%s/%s.siphon" % (args.output, close_siphon[0])
241 }
242
243 # Copy directives for the file
244 details = {}
245 for key in directives:
246 if ":" in key:
247 (sn, label) = key.split(":")
248 if sn == siphon_name:
249 details[label] = directives[key]
250 else:
251 details[key] = directives[key]
252
253 # Copy details for this block
254 details['file'] = filename
255 details['line_start'] = siphon_line
256 details['line_end'] = line_num
257 details['siphon_block'] = siphon_block.strip()
258
259 # Some defaults
260 if "group" not in details:
261 if "group_label" in details:
262 # use the filename since group labels are mostly of file scope
263 details['group'] = details['file']
264 else:
265 details['group'] = directory
266
267 if "group_label" not in details:
268 details['group_label'] = details['group']
269
270 details["block"] = close_siphon[1]
271
272 # Store the item
273 output[siphon_name]['items'].append(details)
274
275 # All done
276 close_siphon = None
277 siphon_block = ""
278
279 # Update globals
280 for key in directives.keys():
281 if ':' not in key:
282 continue
283
284 if filename.endswith("/dir.dox"):
285 # very special! use the parent directory name
286 l = directory
287 else:
288 l = filename
289
290 (sn, label) = key.split(":")
291
292 if sn not in output:
293 output[sn] = {}
294 if 'global' not in output[sn]:
295 output[sn]['global'] = {}
296 if l not in output[sn]['global']:
297 output[sn]['global'][l] = {}
298 if 'file' not in output[sn]:
299 output[sn]['file'] = "%s/%s.siphon" % (args.output, sn)
300 if 'items' not in output[sn]:
301 output[sn]['items'] = []
302
303 output[sn]['global'][l][label] = directives[key]
304
305
306# Write out the data
307for siphon in output.keys():
308 sys.stderr.write("Saving siphon %s...\n" % siphon)
309 s = output[siphon]
310 with open(s['file'], "a") as fp:
311 json.dump(s, fp, separators=(',', ': '), indent=4, sort_keys=True)
312
313# All done