Don't specify program name in passive connection man page fragments.
[sliver-openvswitch.git] / ovsdb / ovsdb-doc.in
1 #! @PYTHON@
2
3 from datetime import date
4 import getopt
5 import os
6 import re
7 import sys
8 import xml.dom.minidom
9
10 import ovs.json
11 from ovs.db import error
12 import ovs.db.schema
13
14 argv0 = sys.argv[0]
15
16 def textToNroff(s, font=r'\fR'):
17     def escape(match):
18         c = match.group(0)
19         if c == '-':
20             if font == r'\fB':
21                 return r'\-'
22             else:
23                 return '-'
24         if c == '\\':
25             return r'\e'
26         elif c == '"':
27             return r'\(dq'
28         elif c == "'":
29             return r'\(cq'
30         else:
31             raise error.Error("bad escape")
32
33     # Escape - \ " ' as needed by nroff.
34     s = re.sub('([-"\'\\\\])', escape, s)
35     if s.startswith('.'):
36         s = '\\' + s
37     return s
38
39 def escapeNroffLiteral(s):
40     return r'\fB%s\fR' % textToNroff(s, r'\fB')
41
42 def inlineXmlToNroff(node, font):
43     if node.nodeType == node.TEXT_NODE:
44         return textToNroff(node.data, font)
45     elif node.nodeType == node.ELEMENT_NODE:
46         if node.tagName in ['code', 'em', 'option']:
47             s = r'\fB'
48             for child in node.childNodes:
49                 s += inlineXmlToNroff(child, r'\fB')
50             return s + font
51         elif node.tagName == 'ref':
52             s = r'\fB'
53             if node.hasAttribute('column'):
54                 s += node.attributes['column'].nodeValue
55                 if node.hasAttribute('key'):
56                     s += ':' + node.attributes['key'].nodeValue
57             elif node.hasAttribute('table'):
58                 s += node.attributes['table'].nodeValue
59             elif node.hasAttribute('group'):
60                 s += node.attributes['group'].nodeValue
61             else:
62                 raise error.Error("'ref' lacks required attributes: %s" % node.attributes.keys())
63             return s + font
64         elif node.tagName == 'var':
65             s = r'\fI'
66             for child in node.childNodes:
67                 s += inlineXmlToNroff(child, r'\fI')
68             return s + font
69         else:
70             raise error.Error("element <%s> unknown or invalid here" % node.tagName)
71     else:
72         raise error.Error("unknown node %s in inline xml" % node)
73
74 def blockXmlToNroff(nodes, para='.PP'):
75     s = ''
76     for node in nodes:
77         if node.nodeType == node.TEXT_NODE:
78             s += textToNroff(node.data)
79             s = s.lstrip()
80         elif node.nodeType == node.ELEMENT_NODE:
81             if node.tagName in ['ul', 'ol']:
82                 if s != "":
83                     s += "\n"
84                 s += ".RS\n"
85                 i = 0
86                 for liNode in node.childNodes:
87                     if (liNode.nodeType == node.ELEMENT_NODE
88                         and liNode.tagName == 'li'):
89                         i += 1
90                         if node.tagName == 'ul':
91                             s += ".IP \\(bu\n"
92                         else:
93                             s += ".IP %d. .25in\n" % i
94                         s += blockXmlToNroff(liNode.childNodes, ".IP")
95                     elif (liNode.nodeType != node.TEXT_NODE
96                           or not liNode.data.isspace()):
97                         raise error.Error("<%s> element may only have <li> children" % node.tagName)
98                 s += ".RE\n"
99             elif node.tagName == 'dl':
100                 if s != "":
101                     s += "\n"
102                 s += ".RS\n"
103                 prev = "dd"
104                 for liNode in node.childNodes:
105                     if (liNode.nodeType == node.ELEMENT_NODE
106                         and liNode.tagName == 'dt'):
107                         if prev == 'dd':
108                             s += '.TP\n'
109                         else:
110                             s += '.TQ\n'
111                         prev = 'dt'
112                     elif (liNode.nodeType == node.ELEMENT_NODE
113                           and liNode.tagName == 'dd'):
114                         if prev == 'dd':
115                             s += '.IP\n'
116                         prev = 'dd'
117                     elif (liNode.nodeType != node.TEXT_NODE
118                           or not liNode.data.isspace()):
119                         raise error.Error("<dl> element may only have <dt> and <dd> children")
120                     s += blockXmlToNroff(liNode.childNodes, ".IP")
121                 s += ".RE\n"
122             elif node.tagName == 'p':
123                 if s != "":
124                     if not s.endswith("\n"):
125                         s += "\n"
126                     s += para + "\n"
127                 s += blockXmlToNroff(node.childNodes, para)
128             elif node.tagName in ('h1', 'h2', 'h3'):
129                 if s != "":
130                     if not s.endswith("\n"):
131                         s += "\n"
132                 nroffTag = {'h1': 'SH', 'h2': 'SS', 'h3': 'ST'}[node.tagName]
133                 s += ".%s " % nroffTag
134                 for child_node in node.childNodes:
135                     s += inlineXmlToNroff(child_node, r'\fR')
136                 s += "\n"
137             else:
138                 s += inlineXmlToNroff(node, r'\fR')
139         else:
140             raise error.Error("unknown node %s in block xml" % node)
141     if s != "" and not s.endswith('\n'):
142         s += '\n'
143     return s
144
145 def typeAndConstraintsToNroff(column):
146     type = column.type.toEnglish(escapeNroffLiteral)
147     constraints = column.type.constraintsToEnglish(escapeNroffLiteral)
148     if constraints:
149         type += ", " + constraints
150     if column.unique:
151         type += " (must be unique within table)"
152     return type
153
154 def columnGroupToNroff(table, groupXml):
155     introNodes = []
156     columnNodes = []
157     for node in groupXml.childNodes:
158         if (node.nodeType == node.ELEMENT_NODE
159             and node.tagName in ('column', 'group')):
160             columnNodes += [node]
161         else:
162             if (columnNodes
163                 and not (node.nodeType == node.TEXT_NODE
164                          and node.data.isspace())):
165                 raise error.Error("text follows <column> or <group> inside <group>: %s" % node)
166             introNodes += [node]
167
168     summary = []
169     intro = blockXmlToNroff(introNodes)
170     body = ''
171     for node in columnNodes:
172         if node.tagName == 'column':
173             name = node.attributes['name'].nodeValue
174             column = table.columns[name]
175             if node.hasAttribute('key'):
176                 key = node.attributes['key'].nodeValue
177                 if node.hasAttribute('type'):
178                     type_string = node.attributes['type'].nodeValue
179                     type_json = ovs.json.from_string(str(type_string))
180                     if type(type_json) in (str, unicode):
181                         raise error.Error("%s %s:%s has invalid 'type': %s" 
182                                           % (table.name, name, key, type_json))
183                     type_ = ovs.db.types.BaseType.from_json(type_json)
184                 else:
185                     type_ = column.type.value
186
187                 nameNroff = "%s : %s" % (name, key)
188                 typeNroff = "optional %s" % column.type.value.toEnglish()
189                 if (column.type.value.type == ovs.db.types.StringType and
190                     type_.type == ovs.db.types.BooleanType):
191                     # This is a little more explicit and helpful than
192                     # "containing a boolean"
193                     typeNroff += r", either \fBtrue\fR or \fBfalse\fR"
194                 else:
195                     if type_.type != column.type.value.type:
196                         type_english = type_.toEnglish()
197                         if type_english[0] in 'aeiou':
198                             typeNroff += ", containing an %s" % type_english
199                         else:
200                             typeNroff += ", containing a %s" % type_english
201                     constraints = type_.constraintsToEnglish(escapeNroffLiteral)
202                     if constraints:
203                         typeNroff += ", %s" % constraints
204             else:
205                 nameNroff = name
206                 typeNroff = typeAndConstraintsToNroff(column)
207             body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
208             body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
209             summary += [('column', nameNroff, typeNroff)]
210         elif node.tagName == 'group':
211             title = node.attributes["title"].nodeValue
212             subSummary, subIntro, subBody = columnGroupToNroff(table, node)
213             summary += [('group', title, subSummary)]
214             body += '.ST "%s:"\n' % textToNroff(title)
215             body += subIntro + subBody
216         else:
217             raise error.Error("unknown element %s in <table>" % node.tagName)
218     return summary, intro, body
219
220 def tableSummaryToNroff(summary, level=0):
221     s = ""
222     for type, name, arg in summary:
223         if type == 'column':
224             s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
225         else:
226             s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
227             s += tableSummaryToNroff(arg, level + 1)
228             s += ".RE\n"
229     return s
230
231 def tableToNroff(schema, tableXml):
232     tableName = tableXml.attributes['name'].nodeValue
233     table = schema.tables[tableName]
234
235     s = """.bp
236 .SH "%s TABLE"
237 """ % tableName
238     summary, intro, body = columnGroupToNroff(table, tableXml)
239     s += intro
240     s += '.SS "Summary:\n'
241     s += tableSummaryToNroff(summary)
242     s += '.SS "Details:\n'
243     s += body
244     return s
245
246 def docsToNroff(schemaFile, xmlFile, erFile, title=None):
247     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
248     doc = xml.dom.minidom.parse(xmlFile).documentElement
249
250     schemaDate = os.stat(schemaFile).st_mtime
251     xmlDate = os.stat(xmlFile).st_mtime
252     d = date.fromtimestamp(max(schemaDate, xmlDate))
253
254     if title == None:
255         title = schema.name
256
257     # Putting '\" p as the first line tells "man" that the manpage
258     # needs to be preprocessed by "pic".
259     s = r''''\" p
260 .TH %s 5 "%s" "Open vSwitch" "Open vSwitch Manual"
261 .\" -*- nroff -*-
262 .de TQ
263 .  br
264 .  ns
265 .  TP "\\$1"
266 ..
267 .de ST
268 .  PP
269 .  RS -0.15in
270 .  I "\\$1"
271 .  RE
272 ..
273 ''' % (title, d.strftime("%B %Y"))
274
275     s += '.SH "%s DATABASE"\n' % schema.name
276
277     tables = ""
278     introNodes = []
279     tableNodes = []
280     summary = []
281     for dbNode in doc.childNodes:
282         if (dbNode.nodeType == dbNode.ELEMENT_NODE
283             and dbNode.tagName == "table"):
284             tableNodes += [dbNode]
285
286             name = dbNode.attributes['name'].nodeValue
287             if dbNode.hasAttribute("title"):
288                 title = dbNode.attributes['title'].nodeValue
289             else:
290                 title = name + " configuration."
291             summary += [(name, title)]
292         else:
293             introNodes += [dbNode]
294
295     s += blockXmlToNroff(introNodes) + "\n"
296
297     s += r"""
298 .SH "TABLE SUMMARY"
299 .PP
300 The following list summarizes the purpose of each of the tables in the
301 \fB%s\fR database.  Each table is described in more detail on a later
302 page.
303 .IP "Table" 1in
304 Purpose
305 """ % schema.name
306     for name, title in summary:
307         s += r"""
308 .TQ 1in
309 \fB%s\fR
310 %s
311 """ % (name, textToNroff(title))
312
313     if erFile:
314         s += """
315 .\\" check if in troff mode (TTY)
316 .if t \{
317 .bp
318 .SH "TABLE RELATIONSHIPS"
319 .PP
320 The following diagram shows the relationship among tables in the
321 database.  Each node represents a table.  Tables that are part of the
322 ``root set'' are shown with double borders.  Each edge leads from the
323 table that contains it and points to the table that its value
324 represents.  Edges are labeled with their column names, followed by a
325 constraint on the number of allowed values: \\fB?\\fR for zero or one,
326 \\fB*\\fR for zero or more, \\fB+\\fR for one or more.  Thick lines
327 represent strong references; thin lines represent weak references.
328 .RS -1in
329 """
330         erStream = open(erFile, "r")
331         for line in erStream:
332             s += line + '\n'
333         erStream.close()
334         s += ".RE\\}\n"
335
336     for node in tableNodes:
337         s += tableToNroff(schema, node) + "\n"
338     return s
339
340 def usage():
341     print """\
342 %(argv0)s: ovsdb schema documentation generator
343 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
344 usage: %(argv0)s [OPTIONS] SCHEMA XML
345 where SCHEMA is an OVSDB schema in JSON format
346   and XML is OVSDB documentation in XML format.
347
348 The following options are also available:
349   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
350   --title=TITLE               use TITLE as title instead of schema name
351   -h, --help                  display this help message
352   -V, --version               display version information\
353 """ % {'argv0': argv0}
354     sys.exit(0)
355
356 if __name__ == "__main__":
357     try:
358         try:
359             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
360                                               ['er-diagram=', 'title=',
361                                                'help', 'version'])
362         except getopt.GetoptError, geo:
363             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
364             sys.exit(1)
365
366         er_diagram = None
367         title = None
368         for key, value in options:
369             if key == '--er-diagram':
370                 er_diagram = value
371             elif key == '--title':
372                 title = value
373             elif key in ['-h', '--help']:
374                 usage()
375             elif key in ['-V', '--version']:
376                 print "ovsdb-doc (Open vSwitch) @VERSION@"
377             else:
378                 sys.exit(0)
379
380         if len(args) != 2:
381             sys.stderr.write("%s: exactly 2 non-option arguments required "
382                              "(use --help for help)\n" % argv0)
383             sys.exit(1)
384
385         # XXX we should warn about undocumented tables or columns
386         s = docsToNroff(args[0], args[1], er_diagram)
387         for line in s.split("\n"):
388             line = line.strip()
389             if len(line):
390                 print line
391
392     except error.Error, e:
393         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
394         sys.exit(1)
395
396 # Local variables:
397 # mode: python
398 # End: