]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
Merge branch 'lilypond/translation' into staging
[lilypond.git] / scripts / lilypond-book.py
1 #!@TARGET_PYTHON@
2 # -*- coding: utf-8 -*-
3
4 # This file is part of LilyPond, the GNU music typesetter.
5 #
6 # LilyPond is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # LilyPond is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with LilyPond.  If not, see <http://www.gnu.org/licenses/>.
18
19 '''
20 Example usage:
21
22 test:
23   lilypond-book --filter="tr '[a-z]' '[A-Z]'" BOOK
24
25 convert-ly on book:
26   lilypond-book --filter="convert-ly --no-version --from=1.6.11 -" BOOK
27
28 classic lilypond-book:
29   lilypond-book --process="lilypond" BOOK.tely
30
31 TODO:
32
33   *  ly-options: intertext?
34   *  --line-width?
35   *  eps in latex / eps by lilypond -b ps?
36   *  check latex parameters, twocolumn, multicolumn?
37   *  use --png --ps --pdf for making images?
38
39   *  Converting from lilypond-book source, substitute:
40    @mbinclude foo.itely -> @include foo.itely
41    \mbinput -> \input
42
43 '''
44
45
46 # TODO: Better solve the global_options copying to the snippets...
47
48 import glob
49 import os
50 import re
51 import stat
52 import sys
53 import tempfile
54 import imp
55 from optparse import OptionGroup
56
57
58 """
59 @relocate-preamble@
60 """
61
62 import lilylib as ly
63 import fontextract
64 import langdefs
65 global _;_=ly._
66
67 import book_base as BookBase
68 import book_snippets as BookSnippet
69 import book_html
70 import book_docbook
71 import book_texinfo
72 import book_latex
73
74 ly.require_python_version ()
75
76 original_dir = os.getcwd ()
77 backend = 'ps'
78
79 help_summary = (
80 _ ("Process LilyPond snippets in hybrid HTML, LaTeX, texinfo or DocBook document.")
81 + '\n\n'
82 + _ ("Examples:")
83 + '''
84  $ lilypond-book --filter="tr '[a-z]' '[A-Z]'" %(BOOK)s
85  $ lilypond-book -F "convert-ly --no-version --from=2.0.0 -" %(BOOK)s
86  $ lilypond-book --process='lilypond -I include' %(BOOK)s
87 ''' % {'BOOK': _ ("BOOK")})
88
89 authors = ('Jan Nieuwenhuizen <janneke@gnu.org>',
90            'Han-Wen Nienhuys <hanwen@xs4all.nl>')
91
92 ################################################################
93 def exit (i):
94     if ly.is_verbose ():
95         raise Exception (_ ('Exiting (%d)...') % i)
96     else:
97         sys.exit (i)
98
99 def identify ():
100     ly.encoded_write (sys.stdout, '%s (GNU LilyPond) %s\n' % (ly.program_name, ly.program_version))
101
102 progress = ly.progress
103 warning = ly.warning
104 error = ly.error
105
106
107 def warranty ():
108     identify ()
109     ly.encoded_write (sys.stdout, '''
110 %s
111
112   %s
113
114 %s
115 %s
116 ''' % ( _ ('Copyright (c) %s by') % '2001--2011',
117         '\n  '.join (authors),
118         _ ("Distributed under terms of the GNU General Public License."),
119         _ ("It comes with NO WARRANTY.")))
120
121
122 def get_option_parser ():
123     p = ly.get_option_parser (usage=_ ("%s [OPTION]... FILE") % 'lilypond-book',
124                               description=help_summary,
125                               conflict_handler="resolve",
126                               add_help_option=False)
127
128     p.add_option ('-F', '--filter', metavar=_ ("FILTER"),
129                   action="store",
130                   dest="filter_cmd",
131                   help=_ ("pipe snippets through FILTER [default: `convert-ly -n -']"),
132                   default=None)
133
134     p.add_option ('-f', '--format',
135                   help=_ ("use output format FORMAT (texi [default], texi-html, latex, html, docbook)"),
136                   metavar=_ ("FORMAT"),
137                   action='store')
138
139     p.add_option("-h", "--help",
140                  action="help",
141                  help=_ ("show this help and exit"))
142
143     p.add_option ("-I", '--include', help=_ ("add DIR to include path"),
144                   metavar=_ ("DIR"),
145                   action='append', dest='include_path',
146                   default=[os.path.abspath (os.getcwd ())])
147
148     p.add_option ('--info-images-dir',
149                   help=_ ("format Texinfo output so that Info will "
150                           "look for images of music in DIR"),
151                   metavar=_ ("DIR"),
152                   action='store', dest='info_images_dir',
153                   default='')
154
155     p.add_option ('--left-padding',
156                   metavar=_ ("PAD"),
157                   dest="padding_mm",
158                   help=_ ("pad left side of music to align music inspite of uneven bar numbers (in mm)"),
159                   type="float",
160                   default=3.0)
161
162     p.add_option ('--lily-loglevel',
163                   help=_ ("Print lilypond log messages according to LOGLEVEL"),
164                   metavar=_ ("LOGLEVEL"),
165                   action='store', dest='lily_loglevel',
166                   default=os.environ.get ("LILYPOND_LOGLEVEL", None))
167
168     p.add_option ('--lily-output-dir',
169                   help=_ ("write lily-XXX files to DIR, link into --output dir"),
170                   metavar=_ ("DIR"),
171                   action='store', dest='lily_output_dir',
172                   default=None)
173
174     p.add_option ('--load-custom-package', help=_ ("Load the additional python PACKAGE (containing e.g. a custom output format)"),
175                   metavar=_ ("PACKAGE"),
176                   action='append', dest='custom_packages',
177                   default=[])
178
179     p.add_option ("-l", "--loglevel",
180                   help=_ ("Print log messages according to LOGLEVEL "
181                           "(NONE, ERROR, WARNING, PROGRESS (default), DEBUG)"),
182                   metavar=_ ("LOGLEVEL"),
183                   action='callback',
184                   callback=ly.handle_loglevel_option,
185                   type='string')
186
187     p.add_option ("-o", '--output', help=_ ("write output to DIR"),
188                   metavar=_ ("DIR"),
189                   action='store', dest='output_dir',
190                   default='')
191
192     p.add_option ('-P', '--process', metavar=_ ("COMMAND"),
193                   help = _ ("process ly_files using COMMAND FILE..."),
194                   action='store',
195                   dest='process_cmd', default='')
196
197     p.add_option ('--redirect-lilypond-output',
198                   help = _ ("Redirect the lilypond output"),
199                   action='store_true',
200                   dest='redirect_output', default=False)
201
202     p.add_option ('-s', '--safe', help=_ ("Compile snippets in safe mode"),
203                   action="store_true",
204                   default=False,
205                   dest="safe_mode")
206
207     p.add_option ('--skip-lily-check',
208                   help=_ ("do not fail if no lilypond output is found"),
209                   metavar=_ ("DIR"),
210                   action='store_true', dest='skip_lilypond_run',
211                   default=False)
212
213     p.add_option ('--skip-png-check',
214                   help=_ ("do not fail if no PNG images are found for EPS files"),
215                   metavar=_ ("DIR"),
216                   action='store_true', dest='skip_png_check',
217                   default=False)
218
219     p.add_option ('--use-source-file-names',
220                   help=_ ("write snippet output files with the same base name as their source file"),
221                   action='store_true', dest='use_source_file_names',
222                   default=False)
223
224     p.add_option ('-V', '--verbose', help=_ ("be verbose"),
225                   action="callback",
226                   callback=ly.handle_loglevel_option,
227                   callback_args=("DEBUG",))
228
229     p.version = "@TOPLEVEL_VERSION@"
230     p.add_option("--version",
231                  action="version",
232                  help=_ ("show version number and exit"))
233
234     p.add_option ('-w', '--warranty',
235                   help=_ ("show warranty and copyright"),
236                   action='store_true')
237
238     group = OptionGroup (p, "Options only for the latex and texinfo backends")
239     group.add_option ('--latex-program',
240               help=_ ("run executable PROG instead of latex, or in\n\
241 case --pdf option is set instead of pdflatex"),
242               metavar=_ ("PROG"),
243               action='store', dest='latex_program',
244               default='latex')
245     group.add_option ('--texinfo-program',
246               help=_ ("run executable PROG instead of texi2pdf"),
247               metavar=_ ("PROG"),
248               action='store', dest='texinfo_program',
249               default='texi2pdf')
250     group.add_option ('--pdf',
251               action="store_true",
252               dest="create_pdf",
253               help=_ ("create PDF files for use with PDFTeX"),
254               default=False)
255     p.add_option_group (group)
256
257     p.add_option_group ('',
258                         description=(
259         _ ("Report bugs via %s")
260         % ' http://post.gmane.org/post.php'
261         '?group=gmane.comp.gnu.lilypond.bugs') + '\n')
262
263
264     for formatter in BookBase.all_formats:
265       formatter.add_options (p)
266
267     return p
268
269 lilypond_binary = os.path.join ('@bindir@', 'lilypond')
270
271 # If we are called with full path, try to use lilypond binary
272 # installed in the same path; this is needed in GUB binaries, where
273 # @bindir is always different from the installed binary path.
274 if 'bindir' in globals () and bindir:
275     lilypond_binary = os.path.join (bindir, 'lilypond')
276
277 # Only use installed binary when we are installed too.
278 if '@bindir@' == ('@' + 'bindir@') or not os.path.exists (lilypond_binary):
279     lilypond_binary = 'lilypond'
280
281 global_options = None
282
283
284
285
286 def find_linestarts (s):
287     nls = [0]
288     start = 0
289     end = len (s)
290     while 1:
291         i = s.find ('\n', start)
292         if i < 0:
293             break
294
295         i = i + 1
296         nls.append (i)
297         start = i
298
299     nls.append (len (s))
300     return nls
301
302 def find_toplevel_snippets (input_string, formatter):
303     res = {}
304     types = formatter.supported_snippet_types ()
305     for t in types:
306         res[t] = re.compile (formatter.snippet_regexp (t))
307
308     snippets = []
309     index = 0
310     found = dict ([(t, None) for t in types])
311
312     line_starts = find_linestarts (input_string)
313     line_start_idx = 0
314     # We want to search for multiple regexes, without searching
315     # the string multiple times for one regex.
316     # Hence, we use earlier results to limit the string portion
317     # where we search.
318     # Since every part of the string is traversed at most once for
319     # every type of snippet, this is linear.
320     while 1:
321         first = None
322         endex = 1 << 30
323         for type in types:
324             if not found[type] or found[type][0] < index:
325                 found[type] = None
326
327                 m = res[type].search (input_string[index:endex])
328                 if not m:
329                     continue
330
331                 klass = global_options.formatter.snippet_class (type)
332
333                 start = index + m.start ('match')
334                 line_number = line_start_idx
335                 while (line_starts[line_number] < start):
336                     line_number += 1
337
338                 line_number += 1
339                 snip = klass (type, m, formatter, line_number, global_options)
340
341                 found[type] = (start, snip)
342
343             if (found[type]
344                 and (not first
345                      or found[type][0] < found[first][0])):
346                 first = type
347
348                 # FIXME.
349
350                 # Limiting the search space is a cute
351                 # idea, but this *requires* to search
352                 # for possible containing blocks
353                 # first, at least as long as we do not
354                 # search for the start of blocks, but
355                 # always/directly for the entire
356                 # @block ... @end block.
357
358                 endex = found[first][0]
359
360         if not first:
361             snippets.append (BookSnippet.Substring (input_string, index, len (input_string), line_start_idx))
362             break
363
364         while (start > line_starts[line_start_idx+1]):
365             line_start_idx += 1
366
367         (start, snip) = found[first]
368         snippets.append (BookSnippet.Substring (input_string, index, start, line_start_idx + 1))
369         snippets.append (snip)
370         found[first] = None
371         index = start + len (snip.match.group ('match'))
372
373     return snippets
374
375 def system_in_directory (cmd, directory, logfile):
376     """Execute a command in a different directory.
377
378     Because of win32 compatibility, we can't simply use subprocess.
379     """
380
381     current = os.getcwd()
382     os.chdir (directory)
383     """NB - ignore_error is deliberately set to the same value
384     as redirect_output - this is not a typo."""
385     retval = ly.system(cmd,
386               be_verbose=ly.is_verbose (),
387               redirect_output=global_options.redirect_output,
388               log_file=logfile,
389               progress_p=1,
390               ignore_error=global_options.redirect_output)
391     if retval != 0:
392         print ("Error trapped by lilypond-book")
393         print ("\nPlease see " + logfile + ".log\n")
394         sys.exit(1)
395
396     os.chdir (current)
397
398
399 def process_snippets (cmd, snippets,
400                       formatter, lily_output_dir):
401     """Run cmd on all of the .ly files from snippets."""
402
403     if not snippets:
404         return
405
406     cmd = formatter.adjust_snippet_command (cmd)
407
408     checksum = snippet_list_checksum (snippets)
409     contents = '\n'.join (['snippet-map-%d.ly' % checksum]
410                           + list (set ([snip.basename() + '.ly' for snip in snippets])))
411     name = os.path.join (lily_output_dir,
412                          'snippet-names-%d.ly' % checksum)
413     logfile = name.replace('.ly', '')
414     file (name, 'wb').write (contents)
415
416     system_in_directory (' '.join ([cmd, ly.mkarg (name)]),
417                          lily_output_dir,
418                          logfile)
419
420 def snippet_list_checksum (snippets):
421     return hash (' '.join([l.basename() for l in snippets]))
422
423 def write_file_map (lys, name):
424     snippet_map = file (os.path.join (
425         global_options.lily_output_dir,
426         'snippet-map-%d.ly' % snippet_list_checksum (lys)), 'w')
427
428     snippet_map.write ("""
429 #(define version-seen #t)
430 #(define output-empty-score-list #f)
431 #(ly:add-file-name-alist '(%s
432     ))\n
433 """ % '\n'.join(['("%s.ly" . "%s")\n' % (ly.basename ().replace('\\','/'), name)
434                  for ly in lys]))
435
436 def split_output_files(directory):
437     """Returns directory entries in DIRECTORY/XX/ , where XX are hex digits.
438
439     Return value is a set of strings.
440     """
441     files = []
442     for subdir in glob.glob (os.path.join (directory, '[a-f0-9][a-f0-9]')):
443         base_subdir = os.path.split (subdir)[1]
444         sub_files = [os.path.join (base_subdir, name)
445                      for name in os.listdir (subdir)]
446         files += sub_files
447     return set (files)
448
449 def do_process_cmd (chunks, input_name, options):
450     snippets = [c for c in chunks if isinstance (c, BookSnippet.LilypondSnippet)]
451
452     output_files = split_output_files (options.lily_output_dir)
453     outdated = [c for c in snippets if c.is_outdated (options.lily_output_dir, output_files)]
454
455     write_file_map (outdated, input_name)
456     progress (_ ("Writing snippets..."))
457     for snippet in outdated:
458         snippet.write_ly()
459     progress ('\n')
460
461     if outdated:
462         progress (_ ("Processing..."))
463         progress ('\n')
464         process_snippets (options.process_cmd, outdated,
465                           options.formatter, options.lily_output_dir)
466
467     else:
468         progress (_ ("All snippets are up to date..."))
469
470     if options.lily_output_dir != options.output_dir:
471         output_files = split_output_files (options.lily_output_dir)
472         for snippet in snippets:
473             snippet.link_all_output_files (options.lily_output_dir,
474                                            output_files,
475                                            options.output_dir)
476
477     progress ('\n')
478
479
480 ###
481 # Format guessing data
482
483 def guess_format (input_filename):
484     format = None
485     e = os.path.splitext (input_filename)[1]
486     for formatter in BookBase.all_formats:
487       if formatter.can_handle_extension (e):
488         return formatter
489     error (_ ("cannot determine format for: %s" % input_filename))
490     exit (1)
491
492 def write_if_updated (file_name, lines):
493     try:
494         f = file (file_name)
495         oldstr = f.read ()
496         new_str = ''.join (lines)
497         if oldstr == new_str:
498             progress (_ ("%s is up to date.") % file_name)
499             progress ('\n')
500
501             # this prevents make from always rerunning lilypond-book:
502             # output file must be touched in order to be up to date
503             os.utime (file_name, None)
504             return
505     except:
506         pass
507
508     output_dir = os.path.dirname (file_name)
509     if not os.path.exists (output_dir):
510         os.makedirs (output_dir)
511
512     progress (_ ("Writing `%s'...") % file_name)
513     file (file_name, 'w').writelines (lines)
514     progress ('\n')
515
516
517 def note_input_file (name, inputs=[]):
518     ## hack: inputs is mutable!
519     inputs.append (name)
520     return inputs
521
522 def samefile (f1, f2):
523     try:
524         return os.path.samefile (f1, f2)
525     except AttributeError:                # Windoze
526         f1 = re.sub ("//*", "/", f1)
527         f2 = re.sub ("//*", "/", f2)
528         return f1 == f2
529
530 def do_file (input_filename, included=False):
531     # Ugh.
532     input_absname = input_filename
533     if not input_filename or input_filename == '-':
534         in_handle = sys.stdin
535         input_fullname = '<stdin>'
536     else:
537         if os.path.exists (input_filename):
538             input_fullname = input_filename
539         else:
540             input_fullname = global_options.formatter.input_fullname (input_filename)
541         # Normalize path to absolute path, since we will change cwd to the output dir!
542         # Otherwise, "lilypond-book -o out test.tex" will complain that it is
543         # overwriting the input file (which it is actually not), since the
544         # input filename is relative to the CWD...
545         input_absname = os.path.abspath (input_fullname)
546
547         note_input_file (input_fullname)
548         in_handle = file (input_fullname)
549
550     if input_filename == '-':
551         input_base = 'stdin'
552     elif included:
553         input_base = os.path.splitext (input_filename)[0]
554     else:
555         input_base = os.path.basename (
556             os.path.splitext (input_filename)[0])
557
558     # don't complain when global_options.output_dir is existing
559     if not global_options.output_dir:
560         global_options.output_dir = os.getcwd()
561     else:
562         global_options.output_dir = os.path.abspath(global_options.output_dir)
563
564         if not os.path.isdir (global_options.output_dir):
565             os.mkdir (global_options.output_dir, 0777)
566         os.chdir (global_options.output_dir)
567
568     output_filename = os.path.join(global_options.output_dir,
569                                    input_base + global_options.formatter.default_extension)
570     if (os.path.exists (input_filename)
571         and os.path.exists (output_filename)
572         and samefile (output_filename, input_absname)):
573      error (
574      _ ("Output would overwrite input file; use --output."))
575      exit (2)
576
577     try:
578         progress (_ ("Reading %s...") % input_fullname)
579         source = in_handle.read ()
580         progress ('\n')
581
582         if not included:
583             global_options.formatter.init_default_snippet_options (source)
584
585
586         progress (_ ("Dissecting..."))
587         chunks = find_toplevel_snippets (source, global_options.formatter)
588
589         # Let the formatter modify the chunks before further processing
590         chunks = global_options.formatter.process_chunks (chunks)
591         progress ('\n')
592
593         if global_options.filter_cmd:
594             write_if_updated (output_filename,
595                      [c.filter_text () for c in chunks])
596         elif global_options.process_cmd:
597             do_process_cmd (chunks, input_fullname, global_options)
598             progress (_ ("Compiling %s...") % output_filename)
599             progress ('\n')
600             write_if_updated (output_filename,
601                      [s.replacement_text ()
602                      for s in chunks])
603
604         def process_include (snippet):
605             os.chdir (original_dir)
606             name = snippet.substring ('filename')
607             progress (_ ("Processing include: %s") % name)
608             progress ('\n')
609             return do_file (name, included=True)
610
611         include_chunks = map (process_include,
612                               filter (lambda x: isinstance (x, BookSnippet.IncludeSnippet),
613                                       chunks))
614
615         return chunks + reduce (lambda x, y: x + y, include_chunks, [])
616
617     except BookSnippet.CompileError:
618         os.chdir (original_dir)
619         progress (_ ("Removing `%s'") % output_filename)
620         progress ('\n')
621         raise BookSnippet.CompileError
622
623 def do_options ():
624     global global_options
625
626     opt_parser = get_option_parser()
627     (global_options, args) = opt_parser.parse_args ()
628
629     global_options.information = {'program_version': ly.program_version, 'program_name': ly.program_name }
630
631     global_options.include_path =  map (os.path.abspath, global_options.include_path)
632
633     # Load the python packages (containing e.g. custom formatter classes)
634     # passed on the command line
635     nr = 0
636     for i in global_options.custom_packages:
637         nr += 1
638         print imp.load_source ("book_custom_package%s" % nr, i)
639
640
641     if global_options.warranty:
642         warranty ()
643         exit (0)
644     if not args or len (args) > 1:
645         opt_parser.print_help ()
646         exit (2)
647
648     return args
649
650 def main ():
651     # FIXME: 85 lines of `main' macramee??
652     if (os.environ.has_key ("LILYPOND_BOOK_LOGLEVEL")):
653         ly.set_loglevel (os.environ["LILYPOND_BOOK_LOGLEVEL"])
654     files = do_options ()
655
656     basename = os.path.splitext (files[0])[0]
657     basename = os.path.split (basename)[1]
658
659     if global_options.format:
660       # Retrieve the formatter for the given format
661       for formatter in BookBase.all_formats:
662         if formatter.can_handle_format (global_options.format):
663           global_options.formatter = formatter
664     else:
665         global_options.formatter = guess_format (files[0])
666         global_options.format = global_options.formatter.format
667
668     # make the global options available to the formatters:
669     global_options.formatter.global_options = global_options
670     formats = global_options.formatter.image_formats
671
672     if global_options.process_cmd == '':
673         global_options.process_cmd = (lilypond_binary
674                                       + ' --formats=%s -dbackend=eps ' % formats)
675
676     if global_options.process_cmd:
677         includes = global_options.include_path
678         if global_options.lily_output_dir:
679             # This must be first, so lilypond prefers to read .ly
680             # files in the other lybookdb dir.
681             includes = [os.path.abspath(global_options.lily_output_dir)] + includes
682         global_options.process_cmd += ' '.join ([' -I %s' % ly.mkarg (p)
683                                                  for p in includes])
684
685     global_options.formatter.process_options (global_options)
686
687     if global_options.lily_loglevel:
688         ly.debug_output (_ ("Setting LilyPond's loglevel to %s") % global_options.lily_loglevel, True)
689         global_options.process_cmd += " --loglevel=%s" % global_options.lily_loglevel
690     elif ly.is_verbose ():
691         if os.environ.get ("LILYPOND_LOGLEVEL", None):
692             ly.debug_output (_ ("Setting LilyPond's loglevel to %s (from environment variable LILYPOND_LOGLEVEL)") % os.environ.get ("LILYPOND_LOGLEVEL", None), True)
693             global_options.process_cmd += " --loglevel=%s" % os.environ.get ("LILYPOND_LOGLEVEL", None)
694         else:
695             ly.debug_output (_ ("Setting LilyPond's output to --verbose, implied by lilypond-book's setting"), True)
696             global_options.process_cmd += " --verbose"
697
698     if global_options.padding_mm:
699         global_options.process_cmd += " -deps-box-padding=%f " % global_options.padding_mm
700
701     global_options.process_cmd += " -dread-file-list -dno-strip-output-dir"
702
703     if global_options.lily_output_dir:
704         global_options.lily_output_dir = os.path.abspath(global_options.lily_output_dir)
705         if not os.path.isdir (global_options.lily_output_dir):
706             os.makedirs (global_options.lily_output_dir)
707     else:
708         global_options.lily_output_dir = os.path.abspath(global_options.output_dir)
709
710     relative_output_dir = global_options.output_dir
711
712     identify ()
713     try:
714         chunks = do_file (files[0])
715     except BookSnippet.CompileError:
716         exit (1)
717
718     inputs = note_input_file ('')
719     inputs.pop ()
720
721     base_file_name = os.path.splitext (os.path.basename (files[0]))[0]
722     dep_file = os.path.join (global_options.output_dir, base_file_name + '.dep')
723     final_output_file = os.path.join (relative_output_dir,
724                      base_file_name + global_options.formatter.default_extension)
725
726     os.chdir (original_dir)
727     file (dep_file, 'w').write ('%s: %s'
728                                 % (final_output_file, ' '.join (inputs)))
729
730 if __name__ == '__main__':
731     main ()