#!/usr/bin/env texlua VERSION = "0.23b" --[[ musixtex.lua: processes MusiXTeX files using xml2pmx and/or prepmx and/or pmxab and/or autosp as pre-processors (and deletes intermediate files) (c) Copyright 2011-2021 Bob Tennent rdt@cs.queensu.ca and Dirk Laurie dirk.laurie@gmail.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --]] --[[ ChangeLog: version 0.23b 2020-05-27 RDT improve the -h output version 0.23a 2020-08-14 RDT pmxprep files now deleted by xml2pmx version 0.23 2020-05-21 RDT added support for xml2pmx pre-preprocessing version 0.22 2020-03-20 RDT add -X option add -version, --version, -help, --help options version 0.21 2018-07-27 RDT add -P option. version 0.20 2018-06-11 RDT remove .mx1 file before tex processing version 0.19 2017-12-10 RDT Allow non-standard extensions. Add -M and -A options. version 0.18 2017-06-13 RDT Allow autosp to generate .ltx files version 0.17a 2017-01-08 RDT Added -D option. Avoid writing or concatenating a nil value. version 0.16e 2016-03-02 DL missing version information (caused by batchmode in 0.16c) fixed version 0.16d 2016-03-02 RDT filename argument in autosp failure message fixed version 0.16c 2016-02-24 RDT -interaction batchmode for -q report_error reports only the first error version 0.16b 2016-02-20 DL Improved help message as suggested by Bob Tennent. version 0.16a 2015-12-30 DL Corrects bug in -g option reported by Christian Mondrup. version 0.16 2015-12-24 DL Versions read from tempfile and written to musixtex.log. version 0.15 2015-12-03 DL Option -q added, which redirects most screen output into a temporary file. If an error occurs at the TeX stage, processing halts immediately and the tail of the log file is sent to stderr. version 0.14 2015-12-01 DL Writes `musixtex.log` and deletes other logfiles (except with -i) Hierarchy of extensions emphasized in usage() and consistently used elsewhere. Unknown option-like arguments reported and ignored, not treated as filenames Warnings and error messages generated by musixtex.lua start with respctively `!` and `!!`. version 0.13 2015-11-30 RDT Process .mtx and .pmx files (suggested by Dirk Laurie) exit_code is now an error count Use pmxab exit code to distinguish between errors and warnings version 0.12 2015-11-28 RDT Process .ltx files, with -l implied version 0.11 2015-07-16 RDT Automatic autosp preprocessing. version 0.10 2015-04-23 RDT Add -a option to preprocess using autosp version 0.9 2015-02-13 RDT Add an additional latex pass to resolve cross-references. Add -e0 option to dvips as given in musixdoc.tex Add -x option to call makeindex version 0.8 2014-05-18 RDT Add -g option version 0.7 2013-12-11 RDT Add -F fmt option version 0.6 2012-09-14 RDT Add -1 (one-pass [pdf][la]tex processing) option. version 0.5 2011-11-28 RDT Add -i (retain intermediate files) option. version 0.4 2011-04-30 RDT Allow multiple filenames (and options). Add -f (default) and -l (latex) options. version 0.3 2011-04-25 RDT Add -d (dvipdfm) and -s (stop at dvi) options. version 0.2 2011-04-21 RDT Allow basename.tex as filename. Add -p option for pdfetex processing. Add standard -v -h options. --]] local orig_print = print function usage() orig_print [[ Usage: [texlua] musixtex.lua { option | basename[ .xml | .mtx | .pmx | .aspc | .tex | .ltx] } ... When no extension is given, extensions are tried in the above order until a source file is found. Preprocessing goes .xml - .pmx - .tex .mtx - .pmx - .tex .pmx - .tex .aspc - .tex with the entry point determined by the filename extension. The normal route after preprocessing goes .tex - .dvi - .ps - .pdf, but shorter routes are also available; see the options. The default 3-pass processing route for .tex files is etex - musixflx - etex. .ltx files, possibly after autosp preprocessing, are treated as latex source and processed by latex (or pdflatex) rather than etex. Options: -v, --version version -h, --help help -l latex source; implied by .ltx extension -p direct tex-pdf (pdftex etc) -F fmt use fmt as the TeX processor -d tex-dvi-pdf (using dvipdfm if -D not used) -D dvixx use dvixx as the dvi processor -P ps2pdfxx use ps2pdfxx as the Postscript processor -c preprocess pmx file using pmxchords -m stop at pmx -M prepmxx use prepmxx as the mtx preprocessor -A autospx use autospx as the aspc preprocessor -X pmxabx use pmxabx as the pmx preprocessor -L xmlx use xmlx as the xml preprocessor -t stop at tex/mid -s stop at dvi -g stop at ps -i retain intermediate and log files -q quiet mode (redirect screen logs, write summary on musixtex.log) -1 one-pass [pdf][la]tex processing -x run makeindex -f restore default processing Four TeX engines are available via the -l and -p options. etex default latex -l pdfetex -p pdflatex -l -p If the -F option is used, options -l and -p need to be set if the engine name does not contain "latex" and "pdf" respectively. For example, the above four engines can be replaced by: -F "luatex --output-format=dvi" -F "lualatex --output-format=dvi" -F "luatex" -p -F "lualatex" -p ]] end function whoami () print("This is musixtex.lua version ".. VERSION .. ".") end function exists (filename, nolog) local f = io.open(filename, "r") if f then f:close() if not nolog then musixlog:write("Processing " .. filename,'\n' ) end return true else return false end end -- System commands for the various programs are mostly -- set to nil if the step is to be omitted, which can be -- tested by a simple "if" statement. -- Exceptions: -- 'tex' is the command for processing a TeX file, but it is important -- to know whether the user has explicitly specified an option that -- affects this choice, in which case the automatic selection of the -- command is restricted or disabled. -- force_engine "use the command provided" -- override records when 'l' and/or 'p' options are in effect -- 'dvi' is the command for processing a DVI file, but there are two -- reasons for not needing it and it is important for the automatic -- selection of a TeX engine to know which is in effect. This is -- possible by exploiting the the fact that Lua has two false values. -- dvi == nil "do not produce a DVI file" (but maybe PDF) -- dvi == false "do not process the DVI file" (but stop after TeX) local dvips = "dvips -e0" -- option -e0 suppresses dvips "feature" of adjusting location to align -- characters in words of text function defaults() xml2pmx = "xml2pmx" prepmx = "prepmx" pmx = "pmxab" autosp = "autosp" tex = "etex" musixflx = "musixflx" dvi = dvips ps2pdf = "ps2pdf" cleanup = true -- clean up intermediate and log files index = false latex = false force_engine = false -- indicates whether -F is specified override = "" passes = 2 quiet = "" -- set by "-q" end ------------------------------------------------------------------------ if #arg == 0 then whoami() usage() os.exit(0) end -- Logging on `musixtex.log` everything that is printed, all os calls, -- and warnings from other log files. musixlog = io.open("musixtex.log","w") if not musixlog then print"!! Can't open files for writing in current directory, aborting" os.exit(1) end ------------------------------------------------------------------------ print = function(...) orig_print(...) musixlog:write(...,"\n") -- only the first argument gets written! end local orig_remove = os.remove remove = function(filename) if exists(filename,"nolog") then musixlog:write(" removing ",filename,"\n") return orig_remove(filename) end end local orig_execute = os.execute execute = function(command) musixlog:write(" ",command,"\n") return orig_execute(command .. quiet) end function report_warnings(filename,pattern) local log = io.open(filename) if not log then print("! No file "..filename) return end for line in log:lines() do if line:match(pattern) then musixlog:write("! "..line,"\n") end end end function report_error(filename) local log = io.open(filename) if not log then print("! No file "..filename) return end local trigger = false for line in log:lines() do if trigger and line:match"^!" then -- report just the first error break end trigger = trigger or line:match"^!" if trigger then io.stderr:write("! "..line,"\n") end end end function process_option(this_arg) if this_arg == "-v" or this_arg == "-version" or this_arg == "--version" then os.exit(0) elseif this_arg == "-h" or this_arg == "-help" or this_arg == "--help" then usage() os.exit(0) elseif this_arg == "-l" then latex = true override = override .. 'l' elseif this_arg == "-p" then dvi = nil override = override .. 'p' elseif this_arg == "-d" then dvi = "dvipdfm"; ps2pdf = nil elseif this_arg == "-D" then narg = narg+1 dvi = arg[narg] elseif this_arg == "-c" then pmx = "pmxchords" elseif this_arg == "-F" then narg = narg+1 tex = arg[narg] force_engine = true elseif this_arg == "-s" then dvi = false; ps2pdf = nil; protect.dvi = true elseif this_arg == "-g" then dvi = dvips; ps2pdf = nil; protect.ps = true elseif this_arg == "-i" then cleanup = false elseif this_arg == "-x" then index = true elseif this_arg == "-1" then passes = 1 elseif this_arg == "-f" then defaults() elseif this_arg == "-t" then tex, dvi, ps2pdf = nil,nil,nil protect.tex = true elseif this_arg == "-m" then pmx, tex, dvi, ps2pdf = nil,nil,nil,nil protect.pmx = true elseif this_arg == "-M" then narg = narg+1 prepmx = arg[narg] elseif this_arg == "-A" then narg = narg+1 autosp = arg[narg] elseif this_arg == "-L" then narg = narg+1 xml2pmx = arg[narg] elseif this_arg == "-q" then if not tempname then tempname = tempname or os.tmpname() print("Redirecting screen output to "..tempname) end if dvi == dvips then dvi = dvi .. " -q" end quiet = " >> " .. tempname elseif this_arg == "-P" then narg = narg+1 ps2pdf = arg[narg] elseif this_arg == "-X" then narg = narg+1 pmx = arg[narg] else print("! Unknown option "..this_arg.." ignored") end end function find_file(this_arg) basename, extension = this_arg:match"(.*)%.(.*)" extensions = {["xml"] = true, ["mtx"] = true, ["pmx"] = true, ["aspc"] = true, ["tex"] = true, ["ltx"] = true} if extensions[extension] then return basename, extension end basename, extension = this_arg, null for ext in ("xml,mtx,pmx,aspc,tex,ltx"):gmatch"[^,]+" do if exists (basename .. "." .. ext) then extension = ext break end end if extension == null then print("!! No file " .. basename .. ".[xml|mtx|pmx|aspc|tex|ltx]") exit_code = exit_code+1 return end return basename, extension end function preprocess(basename,extension) if not (basename and extension) then return end if extension == "xml" then if execute(xml2pmx .. " " .. basename .. ".xml" .. " " .. basename .. ".pmx" ) == 0 then extension = "pmx" else print ("!! xml2pmx preprocessing of " .. basename .. ".xml fails.") return end elseif extension == "mtx" then if execute(prepmx .. " " .. basename ) == 0 then extension = "pmx" else print ("!! prepmx preprocessing of " .. basename .. ".mtx fails.") return end end if extension == "pmx" then local OK = true if pmx then local code = execute(pmx .. " " .. basename) local pmxaerr = io.open("pmxaerr.dat", "r") if (not pmxaerr) then OK = false print("!! No pmx log file.") else extension = "tex" local linebuf = pmxaerr:read() local err = tonumber(linebuf) if (code == 0) then if err ~=0 then print ("! pmx produced a warning on line "..err.. " of "..basename..".pmx") end else OK = false print ("!! pmx processing of " .. basename .. ".pmx fails.") end pmxaerr:close() end end if not OK then exit_code = exit_code+1 return end end if extension == "aspc" then if execute (autosp .. " " .. basename .. ".aspc" ) == 0 then if exists ( basename .. ".ltx") then extension = "ltx" else extension = "tex" end else print ("!! autosp preprocessing of " .. basename .. ".aspc fails.") exit_code = exit_code+1 return end end return extension end function tex_process(tex,basename,extension) if not (extension == "tex" or extension == "ltx") or not tex then return end remove(basename .. ".mx1") remove(basename .. ".mx2") local filename = basename .. "." ..extension -- .ltx extension re-selects engine only for the current file, and only -- if default processing is plain TeX local latex = latex if extension == "ltx" then if not force_engine and not latex then if dvi then tex = "latex" else tex = "pdflatex" end end latex = true end if quiet ~= "" then tex = tex .. " -interaction batchmode" end local OK = (execute(tex .. " " .. filename) == 0) if passes ~= 1 then OK = OK and (execute(musixflx .. " " .. basename) == 0) and (execute(tex .. " " .. filename) == 0) if latex and index then OK = OK and (execute("makeindex -q " .. basename) == 0) and (execute(tex .. " " .. filename) == 0) if exists (basename .. ".toc","nolog") then -- an extra line for the index will have been added OK = OK and (execute(tex .. " " .. filename) == 0) -- there is a tiny possibility that the extra line for the index -- has changed the pagination. If you feel this should not be -- ignored, here are the possibilities: -- a. Do an extra TeX pass regardless. -- b. Provide a user option -4 to force the extra pass. -- c. Compare aux files to find out. end end end if (quiet ~= "") and not OK then report_error(basename..".log") end if OK and latex then report_warnings(basename..".log","LaTeX.*Warning") end if dvi then OK = OK and (execute(dvi .. " " .. basename) == 0) end if dvi and ps2pdf then OK = OK and (execute(ps2pdf .. " " .. basename .. ".ps") == 0) if OK then print(basename .. ".pdf generated by " .. ps2pdf .. ".") end end if not OK then print("!! Processing of " .. filename .. " fails.\n") exit_code = exit_code+1 end end ---- Report version information on musixtex.log -- File names and message signatures to be looked for in a TeX log file. logtargets = { mtxtex = {"mtx%.tex","mtxTeX"}, mtxlatex = {"mtxlatex%.sty","mtxLaTeX"}, pmxtex = {"pmx%.tex","PMX"}, musixtex = {"musixtex%.tex","MusiXTeX"}, musixltx = {"musixltx%.tex","MusiXLaTeX"}, musixlyr = {"musixlyr%.tex","MusiXLYR"} } -- Signatures of messages displayed on standard output by programs capturetargets = { MTx = "This is (M%-Tx.->)", PMX = "This is (PMX[^\n]+)", pdftex = "This is (pdfTeX[^\n]+)", musixflx = "Musixflx%S+", autosp = "This is autosp.*$", index = "autosp,MTx,PMX,musixflx,pdftex" } function report_texfiles(logname) local log = logname and io.open(logname) if not log then return end local lines = {"--- TeX files actually included according to "..logname.." ---"} log = log:read"*a" -- The following pattern matches filenames input by TeX, even if -- interrupted by a line break. It may include the first word of -- an \immediate\write10 message emitted by the file. for pos,filename in log:gmatch"%(()(%.?[/]%a[%-/%.%w\n]+)" do local hit repeat local oldfilename = filename filename = oldfilename:match"[^\n]+" -- up to next line break hit = io.open(filename) -- success if the file exists if hit then break end filename = oldfilename:gsub("\n","",1) -- remove line break until filename==oldfilename if hit then for target,sig in pairs(logtargets) do if filename:match(sig[1]) then local i,j = log:find(sig[2].."[^\n]+",pos) if j then lines[#lines+1] = filename.."\n "..log:sub(i,j) end end end end end return table.concat(lines,'\n').."\n" end function report_versions(tempname) if not tempname then return end -- only available with -q local logs = io.open(tempname) if not logs then musixlog:write ("No version information: could not open "..tempname) return end local versions = {} musixlog:write("--- Programs actually executed according to "..tempname.." ---\n") for line in logs:lines() do for target in capturetargets.index:gmatch"[^,]+" do if not versions[target] then local found = line:match(capturetargets[target]) if found then versions[target] = found musixlog:write(found,"\n") end end end end logs:close() return end ------------------------------------------------------------------------ whoami() defaults() exit_code = 0 narg = 1 protect = {} -- extensions not to be deleted, even if cleanup requested repeat this_arg = arg[narg] if this_arg:match"^%-" then process_option(this_arg) else basename, extension = find_file(this_arg) -- nil,nil if not found if tex then -- tex output enabled, now select engine if tex:match"pdf" then dvi = nil end if not dvi then ps2pdf = nil end -- .ltx extension will be taken into account later, in `process` -- deduce tex/latex from current engine name if -l is not specified if not override:match"l" then latex = tex:match"latex" end if not force_engine then -- select appropriate default engine if latex then if dvi==nil then tex = "pdflatex" else tex = "latex" end else if dvi==nil then tex = "pdfetex" else tex = "etex" end end end end extension = preprocess(basename, extension) tex_process(tex,basename,extension) if basename and io.open(basename..".log") then -- to be printed later versions = report_texfiles(basename..".log") end if basename and cleanup then remove("pmxaerr.dat") for ext in ("mx1,mx2,dvi,ps,idx,log,ilg,pml"):gmatch"[^,]+" do if not protect[ext] then remove(basename.."."..ext) end end end protect = {} end narg = narg+1 until narg > #arg if versions then musixlog:write(versions) end report_versions(tempname) musixlog:close() os.exit( exit_code )