VPP-221 CLI auto-documentation infrastructure
[vpp.git] / doxygen / siphon_generate.py
1 #!/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
20 import os, sys, re, argparse, json
21
22 DEFAULT_OUTPUT = "build-root/docs/siphons"
23 DEFAULT_PREFIX = os.getcwd()
24
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()
33
34 """Patterns that match the start of code blocks we want to siphon"""
35 siphon_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"""
40 siphon_block_start = re.compile("^\s*/\*\?\s*(.*)$")
41
42 """Matches a siphon comment block stop"""
43 siphon_block_stop = re.compile("^(.*)\s*\?\*/\s*$")
44
45 """Siphon block directive delimiter"""
46 siphon_block_delimiter = "%%"
47
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))
51
52 """Matches the start of an initializer block"""
53 siphon_initializer = re.compile("\s*=")
54
55 """
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
59 found.
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
62 current depth
63 """
64 def 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
81 output = {}
82
83 # Pre-process file names in case they indicate a file with
84 # a list of files
85 files = []
86 for 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
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:]
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
307 for 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