Move 2.0 changes collected in branches/pending back to trunk for further
[scons.git] / bin / scons-proc.py
1 #!/usr/bin/env python
2 #
3 # Process a list of Python and/or XML files containing SCons documentation.
4 #
5 # This script creates formatted lists of the Builders, functions, Tools
6 # or construction variables documented in the specified XML files.
7 #
8 # Dependening on the options, the lists are output in either
9 # DocBook-formatted generated XML files containing the summary text
10 # and/or .mod files contining the ENTITY definitions for each item,
11 # or in man-page-formatted output.
12 #
13 import getopt
14 import os.path
15 import re
16 import StringIO
17 import sys
18 import xml.sax
19
20 import SConsDoc
21
22 base_sys_path = [os.getcwd() + '/build/test-tar-gz/lib/scons'] + sys.path
23
24 helpstr = """\
25 Usage: scons-proc.py [--man|--xml]
26                      [-b file(s)] [-f file(s)] [-t file(s)] [-v file(s)]
27                      [infile ...]
28 Options:
29   -b file(s)        dump builder information to the specified file(s)
30   -f file(s)        dump function information to the specified file(s)
31   -t file(s)        dump tool information to the specified file(s)
32   -v file(s)        dump variable information to the specified file(s)
33   --man             print info in man page format, each -[btv] argument
34                     is a single file name
35   --xml             (default) print info in SML format, each -[btv] argument
36                     is a pair of comma-separated .gen,.mod file names
37 """
38
39 opts, args = getopt.getopt(sys.argv[1:],
40                            "b:f:ht:v:",
41                            ['builders=', 'help',
42                             'man', 'xml', 'tools=', 'variables='])
43
44 buildersfiles = None
45 functionsfiles = None
46 output_type = '--xml'
47 toolsfiles = None
48 variablesfiles = None
49
50 for o, a in opts:
51     if o in ['-b', '--builders']:
52         buildersfiles = a
53     elif o in ['-f', '--functions']:
54         functionsfiles = a
55     elif o in ['-h', '--help']:
56         sys.stdout.write(helpstr)
57         sys.exit(0)
58     elif o in ['--man', '--xml']:
59         output_type = o
60     elif o in ['-t', '--tools']:
61         toolsfiles = a
62     elif o in ['-v', '--variables']:
63         variablesfiles = a
64
65 h = SConsDoc.SConsDocHandler()
66 saxparser = xml.sax.make_parser()
67 saxparser.setContentHandler(h)
68 saxparser.setErrorHandler(h)
69
70 xml_preamble = """\
71 <?xml version="1.0"?>
72 <scons_doc>
73 """
74
75 xml_postamble = """\
76 </scons_doc>
77 """
78
79 for f in args:
80     _, ext = os.path.splitext(f)
81     if ext == '.py':
82         dir, _ = os.path.split(f)
83         if dir:
84             sys.path = [dir] + base_sys_path
85         module = SConsDoc.importfile(f)
86         h.set_file_info(f, len(xml_preamble.split('\n')))
87         try:
88             content = module.__scons_doc__
89         except AttributeError:
90             content = None
91         else:
92             del module.__scons_doc__
93     else:
94         h.set_file_info(f, len(xml_preamble.split('\n')))
95         content = open(f).read()
96     if content:
97         content = content.replace('&', '&amp;')
98         # Strip newlines after comments so they don't turn into
99         # spurious paragraph separators.
100         content = content.replace('-->\n', '-->')
101         input = xml_preamble + content + xml_postamble
102         try:
103             saxparser.parse(StringIO.StringIO(input))
104         except:
105             sys.stderr.write("error in %s\n" % f)
106             raise
107
108 Warning = """\
109 <!--
110 THIS IS AN AUTOMATICALLY-GENERATED FILE.  DO NOT EDIT.
111 -->
112 """
113
114 Regular_Entities_Header = """\
115 <!--
116
117   Regular %s entities.
118
119 -->
120 """
121
122 Link_Entities_Header = """\
123 <!--
124
125   Entities that are links to the %s entries in the appendix.
126
127 -->
128 """
129
130 class SCons_XML:
131     def __init__(self, entries, **kw):
132         self.values = entries
133         for k, v in kw.items():
134             setattr(self, k, v)
135     def fopen(self, name):
136         if name == '-':
137             return sys.stdout
138         return open(name, 'w')
139
140 class SCons_XML_to_XML(SCons_XML):
141     def write(self, files):
142         gen, mod = files.split(',')
143         g.write_gen(gen)
144         g.write_mod(mod)
145     def write_gen(self, filename):
146         if not filename:
147             return
148         f = self.fopen(filename)
149         for v in self.values:
150             f.write('\n<varlistentry id="%s%s">\n' %
151                         (v.prefix, v.idfunc()))
152             for term in v.termfunc():
153                 f.write('<term><%s>%s</%s></term>\n' %
154                         (v.tag, term, v.tag))
155             f.write('<listitem>\n')
156             for chunk in v.summary.body:
157                 f.write(str(chunk))
158             if v.sets:
159                 s = ['&cv-link-%s;' % x for x in v.sets]
160                 f.write('<para>\n')
161                 f.write('Sets:  ' + ', '.join(s) + '.\n')
162                 f.write('</para>\n')
163             if v.uses:
164                 u = ['&cv-link-%s;' % x for x in v.uses]
165                 f.write('<para>\n')
166                 f.write('Uses:  ' + ', '.join(u) + '.\n')
167                 f.write('</para>\n')
168             f.write('</listitem>\n')
169             f.write('</varlistentry>\n')
170     def write_mod(self, filename):
171         description = self.values[0].description
172         if not filename:
173             return
174         f = self.fopen(filename)
175         f.write(Warning)
176         f.write('\n')
177         f.write(Regular_Entities_Header % description)
178         f.write('\n')
179         for v in self.values:
180             f.write('<!ENTITY %s%s "<%s>%s</%s>">\n' %
181                         (v.prefix, v.idfunc(),
182                          v.tag, v.entityfunc(), v.tag))
183         if self.env_signatures:
184             f.write('\n')
185             for v in self.values:
186                 f.write('<!ENTITY %senv-%s "<%s>env.%s</%s>">\n' %
187                             (v.prefix, v.idfunc(),
188                              v.tag, v.entityfunc(), v.tag))
189         f.write('\n')
190         f.write(Warning)
191         f.write('\n')
192         f.write(Link_Entities_Header % description)
193         f.write('\n')
194         for v in self.values:
195             f.write('<!ENTITY %slink-%s \'<link linkend="%s%s"><%s>%s</%s></link>\'>\n' %
196                         (v.prefix, v.idfunc(),
197                          v.prefix, v.idfunc(),
198                          v.tag, v.entityfunc(), v.tag))
199         if self.env_signatures:
200             f.write('\n')
201             for v in self.values:
202                 f.write('<!ENTITY %slink-env-%s \'<link linkend="%s%s"><%s>env.%s</%s></link>\'>\n' %
203                             (v.prefix, v.idfunc(),
204                              v.prefix, v.idfunc(),
205                              v.tag, v.entityfunc(), v.tag))
206         f.write('\n')
207         f.write(Warning)
208
209 class SCons_XML_to_man(SCons_XML):
210     def write(self, filename):
211         if not filename:
212             return
213         f = self.fopen(filename)
214         chunks = []
215         for v in self.values:
216             chunks.extend(v.mansep())
217             chunks.extend(v.initial_chunks())
218             chunks.extend(list(map(str, v.summary.body)))
219
220         body = ''.join(chunks)
221         body = body.replace('<programlisting>', '.ES')
222         body = body.replace('</programlisting>', '.EE')
223         body = body.replace('\n</para>\n<para>\n', '\n\n')
224         body = body.replace('<para>\n', '')
225         body = body.replace('<para>', '\n')
226         body = body.replace('</para>\n', '')
227
228         body = string.replace(body, '<variablelist>\n', '.RS 10\n')
229         # Handling <varlistentry> needs to be rationalized and made
230         # consistent.  Right now, the <term> values map to arbitrary,
231         # ad-hoc idioms in the current man page.
232         body = re.compile(r'<varlistentry>\n<term><literal>([^<]*)</literal></term>\n<listitem>\n').sub(r'.TP 6\n.B \1\n', body)
233         body = re.compile(r'<varlistentry>\n<term><parameter>([^<]*)</parameter></term>\n<listitem>\n').sub(r'.IP \1\n', body)
234         body = re.compile(r'<varlistentry>\n<term>([^<]*)</term>\n<listitem>\n').sub(r'.HP 6\n.B \1\n', body)
235         body = string.replace(body, '</listitem>\n', '')
236         body = string.replace(body, '</varlistentry>\n', '')
237         body = string.replace(body, '</variablelist>\n', '.RE\n')
238
239         body = re.sub(r'\.EE\n\n+(?!\.IP)', '.EE\n.IP\n', body)
240         body = string.replace(body, '\n.IP\n\'\\"', '\n\n\'\\"')
241         body = re.sub('&(scons|SConstruct|SConscript|jar|Make|lambda);', r'\\fB\1\\fP', body)
242         body = re.sub('&(TARGET|TARGETS|SOURCE|SOURCES);', r'\\fB$\1\\fP', body)
243         body = body.replace('&Dir;', r'\fBDir\fP')
244         body = body.replace('&target;', r'\fItarget\fP')
245         body = body.replace('&source;', r'\fIsource\fP')
246         body = re.sub('&b(-link)?-([^;]*);', r'\\fB\2\\fP()', body)
247         body = re.sub('&cv(-link)?-([^;]*);', r'$\2', body)
248         body = re.sub('&f(-link)?-env-([^;]*);', r'\\fBenv.\2\\fP()', body)
249         body = re.sub('&f(-link)?-([^;]*);', r'\\fB\2\\fP()', body)
250         body = re.sub(r'<(application|command|envar|filename|function|literal|option)>([^<]*)</\1>',
251                       r'\\fB\2\\fP', body)
252         body = re.sub(r'<(classname|emphasis|varname)>([^<]*)</\1>',
253                       r'\\fI\2\\fP', body)
254         body = re.compile(r'^\\f([BI])([^\\]* [^\\]*)\\fP\s*$', re.M).sub(r'.\1 "\2"', body)
255         body = re.compile(r'^\\f([BI])(.*)\\fP\s*$', re.M).sub(r'.\1 \2', body)
256         body = re.compile(r'^\\f([BI])(.*)\\fP(\S+)$', re.M).sub(r'.\1R \2 \3', body)
257         body = re.compile(r'^(\S+)\\f([BI])(.*)\\fP$', re.M).sub(r'.R\2 \1 \3', body)
258         body = re.compile(r'^(\S+)\\f([BI])(.*)\\fP([^\s\\]+)$', re.M).sub(r'.R\2 \1 \3 \4', body)
259         body = re.compile(r'^(\.R[BI].*[\S])\s+$;', re.M).sub(r'\1', body)
260         body = body.replace('&lt;', '<')
261         body = body.replace('&gt;', '>')
262         body = re.sub(r'\\([^f])', r'\\\\\1', body)
263         body = re.compile("^'\\\\\\\\", re.M).sub("'\\\\", body)
264         body = re.compile(r'^\.([BI]R?) --', re.M).sub(r'.\1 \-\-', body)
265         body = re.compile(r'^\.([BI]R?) -', re.M).sub(r'.\1 \-', body)
266         body = re.compile(r'^\.([BI]R?) (\S+)\\\\(\S+)$', re.M).sub(r'.\1 "\2\\\\\\\\\2"', body)
267         body = re.compile(r'\\f([BI])-', re.M).sub(r'\\f\1\-', body)
268         f.write(body)
269
270 class Proxy:
271     def __init__(self, subject):
272         """Wrap an object as a Proxy object"""
273         self.__subject = subject
274
275     def __getattr__(self, name):
276         """Retrieve an attribute from the wrapped object.  If the named
277            attribute doesn't exist, AttributeError is raised"""
278         return getattr(self.__subject, name)
279
280     def get(self):
281         """Retrieve the entire wrapped object"""
282         return self.__subject
283
284     def __cmp__(self, other):
285         if issubclass(other.__class__, self.__subject.__class__):
286             return cmp(self.__subject, other)
287         return cmp(self.__dict__, other.__dict__)
288
289 class Builder(Proxy):
290     description = 'builder'
291     prefix = 'b-'
292     tag = 'function'
293     def idfunc(self):
294         return self.name
295     def termfunc(self):
296         return ['%s()' % self.name, 'env.%s()' % self.name]
297     def entityfunc(self):
298         return self.name
299     def mansep(self):
300         return ['\n', "'\\" + '"'*69 + '\n']
301     def initial_chunks(self):
302         return [ '.IP %s\n' % t for t in self.termfunc() ]
303
304 class Function(Proxy):
305     description = 'function'
306     prefix = 'f-'
307     tag = 'function'
308     def idfunc(self):
309         return self.name
310     def termfunc(self):
311         return ['%s()' % self.name, 'env.%s()' % self.name]
312     def entityfunc(self):
313         return self.name
314     def mansep(self):
315         return ['\n', "'\\" + '"'*69 + '\n']
316     def initial_chunks(self):
317         try:
318             arguments = self.arguments
319         except AttributeError:
320             arguments = ['()']
321         result = []
322         for arg in arguments:
323             try:
324                 signature = arg.signature
325             except AttributeError:
326                 signature = "both"
327             if signature in ('both', 'global'):
328                 result.append('.TP\n.RI %s%s\n' % (self.name, arg))
329             if signature in ('both', 'env'):
330                 result.append('.TP\n.IR env .%s%s\n' % (self.name, arg))
331         return result
332
333 class Tool(Proxy):
334     description = 'tool'
335     prefix = 't-'
336     tag = 'literal'
337     def idfunc(self):
338         return string.replace(self.name, '+', 'X')
339     def termfunc(self):
340         return [self.name]
341     def entityfunc(self):
342         return self.name
343     def mansep(self):
344         return ['\n']
345     def initial_chunks(self):
346         return ['.IP %s\n' % self.name]
347
348 class Variable(Proxy):
349     description = 'construction variable'
350     prefix = 'cv-'
351     tag = 'envar'
352     def idfunc(self):
353         return self.name
354     def termfunc(self):
355         return [self.name]
356     def entityfunc(self):
357         return '$' + self.name
358     def mansep(self):
359         return ['\n']
360     def initial_chunks(self):
361         return ['.IP %s\n' % self.name]
362
363 if output_type == '--man':
364     processor_class = SCons_XML_to_man
365 elif output_type == '--xml':
366     processor_class = SCons_XML_to_XML
367 else:
368     sys.stderr.write("Unknown output type '%s'\n" % output_type)
369     sys.exit(1)
370
371 if buildersfiles:
372     g = processor_class([ Builder(b) for b in sorted(h.builders.values()) ],
373                         env_signatures=True)
374     g.write(buildersfiles)
375
376 if functionsfiles:
377     g = processor_class([ Function(b) for b in sorted(h.functions.values()) ],
378                         env_signatures=True)
379     g.write(functionsfiles)
380
381 if toolsfiles:
382     g = processor_class([ Tool(t) for t in sorted(h.tools.values()) ],
383                         env_signatures=False)
384     g.write(toolsfiles)
385
386 if variablesfiles:
387     g = processor_class([ Variable(v) for v in sorted(h.cvars.values()) ],
388                         env_signatures=False)
389     g.write(variablesfiles)
390
391 # Local Variables:
392 # tab-width:4
393 # indent-tabs-mode:nil
394 # End:
395 # vim: set expandtab tabstop=4 shiftwidth=4: