#!/usr/bin/env python from optparse import OptionParser import sys import os import os.path import re import subprocess parser = OptionParser(usage="%prog [-v] DIR/FILE [...]", version="%prog $Id: $") parser.add_option("-n", "--dry-run", action="store_false", dest="act", default=True, help="do not run any program") parser.add_option("-f", "--force", action="store_true", dest="force", default=False, help="regenerate up-to-date files") parser.add_option("-R", "--non-recursive", action="store_false", dest="recurse", default=True, help="disable recursion into subdirectories") parser.add_option("-s", "--summary", dest="summary", help="summarise failures and processed files at the end of the run (one of failures (default), files, both, no)", choices=("no","files","failures","both"), default="failures") parser.add_option("-v", "--verbose", action="count", dest="verbosity", default=1, help="give reasons for actions, may be repeated") parser.add_option("-q", "--quiet", action="store_false", dest="progress", default=True, help="suppress progress reports") parser.add_option("-i", "--ignore", action="store", dest="ignorepattern", default=".*vorlage.*|\.\#.*", help="regular expression of filenames to ignore") parser.add_option("-p", "--required-pattern", action="store", dest="requiredpattern", default='^\\\\documentclass|%%% TeX-master: t', help="regular expression that must match the file content") parser.add_option("-T", "--preserve-tempfiles", action="store_false", dest="deletetempfiles", default=True, help="preserve temporary files (default: delete)") (options, args) = parser.parse_args() interactionmode = "batchmode" if options.verbosity>2: interactionmode = "nonstopmode" re_ignore = None if options.ignorepattern != "": re_ignore = re.compile(options.ignorepattern) re_required = re.compile(options.requiredpattern) re_texfile = re.compile(".*\.tex$", re.I) errors = [] procfiles = [] errstr = {False: "ERROR", True: "WARNING"} def error(file, desc, warning=False): msg = "%s: %s: %s"%(errstr[warning], os.path.normpath(file), desc) if options.progress: print msg errors.append(msg) path = os.path.expandvars("$PATH") if path == "$PATH": path = os.path.defpath path = path.split(os.path.pathsep) def isinpath(name): for p in path: if os.path.isfile(os.path.join(p, name)): return True return False if isinpath("rmligs"): rmligs_name = "rmligs" elif isinpath("rmligs-german"): rmligs_name = "rmligs-german" else: rmligs_name = "" error("rmligs", "Command not found. Please install rmligs (or rmligs-german) for optimal results", warning=True) # iterate over all .tex files that are not ignored by -i or -p def alltexfiles(args): for a in args: if os.path.isfile(a): if re_texfile.match(a): (dirname, filename) = os.path.split(a) if dirname == "": dirname = "." yield (dirname, filename) else: error(a, "is no tex file; skipped") elif os.path.isdir(a): for dir, subdirs, files in os.walk(a): if not options.recurse: subdirs[:] = [] for name in files: if re_texfile.match(name): if re_ignore.match(name): if options.verbosity > 1: print "%s: skipped because of ignored name"%os.path.normpath(os.path.join(dir,name)) continue for m in texgrep(re_required, dir, name): yield (dir, name) break else: if options.verbosity > 1: print "%s: skipped because no main latex file"%os.path.normpath(os.path.join(dir,name)) def texgrep(matcher, dirname, filename, recurse=False): try: f = open(os.path.join(dirname,filename)) content = f.read() for m in matcher.finditer(content): yield m if recurse: for dep in dependencies(dirname, filename, texonly=True): for m in texgrep(matcher, "", dep, recurse=False): yield m except IOError, (errno, strerror): error(os.path.join(dirname, filename), "could not be read: %s"%strerror) except StopIteration: f.close() else: f.close() kpsepaths={} def texpath(dirname,filename,pathtype="tex",progname="latex",extlist=[],allowsystemfiles=False): if not kpsepaths.has_key((pathtype,progname)): kpse=subprocess.Popen(["kpsepath", "-n", progname, pathtype], stdout=subprocess.PIPE) (out,err)=kpse.communicate() if kpse.wait() != 0: error("kpsepath", "failed to get paths for %s"%pathtype) kpsepaths[(pathtype,progname)] = out[:-1].split(":") if os.path.isfile(os.path.join(dirname,filename)): return os.path.join(dirname,filename) for ext in extlist: if os.path.isfile(os.path.join(dirname,filename+ext)): return os.path.join(dirname,filename+ext) RECURSIONDEPTH=10 re_inputinclude = re.compile('\\\\(input|include|@input)\\{([^}]*)\\}') re_usepackage = re.compile('\\\\usepackage(\\[.*?\\])?\\{([^}]*)\\}') re_graphics = re.compile('\\\\includegraphics(\\[.*?\\])?\\{([^}]*)\\}') graphics_ext = ["", ".pdf", ".eps", ".png", ".jpg"] re_bibliography = re.compile('\\\\bibliography\\{([^}]*)\\}') re_commawhitespace = re.compile('\\s*,\\s*') def dependencies(dirname, filename, texonly=False, recurse=RECURSIONDEPTH): if not recurse: return try: f = open(os.path.join(dirname,filename)) content = f.read() for m in re_inputinclude.finditer(content): texs = m.group(2) for tex in re_commawhitespace.split(texs): if not os.path.isfile(os.path.join(dirname,tex)): tex+=".tex" if os.path.isfile(os.path.join(dirname,tex)): yield os.path.normpath(os.path.join(dirname,tex)) for m in dependencies(dirname, tex, texonly, recurse-1): yield m for m in re_usepackage.finditer(content): stys = m.group(2) for sty in re_commawhitespace.split(stys): if not os.path.isfile(os.path.join(dirname,sty)): sty+=".sty" if os.path.isfile(os.path.join(dirname,sty)): yield os.path.normpath(os.path.join(dirname,sty)) for m in dependencies(dirname, sty, texonly, recurse-1): yield m if not texonly: for m in re_graphics.finditer(content): names = m.group(2) for name in re_commawhitespace.split(names): for ext in graphics_ext: if os.path.isfile(os.path.join(dirname,name+ext)): yield os.path.normpath(os.path.join(dirname,name+ext)) for m in re_bibliography.finditer(content): bibs = m.group(1) for bib in re_commawhitespace.split(bibs): if bib[-4:] != ".bib": bib += ".bib" if bib[-8:] != "-blx.bib": yield os.path.normpath(os.path.join(dirname,os.path.expanduser(bib))) except IOError, (errno, strerror): error(os.path.join(dirname, filename), "could not be read: %s"%strerror) except StopIteration: f.close() else: f.close() def bibfiles(dirname, texname): for m in texgrep(re_bibliography, dirname, texname, recurse=True): bibs = m.group(1) for bib in re_commawhitespace.split(bibs): if bib[-4:] != ".bib": bib += ".bib" if bib[-8:] != "-blx.bib": yield os.path.normpath(os.path.join(dirname,os.path.expanduser(bib))) def outdatedtexfiles(args): for dirname, texname in alltexfiles(args): #strip .tex extension jobname=texname[:-4] pdfname=jobname+".pdf" reason = "" # check if an update is needed: if options.force and not options.verbosity: yield (dirname, texname, reason) elif not os.path.isfile(os.path.join(dirname,pdfname)): if options.verbosity: reason = " (because .pdf does not exist)" yield (dirname, texname, reason) else: pdftime = os.path.getmtime(os.path.join(dirname, pdfname)) if pdftime < os.path.getmtime(os.path.join(dirname, texname)): if options.verbosity: reason = " (because .tex is newer than .pdf)" yield (dirname, texname, reason) else: for f in dependencies(dirname, texname): if not os.path.isfile(f): error(os.path.join(dirname,texname), "dependency not found: %s"%f, warning=True) continue if pdftime < os.path.getmtime(f): if options.verbosity: reason = " (because %s is newer than .pdf)"%f yield (dirname, texname, reason) break else: r = haderrors(dirname, jobname) if r: if options.verbosity: reason = r yield (dirname, texname, reason) elif options.force: reason = " (because of --force)" yield (dirname, texname, reason) elif options.verbosity > 1: print "%s: skipped because up-to-date"%os.path.normpath(os.path.join(dirname, texname)) re_nopdftex = re.compile('\\\\usepackage(\\[.*?\\])?\\{[^}]*pstricks[^}]*\\}') def detecttextype(dirname, texname): for m in texgrep(re_nopdftex, dirname, texname, recurse=True): return "latex" return "pdflatex" re_rmligs = re.compile('\\\usepackage((\\[.*?\\])?\\{n?german\\}|\\[[^]]*?german[^]]*?\\]\\{babel\\})') def detectrmligs(dirname, texname): for m in texgrep(re_rmligs, dirname, texname, recurse=True): return True return False MAXRUNS=5 def processtexfiles(args): for dirname, texname, reason in outdatedtexfiles(args): procfiles.append(os.path.normpath(os.path.join(dirname,texname))) if options.progress: print "processing %s%s..."%(os.path.normpath(os.path.join(dirname,texname)),reason) # support for pstricks/plain latex: tex = detecttextype(dirname, texname) # detect usage of german/ngerman rmligs = detectrmligs(dirname, texname) runtex(tex, texname, dirname, rmligs) #strip .tex extension jobname=texname[:-4] # run bibtex if any bibfile changed: for bib in bibfiles(dirname, texname): bbl = os.path.normpath(os.path.join(dirname, jobname+".bbl")) if options.force or not os.path.isfile(bbl) or os.path.getmtime(bbl) < os.path.getmtime(bib): reason = "" if options.verbosity: if not os.path.isfile(bbl): reason = " (because .bbl does not exist yet)" elif os.path.getmtime(bbl) < os.path.getmtime(bib): reason = " (because %s is newer than .bbl)"%bib else: reason = " (because of --force)" run(["bibtex8", "--wolfgang", jobname], dirname, reason=reason) runtex(tex, texname, dirname, rmligs, reason=" (because of updated .bbl)") break # check for undefined references and run requests numrun=0 reqs=options.act while reqs: reqs = requests(os.path.join(dirname, jobname+".log")) pri = reqs.values() pri.sort(reverse=True) for p in pri: for req in reqs.keys(): rp = reqs[req] if rp == p: reason = "" if options.verbosity: reason = " (because of request in .aux file, priority %d)"%rp if req == "latex": runtex(tex, texname, dirname, rmligs, reason=reason) elif req == "bibtex": run(["bibtex8", "--wolfgang", jobname], dirname, reason=reason) else: error(os.path.join(dirname,jobname+".aux"), "unsupported request: %s"%req) numrun+=1 if numrun==MAXRUNS: error(os.path.join(dirname, texname), "does not stabilise after %i runs"%MAXRUNS) break # update index if it exists - no way of knowing if it was updated idx = jobname + ".idx" if os.path.isfile(os.path.join(dirname, idx)): run(["makeindex", jobname], dirname, reason=" (because .idx file might have changed)") runtex(tex, texname, dirname, rmligs, reason=" (because .ind file might have changed)") if tex == "latex": run(["dvips", jobname+".dvi"], dirname) run(["ps2pdf", jobname+".ps"], dirname) rmtempfile(jobname+"-rmligs.tex", dirname) re_logmatcher = re.compile("^LaTeX Warning: There were undefined references\.$|^LaTeX Warning: Label\(s\) may have changed\. Rerun to get cross-references right\.$|^REQ:(\d+):(\w+):") def requests(logpath): reqs = {} def addrequest(name, priority): if reqs.has_key(name): reqs[name] = max(reqs[name], priority) else: reqs[name] = priority try: log = open(logpath) for line in log: m = re_logmatcher.match(line) if m: if m.group() == 'LaTeX Warning: There were undefined references.': addrequest("latex",0) elif m.group() == 'LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.': addrequest("latex",0) elif m.group(1)!=None: addrequest(m.group(2), int(m.group(1))) # TODO: add support for multiple bibliographies, see biblatex.pdf Sec. 2.4.4 except IOError, (errno, strerror): error(logpath, "could not be read: %s"%strerror) else: log.close() return reqs re_error = re.compile('^! ') def haderrors(dirname, jobname): logpath = os.path.join(dirname, jobname+".log") if not os.path.isfile(logpath): return None try: f = open(logpath) for line in f: if re_error.match(line): return " (because of error in .log file)" if re_logmatcher.match(line): return " (because of request in .log file)" except IOError, (errno, strerror): error(logpath, "could not be read: %s"%strerror) else: f.close() return False def rmtempfile(filename, dirname): fullname = os.path.join(dirname, filename) if options.deletetempfiles and os.path.isfile(fullname): if options.progress: print " removing %s"%filename os.remove(fullname) def run(arglist, dirname, reason="", infile=None, outfile=None): if options.verbosity>2: # use default output=None else: output=file("/dev/null") if infile: try: stdin=open(os.path.join(dirname, infile), "r") indesc=" < %s"%infile except IOError, (errno, strerror): if output: output.close() error(infile, "input file could not be read: %s"%strerror) return else: stdin=None indesc="" if outfile: try: stdout=open(os.path.join(dirname, outfile), "w") outdesc=" > %s"%outfile except IOError, (errno, strerror): if output: output.close() if stdin: stdin.close() error(outfile, "output file could not be read: %s"%strerror) return else: stdout=output outdesc="" if options.progress: print " running %s%s%s%s..."%(" ".join(arglist), indesc, outdesc,reason) if options.act: ret = subprocess.call(arglist, stdin=stdin, stdout=stdout, stderr=output, cwd=dirname) if ret: error(dirname, "failed command: %s"%(" ".join(arglist))) if output: output.close() if stdin: stdin.close() if stdout and stdout is not output: stdout.close() def runtex(tex, texname, dirname, rmligs=False, reason=""): if rmligs and rmligs_name != "": jobname=texname[:-4] rmligsname=jobname+"-rmligs.tex" run([rmligs_name, "-f"], dirname, infile=texname, outfile=rmligsname, reason=reason) run([tex, "-interaction", interactionmode, "-jobname", jobname, rmligsname], dirname, reason) else: run([tex, "-interaction", interactionmode, texname], dirname, reason) # main program: try: if args: processtexfiles(args) else: processtexfiles(["."]) if options.summary in [ "files", "both" ] : if procfiles: print "Processed the following files:" for f in procfiles: print " %s"%f else: print "Processed no files." if options.summary in [ "failures", "both" ] : if errors: print "The following problems occured:" for f in errors: print " %s"%f if errors: sys.exit(1) except KeyboardInterrupt: sys.exit(2)