]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
release: 1.5.27
[lilypond.git] / scripts / lilypond-book.py
1 #!@PYTHON@
2 # vim: set noexpandtab:
3 # TODO:
4 # * junk --outdir for --output 
5 # * Figure out clean set of options.
6 # * 
7 # * EndLilyPondOutput is def'd as vfil. Causes large white gaps.
8 # * texinfo: add support for @pagesize
9
10 # todo: dimension handling (all the x2y) is clumsy. (tca: Thats
11 #       because the values are taken directly from texinfo.tex,
12 #       geometry.sty and article.cls. Give me a hint, and I'll
13 #       fix it.)
14
15 #
16 # TODO: magnification support should also work for texinfo -> html: eg. add as option to dvips. 
17
18
19 # This is was the idea for handling of comments:
20 #       Multiline comments, @ignore .. @end ignore is scanned for
21 #       in read_doc_file, and the chunks are marked as 'ignore', so
22 #       lilypond-book will not touch them any more. The content of the
23 #       chunks are written to the output file. Also 'include' and 'input'
24 #       regex has to check if they are commented out.
25 #
26 #       Then it is scanned for 'lilypond', 'lilypond-file' and 'lilypond-block'.
27 #       These three regex's has to check if they are on a commented line,
28 #       % for latex, @c for texinfo.
29 #
30 #       Then lines that are commented out with % (latex) and @c (Texinfo)
31 #       are put into chunks marked 'ignore'. This cannot be done before
32 #       searching for the lilypond-blocks because % is also the comment character
33 #       for lilypond.
34 #
35 #       The the rest of the rexeces are searched for. They don't have to test
36 #       if they are on a commented out line.
37
38
39
40 import os
41 import stat
42 import string
43 import re
44 import getopt
45 import sys
46 import __main__
47 import operator
48
49
50 program_version = '@TOPLEVEL_VERSION@'
51 if program_version == '@' + 'TOPLEVEL_VERSION' + '@':
52         program_version = '1.5.18'
53
54 #
55 # Try to cater for bad installations of LilyPond, that have
56 # broken TeX setup.  Just hope this doesn't hurt good TeX
57 # setups.  Maybe we should check if kpsewhich can find
58 # feta16.{afm,mf,tex,tfm}, and only set env upon failure.
59 #
60 datadir = '@datadir@'
61 environment = {
62         'MFINPUTS' : datadir + '/mf:',
63         'TEXINPUTS': datadir + '/tex:' + datadir + '/ps:.:',
64         'TFMFONTS' : datadir + '/tfm:',
65         'GS_FONTPATH' : datadir + '/afm:' + datadir + '/pfa',
66         'GS_LIB' : datadir + '/ps',
67 }
68
69 def setup_environment ():
70         for key in environment.keys ():
71                 val = environment[key]
72                 if os.environ.has_key (key):
73                         val = val + os.pathsep + os.environ[key]
74                 os.environ[key] = val
75
76
77
78 include_path = [os.getcwd()]
79
80
81 # g_ is for global (?)
82
83 g_here_dir = os.getcwd ()
84 g_dep_prefix = ''
85 g_outdir = ''
86 g_force_lilypond_fontsize = 0
87 g_read_lys = 0
88 g_do_pictures = 1
89 g_num_cols = 1
90 format = ''
91 g_run_lilypond = 1
92 no_match = 'a\ba'
93
94 default_music_fontsize = 16
95 default_text_fontsize = 12
96 paperguru = None
97
98 # this code is ugly. It should be cleaned
99 class LatexPaper:
100         def __init__(self):
101                 self.m_paperdef =  {
102                         # the dimensions are from geometry.sty
103                         'a0paper': (mm2pt(841), mm2pt(1189)),
104                         'a1paper': (mm2pt(595), mm2pt(841)),
105                         'a2paper': (mm2pt(420), mm2pt(595)),
106                         'a3paper': (mm2pt(297), mm2pt(420)),
107                         'a4paper': (mm2pt(210), mm2pt(297)),
108                         'a5paper': (mm2pt(149), mm2pt(210)),
109                         'b0paper': (mm2pt(1000), mm2pt(1414)),
110                         'b1paper': (mm2pt(707), mm2pt(1000)),
111                         'b2paper': (mm2pt(500), mm2pt(707)),
112                         'b3paper': (mm2pt(353), mm2pt(500)),
113                         'b4paper': (mm2pt(250), mm2pt(353)),
114                         'b5paper': (mm2pt(176), mm2pt(250)),
115                         'letterpaper': (in2pt(8.5), in2pt(11)),
116                         'legalpaper': (in2pt(8.5), in2pt(14)),
117                         'executivepaper': (in2pt(7.25), in2pt(10.5))}
118                 self.m_use_geometry = None
119                 self.m_papersize = 'letterpaper'
120                 self.m_fontsize = 10
121                 self.m_num_cols = 1
122                 self.m_landscape = 0
123                 self.m_geo_landscape = 0
124                 self.m_geo_width = None
125                 self.m_geo_textwidth = None
126                 self.m_geo_lmargin = None
127                 self.m_geo_rmargin = None
128                 self.m_geo_includemp = None
129                 self.m_geo_marginparwidth = {10: 57, 11: 50, 12: 35}
130                 self.m_geo_marginparsep = {10: 11, 11: 10, 12: 10}
131                 self.m_geo_x_marginparwidth = None
132                 self.m_geo_x_marginparsep = None
133                 self.__body = None
134         def set_geo_option(self, name, value):
135
136                 if type(value) == type(""):
137                         m = re.match ("([0-9.]+)(cm|in|pt|mm|em|ex)",value)
138                         if m:
139                                 unit = m.group (2)
140                                 num = string.atof(m.group (1))
141                                 conv =  dimension_conversion_dict[m.group(2)]
142
143                                 value = conv(num)
144
145                 if name == 'body' or name == 'text':
146                         if type(value) == type(""):
147                                 self.m_geo_textwidth =  value
148                         else:
149                                 self.m_geo_textwidth =  value[0]
150                         self.__body = 1
151                 elif name == 'portrait':
152                         self.m_geo_landscape = 0
153                 elif name == 'reversemp' or name == 'reversemarginpar':
154                         if self.m_geo_includemp == None:
155                                 self.m_geo_includemp = 1
156                 elif name == 'marginparwidth' or name == 'marginpar':
157                         self.m_geo_x_marginparwidth =  value
158                         self.m_geo_includemp = 1
159                 elif name == 'marginparsep':
160                         self.m_geo_x_marginparsep =  value
161                         self.m_geo_includemp = 1
162                 elif name == 'scale':
163                         if type(value) == type(""):
164                                 self.m_geo_width = self.get_paperwidth() * float(value)
165                         else:
166                                 self.m_geo_width = self.get_paperwidth() * float(value[0])
167                 elif name == 'hscale':
168                         self.m_geo_width = self.get_paperwidth() * float(value)
169                 elif name == 'left' or name == 'lmargin':
170                         self.m_geo_lmargin =  value
171                 elif name == 'right' or name == 'rmargin':
172                         self.m_geo_rmargin =  value
173                 elif name == 'hdivide' or name == 'divide':
174                         if value[0] not in ('*', ''):
175                                 self.m_geo_lmargin =  value[0]
176                         if value[1] not in ('*', ''):
177                                 self.m_geo_width =  value[1]
178                         if value[2] not in ('*', ''):
179                                 self.m_geo_rmargin =  value[2]
180                 elif name == 'hmargin':
181                         if type(value) == type(""):
182                                 self.m_geo_lmargin =  value
183                                 self.m_geo_rmargin =  value
184                         else:
185                                 self.m_geo_lmargin =  value[0]
186                                 self.m_geo_rmargin =  value[1]
187                 elif name == 'margin':#ugh there is a bug about this option in
188                                         # the geometry documentation
189                         if type(value) == type(""):
190                                 self.m_geo_lmargin =  value
191                                 self.m_geo_rmargin =  value
192                         else:
193                                 self.m_geo_lmargin =  value[0]
194                                 self.m_geo_rmargin =  value[0]
195                 elif name == 'total':
196                         if type(value) == type(""):
197                                 self.m_geo_width =  value
198                         else:
199                                 self.m_geo_width =  value[0]
200                 elif name == 'width' or name == 'totalwidth':
201                         self.m_geo_width =  value
202                 elif name == 'paper' or name == 'papername':
203                         self.m_papersize = value
204                 elif name[-5:] == 'paper':
205                         self.m_papersize = name
206                 else:
207                         pass 
208                         # what is _set_dimen ?? /MB
209                         #self._set_dimen('m_geo_'+name, value)
210         def __setattr__(self, name, value):
211                 if type(value) == type("") and \
212                    dimension_conversion_dict.has_key (value[-2:]):
213                         f = dimension_conversion_dict[value[-2:]]
214                         self.__dict__[name] = f(float(value[:-2]))
215                 else:
216                         self.__dict__[name] = value
217                         
218         def __str__(self):
219                 s =  "LatexPaper:\n-----------"
220                 for v in self.__dict__.keys():
221                         if v[:2] == 'm_':
222                                 s = s +  str (v) + ' ' + str (self.__dict__[v])
223                 s = s +  "-----------"
224                 return s
225         
226         def get_linewidth(self):
227                 w = self._calc_linewidth()
228                 if self.m_num_cols == 2:
229                         return (w - 10) / 2
230                 else:
231                         return w
232         def get_paperwidth(self):
233                 #if self.m_use_geometry:
234                         return self.m_paperdef[self.m_papersize][self.m_landscape or self.m_geo_landscape]
235                 #return self.m_paperdef[self.m_papersize][self.m_landscape]
236         
237         def _calc_linewidth(self):
238                 # since geometry sometimes ignores 'includemp', this is
239                 # more complicated than it should be
240                 mp = 0
241                 if self.m_geo_includemp:
242                         if self.m_geo_x_marginparsep is not None:
243                                 mp = mp + self.m_geo_x_marginparsep
244                         else:
245                                 mp = mp + self.m_geo_marginparsep[self.m_fontsize]
246                         if self.m_geo_x_marginparwidth is not None:
247                                 mp = mp + self.m_geo_x_marginparwidth
248                         else:
249                                 mp = mp + self.m_geo_marginparwidth[self.m_fontsize]
250
251                 #ugh test if this is necessary                          
252                 if self.__body:
253                         mp = 0
254
255                 if not self.m_use_geometry:
256                         return latex_linewidths[self.m_papersize][self.m_fontsize]
257                 else:
258                         geo_opts = (self.m_geo_lmargin == None,
259                                     self.m_geo_width == None,
260                                     self.m_geo_rmargin == None)
261
262                         if geo_opts == (1, 1, 1):
263                                 if self.m_geo_textwidth:
264                                         return self.m_geo_textwidth
265                                 w = self.get_paperwidth() * 0.8
266                                 return w - mp
267                         elif geo_opts == (0, 1, 1):
268                                  if self.m_geo_textwidth:
269                                         return self.m_geo_textwidth
270                                  return self.f1(self.m_geo_lmargin, mp)
271                         elif geo_opts == (1, 1, 0):
272                                  if self.m_geo_textwidth:
273                                         return self.m_geo_textwidth
274                                  return self.f1(self.m_geo_rmargin, mp)
275                         elif geo_opts \
276                                         in ((0, 0, 1), (1, 0, 0), (1, 0, 1)):
277                                 if self.m_geo_textwidth:
278                                         return self.m_geo_textwidth
279                                 return self.m_geo_width - mp
280                         elif geo_opts in ((0, 1, 0), (0, 0, 0)):
281                                 w = self.get_paperwidth() \
282                                   - self.m_geo_lmargin - self.m_geo_rmargin - mp
283                                 if w < 0:
284                                         w = 0
285                                 return w
286                         raise "Never do this!"
287         def f1(self, m, mp):
288                 tmp = self.get_paperwidth() - m * 2 - mp
289                 if tmp < 0:
290                         tmp = 0
291                 return tmp
292         def f2(self):
293                 tmp = self.get_paperwidth() - self.m_geo_lmargin \
294                         - self.m_geo_rmargin
295                 if tmp < 0:
296                         return 0
297                 return tmp
298
299 class TexiPaper:
300         def __init__(self):
301                 self.m_papersize = 'letterpaper'
302                 self.m_fontsize = 12
303         def get_linewidth(self):
304                 return texi_linewidths[self.m_papersize][self.m_fontsize]
305
306 def mm2pt(x):
307         return x * 2.8452756
308 def in2pt(x):
309         return x * 72.26999
310 def em2pt(x, fontsize = 10):
311         return {10: 10.00002, 11: 10.8448, 12: 11.74988}[fontsize] * x
312 def ex2pt(x, fontsize = 10):
313         return {10: 4.30554, 11: 4.7146, 12: 5.16667}[fontsize] * x
314
315 def pt2pt(x):
316         return x
317
318 dimension_conversion_dict ={
319         'mm': mm2pt,
320         'cm': lambda x: mm2pt(10*x),
321         'in': in2pt,
322         'em': em2pt,
323         'ex': ex2pt,
324         'pt': pt2pt
325         }
326
327         
328 # latex linewidths:
329 # indices are no. of columns, papersize,  fontsize
330 # Why can't this be calculated?
331 latex_linewidths = {
332         'a4paper':{10: 345, 11: 360, 12: 390},
333         'a4paper-landscape': {10: 598, 11: 596, 12:592},
334         'a5paper':{10: 276, 11: 276, 12: 276},
335         'b5paper':{10: 345, 11: 356, 12: 356},
336         'letterpaper':{10: 345, 11: 360, 12: 390},
337         'letterpaper-landscape':{10: 598, 11: 596, 12:596},
338         'legalpaper': {10: 345, 11: 360, 12: 390},
339         'executivepaper':{10: 345, 11: 360, 12: 379}}
340
341 texi_linewidths = {
342         'afourpaper': {12: mm2pt(160)},
343         'afourwide': {12: in2pt(6.5)},
344         'afourlatex': {12: mm2pt(150)},
345         'smallbook': {12: in2pt(5)},
346         'letterpaper': {12: in2pt(6)}}
347
348 option_definitions = [
349   ('EXT', 'f', 'format', 'set format.  EXT is one of texi and latex.'),
350   ('DIM',  '', 'default-music-fontsize', 'default fontsize for music.  DIM is assumed to be in points'),
351   ('DIM',  '', 'default-lilypond-fontsize', 'deprecated, use --default-music-fontsize'),
352   ('DIM', '', 'force-music-fontsize', 'force fontsize for all inline lilypond. DIM is assumed be to in points'),
353   ('DIM', '', 'force-lilypond-fontsize', 'deprecated, use --force-music-fontsize'),
354   ('DIR', 'I', 'include', 'include path'),
355   ('', 'M', 'dependencies', 'write dependencies'),
356   ('PREF', '',  'dep-prefix', 'prepend PREF before each -M dependency'),
357   ('', 'n', 'no-lily', 'don\'t run lilypond'),
358   ('', '', 'no-pictures', "don\'t generate pictures"),
359   ('', '', 'read-lys', "don't write ly files."),
360   ('FILE', 'o', 'outname', 'filename main output file'),
361   ('FILE', '', 'outdir', "where to place generated files"),
362   ('', 'v', 'version', 'print version information' ),
363   ('', 'h', 'help', 'print help'),
364   ]
365
366 # format specific strings, ie. regex-es for input, and % strings for output
367 output_dict= {
368         'latex': {
369                 'output-lilypond-fragment' : r"""\begin[eps,singleline,%s]{lilypond}
370   \context Staff <
371     \context Voice{
372       %s
373     }
374   >
375 \end{lilypond}""",
376                 'output-filename' : r'''
377
378 \verb+%s+:''',
379                 'output-lilypond': r"""\begin[%s]{lilypond}
380 %s
381 \end{lilypond}""",
382                 'output-verbatim': "\\begin{verbatim}%s\\end{verbatim}",
383                 'output-default-post': "\\def\postLilypondExample{}\n",
384                 'output-default-pre': "\\def\preLilypondExample{}\n",
385                 'usepackage-graphics': '\\usepackage{graphics}\n',
386                 'output-eps': '\\noindent\\parbox{\\lilypondepswidth{%(fn)s.eps}}{\includegraphics{%(fn)s.eps}}',
387                 'output-tex': '{\\preLilypondExample \\input %(fn)s.tex \\postLilypondExample\n}',
388                 'pagebreak': r'\pagebreak',
389                 },
390         'texi' : {'output-lilypond': """@lilypond[%s]
391 %s
392 @end lilypond 
393 """,
394                 'output-filename' : r'''
395
396 @file{%s}:''',    
397                   'output-lilypond-fragment': """@lilypond[%s]
398 \context Staff\context Voice{ %s }
399 @end lilypond """,
400                   'pagebreak': None,
401                   'output-verbatim': r"""@example
402 %s
403 @end example
404 """,
405
406 # do some tweaking: @ is needed in some ps stuff.
407 # override EndLilyPondOutput, since @tex is done
408 # in a sandbox, you can't do \input lilyponddefs at the
409 # top of the document.
410
411 # should also support fragment in
412                   
413                   'output-all': r"""
414 @tex
415 \catcode`\@=12
416 \input lilyponddefs
417 \def\EndLilyPondOutput{}
418 \input %(fn)s.tex
419 \catcode`\@=0
420 @end tex
421 @html
422 <p>
423 <a href="%(fn)s.png">
424 <img border=0 src="%(fn)s.png" alt="[picture of music]">
425 </a>
426 @end html
427 """,
428                 }
429         }
430
431 def output_verbatim (body):
432         if __main__.format == 'texi':
433                 body = re.sub ('([@{}])', '@\\1', body)
434         return get_output ('output-verbatim') % body
435
436
437 re_dict = {
438         'latex': {'input': r'(?m)^[^%\n]*?(?P<match>\\mbinput{?([^}\t \n}]*))',
439                   'include': r'(?m)^[^%\n]*?(?P<match>\\mbinclude{(?P<filename>[^}]+)})',
440                   'option-sep' : ',\s*',
441                   'header': r"\\documentclass\s*(\[.*?\])?",
442                   'geometry': r"^(?m)[^%\n]*?\\usepackage\s*(\[(?P<options>.*)\])?\s*{geometry}",
443                   'preamble-end': r'(?P<code>\\begin{document})',
444                   'verbatim': r"(?s)(?P<code>\\begin{verbatim}.*?\\end{verbatim})",
445                   'verb': r"(?P<code>\\verb(?P<del>.).*?(?P=del))",
446                   'lilypond-file': r'(?m)^[^%\n]*?(?P<match>\\lilypondfile\s*(\[(?P<options>.*?)\])?\s*\{(?P<filename>.+)})',
447                   'lilypond' : r'(?m)^[^%\n]*?(?P<match>\\lilypond\s*(\[(?P<options>.*?)\])?\s*{(?P<code>.*?)})',
448                   'lilypond-block': r"(?sm)^[^%\n]*?(?P<match>\\begin\s*(\[(?P<options>.*?)\])?\s*{lilypond}(?P<code>.*?)\\end{lilypond})",
449                   'def-post-re': r"\\def\\postLilypondExample",
450                   'def-pre-re': r"\\def\\preLilypondExample",             
451                   'usepackage-graphics': r"\usepackage{graphics}",
452                   'intertext': r',?\s*intertext=\".*?\"',
453                   'multiline-comment': no_match,
454                   'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>^%.*$\n+))",
455                   'numcols': r"(?P<code>\\(?P<num>one|two)column)",
456                   },
457
458
459         # why do we have distinction between @mbinclude and @include? 
460         'texi': {
461                  'include':  '(?m)^[^%\n]*?(?P<match>@mbinclude[ \n\t]+(?P<filename>[^\t \n]*))',
462                  'input': no_match,
463                  'header': no_match,
464                  'preamble-end': no_match,
465                  'landscape': no_match,
466                  'verbatim': r"""(?s)(?P<code>@example\s.*?@end example\s)""",
467                  'verb': r"""(?P<code>@code{.*?})""",
468                  'lilypond-file': '(?m)^(?!@c)(?P<match>@lilypondfile(\[(?P<options>.*?)\])?{(?P<filename>[^}]+)})',
469                  'lilypond' : '(?m)^(?!@c)(?P<match>@lilypond(\[(?P<options>.*?)\])?{(?P<code>.*?)})',
470 # pyton2.2b2 barfs on this
471                  'lilypond-block': r"""(?m)^(?!@c)(?P<match>(?s)(?P<match>@lilypond(\[(?P<options>.*?)\])?\s(?P<code>.*?)@end lilypond\s))""",
472
473 # 1.5.2 barfs on this. 
474 # 'lilypond-block': r"""(?m)^(?!@c)(?P<match>@lilypond(\[(?P<options>.*?)\])?\s(?P<code>.*?)@end lilypond\s)""",
475                   'option-sep' : ',\s*',
476                   'intertext': r',?\s*intertext=\".*?\"',
477                   'multiline-comment': r"(?sm)^\s*(?!@c\s+)(?P<code>@ignore\s.*?@end ignore)\s",
478                   'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>@c.*$\n+))",
479                   'numcols': no_match,
480                  }
481         }
482
483
484 for r in re_dict.keys ():
485         olddict = re_dict[r]
486         newdict = {}
487         for k in olddict.keys ():
488                 newdict[k] = re.compile (olddict[k])
489         re_dict[r] = newdict
490
491         
492 def uniq (list):
493         list.sort ()
494         s = list
495         list = []
496         for x in s:
497                 if x not in list:
498                         list.append (x)
499         return list
500                 
501
502 def get_output (name):
503         return  output_dict[format][name]
504
505 def get_re (name):
506         return  re_dict[format][name]
507
508 def bounding_box_dimensions(fname):
509         if g_outdir:
510                 fname = os.path.join(g_outdir, fname)
511         try:
512                 fd = open(fname)
513         except IOError:
514                 error ("Error opening `%s'" % fname)
515         str = fd.read ()
516         s = re.search('%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)', str)
517         if s:
518                 return (int (s.group (3) - s.group (1) + 0.5),
519                         int (s.group (4) - s.group (2) + 0.5))
520         else:
521                 return (0,0)
522
523 def error (str):
524         sys.stderr.write (str + "\n  Exiting ... \n\n")
525         raise 'Exiting.'
526
527
528 def compose_full_body (body, opts):
529         """Construct the lilypond code to send to Lilypond.
530         Add stuff to BODY using OPTS as options."""
531         music_size = default_music_fontsize
532         latex_size = default_text_fontsize
533         for o in opts:
534                 if g_force_lilypond_fontsize:
535                         music_size = g_force_lilypond_fontsize
536                 else:
537                         m = re.match ('([0-9]+)pt', o)
538                         if m:
539                                 music_size = string.atoi(m.group (1))
540
541                 m = re.match ('latexfontsize=([0-9]+)pt', o)
542                 if m:
543                         latex_size = string.atoi (m.group (1))
544
545         if re.search ('\\\\score', body):
546                 is_fragment = 0
547         else:
548                 is_fragment = 1
549         if 'fragment' in opts:
550                 is_fragment = 1
551         if 'nofragment' in opts:
552                 is_fragment = 0
553
554         if is_fragment and not 'multiline' in opts:
555                 opts.append('singleline')
556         if 'singleline' in opts:
557                 l = -1.0;
558         else:
559                 l = __main__.paperguru.get_linewidth()
560
561         for o in opts:
562                 m= re.search ('relative(.*)', o)
563                 v = 0
564                 if m:
565                         try:
566                                 v = string.atoi (m.group (1))
567                         except ValueError:
568                                 pass
569
570                         v = v + 1
571                         pitch = 'c'
572                         if v < 0:
573                                 pitch = pitch + '\,' * v
574                         elif v > 0:
575                                 pitch = pitch + '\'' * v
576
577                         body = '\\relative %s { %s }' %(pitch, body)
578         
579         if is_fragment:
580                 body = r"""\score { 
581  \notes { %s }
582   \paper { }  
583 }""" % body
584
585         opts = uniq (opts)
586         optstring = string.join (opts, ' ')
587         optstring = re.sub ('\n', ' ', optstring)
588         body = r"""
589 %% Generated automatically by: lilypond-book.py
590 %% options are %s  
591 \include "paper%d.ly"
592 \paper  { linewidth = %f \pt } 
593 """ % (optstring, music_size, l) + body
594
595         # ughUGH not original options
596         return body
597
598 def parse_options_string(s):
599         d = {}
600         r1 = re.compile("((\w+)={(.*?)})((,\s*)|$)")
601         r2 = re.compile("((\w+)=(.*?))((,\s*)|$)")
602         r3 = re.compile("(\w+?)((,\s*)|$)")
603         while s:
604                 m = r1.match(s)
605                 if m:
606                         s = s[m.end():]
607                         d[m.group(2)] = re.split(",\s*", m.group(3))
608                         continue
609                 m = r2.match(s)
610                 if m:
611                         s = s[m.end():]
612                         d[m.group(2)] = m.group(3)
613                         continue
614                 m = r3.match(s)
615                 if m:
616                         s = s[m.end():]
617                         d[m.group(1)] = 1
618                         continue
619                 
620                 error ("format of option string invalid (was `%')" % s)
621         return d
622
623 def scan_latex_preamble(chunks):
624         # first we want to scan the \documentclass line
625         # it should be the first non-comment line
626         idx = 0
627         while 1:
628                 if chunks[idx][0] == 'ignore':
629                         idx = idx + 1
630                         continue
631                 m = get_re ('header').match(chunks[idx][1])
632                 if m <> None and m.group (1):
633                         options = re.split (',[\n \t]*', m.group(1)[1:-1])
634                 else:
635                         options = []
636                 for o in options:
637                         if o == 'landscape':
638                                 paperguru.m_landscape = 1
639                         m = re.match("(.*?)paper", o)
640                         if m:
641                                 paperguru.m_papersize = m.group()
642                         else:
643                                 m = re.match("(\d\d)pt", o)
644                                 if m:
645                                         paperguru.m_fontsize = int(m.group(1))
646                 break
647         
648         while chunks[idx][0] != 'preamble-end':
649                 if chunks[idx] == 'ignore':
650                         idx = idx + 1
651                         continue
652                 m = get_re ('geometry').search(chunks[idx][1])
653                 if m:
654                         paperguru.m_use_geometry = 1
655                         o = parse_options_string(m.group('options'))
656                         for k in o.keys():
657                                 paperguru.set_geo_option(k, o[k])
658                 idx = idx + 1
659
660 def scan_texi_preamble (chunks):
661         # this is not bulletproof..., it checks the first 10 chunks
662         for c in chunks[:10]: 
663                 if c[0] == 'input':
664                         for s in ('afourpaper', 'afourwide', 'letterpaper',
665                                   'afourlatex', 'smallbook'):
666                                 if string.find(c[1], "@%s" % s) != -1:
667                                         paperguru.m_papersize = s
668
669 def scan_preamble (chunks):
670         if __main__.format == 'texi':
671                 scan_texi_preamble(chunks)
672         else:
673                 assert __main__.format == 'latex'
674                 scan_latex_preamble(chunks)
675                 
676
677 def completize_preamble (chunks):
678         if __main__.format == 'texi':
679                 return chunks
680         pre_b = post_b = graphics_b = None
681         for chunk in chunks:
682                 if chunk[0] == 'preamble-end':
683                         break
684                 if chunk[0] == 'input':
685                         m = get_re('def-pre-re').search(chunk[1])
686                         if m:
687                                 pre_b = 1
688                 if chunk[0] == 'input':
689                         m = get_re('def-post-re').search(chunk[1])
690                         if m:
691                                 post_b = 1
692                 if chunk[0] == 'input':
693                         m = get_re('usepackage-graphics').search(chunk[1])
694                         if m:
695                                 graphics_b = 1
696         x = 0
697         while chunks[x][0] != 'preamble-end':
698                 x = x + 1
699         if not pre_b:
700                 chunks.insert(x, ('input', get_output ('output-default-pre')))
701         if not post_b:
702                 chunks.insert(x, ('input', get_output ('output-default-post')))
703         if not graphics_b:
704                 chunks.insert(x, ('input', get_output ('usepackage-graphics')))
705         return chunks
706
707
708 read_files = []
709 def find_file (name):
710         """
711         Search the include path for NAME. If found, return the (CONTENTS, PATH) of the file.
712         """
713         
714         f = None
715         nm = ''
716         for a in include_path:
717                 try:
718                         nm = os.path.join (a, name)
719                         f = open (nm)
720                         __main__.read_files.append (nm)
721                         break
722                 except IOError:
723                         pass
724         if f:
725                 sys.stderr.write ("Reading `%s'\n" % nm)
726                 return (f.read (), nm)
727         else:
728                 error ("File not found `%s'\n" % name)
729                 return ('', '')
730
731 def do_ignore(match_object):
732         return [('ignore', match_object.group('code'))]
733 def do_preamble_end(match_object):
734         return [('preamble-end', match_object.group('code'))]
735
736 def make_verbatim(match_object):
737         return [('verbatim', match_object.group('code'))]
738
739 def make_verb(match_object):
740         return [('verb', match_object.group('code'))]
741
742 def do_include_file(m):
743         "m: MatchObject"
744         return [('input', get_output ('pagebreak'))] \
745              + read_doc_file(m.group('filename')) \
746              + [('input', get_output ('pagebreak'))] 
747
748 def do_input_file(m):
749         return read_doc_file(m.group('filename'))
750
751 def make_lilypond(m):
752         if m.group('options'):
753                 options = m.group('options')
754         else:
755                 options = ''
756         return [('input', get_output('output-lilypond-fragment') % 
757                         (options, m.group('code')))]
758
759 def make_lilypond_file(m):
760         """
761
762         Find @lilypondfile{bla.ly} occurences and substitute bla.ly
763         into a @lilypond .. @end lilypond block.
764         
765         """
766         
767         if m.group('options'):
768                 options = m.group('options')
769         else:
770                 options = ''
771         (content, nm) = find_file(m.group('filename'))
772         options = "filename=%s," % nm + options
773
774         return [('input', get_output('output-lilypond') %
775                         (options, content))]
776
777 def make_lilypond_block(m):
778         if m.group('options'):
779                 options = get_re('option-sep').split (m.group('options'))
780         else:
781             options = []
782         options = filter(lambda s: s != '', options)
783         return [('lilypond', m.group('code'), options)]
784
785 def do_columns(m):
786         if __main__.format != 'latex':
787                 return []
788         if m.group('num') == 'one':
789                 return [('numcols', m.group('code'), 1)]
790         if m.group('num') == 'two':
791                 return [('numcols', m.group('code'), 2)]
792         
793 def chop_chunks(chunks, re_name, func, use_match=0):
794     newchunks = []
795     for c in chunks:
796         if c[0] == 'input':
797             str = c[1]
798             while str:
799                 m = get_re (re_name).search (str)
800                 if m == None:
801                     newchunks.append (('input', str))
802                     str = ''
803                 else:
804                     if use_match:
805                         newchunks.append (('input', str[:m.start ('match')]))
806                     else:
807                         newchunks.append (('input', str[:m.start (0)]))
808                     #newchunks.extend(func(m))
809                     # python 1.5 compatible:
810                     newchunks = newchunks + func(m)
811                     str = str [m.end(0):]
812         else:
813             newchunks.append(c)
814     return newchunks
815
816 def determine_format (str):
817         if __main__.format == '':
818                 
819                 latex =  re.search ('\\\\document', str[:200])
820                 texinfo =  re.search ('@node|@setfilename', str[:200])
821
822                 f = ''
823                 g = None
824                 
825                 if texinfo and latex == None:
826                         f = 'texi'
827                 elif latex and texinfo == None: 
828                         f = 'latex'
829                 else:
830                         error("error: can't determine format, please specify")
831                 __main__.format = f
832
833         if __main__.paperguru == None:
834                 if __main__.format == 'texi':
835                         g = TexiPaper()
836                 else:
837                         g = LatexPaper()
838                         
839                 __main__.paperguru = g
840
841
842 def read_doc_file (filename):
843         """Read the input file, find verbatim chunks and do \input and \include
844         """
845         (str, path) = find_file(filename)
846         determine_format (str)
847         
848         chunks = [('input', str)]
849         
850         # we have to check for verbatim before doing include,
851         # because we don't want to include files that are mentioned
852         # inside a verbatim environment
853         chunks = chop_chunks(chunks, 'verbatim', make_verbatim)
854         chunks = chop_chunks(chunks, 'verb', make_verb)
855         chunks = chop_chunks(chunks, 'multiline-comment', do_ignore)
856         #ugh fix input
857         chunks = chop_chunks(chunks, 'include', do_include_file, 1)
858         chunks = chop_chunks(chunks, 'input', do_input_file, 1)
859         return chunks
860
861
862 taken_file_names = {}
863 def schedule_lilypond_block (chunk):
864         """Take the body and options from CHUNK, figure out how the
865         real .ly should look, and what should be left MAIN_STR (meant
866         for the main file).  The .ly is written, and scheduled in
867         TODO.
868
869         Return: a chunk (TYPE_STR, MAIN_STR, OPTIONS, TODO, BASE)
870
871         TODO has format [basename, extension, extension, ... ]
872         
873         """
874         (type, body, opts) = chunk
875         assert type == 'lilypond'
876         file_body = compose_full_body (body, opts)
877         basename = 'lily-' + `abs(hash (file_body))`
878         for o in opts:
879                 m = re.search ('filename="(.*?)"', o)
880                 if m:
881                         basename = m.group (1)
882                         if not taken_file_names.has_key(basename):
883                             taken_file_names[basename] = 0
884                         else:
885                             taken_file_names[basename] = taken_file_names[basename] + 1
886                             basename = basename + "-%i" % taken_file_names[basename]
887         if not g_read_lys:
888                 update_file(file_body, os.path.join(g_outdir, basename) + '.ly')
889         needed_filetypes = ['tex']
890
891         if format  == 'texi':
892                 needed_filetypes.append('eps')
893                 needed_filetypes.append('png')
894         if 'eps' in opts and not ('eps' in needed_filetypes):
895                 needed_filetypes.append('eps')
896         pathbase = os.path.join (g_outdir, basename)
897         def f(base, ext1, ext2):
898                 a = os.path.isfile(base + ext2)
899                 if (os.path.isfile(base + ext1) and
900                     os.path.isfile(base + ext2) and
901                                 os.stat(base+ext1)[stat.ST_MTIME] >
902                                 os.stat(base+ext2)[stat.ST_MTIME]) or \
903                                 not os.path.isfile(base + ext2):
904                         return 1
905         todo = []
906         if 'tex' in needed_filetypes and f(pathbase, '.ly', '.tex'):
907                 todo.append('tex')
908         if 'eps' in needed_filetypes and f(pathbase, '.tex', '.eps'):
909                 todo.append('eps')
910         if 'png' in needed_filetypes and f(pathbase, '.eps', '.png'):
911                 todo.append('png')
912         newbody = ''
913
914         if 'printfilename' in opts:
915                 for o in opts:
916                         m= re.match ("filename=(.*)", o)
917                         if m:
918                                 newbody = newbody + get_output ("output-filename") % m.group(1)
919                                 break
920                 
921         
922         if 'verbatim' in opts:
923                 newbody = output_verbatim (body)
924
925         for o in opts:
926                 m = re.search ('intertext="(.*?)"', o)
927                 if m:
928                         newbody = newbody  + m.group (1) + "\n\n"
929         if format == 'latex':
930                 if 'eps' in opts:
931                         s = 'output-eps'
932                 else:
933                         s = 'output-tex'
934         else: # format == 'texi'
935                 s = 'output-all'
936         newbody = newbody + get_output (s) % {'fn': basename }
937         return ('lilypond', newbody, opts, todo, basename)
938
939 def process_lilypond_blocks(outname, chunks):#ugh rename
940         newchunks = []
941         # Count sections/chapters.
942         for c in chunks:
943                 if c[0] == 'lilypond':
944                         c = schedule_lilypond_block (c)
945                 elif c[0] == 'numcols':
946                         paperguru.m_num_cols = c[2]
947                 newchunks.append (c)
948         return newchunks
949
950
951
952 def system (cmd):
953         sys.stderr.write ("invoking `%s'\n" % cmd)
954         st = os.system (cmd)
955         if st:
956                 error ('Error command exited with value %d\n' % st)
957         return st
958
959
960 def get_bbox (filename):
961         f = open (filename)
962         gr = []
963         while 1:
964                 l =f.readline ()
965                 m = re.match ('^%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)', l)
966                 if m:
967                         gr = map (string.atoi, m.groups ())
968                         break
969         
970         return gr
971
972 def make_pixmap (name):
973         bbox = get_bbox (name + '.eps')
974         margin = 3
975         fo = open (name + '.trans.eps' , 'w')
976         fo.write ('%d %d translate\n' % (-bbox[0]+margin, -bbox[1]+margin))
977         fo.close ()
978         
979         res = 90
980
981         x = (2* margin + bbox[2] - bbox[0]) * res / 72.
982         y = (2* margin + bbox[3] - bbox[1]) * res / 72.
983
984         cmd = r"""gs -g%dx%d -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r%d -dNOPAUSE %s %s -c quit | pnmtopng > %s"""
985         
986         cmd = cmd % (x, y, res, name + '.trans.eps', name + '.eps',name + '.png')
987         try:
988                 status = system (cmd)
989         except:
990                 os.unlink (name + '.png')
991                 error ("Removing output file")
992
993 def compile_all_files (chunks):
994         global foutn
995         eps = []
996         tex = []
997         png = []
998
999         for c in chunks:
1000                 if c[0] <> 'lilypond':
1001                         continue
1002                 base  = c[4]
1003                 exts = c[3]
1004                 for e in exts:
1005                         if e == 'eps':
1006                                 eps.append (base)
1007                         elif e == 'tex':
1008                                 #ugh
1009                                 if base + '.ly' not in tex:
1010                                         tex.append (base + '.ly')
1011                         elif e == 'png' and g_do_pictures:
1012                                 png.append (base)
1013         d = os.getcwd()
1014         if g_outdir:
1015                 os.chdir(g_outdir)
1016         if tex:
1017                 # fixme: be sys-independent.
1018                 def incl_opt (x):
1019                         if g_outdir and x[0] <> '/' :
1020                                 x = os.path.join (g_here_dir, x)
1021                         return ' -I %s' % x
1022
1023                 incs = map (incl_opt, include_path)
1024                 lilyopts = string.join (incs, ' ' )
1025                 if do_deps:
1026                         lilyopts = lilyopts + ' --dependencies '
1027                         if g_outdir:
1028                                 lilyopts = lilyopts + '--dep-prefix=' + g_outdir + '/'
1029                 texfiles = string.join (tex, ' ')
1030                 system ('lilypond --header=texidoc %s %s' % (lilyopts, texfiles))
1031
1032                 #
1033                 # Ugh, fixing up dependencies for .tex generation
1034                 #
1035                 if do_deps:
1036                         depfiles=map (lambda x: re.sub ('(.*)\.ly', '\\1.dep', x), tex)
1037                         for i in depfiles:
1038                                 f =open (i)
1039                                 text=f.read ()
1040                                 f.close ()
1041                                 text=re.sub ('\n([^:\n]*):', '\n' + foutn + ':', text)
1042                                 f = open (i, 'w')
1043                                 f.write (text)
1044                                 f.close ()
1045
1046         for e in eps:
1047                 system(r"tex '\nonstopmode \input %s'" % e)
1048                 system(r"dvips -E -o %s %s" % (e + '.eps', e))
1049                 
1050         for g in png:
1051                 make_pixmap (g)
1052                 
1053         os.chdir (d)
1054
1055
1056 def update_file (body, name):
1057         """
1058         write the body if it has changed
1059         """
1060         same = 0
1061         try:
1062                 f = open (name)
1063                 fs = f.read (-1)
1064                 same = (fs == body)
1065         except:
1066                 pass
1067
1068         if not same:
1069                 f = open (name , 'w')
1070                 f.write (body)
1071                 f.close ()
1072         
1073         return not same
1074
1075
1076 def getopt_args (opts):
1077         "Construct arguments (LONG, SHORT) for getopt from  list of options."
1078         short = ''
1079         long = []
1080         for o in opts:
1081                 if o[1]:
1082                         short = short + o[1]
1083                         if o[0]:
1084                                 short = short + ':'
1085                 if o[2]:
1086                         l = o[2]
1087                         if o[0]:
1088                                 l = l + '='
1089                         long.append (l)
1090         return (short, long)
1091
1092 def option_help_str (o):
1093         "Transform one option description (4-tuple ) into neatly formatted string"
1094         sh = '  '       
1095         if o[1]:
1096                 sh = '-%s' % o[1]
1097
1098         sep = ' '
1099         if o[1] and o[2]:
1100                 sep = ','
1101                 
1102         long = ''
1103         if o[2]:
1104                 long= '--%s' % o[2]
1105
1106         arg = ''
1107         if o[0]:
1108                 if o[2]:
1109                         arg = '='
1110                 arg = arg + o[0]
1111         return '  ' + sh + sep + long + arg
1112
1113
1114 def options_help_str (opts):
1115         "Convert a list of options into a neatly formatted string"
1116         w = 0
1117         strs =[]
1118         helps = []
1119
1120         for o in opts:
1121                 s = option_help_str (o)
1122                 strs.append ((s, o[3]))
1123                 if len (s) > w:
1124                         w = len (s)
1125
1126         str = ''
1127         for s in strs:
1128                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
1129         return str
1130
1131 def help():
1132         sys.stdout.write("""Usage: lilypond-book [options] FILE\n
1133 Generate hybrid LaTeX input from Latex + lilypond
1134 Options:
1135 """)
1136         sys.stdout.write (options_help_str (option_definitions))
1137         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
1138
1139
1140
1141 Report bugs to bug-gnu-music@gnu.org.
1142
1143 Written by Tom Cato Amundsen <tca@gnu.org> and
1144 Han-Wen Nienhuys <hanwen@cs.uu.nl>
1145 """)
1146
1147         sys.exit (0)
1148
1149
1150 def write_deps (fn, target, chunks):
1151         global read_files
1152         sys.stdout.write('Writing `%s\'\n' % os.path.join(g_outdir, fn))
1153         f = open (os.path.join(g_outdir, fn), 'w')
1154         f.write ('%s%s: ' % (g_dep_prefix, target))
1155         for d in read_files:
1156                 f.write ('%s ' %  d)
1157         basenames=[]
1158         for c in chunks:
1159                 if c[0] == 'lilypond':
1160                         (type, body, opts, todo, basename) = c;
1161                         basenames.append (basename)
1162         for d in basenames:
1163                 if g_outdir:
1164                         d=g_outdir + '/' + d
1165                 if g_dep_prefix:
1166                         #if not os.isfile (d): # thinko?
1167                         if not re.search ('/', d):
1168                                 d = g_dep_prefix + d
1169                 f.write ('%s.tex ' %  d)
1170         f.write ('\n')
1171         #if len (basenames):
1172         #       for d in basenames:
1173         #               f.write ('%s.ly ' %  d)
1174         #       f.write (' : %s' % target)
1175         f.write ('\n')
1176         f.close ()
1177         read_files = []
1178
1179 def identify():
1180         sys.stdout.write ('lilypond-book (GNU LilyPond) %s\n' % program_version)
1181
1182 def print_version ():
1183         identify()
1184         sys.stdout.write (r"""Copyright 1998--1999
1185 Distributed under terms of the GNU General Public License. It comes with
1186 NO WARRANTY.
1187 """)
1188
1189
1190 def check_texidoc (chunks):
1191         n = []
1192         for c in chunks:
1193                 if c[0] == 'lilypond':
1194                         (type, body, opts, todo, basename) = c;
1195                         pathbase = os.path.join (g_outdir, basename)
1196                         if os.path.isfile (pathbase + '.texidoc'):
1197                                 body = '\n@include %s.texidoc\n' % basename + body
1198                                 c = (type, body, opts, todo, basename)
1199                 n.append (c)
1200         return n
1201
1202
1203 ## what's this? Docme --hwn
1204 ##
1205 def fix_epswidth (chunks):
1206         newchunks = []
1207         for c in chunks:
1208                 if c[0] <> 'lilypond' or 'eps' not in c[2]:
1209                         newchunks.append (c)
1210                         continue
1211
1212                 mag = 1.0
1213                 for o in c[2]:
1214                         m  = re.match ('magnification=([0-9.]+)', o)
1215                         if m:
1216                                 mag = string.atof (m.group (1))
1217
1218                 def replace_eps_dim (match, lmag = mag):
1219                         filename = match.group (1)
1220                         dims = bounding_box_dimensions (filename)
1221
1222                         return '%fpt' % (dims[0] *lmag)
1223         
1224                 body = re.sub (r"""\\lilypondepswidth{(.*?)}""", replace_eps_dim, c[1])
1225                 newchunks.append(('lilypond', body, c[2], c[3], c[4]))
1226                         
1227         return newchunks
1228
1229
1230 foutn=""
1231 def do_file(input_filename):
1232         global foutn
1233         file_settings = {}
1234         if outname:
1235                 my_outname = outname
1236         else:
1237                 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
1238         my_depname = my_outname + '.dep'                
1239
1240         chunks = read_doc_file(input_filename)
1241         chunks = chop_chunks(chunks, 'lilypond', make_lilypond, 1)
1242         chunks = chop_chunks(chunks, 'lilypond-file', make_lilypond_file, 1)
1243         chunks = chop_chunks(chunks, 'lilypond-block', make_lilypond_block, 1)
1244         chunks = chop_chunks(chunks, 'singleline-comment', do_ignore, 1)
1245         chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1246         chunks = chop_chunks(chunks, 'numcols', do_columns)
1247         #print "-" * 50
1248         #for c in chunks: print "c:", c;
1249         #sys.exit()
1250         scan_preamble(chunks)
1251         chunks = process_lilypond_blocks(my_outname, chunks)
1252
1253         foutn = os.path.join (g_outdir, my_outname + '.' + format)
1254
1255         # Do It.
1256         if __main__.g_run_lilypond:
1257                 compile_all_files (chunks)
1258                 chunks = fix_epswidth (chunks)
1259
1260         if __main__.format == 'texi':
1261                 chunks = check_texidoc (chunks)
1262
1263         x = 0
1264         chunks = completize_preamble (chunks)
1265         sys.stderr.write ("Writing `%s'\n" % foutn)
1266         fout = open (foutn, 'w')
1267         for c in chunks:
1268                 fout.write (c[1])
1269         fout.close ()
1270         # should chmod -w
1271
1272         if do_deps:
1273                 write_deps (my_depname, foutn, chunks)
1274
1275
1276 outname = ''
1277 try:
1278         (sh, long) = getopt_args (__main__.option_definitions)
1279         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1280 except getopt.error, msg:
1281         sys.stderr.write("error: %s" % msg)
1282         sys.exit(1)
1283
1284 do_deps = 0
1285 for opt in options:     
1286         o = opt[0]
1287         a = opt[1]
1288
1289         if o == '--include' or o == '-I':
1290                 include_path.append (a)
1291         elif o == '--version' or o == '-v':
1292                 print_version ()
1293                 sys.exit  (0)
1294         elif o == '--format' or o == '-f':
1295                 __main__.format = a
1296         elif o == '--outname' or o == '-o':
1297                 if len(files) > 1:
1298                         #HACK
1299                         sys.stderr.write("Lilypond-book is confused by --outname on multiple files")
1300                         sys.exit(1)
1301                 outname = a
1302         elif o == '--help' or o == '-h':
1303                 help ()
1304         elif o == '--no-lily' or o == '-n':
1305                 __main__.g_run_lilypond = 0
1306         elif o == '--dependencies' or o == '-M':
1307                 do_deps = 1
1308         elif o == '--default-music-fontsize':
1309                 default_music_fontsize = string.atoi (a)
1310         elif o == '--default-lilypond-fontsize':
1311                 print "--default-lilypond-fontsize is deprecated, use --default-music-fontsize"
1312                 default_music_fontsize = string.atoi (a)
1313         elif o == '--force-music-fontsize':
1314                 g_force_lilypond_fontsize = string.atoi(a)
1315         elif o == '--force-lilypond-fontsize':
1316                 print "--force-lilypond-fontsize is deprecated, use --default-lilypond-fontsize"
1317                 g_force_lilypond_fontsize = string.atoi(a)
1318         elif o == '--dep-prefix':
1319                 g_dep_prefix = a
1320         elif o == '--no-pictures':
1321                 g_do_pictures = 0
1322         elif o == '--read-lys':
1323                 g_read_lys = 1
1324         elif o == '--outdir':
1325                 g_outdir = a
1326
1327 identify()
1328 if g_outdir:
1329         if os.path.isfile(g_outdir):
1330                 error ("outdir is a file: %s" % g_outdir)
1331         if not os.path.exists(g_outdir):
1332                 os.mkdir(g_outdir)
1333 setup_environment ()
1334 for input_filename in files:
1335         do_file(input_filename)
1336         
1337 #
1338 # Petr, ik zou willen dat ik iets zinvoller deed,
1339 # maar wat ik kan ik doen, het verandert toch niets?
1340 #   --hwn 20/aug/99