texlive[51927] trunk: cluttex (21aug19)

commits+karl at tug.org commits+karl at tug.org
Wed Aug 21 22:40:52 CEST 2019


Revision: 51927
          http://tug.org/svn/texlive?view=revision&revision=51927
Author:   karl
Date:     2019-08-21 22:40:52 +0200 (Wed, 21 Aug 2019)
Log Message:
-----------
cluttex (21aug19)

Modified Paths:
--------------
    trunk/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua
    trunk/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md
    trunk/Master/texmf-dist/doc/support/cluttex/Makefile
    trunk/Master/texmf-dist/doc/support/cluttex/README.md
    trunk/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat
    trunk/Master/texmf-dist/doc/support/cluttex/build.lua
    trunk/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf
    trunk/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex
    trunk/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf
    trunk/Master/texmf-dist/doc/support/cluttex/doc/manual.tex
    trunk/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua
    trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua
    trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua
    trunk/Master/texmf-dist/scripts/cluttex/cluttex.lua

Added Paths:
-----------
    trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/safename.lua

Modified: trunk/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua
===================================================================
--- trunk/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua	2019-08-21 20:40:52 UTC (rev 51927)
@@ -692,7 +692,7 @@
 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 @@
 --[[
 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 @@
     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 @@
 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 @@
       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 @@
                                      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 @@
       --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
@@ -1454,6 +1460,10 @@
     param = true,
   },
   {
+    long = "engine-executable",
+    param = true,
+  },
+  {
     short = "o",
     long = "output",
     param = true,
@@ -1501,6 +1511,13 @@
     long = "make-depends",
     param = true
   },
+  {
+    long = "print-output-directory",
+  },
+  {
+    long = "package-support",
+    param = true
+  },
   -- Options for TeX
   {
     long = "synctex",
@@ -1615,6 +1632,7 @@
   local options = {
     tex_extraoptions = {},
     dvipdfmx_extraoptions = {},
+    package_support = {},
   }
   CLUTTEX_VERBOSITY = 0
   for _,option in ipairs(option_and_params) do
@@ -1625,6 +1643,10 @@
       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 @@
       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,7 +2595,71 @@
   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 <http://www.gnu.org/licenses/>.
+]]
+
+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
 
   This file is part of ClutTeX.
@@ -2579,7 +2678,7 @@
   along with ClutTeX.  If not, see <http://www.gnu.org/licenses/>.
 ]]
 
-CLUTTEX_VERSION = "v0.3"
+CLUTTEX_VERSION = "v0.4"
 
 -- Standard libraries
 local coroutine = coroutine
@@ -2598,6 +2697,7 @@
 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 @@
 
 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 @@
 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 @@
   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 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 @@
 -- 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,8 +2850,10 @@
     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
       local biblines = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
@@ -2754,14 +2871,38 @@
   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 @@
     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 @@
 
 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 @@
     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)
+        message.info(string.format("%s %s", result.action, result.path))
       end
-      local success, status = do_typeset()
-      if not success then
-        -- Not successful
-      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
-      if found then
-        local success, status = do_typeset()
-        if not success then
-          -- Not successful
+      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
+        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
-      if found then
-        local success, status = do_typeset()
-        if not success then
-          -- Not successful
+      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
+        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 @@
     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()

Modified: trunk/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md	2019-08-21 20:40:52 UTC (rev 51927)
@@ -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)
 -----
 

Modified: trunk/Master/texmf-dist/doc/support/cluttex/Makefile
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/Makefile	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/doc/support/cluttex/Makefile	2019-08-21 20:40:52 UTC (rev 51927)
@@ -20,6 +20,7 @@
  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

Modified: trunk/Master/texmf-dist/doc/support/cluttex/README.md
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/README.md	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/doc/support/cluttex/README.md	2019-08-21 20:40:52 UTC (rev 51927)
@@ -83,6 +83,13 @@
   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:
 

Modified: trunk/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat	2019-08-21 20:40:52 UTC (rev 51927)
@@ -695,7 +695,7 @@
 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 @@
 --[[
 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 @@
     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 @@
 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 @@
       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 @@
                                      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 @@
       --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
@@ -1457,6 +1463,10 @@
     param = true,
   },
   {
+    long = "engine-executable",
+    param = true,
+  },
+  {
     short = "o",
     long = "output",
     param = true,
@@ -1504,6 +1514,13 @@
     long = "make-depends",
     param = true
   },
+  {
+    long = "print-output-directory",
+  },
+  {
+    long = "package-support",
+    param = true
+  },
   -- Options for TeX
   {
     long = "synctex",
@@ -1618,6 +1635,7 @@
   local options = {
     tex_extraoptions = {},
     dvipdfmx_extraoptions = {},
+    package_support = {},
   }
   CLUTTEX_VERBOSITY = 0
   for _,option in ipairs(option_and_params) do
@@ -1628,6 +1646,10 @@
       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 @@
       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,7 +2598,71 @@
   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 <http://www.gnu.org/licenses/>.
+]]
+
+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
 
   This file is part of ClutTeX.
@@ -2582,7 +2681,7 @@
   along with ClutTeX.  If not, see <http://www.gnu.org/licenses/>.
 ]]
 
-CLUTTEX_VERSION = "v0.3"
+CLUTTEX_VERSION = "v0.4"
 
 -- Standard libraries
 local coroutine = coroutine
@@ -2601,6 +2700,7 @@
 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 @@
 
 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 @@
 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 @@
   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 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 @@
 -- 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,8 +2853,10 @@
     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
       local biblines = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
@@ -2757,14 +2874,38 @@
   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 @@
     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 @@
 
 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 @@
     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)
+        message.info(string.format("%s %s", result.action, result.path))
       end
-      local success, status = do_typeset()
-      if not success then
-        -- Not successful
-      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
-      if found then
-        local success, status = do_typeset()
-        if not success then
-          -- Not successful
+      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
+        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
-      if found then
-        local success, status = do_typeset()
-        if not success then
-          -- Not successful
+      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
+        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 @@
     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()

Modified: trunk/Master/texmf-dist/doc/support/cluttex/build.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/build.lua	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/doc/support/cluttex/build.lua	2019-08-21 20:40:52 UTC (rev 51927)
@@ -87,6 +87,10 @@
     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"}

Modified: trunk/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex	2019-08-21 20:40:52 UTC (rev 51927)
@@ -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 @@
 \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 @@
 
 例えば、\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}

Modified: trunk/Master/texmf-dist/doc/support/cluttex/doc/manual.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/support/cluttex/doc/manual.tex
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/doc/manual.tex	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/doc/support/cluttex/doc/manual.tex	2019-08-21 20:40:52 UTC (rev 51927)
@@ -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 @@
   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 @@
 \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 @@
 \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 @@
 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}

Modified: trunk/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua	2019-08-21 20:40:52 UTC (rev 51927)
@@ -18,7 +18,7 @@
   along with ClutTeX.  If not, see <http://www.gnu.org/licenses/>.
 ]]
 
-CLUTTEX_VERSION = "v0.3"
+CLUTTEX_VERSION = "v0.4"
 
 -- Standard libraries
 local table = table
@@ -42,6 +42,7 @@
 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 @@
 
 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 @@
 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 @@
   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 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 @@
 -- 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,8 +195,10 @@
     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
       local biblines = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
@@ -198,14 +216,38 @@
   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 @@
     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 @@
 
 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 @@
     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)
+        message.info(string.format("%s %s", result.action, result.path))
       end
-      local success, status = do_typeset()
-      if not success then
-        -- Not successful
-      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
-      if found then
-        local success, status = do_typeset()
-        if not success then
-          -- Not successful
+      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
+        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
-      if found then
-        local success, status = do_typeset()
-        if not success then
-          -- Not successful
+      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
+        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 @@
     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()

Modified: trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/handleoption.lua	2019-08-21 20:40:52 UTC (rev 51927)
@@ -36,6 +36,9 @@
                                      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 @@
       --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
@@ -86,6 +96,10 @@
     param = true,
   },
   {
+    long = "engine-executable",
+    param = true,
+  },
+  {
     short = "o",
     long = "output",
     param = true,
@@ -133,6 +147,13 @@
     long = "make-depends",
     param = true
   },
+  {
+    long = "print-output-directory",
+  },
+  {
+    long = "package-support",
+    param = true
+  },
   -- Options for TeX
   {
     long = "synctex",
@@ -247,6 +268,7 @@
   local options = {
     tex_extraoptions = {},
     dvipdfmx_extraoptions = {},
+    package_support = {},
   }
   CLUTTEX_VERBOSITY = 0
   for _,option in ipairs(option_and_params) do
@@ -257,6 +279,10 @@
       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 @@
       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")

Added: trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/safename.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/safename.lua	                        (rev 0)
+++ trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/safename.lua	2019-08-21 20:40:52 UTC (rev 51927)
@@ -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 <http://www.gnu.org/licenses/>.
+]]
+
+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,
+}


Property changes on: trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/safename.lua
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/tex_engine.lua	2019-08-21 20:40:52 UTC (rev 51927)
@@ -1,5 +1,5 @@
 --[[
-  Copyright 2016 ARATA Mizuki
+  Copyright 2016,2019 ARATA Mizuki
 
   This file is part of ClutTeX.
 
@@ -26,7 +26,7 @@
 --[[
 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 @@
     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 @@
 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 @@
       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
 

Modified: trunk/Master/texmf-dist/scripts/cluttex/cluttex.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/cluttex/cluttex.lua	2019-08-21 20:40:27 UTC (rev 51926)
+++ trunk/Master/texmf-dist/scripts/cluttex/cluttex.lua	2019-08-21 20:40:52 UTC (rev 51927)
@@ -692,7 +692,7 @@
 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 @@
 --[[
 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 @@
     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 @@
 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 @@
       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 @@
                                      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 @@
       --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
@@ -1454,6 +1460,10 @@
     param = true,
   },
   {
+    long = "engine-executable",
+    param = true,
+  },
+  {
     short = "o",
     long = "output",
     param = true,
@@ -1501,6 +1511,13 @@
     long = "make-depends",
     param = true
   },
+  {
+    long = "print-output-directory",
+  },
+  {
+    long = "package-support",
+    param = true
+  },
   -- Options for TeX
   {
     long = "synctex",
@@ -1615,6 +1632,7 @@
   local options = {
     tex_extraoptions = {},
     dvipdfmx_extraoptions = {},
+    package_support = {},
   }
   CLUTTEX_VERBOSITY = 0
   for _,option in ipairs(option_and_params) do
@@ -1625,6 +1643,10 @@
       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 @@
       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,7 +2595,71 @@
   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 <http://www.gnu.org/licenses/>.
+]]
+
+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
 
   This file is part of ClutTeX.
@@ -2579,7 +2678,7 @@
   along with ClutTeX.  If not, see <http://www.gnu.org/licenses/>.
 ]]
 
-CLUTTEX_VERSION = "v0.3"
+CLUTTEX_VERSION = "v0.4"
 
 -- Standard libraries
 local coroutine = coroutine
@@ -2598,6 +2697,7 @@
 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 @@
 
 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 @@
 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 @@
   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 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 @@
 -- 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,8 +2850,10 @@
     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
       local biblines = extract_bibtex_from_aux_file(mainauxfile, options.output_directory)
@@ -2754,14 +2871,38 @@
   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 @@
     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 @@
 
 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 @@
     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)
+        message.info(string.format("%s %s", result.action, result.path))
       end
-      local success, status = do_typeset()
-      if not success then
-        -- Not successful
-      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
-      if found then
-        local success, status = do_typeset()
-        if not success then
-          -- Not successful
+      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
+        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
-      if found then
-        local success, status = do_typeset()
-        if not success then
-          -- Not successful
+      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
+        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 @@
     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()



More information about the tex-live-commits mailing list