2 # Copyright (c) 2016 Comcast Cable Communications Management, LLC.
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:
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
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.
20 import os, sys, re, argparse, json
22 DEFAULT_OUTPUT = "build-root/docs/siphons"
23 DEFAULT_PREFIX = os.getcwd()
25 ap = argparse.ArgumentParser()
26 ap.add_argument("--output", '-o', metavar="directory", default=DEFAULT_OUTPUT,
27 help="Output directory for .siphon files [%s]" % DEFAULT_OUTPUT)
28 ap.add_argument("--input-prefix", metavar="path", default=DEFAULT_PREFIX,
29 help="Prefix to strip from input pathnames [%s]" % DEFAULT_PREFIX)
30 ap.add_argument("input", nargs='+', metavar="input_file",
31 help="Input C source files")
32 args = ap.parse_args()
34 """Patterns that match the start of code blocks we want to siphon"""
36 ( re.compile("(?P<m>VLIB_CLI_COMMAND)\s*[(](?P<name>[a-zA-Z0-9_]+)(,[^)]*)?[)]"), "clicmd" ),
39 """Matches a siphon comment block start"""
40 siphon_block_start = re.compile("^\s*/\*\?\s*(.*)$")
42 """Matches a siphon comment block stop"""
43 siphon_block_stop = re.compile("^(.*)\s*\?\*/\s*$")
45 """Siphon block directive delimiter"""
46 siphon_block_delimiter = "%%"
48 """Matches a siphon block directive such as '%clicmd:group_label Debug CLI%'"""
49 siphon_block_directive = re.compile("(%s)\s*([a-zA-Z0-9_:]+)\s+(.*)\s*(%s)" % \
50 (siphon_block_delimiter, siphon_block_delimiter))
52 """Matches the start of an initializer block"""
53 siphon_initializer = re.compile("\s*=")
56 count open and close braces in str
57 return (0, index) when braces were found and count becomes 0.
58 index indicates the position at which the last closing brace was
60 return (-1, -1) if a closing brace is found before any opening one.
61 return (count, -1) if not all opening braces are closed, count is the
64 def count_braces(str, count=0, found=False):
65 for index in range(0, len(str)):
69 elif str[index] == '}':
71 # means we never found an open brace
75 if count == 0 and found:
80 # Collated output for each siphon
83 # Pre-process file names in case they indicate a file with
86 for filename in args.input:
87 if filename.startswith('@'):
88 with open(filename[1:], 'r') as fp:
89 lines = fp.readlines()
91 files.append(line.strip())
94 files.append(filename)
96 # Iterate all the input files we've been given
97 for 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:]
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:]
114 # Open the file and explore its contents...
115 sys.stderr.write("Siphoning from %s...\n" % filename)
117 with open(filename) as fd:
127 str = line[:-1] # filter \n
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)
134 v = m.group(3).strip()
136 # Return only the parts we did not match
137 return str[0:m.start(1)] + str[m.end(4):]
141 def process_block_prefix(str):
142 if str.startswith(" * "):
149 # See if the line contains the start of a siphon doc block
150 m = siphon_block_start.search(str)
155 # Now check if the block closes on the same line
156 m = siphon_block_stop.search(t)
161 # Check for directives
162 t = process_block_directive(t, directives)
164 # Filter for normal comment prefixes
165 t = process_block_prefix(t)
174 # Check to see if we have an end block marker
175 m = siphon_block_stop.search(str)
182 # Check for directives
183 t = process_block_directive(t, directives)
185 # Filter for normal comment prefixes
186 t = process_block_prefix(t)
189 siphon_block += t + "\n"
196 # Look for blocks we need to siphon
197 for p in siphon_patterns:
199 siphon = [ p[1], str + "\n", 0 ]
200 siphon_line = line_num
202 # see if we have an initializer
203 m = siphon_initializer.search(str)
205 # count the braces on this line
206 (count, index) = count_braces(str[m.start():])
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
213 # close_siphon = siphon
216 # no initializer: close the siphon right now
217 close_siphon = siphon
220 # See if we should end the siphon here - do we have balanced
222 (count, index) = count_braces(str, count=siphon[2], found=True)
224 # braces balanced - add the substring and close the siphon
225 siphon[1] += str[:index+1] + ";\n"
226 close_siphon = siphon
229 # add the whole string, move on
231 siphon[1] += str + "\n"
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] = {
240 "file": "%s/%s.siphon" % (args.output, close_siphon[0])
243 # Copy directives for the file
245 for key in directives:
247 (sn, label) = key.split(":")
248 if sn == siphon_name:
249 details[label] = directives[key]
251 details[key] = directives[key]
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()
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']
265 details['group'] = directory
267 if "group_label" not in details:
268 details['group_label'] = details['group']
270 details["block"] = close_siphon[1]
273 output[siphon_name]['items'].append(details)
280 for key in directives.keys():
284 if filename.endswith("/dir.dox"):
285 # very special! use the parent directory name
290 (sn, label) = key.split(":")
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'] = []
303 output[sn]['global'][l][label] = directives[key]
307 for siphon in output.keys():
308 sys.stderr.write("Saving siphon %s...\n" % siphon)
310 with open(s['file'], "a") as fp:
311 json.dump(s, fp, separators=(',', ': '), indent=4, sort_keys=True)