From b4f7ef9aa8b16f07d9b4c5ef73f9f8715a0c769e Mon Sep 17 00:00:00 2001 From: Karl Berry Date: Wed, 21 Aug 2019 20:40:52 +0000 Subject: cluttex (21aug19) git-svn-id: svn://tug.org/texlive/trunk@51927 c570f23f-e606-0410-a88d-b1316a301751 --- .../texlive/linked_scripts/cluttex/cluttex.lua | 327 +++++++++++++++------ Master/texmf-dist/doc/support/cluttex/CHANGELOG.md | 9 + Master/texmf-dist/doc/support/cluttex/Makefile | 1 + Master/texmf-dist/doc/support/cluttex/README.md | 7 + .../texmf-dist/doc/support/cluttex/bin/cluttex.bat | 327 +++++++++++++++------ Master/texmf-dist/doc/support/cluttex/build.lua | 4 + .../doc/support/cluttex/doc/manual-ja.pdf | Bin 462174 -> 476078 bytes .../doc/support/cluttex/doc/manual-ja.tex | 36 ++- .../texmf-dist/doc/support/cluttex/doc/manual.pdf | Bin 166892 -> 173012 bytes .../texmf-dist/doc/support/cluttex/doc/manual.tex | 35 ++- .../texmf-dist/doc/support/cluttex/src/cluttex.lua | 200 ++++++++----- .../support/cluttex/src/texrunner/handleoption.lua | 47 ++- .../doc/support/cluttex/src/texrunner/safename.lua | 62 ++++ .../support/cluttex/src/texrunner/tex_engine.lua | 16 +- Master/texmf-dist/scripts/cluttex/cluttex.lua | 327 +++++++++++++++------ 15 files changed, 1056 insertions(+), 342 deletions(-) create mode 100644 Master/texmf-dist/doc/support/cluttex/src/texrunner/safename.lua diff --git a/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua b/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua index dfbd5c5694f..a3021e48a02 100755 --- a/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua +++ b/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua @@ -692,7 +692,7 @@ return { end package.preload["texrunner.tex_engine"] = function(...) --[[ - Copyright 2016 ARATA Mizuki + Copyright 2016,2019 ARATA Mizuki This file is part of ClutTeX. @@ -719,7 +719,7 @@ local shellutil = require "texrunner.shellutil" --[[ engine.name: string engine.type = "onePass" or "twoPass" -engine:build_command(inputfile, options) +engine:build_command(inputline, options) options: halt_on_error: boolean interaction: string @@ -733,7 +733,6 @@ engine:build_command(inputfile, options) output_format: "pdf" or "dvi" draftmode: boolean (pdfTeX / XeTeX / LuaTeX) fmt: string - tex_injection: string lua_initialization_script: string (LuaTeX only) engine.executable: string engine.supports_pdf_generation: boolean @@ -745,8 +744,9 @@ engine.is_luatex: true or nil local engine_meta = {} engine_meta.__index = engine_meta engine_meta.dvi_extension = "dvi" -function engine_meta:build_command(inputfile, options) - local command = {self.executable, "-recorder"} +function engine_meta:build_command(inputline, options) + local executable = options.engine_executable or self.executable + local command = {executable, "-recorder"} if options.fmt then table.insert(command, "-fmt=" .. options.fmt) end @@ -783,11 +783,7 @@ function engine_meta:build_command(inputfile, options) table.insert(command, v) end end - if type(options.tex_injection) == "string" then - table.insert(command, shellutil.escape(options.tex_injection .. "\\input " .. inputfile)) -- TODO: what if filename contains spaces? - else - table.insert(command, shellutil.escape(inputfile)) - end + table.insert(command, shellutil.escape(inputline)) return table.concat(command, " ") end @@ -1404,6 +1400,9 @@ Options: xelatex, xetex, latex, etex, tex, platex, eptex, ptex, uplatex, euptex, uptex, + --engine-executable=COMMAND+OPTIONs + The actual TeX command to use. + [default: ENGINE] -o, --output=FILE The name of output file. [default: JOBNAME.pdf or JOBNAME.dvi] --fresh Clean intermediate files before running TeX. @@ -1419,17 +1418,24 @@ Options: --dvipdfmx-option[s]=OPTION[s] Same for dvipdfmx. --makeindex=COMMAND+OPTIONs Command to generate index, such as `makeindex' or `mendex'. - --bibtex=COMMAND+OPTIONs Command for BibTeX, such as + --bibtex=COMMAND+OPTIONs Command for BibTeX, such as `bibtex' or `pbibtex'. - --biber[=COMMAND+OPTIONs] Command for Biber. + --biber[=COMMAND+OPTIONs] Command for Biber. --makeglossaries[=COMMAND+OPTIONs] Command for makeglossaries. -h, --help Print this message and exit. -v, --version Print version information and exit. -V, --verbose Be more verbose. - --color=WHEN Make ClutTeX's message colorful. WHEN is one of - `always', `auto', or `never'. [default: auto] + --color[=WHEN] Make ClutTeX's message colorful. WHEN is one of + `always', `auto', or `never'. + [default: `auto' if --color is omitted, + `always' if WHEN is omitted] --includeonly=NAMEs Insert '\includeonly{NAMEs}'. --make-depends=FILE Write dependencies as a Makefile rule. + --print-output-directory Print the output directory and exit. + --package-support=PKG1[,PKG2,...] + Enable special support for some shell-escaping + packages. + Currently supported: minted, epstopdf --[no-]shell-escape --shell-restricted @@ -1453,6 +1459,10 @@ local option_spec = { long = "engine", param = true, }, + { + long = "engine-executable", + param = true, + }, { short = "o", long = "output", @@ -1501,6 +1511,13 @@ local option_spec = { long = "make-depends", param = true }, + { + long = "print-output-directory", + }, + { + long = "package-support", + param = true + }, -- Options for TeX { long = "synctex", @@ -1615,6 +1632,7 @@ local function handle_cluttex_options(arg) local options = { tex_extraoptions = {}, dvipdfmx_extraoptions = {}, + package_support = {}, } CLUTTEX_VERBOSITY = 0 for _,option in ipairs(option_and_params) do @@ -1625,6 +1643,10 @@ local function handle_cluttex_options(arg) assert(options.engine == nil, "multiple --engine options") options.engine = param + elseif name == "engine-executable" then + assert(options.engine_executable == nil, "multiple --engine-executable options") + options.engine_executable = param + elseif name == "output" then assert(options.output == nil, "multiple --output options") options.output = param @@ -1674,6 +1696,19 @@ local function handle_cluttex_options(arg) assert(options.make_depends == nil, "multiple --make-depends options") options.make_depends = param + elseif name == "print-output-directory" then + assert(options.print_output_directory == nil, "multiple --print-output-directory options") + options.print_output_directory = true + + elseif name == "package-support" then + local known_packages = {["minted"] = true, ["epstopdf"] = true} + for pkg in string.gmatch(param, "[^,%s]+") do + options.package_support[pkg] = true + if not known_packages[pkg] and CLUTTEX_VERBOSITY >= 1 then + message.warn("ClutTeX provides no special support for '"..pkg.."'.") + end + end + -- Options for TeX elseif name == "synctex" then assert(options.synctex == nil, "multiple --synctex options") @@ -2560,6 +2595,70 @@ return { new = new_watcher, } end +package.preload["texrunner.safename"] = function(...) +--[[ + Copyright 2019 ARATA Mizuki + + This file is part of ClutTeX. + + ClutTeX 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 3 of the License, or + (at your option) any later version. + + ClutTeX 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 ClutTeX. If not, see . +]] + +local string = string +local table = table + +local function dounsafechar(c) + if c == " " then + return "_" + else + return string.format("_%02x", c:byte(1)) + end +end + +local function escapejobname(name) + return (string.gsub(name, "[%s\"$%%&'();<>\\^`|]", dounsafechar)) +end + +local function handlespecialchar(s) + return (string.gsub(s, "[%\\%%^%{%}%~%#]", "~\\%1")) +end + +local function handlespaces(s) + return (string.gsub(s, " +", function(s) return string.rep(" ", #s, "~") end)) +end + +local function handlenonascii(s) + return (string.gsub(s, "[\x80-\xFF]+", "\\detokenize{%1}")) +end + +local function safeinput(name, engine) + local escaped = handlespaces(handlespecialchar(name)) + if engine.name == "pdftex" or engine.name == "pdflatex" then + escaped = handlenonascii(escaped) + end + if name == escaped then + return string.format("\\input\"%s\"", name) + else + return string.format("\\begingroup\\escapechar-1\\let~\\string\\edef\\x{\"%s\" }\\expandafter\\endgroup\\expandafter\\input\\x", escaped) + end +end + +return { + escapejobname = escapejobname, + safeinput = safeinput, +} +end --[[ Copyright 2016,2018-2019 ARATA Mizuki @@ -2579,7 +2678,7 @@ end along with ClutTeX. If not, see . ]] -CLUTTEX_VERSION = "v0.3" +CLUTTEX_VERSION = "v0.4" -- Standard libraries local coroutine = coroutine @@ -2598,6 +2697,7 @@ local reruncheck = require "texrunner.reruncheck" local luatexinit = require "texrunner.luatexinit" local recoverylib = require "texrunner.recovery" local message = require "texrunner.message" +local safename = require "texrunner.safename" local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options @@ -2618,7 +2718,15 @@ end local inputfile, engine, options = handle_cluttex_options(arg) -local jobname = options.jobname or pathutil.basename(pathutil.trimext(inputfile)) +local jobname_for_output +if options.jobname == nil then + local basename = pathutil.basename(pathutil.trimext(inputfile)) + options.jobname = safename.escapejobname(basename) + jobname_for_output = basename +else + jobname_for_output = options.jobname +end +local jobname = options.jobname assert(jobname ~= "", "jobname cannot be empty") if options.output_format == nil then @@ -2632,13 +2740,13 @@ else end if options.output == nil then - options.output = jobname .. "." .. output_extension + options.output = jobname_for_output .. "." .. output_extension end -- Prepare output directory if options.output_directory == nil then local inputfile_abs = pathutil.abspath(inputfile) - options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine) + options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine_executable or options.engine) if not fsutil.isdir(options.output_directory) then assert(fsutil.mkdir_rec(options.output_directory)) @@ -2658,6 +2766,12 @@ elseif options.fresh then os.exit(1) end +-- --print-output-directory +if options.print_output_directory then + io.write(options.output_directory, "\n") + os.exit(0) +end + local pathsep = ":" if os.type == "windows" then pathsep = ";" @@ -2696,6 +2810,7 @@ local recorderfile = path_in_output_directory("fls") local recorderfile2 = path_in_output_directory("cluttex-fls") local tex_options = { + engine_executable = options.engine_executable, interaction = options.interaction, file_line_error = options.file_line_error, halt_on_error = options.halt_on_error, @@ -2722,7 +2837,7 @@ end -- should_rerun, newauxstatus = single_run([auxstatus]) -- This function should be run in a coroutine. local function single_run(auxstatus, iteration) - local minted = false + local minted, epstopdf = false, false local bibtex_aux_hash = nil local mainauxfile = path_in_output_directory("aux") if fsutil.isfile(recorderfile) then @@ -2735,7 +2850,9 @@ local function single_run(auxstatus, iteration) for _,fileinfo in ipairs(filelist) do if string.match(fileinfo.path, "minted/minted%.sty$") then minted = true - break + end + if string.match(fileinfo.path, "epstopdf%.sty$") then + epstopdf = true end end if options.bibtex then @@ -2754,14 +2871,38 @@ local function single_run(auxstatus, iteration) end --local timestamp = os.time() + local tex_injection = "" + if options.includeonly then - tex_options.tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly) + tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly) end - if minted and not (tex_options.tex_injection and string.find(tex_options.tex_injection,"minted") == nil) then - tex_options.tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_options.tex_injection or "", options.output_directory) + if minted or options.package_support["minted"] then + local outdir = options.output_directory + if os.type == "windows" then + outdir = string.gsub(outdir, "\\", "/") -- Use forward slashes + end + tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_injection or "", outdir) + if not options.package_support["minted"] then + message.diag("You may want to use --package-support=minted option.") + end + end + if epstopdf or options.package_support["epstopdf"] then + local outdir = options.output_directory + if os.type == "windows" then + outdir = string.gsub(outdir, "\\", "/") -- Use forward slashes + end + if string.sub(outdir, -1, -1) ~= "/" then + outdir = outdir.."/" -- Must end with a directory separator + end + tex_injection = string.format("%s\\PassOptionsToPackage{outdir=%s}{epstopdf}", tex_injection or "", outdir) + if not options.package_support["epstopdf"] then + message.diag("You may want to use --package-support=epstopdf option.") + end end + local inputline = tex_injection .. safename.safeinput(inputfile, engine) + local current_tex_options, lightweight_mode = tex_options, false if iteration == 1 and options.start_with_draft then current_tex_options = {} @@ -2778,7 +2919,7 @@ local function single_run(auxstatus, iteration) current_tex_options.draftmode = false end - local command = engine:build_command(inputfile, current_tex_options) + local command = engine:build_command(inputline, current_tex_options) local execlog -- the contents of .log file @@ -3048,18 +3189,7 @@ end if options.watch then -- Watch mode - local success, status = do_typeset() - -- TODO: filenames here can be UTF-8 if command_line_encoding=utf-8 - local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) - if engine.is_luatex and fsutil.isfile(recorderfile2) then - filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) - end - local input_files_to_watch = {} - for _,fileinfo in ipairs(filelist) do - if fileinfo.kind == "input" then - table.insert(input_files_to_watch, fileinfo.abspath) - end - end + local fswatcherlib if os.type == "windows" then -- Windows: Try built-in filesystem watcher @@ -3069,73 +3199,71 @@ if options.watch then end fswatcherlib = result end + + local do_watch if fswatcherlib then if CLUTTEX_VERBOSITY >= 2 then message.info("Using built-in filesystem watcher for Windows") end - local watcher = assert(fswatcherlib.new()) - for _,path in ipairs(input_files_to_watch) do - assert(watcher:add_file(path)) - end - while true do + do_watch = function(files) + local watcher = assert(fswatcherlib.new()) + for _,path in ipairs(files) do + assert(watcher:add_file(path)) + end local result = assert(watcher:next()) if CLUTTEX_VERBOSITY >= 2 then - message.info(string.format("%s %s"), result.action, result.path) - end - local success, status = do_typeset() - if not success then - -- Not successful + message.info(string.format("%s %s", result.action, result.path)) end + watcher:close() + return true end elseif shellutil.has_command("fswatch") then - local fswatch_command = {"fswatch", "--event=Updated", "--"} - for _,path in ipairs(input_files_to_watch) do - table.insert(fswatch_command, shellutil.escape(path)) + if CLUTTEX_VERBOSITY >= 2 then + message.info("Using `fswatch' command") end - local fswatch_command_str = table.concat(fswatch_command, " ") - if CLUTTEX_VERBOSITY >= 1 then - message.exec(fswatch_command_str) - end - local fswatch = assert(io.popen(fswatch_command_str, "r")) - for l in fswatch:lines() do - local found = false - for _,path in ipairs(input_files_to_watch) do - if l == path then - found = true - break - end + do_watch = function(files) + local fswatch_command = {"fswatch", "--one-event", "--event=Updated", "--"} + for _,path in ipairs(files) do + table.insert(fswatch_command, shellutil.escape(path)) + end + local fswatch_command_str = table.concat(fswatch_command, " ") + if CLUTTEX_VERBOSITY >= 1 then + message.exec(fswatch_command_str) end - if found then - local success, status = do_typeset() - if not success then - -- Not successful + local fswatch = assert(io.popen(fswatch_command_str, "r")) + for l in fswatch:lines() do + for _,path in ipairs(files) do + if l == path then + fswatch:close() + return true + end end end + return false end elseif shellutil.has_command("inotifywait") then - local inotifywait_command = {"inotifywait", "--monitor", "--event=modify", "--event=attrib", "--format=%w", "--quiet"} - for _,path in ipairs(input_files_to_watch) do - table.insert(inotifywait_command, shellutil.escape(path)) + if CLUTTEX_VERBOSITY >= 2 then + message.info("Using `inotifywait' command") end - local inotifywait_command_str = table.concat(inotifywait_command, " ") - if CLUTTEX_VERBOSITY >= 1 then - message.exec(inotifywait_command_str) - end - local inotifywait = assert(io.popen(inotifywait_command_str, "r")) - for l in inotifywait:lines() do - local found = false - for _,path in ipairs(input_files_to_watch) do - if l == path then - found = true - break - end + do_watch = function(files) + local inotifywait_command = {"inotifywait", "--event=modify", "--event=attrib", "--format=%w", "--quiet"} + for _,path in ipairs(files) do + table.insert(inotifywait_command, shellutil.escape(path)) + end + local inotifywait_command_str = table.concat(inotifywait_command, " ") + if CLUTTEX_VERBOSITY >= 1 then + message.exec(inotifywait_command_str) end - if found then - local success, status = do_typeset() - if not success then - -- Not successful + local inotifywait = assert(io.popen(inotifywait_command_str, "r")) + for l in inotifywait:lines() do + for _,path in ipairs(files) do + if l == path then + inotifywait:close() + return true + end end end + return false end else message.error("Could not watch files because neither `fswatch' nor `inotifywait' was installed.") @@ -3143,6 +3271,37 @@ if options.watch then os.exit(1) end + local success, status = do_typeset() + -- TODO: filenames here can be UTF-8 if command_line_encoding=utf-8 + local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) + if engine.is_luatex and fsutil.isfile(recorderfile2) then + filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) + end + local input_files_to_watch = {} + for _,fileinfo in ipairs(filelist) do + if fileinfo.kind == "input" then + table.insert(input_files_to_watch, fileinfo.abspath) + end + end + + while do_watch(input_files_to_watch) do + local success, status = do_typeset() + if not success then + -- error + else + local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) + if engine.is_luatex and fsutil.isfile(recorderfile2) then + filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) + end + input_files_to_watch = {} + for _,fileinfo in ipairs(filelist) do + if fileinfo.kind == "input" then + table.insert(input_files_to_watch, fileinfo.abspath) + end + end + end + end + else -- Not in watch mode local success, status = do_typeset() diff --git a/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md b/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md index c76e6189391..b07933de056 100644 --- a/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md +++ b/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md @@ -1,3 +1,12 @@ +Version 0.4 (2019-08-21) +----- + +Changes: + +* New options: `--print-output-directory`, `--package-support`, and `--engine-executable` +* Spaces and special characters in the input file name are now appropriately escaped. For example, `cluttex -e pdflatex file%1.tex` now typesets the file `file%1.tex`. +* Watch new input files in watch mode. + Version 0.3 (2019-04-30) ----- diff --git a/Master/texmf-dist/doc/support/cluttex/Makefile b/Master/texmf-dist/doc/support/cluttex/Makefile index c588900ab23..ef12dd5131c 100644 --- a/Master/texmf-dist/doc/support/cluttex/Makefile +++ b/Master/texmf-dist/doc/support/cluttex/Makefile @@ -20,6 +20,7 @@ sources= \ src/texrunner/isatty.lua \ src/texrunner/message.lua \ src/texrunner/fswatcher_windows.lua \ + src/texrunner/safename.lua \ src/cluttex.lua bin/cluttex: $(sources) build.lua diff --git a/Master/texmf-dist/doc/support/cluttex/README.md b/Master/texmf-dist/doc/support/cluttex/README.md index 67354e40eed..3ed578337ec 100644 --- a/Master/texmf-dist/doc/support/cluttex/README.md +++ b/Master/texmf-dist/doc/support/cluttex/README.md @@ -83,6 +83,13 @@ Command-line Options Print version information and exit. * `-V`, `--verbose` Be more verbose. +* `--print-output-directory` + Print the output directory and exit. +* `--package-support=PKG1[,PKG2,...,PKGn]` + Enable special support for shell-escaping packages. + Currently supported packages are `minted` and `epstopdf`. +* `--engine-executable=COMMAND` + The actual TeX command to use. Options to run auxiliary programs: diff --git a/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat b/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat index 7570923a7e7..5779e698db5 100755 --- a/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat +++ b/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat @@ -695,7 +695,7 @@ return { end package.preload["texrunner.tex_engine"] = function(...) --[[ - Copyright 2016 ARATA Mizuki + Copyright 2016,2019 ARATA Mizuki This file is part of ClutTeX. @@ -722,7 +722,7 @@ local shellutil = require "texrunner.shellutil" --[[ engine.name: string engine.type = "onePass" or "twoPass" -engine:build_command(inputfile, options) +engine:build_command(inputline, options) options: halt_on_error: boolean interaction: string @@ -736,7 +736,6 @@ engine:build_command(inputfile, options) output_format: "pdf" or "dvi" draftmode: boolean (pdfTeX / XeTeX / LuaTeX) fmt: string - tex_injection: string lua_initialization_script: string (LuaTeX only) engine.executable: string engine.supports_pdf_generation: boolean @@ -748,8 +747,9 @@ engine.is_luatex: true or nil local engine_meta = {} engine_meta.__index = engine_meta engine_meta.dvi_extension = "dvi" -function engine_meta:build_command(inputfile, options) - local command = {self.executable, "-recorder"} +function engine_meta:build_command(inputline, options) + local executable = options.engine_executable or self.executable + local command = {executable, "-recorder"} if options.fmt then table.insert(command, "-fmt=" .. options.fmt) end @@ -786,11 +786,7 @@ function engine_meta:build_command(inputfile, options) table.insert(command, v) end end - if type(options.tex_injection) == "string" then - table.insert(command, shellutil.escape(options.tex_injection .. "\\input " .. inputfile)) -- TODO: what if filename contains spaces? - else - table.insert(command, shellutil.escape(inputfile)) - end + table.insert(command, shellutil.escape(inputline)) return table.concat(command, " ") end @@ -1407,6 +1403,9 @@ Options: xelatex, xetex, latex, etex, tex, platex, eptex, ptex, uplatex, euptex, uptex, + --engine-executable=COMMAND+OPTIONs + The actual TeX command to use. + [default: ENGINE] -o, --output=FILE The name of output file. [default: JOBNAME.pdf or JOBNAME.dvi] --fresh Clean intermediate files before running TeX. @@ -1422,17 +1421,24 @@ Options: --dvipdfmx-option[s]=OPTION[s] Same for dvipdfmx. --makeindex=COMMAND+OPTIONs Command to generate index, such as `makeindex' or `mendex'. - --bibtex=COMMAND+OPTIONs Command for BibTeX, such as + --bibtex=COMMAND+OPTIONs Command for BibTeX, such as `bibtex' or `pbibtex'. - --biber[=COMMAND+OPTIONs] Command for Biber. + --biber[=COMMAND+OPTIONs] Command for Biber. --makeglossaries[=COMMAND+OPTIONs] Command for makeglossaries. -h, --help Print this message and exit. -v, --version Print version information and exit. -V, --verbose Be more verbose. - --color=WHEN Make ClutTeX's message colorful. WHEN is one of - `always', `auto', or `never'. [default: auto] + --color[=WHEN] Make ClutTeX's message colorful. WHEN is one of + `always', `auto', or `never'. + [default: `auto' if --color is omitted, + `always' if WHEN is omitted] --includeonly=NAMEs Insert '\includeonly{NAMEs}'. --make-depends=FILE Write dependencies as a Makefile rule. + --print-output-directory Print the output directory and exit. + --package-support=PKG1[,PKG2,...] + Enable special support for some shell-escaping + packages. + Currently supported: minted, epstopdf --[no-]shell-escape --shell-restricted @@ -1456,6 +1462,10 @@ local option_spec = { long = "engine", param = true, }, + { + long = "engine-executable", + param = true, + }, { short = "o", long = "output", @@ -1504,6 +1514,13 @@ local option_spec = { long = "make-depends", param = true }, + { + long = "print-output-directory", + }, + { + long = "package-support", + param = true + }, -- Options for TeX { long = "synctex", @@ -1618,6 +1635,7 @@ local function handle_cluttex_options(arg) local options = { tex_extraoptions = {}, dvipdfmx_extraoptions = {}, + package_support = {}, } CLUTTEX_VERBOSITY = 0 for _,option in ipairs(option_and_params) do @@ -1628,6 +1646,10 @@ local function handle_cluttex_options(arg) assert(options.engine == nil, "multiple --engine options") options.engine = param + elseif name == "engine-executable" then + assert(options.engine_executable == nil, "multiple --engine-executable options") + options.engine_executable = param + elseif name == "output" then assert(options.output == nil, "multiple --output options") options.output = param @@ -1677,6 +1699,19 @@ local function handle_cluttex_options(arg) assert(options.make_depends == nil, "multiple --make-depends options") options.make_depends = param + elseif name == "print-output-directory" then + assert(options.print_output_directory == nil, "multiple --print-output-directory options") + options.print_output_directory = true + + elseif name == "package-support" then + local known_packages = {["minted"] = true, ["epstopdf"] = true} + for pkg in string.gmatch(param, "[^,%s]+") do + options.package_support[pkg] = true + if not known_packages[pkg] and CLUTTEX_VERBOSITY >= 1 then + message.warn("ClutTeX provides no special support for '"..pkg.."'.") + end + end + -- Options for TeX elseif name == "synctex" then assert(options.synctex == nil, "multiple --synctex options") @@ -2563,6 +2598,70 @@ return { new = new_watcher, } end +package.preload["texrunner.safename"] = function(...) +--[[ + Copyright 2019 ARATA Mizuki + + This file is part of ClutTeX. + + ClutTeX 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 3 of the License, or + (at your option) any later version. + + ClutTeX 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 ClutTeX. If not, see . +]] + +local string = string +local table = table + +local function dounsafechar(c) + if c == " " then + return "_" + else + return string.format("_%02x", c:byte(1)) + end +end + +local function escapejobname(name) + return (string.gsub(name, "[%s\"$%%&'();<>\\^`|]", dounsafechar)) +end + +local function handlespecialchar(s) + return (string.gsub(s, "[%\\%%^%{%}%~%#]", "~\\%1")) +end + +local function handlespaces(s) + return (string.gsub(s, " +", function(s) return string.rep(" ", #s, "~") end)) +end + +local function handlenonascii(s) + return (string.gsub(s, "[\x80-\xFF]+", "\\detokenize{%1}")) +end + +local function safeinput(name, engine) + local escaped = handlespaces(handlespecialchar(name)) + if engine.name == "pdftex" or engine.name == "pdflatex" then + escaped = handlenonascii(escaped) + end + if name == escaped then + return string.format("\\input\"%s\"", name) + else + return string.format("\\begingroup\\escapechar-1\\let~\\string\\edef\\x{\"%s\" }\\expandafter\\endgroup\\expandafter\\input\\x", escaped) + end +end + +return { + escapejobname = escapejobname, + safeinput = safeinput, +} +end --[[ Copyright 2016,2018-2019 ARATA Mizuki @@ -2582,7 +2681,7 @@ end along with ClutTeX. If not, see . ]] -CLUTTEX_VERSION = "v0.3" +CLUTTEX_VERSION = "v0.4" -- Standard libraries local coroutine = coroutine @@ -2601,6 +2700,7 @@ local reruncheck = require "texrunner.reruncheck" local luatexinit = require "texrunner.luatexinit" local recoverylib = require "texrunner.recovery" local message = require "texrunner.message" +local safename = require "texrunner.safename" local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options @@ -2621,7 +2721,15 @@ end local inputfile, engine, options = handle_cluttex_options(arg) -local jobname = options.jobname or pathutil.basename(pathutil.trimext(inputfile)) +local jobname_for_output +if options.jobname == nil then + local basename = pathutil.basename(pathutil.trimext(inputfile)) + options.jobname = safename.escapejobname(basename) + jobname_for_output = basename +else + jobname_for_output = options.jobname +end +local jobname = options.jobname assert(jobname ~= "", "jobname cannot be empty") if options.output_format == nil then @@ -2635,13 +2743,13 @@ else end if options.output == nil then - options.output = jobname .. "." .. output_extension + options.output = jobname_for_output .. "." .. output_extension end -- Prepare output directory if options.output_directory == nil then local inputfile_abs = pathutil.abspath(inputfile) - options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine) + options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine_executable or options.engine) if not fsutil.isdir(options.output_directory) then assert(fsutil.mkdir_rec(options.output_directory)) @@ -2661,6 +2769,12 @@ elseif options.fresh then os.exit(1) end +-- --print-output-directory +if options.print_output_directory then + io.write(options.output_directory, "\n") + os.exit(0) +end + local pathsep = ":" if os.type == "windows" then pathsep = ";" @@ -2699,6 +2813,7 @@ local recorderfile = path_in_output_directory("fls") local recorderfile2 = path_in_output_directory("cluttex-fls") local tex_options = { + engine_executable = options.engine_executable, interaction = options.interaction, file_line_error = options.file_line_error, halt_on_error = options.halt_on_error, @@ -2725,7 +2840,7 @@ end -- should_rerun, newauxstatus = single_run([auxstatus]) -- This function should be run in a coroutine. local function single_run(auxstatus, iteration) - local minted = false + local minted, epstopdf = false, false local bibtex_aux_hash = nil local mainauxfile = path_in_output_directory("aux") if fsutil.isfile(recorderfile) then @@ -2738,7 +2853,9 @@ local function single_run(auxstatus, iteration) for _,fileinfo in ipairs(filelist) do if string.match(fileinfo.path, "minted/minted%.sty$") then minted = true - break + end + if string.match(fileinfo.path, "epstopdf%.sty$") then + epstopdf = true end end if options.bibtex then @@ -2757,14 +2874,38 @@ local function single_run(auxstatus, iteration) end --local timestamp = os.time() + local tex_injection = "" + if options.includeonly then - tex_options.tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly) + tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly) end - if minted and not (tex_options.tex_injection and string.find(tex_options.tex_injection,"minted") == nil) then - tex_options.tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_options.tex_injection or "", options.output_directory) + if minted or options.package_support["minted"] then + local outdir = options.output_directory + if os.type == "windows" then + outdir = string.gsub(outdir, "\\", "/") -- Use forward slashes + end + tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_injection or "", outdir) + if not options.package_support["minted"] then + message.diag("You may want to use --package-support=minted option.") + end + end + if epstopdf or options.package_support["epstopdf"] then + local outdir = options.output_directory + if os.type == "windows" then + outdir = string.gsub(outdir, "\\", "/") -- Use forward slashes + end + if string.sub(outdir, -1, -1) ~= "/" then + outdir = outdir.."/" -- Must end with a directory separator + end + tex_injection = string.format("%s\\PassOptionsToPackage{outdir=%s}{epstopdf}", tex_injection or "", outdir) + if not options.package_support["epstopdf"] then + message.diag("You may want to use --package-support=epstopdf option.") + end end + local inputline = tex_injection .. safename.safeinput(inputfile, engine) + local current_tex_options, lightweight_mode = tex_options, false if iteration == 1 and options.start_with_draft then current_tex_options = {} @@ -2781,7 +2922,7 @@ local function single_run(auxstatus, iteration) current_tex_options.draftmode = false end - local command = engine:build_command(inputfile, current_tex_options) + local command = engine:build_command(inputline, current_tex_options) local execlog -- the contents of .log file @@ -3051,18 +3192,7 @@ end if options.watch then -- Watch mode - local success, status = do_typeset() - -- TODO: filenames here can be UTF-8 if command_line_encoding=utf-8 - local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) - if engine.is_luatex and fsutil.isfile(recorderfile2) then - filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) - end - local input_files_to_watch = {} - for _,fileinfo in ipairs(filelist) do - if fileinfo.kind == "input" then - table.insert(input_files_to_watch, fileinfo.abspath) - end - end + local fswatcherlib if os.type == "windows" then -- Windows: Try built-in filesystem watcher @@ -3072,73 +3202,71 @@ if options.watch then end fswatcherlib = result end + + local do_watch if fswatcherlib then if CLUTTEX_VERBOSITY >= 2 then message.info("Using built-in filesystem watcher for Windows") end - local watcher = assert(fswatcherlib.new()) - for _,path in ipairs(input_files_to_watch) do - assert(watcher:add_file(path)) - end - while true do + do_watch = function(files) + local watcher = assert(fswatcherlib.new()) + for _,path in ipairs(files) do + assert(watcher:add_file(path)) + end local result = assert(watcher:next()) if CLUTTEX_VERBOSITY >= 2 then - message.info(string.format("%s %s"), result.action, result.path) - end - local success, status = do_typeset() - if not success then - -- Not successful + message.info(string.format("%s %s", result.action, result.path)) end + watcher:close() + return true end elseif shellutil.has_command("fswatch") then - local fswatch_command = {"fswatch", "--event=Updated", "--"} - for _,path in ipairs(input_files_to_watch) do - table.insert(fswatch_command, shellutil.escape(path)) + if CLUTTEX_VERBOSITY >= 2 then + message.info("Using `fswatch' command") end - local fswatch_command_str = table.concat(fswatch_command, " ") - if CLUTTEX_VERBOSITY >= 1 then - message.exec(fswatch_command_str) - end - local fswatch = assert(io.popen(fswatch_command_str, "r")) - for l in fswatch:lines() do - local found = false - for _,path in ipairs(input_files_to_watch) do - if l == path then - found = true - break - end + do_watch = function(files) + local fswatch_command = {"fswatch", "--one-event", "--event=Updated", "--"} + for _,path in ipairs(files) do + table.insert(fswatch_command, shellutil.escape(path)) + end + local fswatch_command_str = table.concat(fswatch_command, " ") + if CLUTTEX_VERBOSITY >= 1 then + message.exec(fswatch_command_str) end - if found then - local success, status = do_typeset() - if not success then - -- Not successful + local fswatch = assert(io.popen(fswatch_command_str, "r")) + for l in fswatch:lines() do + for _,path in ipairs(files) do + if l == path then + fswatch:close() + return true + end end end + return false end elseif shellutil.has_command("inotifywait") then - local inotifywait_command = {"inotifywait", "--monitor", "--event=modify", "--event=attrib", "--format=%w", "--quiet"} - for _,path in ipairs(input_files_to_watch) do - table.insert(inotifywait_command, shellutil.escape(path)) + if CLUTTEX_VERBOSITY >= 2 then + message.info("Using `inotifywait' command") end - local inotifywait_command_str = table.concat(inotifywait_command, " ") - if CLUTTEX_VERBOSITY >= 1 then - message.exec(inotifywait_command_str) - end - local inotifywait = assert(io.popen(inotifywait_command_str, "r")) - for l in inotifywait:lines() do - local found = false - for _,path in ipairs(input_files_to_watch) do - if l == path then - found = true - break - end + do_watch = function(files) + local inotifywait_command = {"inotifywait", "--event=modify", "--event=attrib", "--format=%w", "--quiet"} + for _,path in ipairs(files) do + table.insert(inotifywait_command, shellutil.escape(path)) + end + local inotifywait_command_str = table.concat(inotifywait_command, " ") + if CLUTTEX_VERBOSITY >= 1 then + message.exec(inotifywait_command_str) end - if found then - local success, status = do_typeset() - if not success then - -- Not successful + local inotifywait = assert(io.popen(inotifywait_command_str, "r")) + for l in inotifywait:lines() do + for _,path in ipairs(files) do + if l == path then + inotifywait:close() + return true + end end end + return false end else message.error("Could not watch files because neither `fswatch' nor `inotifywait' was installed.") @@ -3146,6 +3274,37 @@ if options.watch then os.exit(1) end + local success, status = do_typeset() + -- TODO: filenames here can be UTF-8 if command_line_encoding=utf-8 + local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) + if engine.is_luatex and fsutil.isfile(recorderfile2) then + filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) + end + local input_files_to_watch = {} + for _,fileinfo in ipairs(filelist) do + if fileinfo.kind == "input" then + table.insert(input_files_to_watch, fileinfo.abspath) + end + end + + while do_watch(input_files_to_watch) do + local success, status = do_typeset() + if not success then + -- error + else + local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) + if engine.is_luatex and fsutil.isfile(recorderfile2) then + filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) + end + input_files_to_watch = {} + for _,fileinfo in ipairs(filelist) do + if fileinfo.kind == "input" then + table.insert(input_files_to_watch, fileinfo.abspath) + end + end + end + end + else -- Not in watch mode local success, status = do_typeset() diff --git a/Master/texmf-dist/doc/support/cluttex/build.lua b/Master/texmf-dist/doc/support/cluttex/build.lua index 24d0674ffc9..20c58eaebd7 100644 --- a/Master/texmf-dist/doc/support/cluttex/build.lua +++ b/Master/texmf-dist/doc/support/cluttex/build.lua @@ -87,6 +87,10 @@ local modules = { name = "texrunner.fswatcher_windows", path = "texrunner/fswatcher_windows.lua", }, + { + name = "texrunner.safename", + path = "texrunner/safename.lua", + }, } local imported_globals = {"io", "os", "string", "table", "package", "require", "assert", "error", "ipairs", "type", "select", "arg"} diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf index 43292492359..72689f7f3e9 100644 Binary files a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf and b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf differ diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex index cd1a47741a2..b89675888f4 100644 --- a/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex +++ b/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex @@ -10,9 +10,9 @@ \renewcommand\sectionautorefname{セクション} \renewcommand\subsectionautorefname{サブセクション} -\title{\ClutTeX{}マニュアル\\(バージョン0.3)} +\title{\ClutTeX{}マニュアル\\(バージョン0.4)} \author{ARATA Mizuki} -\date{2019年4月30日} +\date{2019年8月21日} \begin{document} \maketitle @@ -82,6 +82,8 @@ \texttt{\texcmd{includeonly}\{\metavar{NAMEs}\}}を挿入する。 \item[\texttt{--make-depends=\metavar{FILE}}] Makefile用の依存関係を\metavar{FILE}に書き出す。 +\item[\texttt{--engine-executable=\metavar{COMMAND}}] + 実際に使う\TeX{}コマンドを指定する。 \item[\texttt{--tex-option=\metavar{OPTION}}, \texttt{--tex-options=\metavar{OPTIONs}}] \TeX{}に追加のオプションを渡す。 \item[\texttt{--dvipdfmx-option=\metavar{OPTION}}, \texttt{--dvipdfmx-options=\metavar{OPTIONs}}] @@ -92,6 +94,11 @@ \item[\texttt{-h}, \texttt{--help}] \item[\texttt{-v}, \texttt{--version}] \item[\texttt{-V}, \texttt{--verbose}] +\item[\texttt{--print-output-directory}] + \texttt{--output-directory}の値を標準出力に出力して、そのまま終了する。 +\item[\texttt{--package-support=PKG1[,PKG2,...,PKGn]}] + 外部コマンドを実行するパッケージ用の個別の対策を有効にする。 + 現在のところ、\texttt{minted}と\texttt{epstopdf}に対応している。 \end{description} 補助コマンド実行用のオプション: @@ -218,8 +225,16 @@ main.pdf: main.tex \item \texttt{--makeindex}, \texttt{--bibtex}, \texttt{--biber}, \texttt{--makeglossaries} \end{itemize} +もし何らかの事情で自動生成された出力ディレクトリの位置を知りたければ、\ClutTeX{}を\texttt{--print-output-directory}オプションを使うとよい。 +例えば、Makefileの\texttt{clean}ターゲットは次のように書ける: +\begin{verbatim} +clean: + -rm -rf $(shell cluttex -e pdflatex --print-output-directory main.tex) + -rm main.pdf +\end{verbatim} + 出力ディレクトリに生成された補助ファイルは、\texttt{--fresh}オプションを指定しない限り、\ClutTeX{}が消去することはない。 -一方、テンポラリディレクトリを使用するということは、PCの再起動時に補助ファイルが削除されるということでもある。 +一方、テンポラリディレクトリを使用するということは、PCの再起動時に補助ファイルが削除される可能性があるということでもある。 \section{エイリアス} Unix用コマンドの中には、自身の名前によって挙動を変えるものがある。 @@ -235,4 +250,19 @@ Unix用コマンドの中には、自身の名前によって挙動を変える 例えば、\texttt{cllualatex}は\texttt{cluttex --engine lualatex}の別名であり、\texttt{clxelatex}は\texttt{cluttex --engine xelatex}の別名である。 +\section{\texpkg{minted}と\texpkg{epstopdf}への対策} +一般に、外部コマンド実行(シェルエスケープ)を行うパッケージは\texttt{-output-directory}を指定した際に正常に動作しない。 +したがって、\ClutTeX{}の下ではそういうパッケージはうまく動かない。 + +一方で、パッケージによっては\texttt{-output-directory}の値を指示するためのパッケージオプションを持っているものがある。 +例えば、\texpkg{minted}の\texttt{outputdir}オプション、\texpkg{epstopdf}の\texttt{outdir}オプションがそれである。 + +\ClutTeX{}からこれらのパッケージオプションを指定することはできるが、そのためには使用するパッケージを\ClutTeX{}が事前に知っておかねばならない。 +使用するパッケージを\ClutTeX{}に知らせるには、\texttt{--package-support}オプションを使う。 + +例えば、\texpkg{minted}を使う文書を処理する場合は次のように実行すれば良い: +\begin{verbatim} +cluttex -e pdflatex --shell-escape --package-support=minted document.tex +\end{verbatim} + \end{document} diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf b/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf index 7e3e6dd6f72..089181455ec 100644 Binary files a/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf and b/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf differ diff --git a/Master/texmf-dist/doc/support/cluttex/doc/manual.tex b/Master/texmf-dist/doc/support/cluttex/doc/manual.tex index 22761006cf1..caf99053eb0 100644 --- a/Master/texmf-dist/doc/support/cluttex/doc/manual.tex +++ b/Master/texmf-dist/doc/support/cluttex/doc/manual.tex @@ -8,9 +8,9 @@ \newcommand\texpkg[1]{\texttt{#1}} \newcommand\metavar[1]{\textnormal{\textsf{#1}}} -\title{\ClutTeX\ manual\\(Version 0.3)} +\title{\ClutTeX\ manual\\(Version 0.4)} \author{ARATA Mizuki} -\date{2019-04-30} +\date{2019-08-21} \begin{document} \maketitle @@ -78,6 +78,8 @@ Basic options: Insert \texttt{\texcmd{includeonly}\{\metavar{NAMEs}\}}. \item[\texttt{--make-depends=\metavar{FILE}}] Write Makefile-style dependencies information to \metavar{FILE}. +\item[\texttt{--engine-executable=\metavar{COMMAND}}] + The actual \TeX\ command to use. \item[\texttt{--tex-option=\metavar{OPTION}}, \texttt{--tex-options=\metavar{OPTIONs}}] Pass extra options to \TeX. \item[\texttt{--dvipdfmx-option=\metavar{OPTION}}, \texttt{--dvipdfmx-options=\metavar{OPTIONs}}] @@ -88,6 +90,11 @@ Basic options: \item[\texttt{-h}, \texttt{--help}] \item[\texttt{-v}, \texttt{--version}] \item[\texttt{-V}, \texttt{--verbose}] +\item[\texttt{--print-output-directory}] + Print the output directory and exit. +\item[\texttt{--package-support=PKG1[,PKG2,...,PKGn]}] + Enable special support for shell-escaping packages. + Currently supported packages are `minted` and `epstopdf`. \end{description} Options for running auxiliary programs: @@ -212,6 +219,13 @@ On the other hand, the following parameters doesn't affect the directory name: \item \texttt{--makeindex}, \texttt{--bibtex}, \texttt{--biber}, \texttt{--makeglossaries} \end{itemize} +If you need to know the exact location of the automatically-generated output directory, you can invoke \ClutTeX\ with \texttt{--print-output-directory}. +For example, \texttt{clean} target of your Makefile could be written as: +\begin{verbatim} +clean: + -rm -rf $(shell cluttex -e pdflatex --print-output-directory main.tex) +\end{verbatim} + \ClutTeX\ itself doesn't erase the auxiliary files, unless \texttt{--fresh} option is set. Note that, the use of a temporary directory means, the auxiliary files may be cleared when the computer is rebooted. @@ -226,6 +240,21 @@ There are several examples in \TeX\ Live: If \ClutTeX\ is called as \texttt{cl}\(\langle\text{\metavar{ENGINE}}\rangle\), the \texttt{--engine} option is set accordingly. For example, \texttt{cllualatex} is an alias for \texttt{cluttex --engine lualatex} and \texttt{clxelatex} for \texttt{cluttex --engine xelatex}. -%The aliases provided by \TeX\ Live are, \texttt{cllualatex} and \texttt{clxelatex}. +% The aliases provided by \TeX\ Live are, \texttt{cllualatex} and \texttt{clxelatex}. + +\section{Support for \texpkg{minted} and \texpkg{epstopdf}} +In general, packages that execute external commands (shell-escape) don't work well with \texttt{-output-directory}. +Therefore, they don't work well with \ClutTeX. + +However, some packages provide a package option to let them know the location of \texttt{-output-directory}. +For example, \texpkg{minted} provides \texttt{outputdir}, and \texpkg{epstopdf} provides \texttt{outdir}. + +\ClutTeX\ can supply them the appropriate options, but only if it knows that the package is going to be used. +To let \ClutTeX\ what packages are going to be used, use \texttt{--package-support} option. + +For example, if you want to typeset a document that uses \texpkg{minted}, run the following: +\begin{verbatim} +cluttex -e pdflatex --shell-escape --package-support=minted document.tex +\end{verbatim} \end{document} diff --git a/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua b/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua index 92952af58df..0ac50f90f77 100644 --- a/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua +++ b/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua @@ -18,7 +18,7 @@ along with ClutTeX. If not, see . ]] -CLUTTEX_VERSION = "v0.3" +CLUTTEX_VERSION = "v0.4" -- Standard libraries local table = table @@ -42,6 +42,7 @@ local reruncheck = require "texrunner.reruncheck" local luatexinit = require "texrunner.luatexinit" local recoverylib = require "texrunner.recovery" local message = require "texrunner.message" +local safename = require "texrunner.safename" local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options @@ -62,7 +63,15 @@ end local inputfile, engine, options = handle_cluttex_options(arg) -local jobname = options.jobname or pathutil.basename(pathutil.trimext(inputfile)) +local jobname_for_output +if options.jobname == nil then + local basename = pathutil.basename(pathutil.trimext(inputfile)) + options.jobname = safename.escapejobname(basename) + jobname_for_output = basename +else + jobname_for_output = options.jobname +end +local jobname = options.jobname assert(jobname ~= "", "jobname cannot be empty") if options.output_format == nil then @@ -76,13 +85,13 @@ else end if options.output == nil then - options.output = jobname .. "." .. output_extension + options.output = jobname_for_output .. "." .. output_extension end -- Prepare output directory if options.output_directory == nil then local inputfile_abs = pathutil.abspath(inputfile) - options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine) + options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine_executable or options.engine) if not fsutil.isdir(options.output_directory) then assert(fsutil.mkdir_rec(options.output_directory)) @@ -102,6 +111,12 @@ elseif options.fresh then os.exit(1) end +-- --print-output-directory +if options.print_output_directory then + io.write(options.output_directory, "\n") + os.exit(0) +end + local pathsep = ":" if os.type == "windows" then pathsep = ";" @@ -140,6 +155,7 @@ local recorderfile = path_in_output_directory("fls") local recorderfile2 = path_in_output_directory("cluttex-fls") local tex_options = { + engine_executable = options.engine_executable, interaction = options.interaction, file_line_error = options.file_line_error, halt_on_error = options.halt_on_error, @@ -166,7 +182,7 @@ end -- should_rerun, newauxstatus = single_run([auxstatus]) -- This function should be run in a coroutine. local function single_run(auxstatus, iteration) - local minted = false + local minted, epstopdf = false, false local bibtex_aux_hash = nil local mainauxfile = path_in_output_directory("aux") if fsutil.isfile(recorderfile) then @@ -179,7 +195,9 @@ local function single_run(auxstatus, iteration) for _,fileinfo in ipairs(filelist) do if string.match(fileinfo.path, "minted/minted%.sty$") then minted = true - break + end + if string.match(fileinfo.path, "epstopdf%.sty$") then + epstopdf = true end end if options.bibtex then @@ -198,14 +216,38 @@ local function single_run(auxstatus, iteration) end --local timestamp = os.time() + local tex_injection = "" + if options.includeonly then - tex_options.tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly) + tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly) end - if minted and not (tex_options.tex_injection and string.find(tex_options.tex_injection,"minted") == nil) then - tex_options.tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_options.tex_injection or "", options.output_directory) + if minted or options.package_support["minted"] then + local outdir = options.output_directory + if os.type == "windows" then + outdir = string.gsub(outdir, "\\", "/") -- Use forward slashes + end + tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_injection or "", outdir) + if not options.package_support["minted"] then + message.diag("You may want to use --package-support=minted option.") + end + end + if epstopdf or options.package_support["epstopdf"] then + local outdir = options.output_directory + if os.type == "windows" then + outdir = string.gsub(outdir, "\\", "/") -- Use forward slashes + end + if string.sub(outdir, -1, -1) ~= "/" then + outdir = outdir.."/" -- Must end with a directory separator + end + tex_injection = string.format("%s\\PassOptionsToPackage{outdir=%s}{epstopdf}", tex_injection or "", outdir) + if not options.package_support["epstopdf"] then + message.diag("You may want to use --package-support=epstopdf option.") + end end + local inputline = tex_injection .. safename.safeinput(inputfile, engine) + local current_tex_options, lightweight_mode = tex_options, false if iteration == 1 and options.start_with_draft then current_tex_options = {} @@ -222,7 +264,7 @@ local function single_run(auxstatus, iteration) current_tex_options.draftmode = false end - local command = engine:build_command(inputfile, current_tex_options) + local command = engine:build_command(inputline, current_tex_options) local execlog -- the contents of .log file @@ -492,18 +534,7 @@ end if options.watch then -- Watch mode - local success, status = do_typeset() - -- TODO: filenames here can be UTF-8 if command_line_encoding=utf-8 - local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) - if engine.is_luatex and fsutil.isfile(recorderfile2) then - filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) - end - local input_files_to_watch = {} - for _,fileinfo in ipairs(filelist) do - if fileinfo.kind == "input" then - table.insert(input_files_to_watch, fileinfo.abspath) - end - end + local fswatcherlib if os.type == "windows" then -- Windows: Try built-in filesystem watcher @@ -513,73 +544,71 @@ if options.watch then end fswatcherlib = result end + + local do_watch if fswatcherlib then if CLUTTEX_VERBOSITY >= 2 then message.info("Using built-in filesystem watcher for Windows") end - local watcher = assert(fswatcherlib.new()) - for _,path in ipairs(input_files_to_watch) do - assert(watcher:add_file(path)) - end - while true do + do_watch = function(files) + local watcher = assert(fswatcherlib.new()) + for _,path in ipairs(files) do + assert(watcher:add_file(path)) + end local result = assert(watcher:next()) if CLUTTEX_VERBOSITY >= 2 then - message.info(string.format("%s %s"), result.action, result.path) - end - local success, status = do_typeset() - if not success then - -- Not successful + message.info(string.format("%s %s", result.action, result.path)) end + watcher:close() + return true end elseif shellutil.has_command("fswatch") then - local fswatch_command = {"fswatch", "--event=Updated", "--"} - for _,path in ipairs(input_files_to_watch) do - table.insert(fswatch_command, shellutil.escape(path)) + if CLUTTEX_VERBOSITY >= 2 then + message.info("Using `fswatch' command") end - local fswatch_command_str = table.concat(fswatch_command, " ") - if CLUTTEX_VERBOSITY >= 1 then - message.exec(fswatch_command_str) - end - local fswatch = assert(io.popen(fswatch_command_str, "r")) - for l in fswatch:lines() do - local found = false - for _,path in ipairs(input_files_to_watch) do - if l == path then - found = true - break - end + do_watch = function(files) + local fswatch_command = {"fswatch", "--one-event", "--event=Updated", "--"} + for _,path in ipairs(files) do + table.insert(fswatch_command, shellutil.escape(path)) + end + local fswatch_command_str = table.concat(fswatch_command, " ") + if CLUTTEX_VERBOSITY >= 1 then + message.exec(fswatch_command_str) end - if found then - local success, status = do_typeset() - if not success then - -- Not successful + local fswatch = assert(io.popen(fswatch_command_str, "r")) + for l in fswatch:lines() do + for _,path in ipairs(files) do + if l == path then + fswatch:close() + return true + end end end + return false end elseif shellutil.has_command("inotifywait") then - local inotifywait_command = {"inotifywait", "--monitor", "--event=modify", "--event=attrib", "--format=%w", "--quiet"} - for _,path in ipairs(input_files_to_watch) do - table.insert(inotifywait_command, shellutil.escape(path)) + if CLUTTEX_VERBOSITY >= 2 then + message.info("Using `inotifywait' command") end - local inotifywait_command_str = table.concat(inotifywait_command, " ") - if CLUTTEX_VERBOSITY >= 1 then - message.exec(inotifywait_command_str) - end - local inotifywait = assert(io.popen(inotifywait_command_str, "r")) - for l in inotifywait:lines() do - local found = false - for _,path in ipairs(input_files_to_watch) do - if l == path then - found = true - break - end + do_watch = function(files) + local inotifywait_command = {"inotifywait", "--event=modify", "--event=attrib", "--format=%w", "--quiet"} + for _,path in ipairs(files) do + table.insert(inotifywait_command, shellutil.escape(path)) + end + local inotifywait_command_str = table.concat(inotifywait_command, " ") + if CLUTTEX_VERBOSITY >= 1 then + message.exec(inotifywait_command_str) end - if found then - local success, status = do_typeset() - if not success then - -- Not successful + local inotifywait = assert(io.popen(inotifywait_command_str, "r")) + for l in inotifywait:lines() do + for _,path in ipairs(files) do + if l == path then + inotifywait:close() + return true + end end end + return false end else message.error("Could not watch files because neither `fswatch' nor `inotifywait' was installed.") @@ -587,6 +616,37 @@ if options.watch then os.exit(1) end + local success, status = do_typeset() + -- TODO: filenames here can be UTF-8 if command_line_encoding=utf-8 + local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) + if engine.is_luatex and fsutil.isfile(recorderfile2) then + filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) + end + local input_files_to_watch = {} + for _,fileinfo in ipairs(filelist) do + if fileinfo.kind == "input" then + table.insert(input_files_to_watch, fileinfo.abspath) + end + end + + while do_watch(input_files_to_watch) do + local success, status = do_typeset() + if not success then + -- error + else + local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) + if engine.is_luatex and fsutil.isfile(recorderfile2) then + filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) + end + input_files_to_watch = {} + for _,fileinfo in ipairs(filelist) do + if fileinfo.kind == "input" then + table.insert(input_files_to_watch, fileinfo.abspath) + end + end + end + end + else -- Not in watch mode local success, status = do_typeset() diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua index 478369914f5..d23ee1aefd9 100644 --- a/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua +++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua @@ -36,6 +36,9 @@ Options: xelatex, xetex, latex, etex, tex, platex, eptex, ptex, uplatex, euptex, uptex, + --engine-executable=COMMAND+OPTIONs + The actual TeX command to use. + [default: ENGINE] -o, --output=FILE The name of output file. [default: JOBNAME.pdf or JOBNAME.dvi] --fresh Clean intermediate files before running TeX. @@ -51,17 +54,24 @@ Options: --dvipdfmx-option[s]=OPTION[s] Same for dvipdfmx. --makeindex=COMMAND+OPTIONs Command to generate index, such as `makeindex' or `mendex'. - --bibtex=COMMAND+OPTIONs Command for BibTeX, such as + --bibtex=COMMAND+OPTIONs Command for BibTeX, such as `bibtex' or `pbibtex'. - --biber[=COMMAND+OPTIONs] Command for Biber. + --biber[=COMMAND+OPTIONs] Command for Biber. --makeglossaries[=COMMAND+OPTIONs] Command for makeglossaries. -h, --help Print this message and exit. -v, --version Print version information and exit. -V, --verbose Be more verbose. - --color=WHEN Make ClutTeX's message colorful. WHEN is one of - `always', `auto', or `never'. [default: auto] + --color[=WHEN] Make ClutTeX's message colorful. WHEN is one of + `always', `auto', or `never'. + [default: `auto' if --color is omitted, + `always' if WHEN is omitted] --includeonly=NAMEs Insert '\includeonly{NAMEs}'. --make-depends=FILE Write dependencies as a Makefile rule. + --print-output-directory Print the output directory and exit. + --package-support=PKG1[,PKG2,...] + Enable special support for some shell-escaping + packages. + Currently supported: minted, epstopdf --[no-]shell-escape --shell-restricted @@ -85,6 +95,10 @@ local option_spec = { long = "engine", param = true, }, + { + long = "engine-executable", + param = true, + }, { short = "o", long = "output", @@ -133,6 +147,13 @@ local option_spec = { long = "make-depends", param = true }, + { + long = "print-output-directory", + }, + { + long = "package-support", + param = true + }, -- Options for TeX { long = "synctex", @@ -247,6 +268,7 @@ local function handle_cluttex_options(arg) local options = { tex_extraoptions = {}, dvipdfmx_extraoptions = {}, + package_support = {}, } CLUTTEX_VERBOSITY = 0 for _,option in ipairs(option_and_params) do @@ -257,6 +279,10 @@ local function handle_cluttex_options(arg) assert(options.engine == nil, "multiple --engine options") options.engine = param + elseif name == "engine-executable" then + assert(options.engine_executable == nil, "multiple --engine-executable options") + options.engine_executable = param + elseif name == "output" then assert(options.output == nil, "multiple --output options") options.output = param @@ -306,6 +332,19 @@ local function handle_cluttex_options(arg) assert(options.make_depends == nil, "multiple --make-depends options") options.make_depends = param + elseif name == "print-output-directory" then + assert(options.print_output_directory == nil, "multiple --print-output-directory options") + options.print_output_directory = true + + elseif name == "package-support" then + local known_packages = {["minted"] = true, ["epstopdf"] = true} + for pkg in string.gmatch(param, "[^,%s]+") do + options.package_support[pkg] = true + if not known_packages[pkg] and CLUTTEX_VERBOSITY >= 1 then + message.warn("ClutTeX provides no special support for '"..pkg.."'.") + end + end + -- Options for TeX elseif name == "synctex" then assert(options.synctex == nil, "multiple --synctex options") diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/safename.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/safename.lua new file mode 100644 index 00000000000..0014b59b929 --- /dev/null +++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/safename.lua @@ -0,0 +1,62 @@ +--[[ + Copyright 2019 ARATA Mizuki + + This file is part of ClutTeX. + + ClutTeX 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 3 of the License, or + (at your option) any later version. + + ClutTeX 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 ClutTeX. If not, see . +]] + +local string = string +local table = table + +local function dounsafechar(c) + if c == " " then + return "_" + else + return string.format("_%02x", c:byte(1)) + end +end + +local function escapejobname(name) + return (string.gsub(name, "[%s\"$%%&'();<>\\^`|]", dounsafechar)) +end + +local function handlespecialchar(s) + return (string.gsub(s, "[%\\%%^%{%}%~%#]", "~\\%1")) +end + +local function handlespaces(s) + return (string.gsub(s, " +", function(s) return string.rep(" ", #s, "~") end)) +end + +local function handlenonascii(s) + return (string.gsub(s, "[\x80-\xFF]+", "\\detokenize{%1}")) +end + +local function safeinput(name, engine) + local escaped = handlespaces(handlespecialchar(name)) + if engine.name == "pdftex" or engine.name == "pdflatex" then + escaped = handlenonascii(escaped) + end + if name == escaped then + return string.format("\\input\"%s\"", name) + else + return string.format("\\begingroup\\escapechar-1\\let~\\string\\edef\\x{\"%s\" }\\expandafter\\endgroup\\expandafter\\input\\x", escaped) + end +end + +return { + escapejobname = escapejobname, + safeinput = safeinput, +} diff --git a/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua b/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua index e15e0f46a37..c0f64eaff69 100644 --- a/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua +++ b/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua @@ -1,5 +1,5 @@ --[[ - Copyright 2016 ARATA Mizuki + Copyright 2016,2019 ARATA Mizuki This file is part of ClutTeX. @@ -26,7 +26,7 @@ local shellutil = require "texrunner.shellutil" --[[ engine.name: string engine.type = "onePass" or "twoPass" -engine:build_command(inputfile, options) +engine:build_command(inputline, options) options: halt_on_error: boolean interaction: string @@ -40,7 +40,6 @@ engine:build_command(inputfile, options) output_format: "pdf" or "dvi" draftmode: boolean (pdfTeX / XeTeX / LuaTeX) fmt: string - tex_injection: string lua_initialization_script: string (LuaTeX only) engine.executable: string engine.supports_pdf_generation: boolean @@ -52,8 +51,9 @@ engine.is_luatex: true or nil local engine_meta = {} engine_meta.__index = engine_meta engine_meta.dvi_extension = "dvi" -function engine_meta:build_command(inputfile, options) - local command = {self.executable, "-recorder"} +function engine_meta:build_command(inputline, options) + local executable = options.engine_executable or self.executable + local command = {executable, "-recorder"} if options.fmt then table.insert(command, "-fmt=" .. options.fmt) end @@ -90,11 +90,7 @@ function engine_meta:build_command(inputfile, options) table.insert(command, v) end end - if type(options.tex_injection) == "string" then - table.insert(command, shellutil.escape(options.tex_injection .. "\\input " .. inputfile)) -- TODO: what if filename contains spaces? - else - table.insert(command, shellutil.escape(inputfile)) - end + table.insert(command, shellutil.escape(inputline)) return table.concat(command, " ") end diff --git a/Master/texmf-dist/scripts/cluttex/cluttex.lua b/Master/texmf-dist/scripts/cluttex/cluttex.lua index dfbd5c5694f..a3021e48a02 100755 --- a/Master/texmf-dist/scripts/cluttex/cluttex.lua +++ b/Master/texmf-dist/scripts/cluttex/cluttex.lua @@ -692,7 +692,7 @@ return { end package.preload["texrunner.tex_engine"] = function(...) --[[ - Copyright 2016 ARATA Mizuki + Copyright 2016,2019 ARATA Mizuki This file is part of ClutTeX. @@ -719,7 +719,7 @@ local shellutil = require "texrunner.shellutil" --[[ engine.name: string engine.type = "onePass" or "twoPass" -engine:build_command(inputfile, options) +engine:build_command(inputline, options) options: halt_on_error: boolean interaction: string @@ -733,7 +733,6 @@ engine:build_command(inputfile, options) output_format: "pdf" or "dvi" draftmode: boolean (pdfTeX / XeTeX / LuaTeX) fmt: string - tex_injection: string lua_initialization_script: string (LuaTeX only) engine.executable: string engine.supports_pdf_generation: boolean @@ -745,8 +744,9 @@ engine.is_luatex: true or nil local engine_meta = {} engine_meta.__index = engine_meta engine_meta.dvi_extension = "dvi" -function engine_meta:build_command(inputfile, options) - local command = {self.executable, "-recorder"} +function engine_meta:build_command(inputline, options) + local executable = options.engine_executable or self.executable + local command = {executable, "-recorder"} if options.fmt then table.insert(command, "-fmt=" .. options.fmt) end @@ -783,11 +783,7 @@ function engine_meta:build_command(inputfile, options) table.insert(command, v) end end - if type(options.tex_injection) == "string" then - table.insert(command, shellutil.escape(options.tex_injection .. "\\input " .. inputfile)) -- TODO: what if filename contains spaces? - else - table.insert(command, shellutil.escape(inputfile)) - end + table.insert(command, shellutil.escape(inputline)) return table.concat(command, " ") end @@ -1404,6 +1400,9 @@ Options: xelatex, xetex, latex, etex, tex, platex, eptex, ptex, uplatex, euptex, uptex, + --engine-executable=COMMAND+OPTIONs + The actual TeX command to use. + [default: ENGINE] -o, --output=FILE The name of output file. [default: JOBNAME.pdf or JOBNAME.dvi] --fresh Clean intermediate files before running TeX. @@ -1419,17 +1418,24 @@ Options: --dvipdfmx-option[s]=OPTION[s] Same for dvipdfmx. --makeindex=COMMAND+OPTIONs Command to generate index, such as `makeindex' or `mendex'. - --bibtex=COMMAND+OPTIONs Command for BibTeX, such as + --bibtex=COMMAND+OPTIONs Command for BibTeX, such as `bibtex' or `pbibtex'. - --biber[=COMMAND+OPTIONs] Command for Biber. + --biber[=COMMAND+OPTIONs] Command for Biber. --makeglossaries[=COMMAND+OPTIONs] Command for makeglossaries. -h, --help Print this message and exit. -v, --version Print version information and exit. -V, --verbose Be more verbose. - --color=WHEN Make ClutTeX's message colorful. WHEN is one of - `always', `auto', or `never'. [default: auto] + --color[=WHEN] Make ClutTeX's message colorful. WHEN is one of + `always', `auto', or `never'. + [default: `auto' if --color is omitted, + `always' if WHEN is omitted] --includeonly=NAMEs Insert '\includeonly{NAMEs}'. --make-depends=FILE Write dependencies as a Makefile rule. + --print-output-directory Print the output directory and exit. + --package-support=PKG1[,PKG2,...] + Enable special support for some shell-escaping + packages. + Currently supported: minted, epstopdf --[no-]shell-escape --shell-restricted @@ -1453,6 +1459,10 @@ local option_spec = { long = "engine", param = true, }, + { + long = "engine-executable", + param = true, + }, { short = "o", long = "output", @@ -1501,6 +1511,13 @@ local option_spec = { long = "make-depends", param = true }, + { + long = "print-output-directory", + }, + { + long = "package-support", + param = true + }, -- Options for TeX { long = "synctex", @@ -1615,6 +1632,7 @@ local function handle_cluttex_options(arg) local options = { tex_extraoptions = {}, dvipdfmx_extraoptions = {}, + package_support = {}, } CLUTTEX_VERBOSITY = 0 for _,option in ipairs(option_and_params) do @@ -1625,6 +1643,10 @@ local function handle_cluttex_options(arg) assert(options.engine == nil, "multiple --engine options") options.engine = param + elseif name == "engine-executable" then + assert(options.engine_executable == nil, "multiple --engine-executable options") + options.engine_executable = param + elseif name == "output" then assert(options.output == nil, "multiple --output options") options.output = param @@ -1674,6 +1696,19 @@ local function handle_cluttex_options(arg) assert(options.make_depends == nil, "multiple --make-depends options") options.make_depends = param + elseif name == "print-output-directory" then + assert(options.print_output_directory == nil, "multiple --print-output-directory options") + options.print_output_directory = true + + elseif name == "package-support" then + local known_packages = {["minted"] = true, ["epstopdf"] = true} + for pkg in string.gmatch(param, "[^,%s]+") do + options.package_support[pkg] = true + if not known_packages[pkg] and CLUTTEX_VERBOSITY >= 1 then + message.warn("ClutTeX provides no special support for '"..pkg.."'.") + end + end + -- Options for TeX elseif name == "synctex" then assert(options.synctex == nil, "multiple --synctex options") @@ -2560,6 +2595,70 @@ return { new = new_watcher, } end +package.preload["texrunner.safename"] = function(...) +--[[ + Copyright 2019 ARATA Mizuki + + This file is part of ClutTeX. + + ClutTeX 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 3 of the License, or + (at your option) any later version. + + ClutTeX 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 ClutTeX. If not, see . +]] + +local string = string +local table = table + +local function dounsafechar(c) + if c == " " then + return "_" + else + return string.format("_%02x", c:byte(1)) + end +end + +local function escapejobname(name) + return (string.gsub(name, "[%s\"$%%&'();<>\\^`|]", dounsafechar)) +end + +local function handlespecialchar(s) + return (string.gsub(s, "[%\\%%^%{%}%~%#]", "~\\%1")) +end + +local function handlespaces(s) + return (string.gsub(s, " +", function(s) return string.rep(" ", #s, "~") end)) +end + +local function handlenonascii(s) + return (string.gsub(s, "[\x80-\xFF]+", "\\detokenize{%1}")) +end + +local function safeinput(name, engine) + local escaped = handlespaces(handlespecialchar(name)) + if engine.name == "pdftex" or engine.name == "pdflatex" then + escaped = handlenonascii(escaped) + end + if name == escaped then + return string.format("\\input\"%s\"", name) + else + return string.format("\\begingroup\\escapechar-1\\let~\\string\\edef\\x{\"%s\" }\\expandafter\\endgroup\\expandafter\\input\\x", escaped) + end +end + +return { + escapejobname = escapejobname, + safeinput = safeinput, +} +end --[[ Copyright 2016,2018-2019 ARATA Mizuki @@ -2579,7 +2678,7 @@ end along with ClutTeX. If not, see . ]] -CLUTTEX_VERSION = "v0.3" +CLUTTEX_VERSION = "v0.4" -- Standard libraries local coroutine = coroutine @@ -2598,6 +2697,7 @@ local reruncheck = require "texrunner.reruncheck" local luatexinit = require "texrunner.luatexinit" local recoverylib = require "texrunner.recovery" local message = require "texrunner.message" +local safename = require "texrunner.safename" local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options @@ -2618,7 +2718,15 @@ end local inputfile, engine, options = handle_cluttex_options(arg) -local jobname = options.jobname or pathutil.basename(pathutil.trimext(inputfile)) +local jobname_for_output +if options.jobname == nil then + local basename = pathutil.basename(pathutil.trimext(inputfile)) + options.jobname = safename.escapejobname(basename) + jobname_for_output = basename +else + jobname_for_output = options.jobname +end +local jobname = options.jobname assert(jobname ~= "", "jobname cannot be empty") if options.output_format == nil then @@ -2632,13 +2740,13 @@ else end if options.output == nil then - options.output = jobname .. "." .. output_extension + options.output = jobname_for_output .. "." .. output_extension end -- Prepare output directory if options.output_directory == nil then local inputfile_abs = pathutil.abspath(inputfile) - options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine) + options.output_directory = genOutputDirectory(inputfile_abs, jobname, options.engine_executable or options.engine) if not fsutil.isdir(options.output_directory) then assert(fsutil.mkdir_rec(options.output_directory)) @@ -2658,6 +2766,12 @@ elseif options.fresh then os.exit(1) end +-- --print-output-directory +if options.print_output_directory then + io.write(options.output_directory, "\n") + os.exit(0) +end + local pathsep = ":" if os.type == "windows" then pathsep = ";" @@ -2696,6 +2810,7 @@ local recorderfile = path_in_output_directory("fls") local recorderfile2 = path_in_output_directory("cluttex-fls") local tex_options = { + engine_executable = options.engine_executable, interaction = options.interaction, file_line_error = options.file_line_error, halt_on_error = options.halt_on_error, @@ -2722,7 +2837,7 @@ end -- should_rerun, newauxstatus = single_run([auxstatus]) -- This function should be run in a coroutine. local function single_run(auxstatus, iteration) - local minted = false + local minted, epstopdf = false, false local bibtex_aux_hash = nil local mainauxfile = path_in_output_directory("aux") if fsutil.isfile(recorderfile) then @@ -2735,7 +2850,9 @@ local function single_run(auxstatus, iteration) for _,fileinfo in ipairs(filelist) do if string.match(fileinfo.path, "minted/minted%.sty$") then minted = true - break + end + if string.match(fileinfo.path, "epstopdf%.sty$") then + epstopdf = true end end if options.bibtex then @@ -2754,14 +2871,38 @@ local function single_run(auxstatus, iteration) end --local timestamp = os.time() + local tex_injection = "" + if options.includeonly then - tex_options.tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly) + tex_injection = string.format("%s\\includeonly{%s}", tex_options.tex_injection or "", options.includeonly) end - if minted and not (tex_options.tex_injection and string.find(tex_options.tex_injection,"minted") == nil) then - tex_options.tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_options.tex_injection or "", options.output_directory) + if minted or options.package_support["minted"] then + local outdir = options.output_directory + if os.type == "windows" then + outdir = string.gsub(outdir, "\\", "/") -- Use forward slashes + end + tex_injection = string.format("%s\\PassOptionsToPackage{outputdir=%s}{minted}", tex_injection or "", outdir) + if not options.package_support["minted"] then + message.diag("You may want to use --package-support=minted option.") + end + end + if epstopdf or options.package_support["epstopdf"] then + local outdir = options.output_directory + if os.type == "windows" then + outdir = string.gsub(outdir, "\\", "/") -- Use forward slashes + end + if string.sub(outdir, -1, -1) ~= "/" then + outdir = outdir.."/" -- Must end with a directory separator + end + tex_injection = string.format("%s\\PassOptionsToPackage{outdir=%s}{epstopdf}", tex_injection or "", outdir) + if not options.package_support["epstopdf"] then + message.diag("You may want to use --package-support=epstopdf option.") + end end + local inputline = tex_injection .. safename.safeinput(inputfile, engine) + local current_tex_options, lightweight_mode = tex_options, false if iteration == 1 and options.start_with_draft then current_tex_options = {} @@ -2778,7 +2919,7 @@ local function single_run(auxstatus, iteration) current_tex_options.draftmode = false end - local command = engine:build_command(inputfile, current_tex_options) + local command = engine:build_command(inputline, current_tex_options) local execlog -- the contents of .log file @@ -3048,18 +3189,7 @@ end if options.watch then -- Watch mode - local success, status = do_typeset() - -- TODO: filenames here can be UTF-8 if command_line_encoding=utf-8 - local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) - if engine.is_luatex and fsutil.isfile(recorderfile2) then - filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) - end - local input_files_to_watch = {} - for _,fileinfo in ipairs(filelist) do - if fileinfo.kind == "input" then - table.insert(input_files_to_watch, fileinfo.abspath) - end - end + local fswatcherlib if os.type == "windows" then -- Windows: Try built-in filesystem watcher @@ -3069,73 +3199,71 @@ if options.watch then end fswatcherlib = result end + + local do_watch if fswatcherlib then if CLUTTEX_VERBOSITY >= 2 then message.info("Using built-in filesystem watcher for Windows") end - local watcher = assert(fswatcherlib.new()) - for _,path in ipairs(input_files_to_watch) do - assert(watcher:add_file(path)) - end - while true do + do_watch = function(files) + local watcher = assert(fswatcherlib.new()) + for _,path in ipairs(files) do + assert(watcher:add_file(path)) + end local result = assert(watcher:next()) if CLUTTEX_VERBOSITY >= 2 then - message.info(string.format("%s %s"), result.action, result.path) - end - local success, status = do_typeset() - if not success then - -- Not successful + message.info(string.format("%s %s", result.action, result.path)) end + watcher:close() + return true end elseif shellutil.has_command("fswatch") then - local fswatch_command = {"fswatch", "--event=Updated", "--"} - for _,path in ipairs(input_files_to_watch) do - table.insert(fswatch_command, shellutil.escape(path)) + if CLUTTEX_VERBOSITY >= 2 then + message.info("Using `fswatch' command") end - local fswatch_command_str = table.concat(fswatch_command, " ") - if CLUTTEX_VERBOSITY >= 1 then - message.exec(fswatch_command_str) - end - local fswatch = assert(io.popen(fswatch_command_str, "r")) - for l in fswatch:lines() do - local found = false - for _,path in ipairs(input_files_to_watch) do - if l == path then - found = true - break - end + do_watch = function(files) + local fswatch_command = {"fswatch", "--one-event", "--event=Updated", "--"} + for _,path in ipairs(files) do + table.insert(fswatch_command, shellutil.escape(path)) + end + local fswatch_command_str = table.concat(fswatch_command, " ") + if CLUTTEX_VERBOSITY >= 1 then + message.exec(fswatch_command_str) end - if found then - local success, status = do_typeset() - if not success then - -- Not successful + local fswatch = assert(io.popen(fswatch_command_str, "r")) + for l in fswatch:lines() do + for _,path in ipairs(files) do + if l == path then + fswatch:close() + return true + end end end + return false end elseif shellutil.has_command("inotifywait") then - local inotifywait_command = {"inotifywait", "--monitor", "--event=modify", "--event=attrib", "--format=%w", "--quiet"} - for _,path in ipairs(input_files_to_watch) do - table.insert(inotifywait_command, shellutil.escape(path)) + if CLUTTEX_VERBOSITY >= 2 then + message.info("Using `inotifywait' command") end - local inotifywait_command_str = table.concat(inotifywait_command, " ") - if CLUTTEX_VERBOSITY >= 1 then - message.exec(inotifywait_command_str) - end - local inotifywait = assert(io.popen(inotifywait_command_str, "r")) - for l in inotifywait:lines() do - local found = false - for _,path in ipairs(input_files_to_watch) do - if l == path then - found = true - break - end + do_watch = function(files) + local inotifywait_command = {"inotifywait", "--event=modify", "--event=attrib", "--format=%w", "--quiet"} + for _,path in ipairs(files) do + table.insert(inotifywait_command, shellutil.escape(path)) + end + local inotifywait_command_str = table.concat(inotifywait_command, " ") + if CLUTTEX_VERBOSITY >= 1 then + message.exec(inotifywait_command_str) end - if found then - local success, status = do_typeset() - if not success then - -- Not successful + local inotifywait = assert(io.popen(inotifywait_command_str, "r")) + for l in inotifywait:lines() do + for _,path in ipairs(files) do + if l == path then + inotifywait:close() + return true + end end end + return false end else message.error("Could not watch files because neither `fswatch' nor `inotifywait' was installed.") @@ -3143,6 +3271,37 @@ if options.watch then os.exit(1) end + local success, status = do_typeset() + -- TODO: filenames here can be UTF-8 if command_line_encoding=utf-8 + local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) + if engine.is_luatex and fsutil.isfile(recorderfile2) then + filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) + end + local input_files_to_watch = {} + for _,fileinfo in ipairs(filelist) do + if fileinfo.kind == "input" then + table.insert(input_files_to_watch, fileinfo.abspath) + end + end + + while do_watch(input_files_to_watch) do + local success, status = do_typeset() + if not success then + -- error + else + local filelist, filemap = reruncheck.parse_recorder_file(recorderfile, options) + if engine.is_luatex and fsutil.isfile(recorderfile2) then + filelist, filemap = reruncheck.parse_recorder_file(recorderfile2, options, filelist, filemap) + end + input_files_to_watch = {} + for _,fileinfo in ipairs(filelist) do + if fileinfo.kind == "input" then + table.insert(input_files_to_watch, fileinfo.abspath) + end + end + end + end + else -- Not in watch mode local success, status = do_typeset() -- cgit v1.2.3