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--2014',
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.replace (os.path.sep, '/'))]),
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 adjust_include_path (path, outpath):
625 """Rewrite an include path relative to the dir where lilypond is launched.
626 Always use forward slashes since this is what lilypond expects."""
627 path = os.path.expanduser (path)
628 path = os.path.expandvars (path)
629 path = os.path.normpath (path)
630 if os.path.isabs (outpath):
631 return os.path.abspath (path).replace (os.path.sep, '/')
632 if os.path.isabs (path):
633 return path.replace (os.path.sep, '/')
634 return os.path.join (inverse_relpath (original_dir, outpath), path).replace (os.path.sep, '/')
636 def inverse_relpath (path, relpath):
637 """Given two paths, the second relative to the first,
638 return the first path relative to the second.
639 Always use forward slashes since this is what lilypond expects."""
640 if os.path.isabs (relpath):
641 return os.path.abspath (path).replace (os.path.sep, '/')
643 parts = os.path.normpath (path).split (os.path.sep)
644 for part in os.path.normpath (relpath).split (os.path.sep):
646 relparts.append (parts[-1])
649 relparts.append ('..')
651 return '/'.join (relparts[::-1])
654 global global_options
656 opt_parser = get_option_parser()
657 (global_options, args) = opt_parser.parse_args ()
659 global_options.information = {'program_version': ly.program_version, 'program_name': ly.program_name }
660 global_options.original_dir = original_dir
662 if global_options.lily_output_dir:
663 global_options.lily_output_dir = os.path.expanduser (global_options.lily_output_dir)
664 for i, path in enumerate(global_options.include_path):
665 global_options.include_path[i] = adjust_include_path (path, global_options.lily_output_dir)
666 global_options.include_path.insert (0, inverse_relpath (original_dir, global_options.lily_output_dir))
668 elif global_options.output_dir:
669 global_options.output_dir = os.path.expanduser (global_options.output_dir)
670 for i, path in enumerate(global_options.include_path):
671 global_options.include_path[i] = adjust_include_path (path, global_options.output_dir)
672 global_options.include_path.insert (0, inverse_relpath (original_dir, global_options.output_dir))
674 global_options.include_path.insert (0, "./")
676 # Load the python packages (containing e.g. custom formatter classes)
677 # passed on the command line
679 for i in global_options.custom_packages:
681 progress (str(imp.load_source ("book_custom_package%s" % nr, i)))
684 if global_options.warranty:
687 if not args or len (args) > 1:
688 opt_parser.print_help ()
694 # FIXME: 85 lines of `main' macramee??
695 if (os.environ.has_key ("LILYPOND_BOOK_LOGLEVEL")):
696 ly.set_loglevel (os.environ["LILYPOND_BOOK_LOGLEVEL"])
697 files = do_options ()
699 basename = os.path.splitext (files[0])[0]
700 basename = os.path.split (basename)[1]
702 if global_options.format:
703 # Retrieve the formatter for the given format
704 for formatter in BookBase.all_formats:
705 if formatter.can_handle_format (global_options.format):
706 global_options.formatter = formatter
708 global_options.formatter = guess_format (files[0])
709 global_options.format = global_options.formatter.format
711 # make the global options available to the formatters:
712 global_options.formatter.global_options = global_options
713 formats = global_options.formatter.image_formats
715 if global_options.process_cmd == '':
716 global_options.process_cmd = (lilypond_binary
717 + ' --formats=%s -dbackend=eps ' % formats)
719 if global_options.process_cmd:
720 includes = global_options.include_path
721 global_options.process_cmd += ' '.join ([' -I %s' % ly.mkarg (p)
724 global_options.formatter.process_options (global_options)
726 if global_options.lily_loglevel:
727 ly.debug_output (_ ("Setting LilyPond's loglevel to %s") % global_options.lily_loglevel, True)
728 global_options.process_cmd += " --loglevel=%s" % global_options.lily_loglevel
729 elif ly.is_verbose ():
730 if os.environ.get ("LILYPOND_LOGLEVEL", None):
731 ly.debug_output (_ ("Setting LilyPond's loglevel to %s (from environment variable LILYPOND_LOGLEVEL)") % os.environ.get ("LILYPOND_LOGLEVEL", None), True)
732 global_options.process_cmd += " --loglevel=%s" % os.environ.get ("LILYPOND_LOGLEVEL", None)
734 ly.debug_output (_ ("Setting LilyPond's output to --verbose, implied by lilypond-book's setting"), True)
735 global_options.process_cmd += " --verbose"
737 if global_options.padding_mm:
738 global_options.process_cmd += " -deps-box-padding=%f " % global_options.padding_mm
740 global_options.process_cmd += " -dread-file-list -dno-strip-output-dir"
742 if global_options.lily_output_dir:
743 global_options.lily_output_dir = os.path.abspath(global_options.lily_output_dir)
744 if not os.path.isdir (global_options.lily_output_dir):
745 os.makedirs (global_options.lily_output_dir)
747 global_options.lily_output_dir = os.path.abspath(global_options.output_dir)
749 relative_output_dir = global_options.output_dir
753 chunks = do_file (files[0])
754 except BookSnippet.CompileError:
757 inputs = note_input_file ('')
760 base_file_name = os.path.splitext (os.path.basename (files[0]))[0]
761 dep_file = os.path.join (global_options.output_dir, base_file_name + '.dep')
762 final_output_file = os.path.join (relative_output_dir,
763 base_file_name + global_options.formatter.default_extension)
765 os.chdir (original_dir)
766 file (dep_file, 'w').write ('%s: %s\n'
767 % (final_output_file, ' '.join (inputs)))
769 if __name__ == '__main__':