2 # -*- coding: utf-8 -*-
4 # This file is part of LilyPond, the GNU music typesetter.
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.
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.
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/>.
23 lilypond-book --filter="tr '[a-z]' '[A-Z]'" BOOK
26 lilypond-book --filter="convert-ly --no-version --from=1.6.11 -" BOOK
28 classic lilypond-book:
29 lilypond-book --process="lilypond" BOOK.tely
33 * ly-options: intertext?
35 * eps in latex / eps by lilypond -b ps?
36 * check latex parameters, twocolumn, multicolumn?
37 * use --png --ps --pdf for making images?
39 * Converting from lilypond-book source, substitute:
40 @mbinclude foo.itely -> @include foo.itely
46 # TODO: Better solve the global_options copying to the snippets...
55 from optparse import OptionGroup
67 import book_base as BookBase
68 import book_snippets as BookSnippet
74 ly.require_python_version ()
76 original_dir = os.getcwd ()
80 _ ("Process LilyPond snippets in hybrid HTML, LaTeX, texinfo or DocBook document.")
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")})
89 authors = ('Jan Nieuwenhuizen <janneke@gnu.org>',
90 'Han-Wen Nienhuys <hanwen@xs4all.nl>')
92 ################################################################
95 raise Exception (_ ('Exiting (%d)...') % i)
99 progress = ly.progress
104 progress('%s (GNU LilyPond) %s' % (ly.program_name, ly.program_version))
108 ly.encoded_write (sys.stdout, '''
115 ''' % ( _ ('Copyright (c) %s by') % '2001--2012',
116 '\n '.join (authors),
117 _ ("Distributed under terms of the GNU General Public License."),
118 _ ("It comes with NO WARRANTY.")))
121 def get_option_parser ():
122 p = ly.get_option_parser (usage=_ ("%s [OPTION]... FILE") % 'lilypond-book',
123 description=help_summary,
124 conflict_handler="resolve",
125 add_help_option=False)
127 p.add_option ('-F', '--filter', metavar=_ ("FILTER"),
130 help=_ ("pipe snippets through FILTER [default: `convert-ly -n -']"),
133 p.add_option ('-f', '--format',
134 help=_ ("use output format FORMAT (texi [default], texi-html, latex, html, docbook)"),
135 metavar=_ ("FORMAT"),
138 p.add_option("-h", "--help",
140 help=_ ("show this help and exit"))
142 p.add_option ("-I", '--include', help=_ ("add DIR to include path"),
144 action='append', dest='include_path',
147 p.add_option ('--info-images-dir',
148 help=_ ("format Texinfo output so that Info will "
149 "look for images of music in DIR"),
151 action='store', dest='info_images_dir',
154 p.add_option ('--left-padding',
157 help=_ ("pad left side of music to align music inspite of uneven bar numbers (in mm)"),
161 p.add_option ('--lily-loglevel',
162 help=_ ("Print lilypond log messages according to LOGLEVEL"),
163 metavar=_ ("LOGLEVEL"),
164 action='store', dest='lily_loglevel',
165 default=os.environ.get ("LILYPOND_LOGLEVEL", None))
167 p.add_option ('--lily-output-dir',
168 help=_ ("write lily-XXX files to DIR, link into --output dir"),
170 action='store', dest='lily_output_dir',
173 p.add_option ('--load-custom-package', help=_ ("Load the additional python PACKAGE (containing e.g. a custom output format)"),
174 metavar=_ ("PACKAGE"),
175 action='append', dest='custom_packages',
178 p.add_option ("-l", "--loglevel",
179 help=_ ("Print log messages according to LOGLEVEL "
180 "(NONE, ERROR, WARNING, PROGRESS (default), DEBUG)"),
181 metavar=_ ("LOGLEVEL"),
183 callback=ly.handle_loglevel_option,
186 p.add_option ("-o", '--output', help=_ ("write output to DIR"),
188 action='store', dest='output_dir',
191 p.add_option ('-P', '--process', metavar=_ ("COMMAND"),
192 help = _ ("process ly_files using COMMAND FILE..."),
194 dest='process_cmd', default='')
196 p.add_option ('--redirect-lilypond-output',
197 help = _ ("Redirect the lilypond output"),
199 dest='redirect_output', default=False)
201 p.add_option ('-s', '--safe', help=_ ("Compile snippets in safe mode"),
206 p.add_option ('--skip-lily-check',
207 help=_ ("do not fail if no lilypond output is found"),
209 action='store_true', dest='skip_lilypond_run',
212 p.add_option ('--skip-png-check',
213 help=_ ("do not fail if no PNG images are found for EPS files"),
215 action='store_true', dest='skip_png_check',
218 p.add_option ('--use-source-file-names',
219 help=_ ("write snippet output files with the same base name as their source file"),
220 action='store_true', dest='use_source_file_names',
223 p.add_option ('-V', '--verbose', help=_ ("be verbose"),
225 callback=ly.handle_loglevel_option,
226 callback_args=("DEBUG",))
228 p.version = "@TOPLEVEL_VERSION@"
229 p.add_option("--version",
231 help=_ ("show version number and exit"))
233 p.add_option ('-w', '--warranty',
234 help=_ ("show warranty and copyright"),
237 group = OptionGroup (p, "Options only for the latex and texinfo backends")
238 group.add_option ('--latex-program',
239 help=_ ("run executable PROG instead of latex, or in\n\
240 case --pdf option is set instead of pdflatex"),
242 action='store', dest='latex_program',
244 group.add_option ('--texinfo-program',
245 help=_ ("run executable PROG instead of texi2pdf"),
247 action='store', dest='texinfo_program',
249 group.add_option ('--pdf',
252 help=_ ("create PDF files for use with PDFTeX"),
254 p.add_option_group (group)
256 p.add_option_group ('',
258 _ ("Report bugs via %s")
259 % ' http://post.gmane.org/post.php'
260 '?group=gmane.comp.gnu.lilypond.bugs') + '\n')
263 for formatter in BookBase.all_formats:
264 formatter.add_options (p)
268 lilypond_binary = os.path.join ('@bindir@', 'lilypond')
270 # If we are called with full path, try to use lilypond binary
271 # installed in the same path; this is needed in GUB binaries, where
272 # @bindir is always different from the installed binary path.
273 if 'bindir' in globals () and bindir:
274 lilypond_binary = os.path.join (bindir, 'lilypond')
276 # Only use installed binary when we are installed too.
277 if '@bindir@' == ('@' + 'bindir@') or not os.path.exists (lilypond_binary):
278 lilypond_binary = 'lilypond'
280 # Need to shell-quote, issue 3468
283 lilypond_binary = pipes.quote (lilypond_binary)
285 global_options = None
290 def find_linestarts (s):
295 i = s.find ('\n', start)
306 def find_toplevel_snippets (input_string, formatter):
308 types = formatter.supported_snippet_types ()
310 res[t] = re.compile (formatter.snippet_regexp (t))
314 found = dict ([(t, None) for t in types])
316 line_starts = find_linestarts (input_string)
318 # We want to search for multiple regexes, without searching
319 # the string multiple times for one regex.
320 # Hence, we use earlier results to limit the string portion
322 # Since every part of the string is traversed at most once for
323 # every type of snippet, this is linear.
328 if not found[type] or found[type][0] < index:
331 m = res[type].search (input_string[index:endex])
335 klass = global_options.formatter.snippet_class (type)
337 start = index + m.start ('match')
338 line_number = line_start_idx
339 while (line_starts[line_number] < start):
343 snip = klass (type, m, formatter, line_number, global_options)
345 found[type] = (start, snip)
349 or found[type][0] < found[first][0])):
354 # Limiting the search space is a cute
355 # idea, but this *requires* to search
356 # for possible containing blocks
357 # first, at least as long as we do not
358 # search for the start of blocks, but
359 # always/directly for the entire
360 # @block ... @end block.
362 endex = found[first][0]
365 snippets.append (BookSnippet.Substring (input_string, index, len (input_string), line_start_idx))
368 while (start > line_starts[line_start_idx+1]):
371 (start, snip) = found[first]
372 snippets.append (BookSnippet.Substring (input_string, index, start, line_start_idx + 1))
373 snippets.append (snip)
375 index = start + len (snip.match.group ('match'))
379 def system_in_directory (cmd, directory, logfile):
380 """Execute a command in a different directory.
382 Because of win32 compatibility, we can't simply use subprocess.
385 current = os.getcwd()
387 """NB - ignore_error is deliberately set to the same value
388 as redirect_output - this is not a typo."""
389 retval = ly.system(cmd,
390 be_verbose=ly.is_verbose (),
391 redirect_output=global_options.redirect_output,
394 ignore_error=global_options.redirect_output)
396 print ("Error trapped by lilypond-book")
397 print ("\nPlease see " + logfile + ".log\n")
403 def process_snippets (cmd, snippets,
404 formatter, lily_output_dir):
405 """Run cmd on all of the .ly files from snippets."""
410 cmd = formatter.adjust_snippet_command (cmd)
412 checksum = snippet_list_checksum (snippets)
413 contents = '\n'.join (['snippet-map-%d.ly' % checksum]
414 + list (set ([snip.basename() + '.ly' for snip in snippets])))
415 name = os.path.join (lily_output_dir,
416 'snippet-names-%d.ly' % checksum)
417 logfile = name.replace('.ly', '')
418 file (name, 'wb').write (contents)
420 system_in_directory (' '.join ([cmd, ly.mkarg (name)]),
424 def snippet_list_checksum (snippets):
425 return hash (' '.join([l.basename() for l in snippets]))
427 def write_file_map (lys, name):
428 snippet_map = file (os.path.join (
429 global_options.lily_output_dir,
430 'snippet-map-%d.ly' % snippet_list_checksum (lys)), 'w')
432 snippet_map.write ("""
433 #(define version-seen #t)
434 #(define output-empty-score-list #f)
435 #(ly:add-file-name-alist '(%s
437 """ % '\n'.join(['("%s.ly" . "%s")\n' % (ly.basename ().replace('\\','/'), name)
440 def split_output_files(directory):
441 """Returns directory entries in DIRECTORY/XX/ , where XX are hex digits.
443 Return value is a set of strings.
447 return re.sub ("[][*?]", r"[\g<0>]", x)
448 for subdir in glob.glob (os.path.join (globquote (directory),
449 '[a-f0-9][a-f0-9]')):
450 base_subdir = os.path.split (subdir)[1]
451 sub_files = [os.path.join (base_subdir, name)
452 for name in os.listdir (subdir)]
456 def do_process_cmd (chunks, input_name, options):
457 snippets = [c for c in chunks if isinstance (c, BookSnippet.LilypondSnippet)]
459 output_files = split_output_files (options.lily_output_dir)
460 outdated = [c for c in snippets if c.is_outdated (options.lily_output_dir, output_files)]
462 write_file_map (outdated, input_name)
463 progress (_ ("Writing snippets..."))
464 for snippet in outdated:
468 progress (_ ("Processing..."))
469 process_snippets (options.process_cmd, outdated,
470 options.formatter, options.lily_output_dir)
473 progress (_ ("All snippets are up to date..."))
475 progress (_ ("Linking files..."))
476 abs_lily_output_dir = os.path.join (options.original_dir, options.lily_output_dir)
477 abs_output_dir = os.path.join (options.original_dir, options.output_dir)
478 if abs_lily_output_dir != abs_output_dir:
479 output_files = split_output_files (abs_lily_output_dir)
480 for snippet in snippets:
481 snippet.link_all_output_files (abs_lily_output_dir,
487 # Format guessing data
489 def guess_format (input_filename):
491 e = os.path.splitext (input_filename)[1]
492 for formatter in BookBase.all_formats:
493 if formatter.can_handle_extension (e):
495 error (_ ("cannot determine format for: %s" % input_filename))
498 def write_if_updated (file_name, lines):
502 new_str = ''.join (lines)
503 if oldstr == new_str:
504 progress (_ ("%s is up to date.") % file_name)
506 # this prevents make from always rerunning lilypond-book:
507 # output file must be touched in order to be up to date
508 os.utime (file_name, None)
513 output_dir = os.path.dirname (file_name)
514 if not os.path.exists (output_dir):
515 os.makedirs (output_dir)
517 progress (_ ("Writing `%s'...") % file_name)
518 file (file_name, 'w').writelines (lines)
521 def note_input_file (name, inputs=[]):
522 ## hack: inputs is mutable!
526 def samefile (f1, f2):
528 return os.path.samefile (f1, f2)
529 except AttributeError: # Windoze
530 f1 = re.sub ("//*", "/", f1)
531 f2 = re.sub ("//*", "/", f2)
534 def do_file (input_filename, included=False):
536 input_absname = input_filename
537 if not input_filename or input_filename == '-':
538 in_handle = sys.stdin
539 input_fullname = '<stdin>'
541 if os.path.exists (input_filename):
542 input_fullname = input_filename
544 input_fullname = global_options.formatter.input_fullname (input_filename)
545 # Normalize path to absolute path, since we will change cwd to the output dir!
546 # Otherwise, "lilypond-book -o out test.tex" will complain that it is
547 # overwriting the input file (which it is actually not), since the
548 # input filename is relative to the CWD...
549 input_absname = os.path.abspath (input_fullname)
551 note_input_file (input_fullname)
552 in_handle = file (input_fullname)
554 if input_filename == '-':
555 global_options.input_dir = os.getcwd ()
558 input_base = os.path.splitext (input_filename)[0]
560 global_options.input_dir = os.path.split (input_absname)[0]
561 input_base = os.path.basename (
562 os.path.splitext (input_filename)[0])
564 # don't complain when global_options.output_dir is existing
565 if not global_options.output_dir:
566 global_options.output_dir = os.getcwd()
568 global_options.output_dir = os.path.abspath(global_options.output_dir)
570 if not os.path.isdir (global_options.output_dir):
571 os.mkdir (global_options.output_dir, 0777)
572 os.chdir (global_options.output_dir)
574 output_filename = os.path.join(global_options.output_dir,
575 input_base + global_options.formatter.default_extension)
576 if (os.path.exists (input_filename)
577 and os.path.exists (output_filename)
578 and samefile (output_filename, input_absname)):
580 _ ("Output would overwrite input file; use --output."))
584 progress (_ ("Reading %s...") % input_fullname)
585 source = in_handle.read ()
588 global_options.formatter.init_default_snippet_options (source)
591 progress (_ ("Dissecting..."))
592 chunks = find_toplevel_snippets (source, global_options.formatter)
594 # Let the formatter modify the chunks before further processing
595 chunks = global_options.formatter.process_chunks (chunks)
597 if global_options.filter_cmd:
598 write_if_updated (output_filename,
599 [c.filter_text () for c in chunks])
600 elif global_options.process_cmd:
601 do_process_cmd (chunks, input_fullname, global_options)
602 progress (_ ("Compiling %s...") % output_filename)
603 write_if_updated (output_filename,
604 [s.replacement_text ()
607 def process_include (snippet):
608 os.chdir (original_dir)
609 name = snippet.substring ('filename')
610 progress (_ ("Processing include: %s") % name)
611 return do_file (name, included=True)
613 include_chunks = map (process_include,
614 filter (lambda x: isinstance (x, BookSnippet.IncludeSnippet),
617 return chunks + reduce (lambda x, y: x + y, include_chunks, [])
619 except BookSnippet.CompileError:
620 os.chdir (original_dir)
621 progress (_ ("Removing `%s'") % output_filename)
622 raise BookSnippet.CompileError
624 def inverse_relpath (path, relpath):
625 """Given two paths, the second relative to the first,
626 return the first path relative to the second."""
627 if os.path.isabs (relpath):
628 return os.path.abspath (path)
630 parts = os.path.normpath (path).split (os.path.sep)
631 for part in os.path.normpath (relpath).split (os.path.sep):
633 relparts.append (parts[-1])
636 relparts.append ('..')
638 return os.path.sep.join (relparts[::-1])
641 global global_options
643 opt_parser = get_option_parser()
644 (global_options, args) = opt_parser.parse_args ()
646 global_options.information = {'program_version': ly.program_version, 'program_name': ly.program_name }
647 global_options.original_dir = original_dir
649 if global_options.lily_output_dir:
650 global_options.lily_output_dir = os.path.expanduser (global_options.lily_output_dir)
651 global_options.include_path.insert (0, inverse_relpath (original_dir, global_options.lily_output_dir))
653 if global_options.output_dir:
654 global_options.output_dir = os.path.expanduser (global_options.output_dir)
655 global_options.include_path.insert (0, inverse_relpath (original_dir, global_options.output_dir))
657 global_options.include_path.insert (0, ".")
659 # Load the python packages (containing e.g. custom formatter classes)
660 # passed on the command line
662 for i in global_options.custom_packages:
664 progress (str(imp.load_source ("book_custom_package%s" % nr, i)))
667 if global_options.warranty:
670 if not args or len (args) > 1:
671 opt_parser.print_help ()
677 # FIXME: 85 lines of `main' macramee??
678 if (os.environ.has_key ("LILYPOND_BOOK_LOGLEVEL")):
679 ly.set_loglevel (os.environ["LILYPOND_BOOK_LOGLEVEL"])
680 files = do_options ()
682 basename = os.path.splitext (files[0])[0]
683 basename = os.path.split (basename)[1]
685 if global_options.format:
686 # Retrieve the formatter for the given format
687 for formatter in BookBase.all_formats:
688 if formatter.can_handle_format (global_options.format):
689 global_options.formatter = formatter
691 global_options.formatter = guess_format (files[0])
692 global_options.format = global_options.formatter.format
694 # make the global options available to the formatters:
695 global_options.formatter.global_options = global_options
696 formats = global_options.formatter.image_formats
698 if global_options.process_cmd == '':
699 global_options.process_cmd = (lilypond_binary
700 + ' --formats=%s -dbackend=eps ' % formats)
702 if global_options.process_cmd:
703 includes = global_options.include_path
704 if global_options.lily_output_dir:
705 # This must be first, so lilypond prefers to read .ly
706 # files in the other lybookdb dir.
707 includes = [global_options.lily_output_dir] + includes
708 global_options.process_cmd += ' '.join ([' -I %s' % ly.mkarg (p)
711 global_options.formatter.process_options (global_options)
713 if global_options.lily_loglevel:
714 ly.debug_output (_ ("Setting LilyPond's loglevel to %s") % global_options.lily_loglevel, True)
715 global_options.process_cmd += " --loglevel=%s" % global_options.lily_loglevel
716 elif ly.is_verbose ():
717 if os.environ.get ("LILYPOND_LOGLEVEL", None):
718 ly.debug_output (_ ("Setting LilyPond's loglevel to %s (from environment variable LILYPOND_LOGLEVEL)") % os.environ.get ("LILYPOND_LOGLEVEL", None), True)
719 global_options.process_cmd += " --loglevel=%s" % os.environ.get ("LILYPOND_LOGLEVEL", None)
721 ly.debug_output (_ ("Setting LilyPond's output to --verbose, implied by lilypond-book's setting"), True)
722 global_options.process_cmd += " --verbose"
724 if global_options.padding_mm:
725 global_options.process_cmd += " -deps-box-padding=%f " % global_options.padding_mm
727 global_options.process_cmd += " -dread-file-list -dno-strip-output-dir"
729 if global_options.lily_output_dir:
730 global_options.lily_output_dir = os.path.abspath(global_options.lily_output_dir)
731 if not os.path.isdir (global_options.lily_output_dir):
732 os.makedirs (global_options.lily_output_dir)
734 global_options.lily_output_dir = os.path.abspath(global_options.output_dir)
736 relative_output_dir = global_options.output_dir
740 chunks = do_file (files[0])
741 except BookSnippet.CompileError:
744 inputs = note_input_file ('')
747 base_file_name = os.path.splitext (os.path.basename (files[0]))[0]
748 dep_file = os.path.join (global_options.output_dir, base_file_name + '.dep')
749 final_output_file = os.path.join (relative_output_dir,
750 base_file_name + global_options.formatter.default_extension)
752 os.chdir (original_dir)
753 file (dep_file, 'w').write ('%s: %s\n'
754 % (final_output_file, ' '.join (inputs)))
756 if __name__ == '__main__':