texlive[50913] trunk: cluttex (30apr19)

commits+karl at tug.org commits+karl at tug.org
Wed May 1 00:36:50 CEST 2019


Revision: 50913
          http://tug.org/svn/texlive?view=revision&revision=50913
Author:   karl
Date:     2019-05-01 00:36:50 +0200 (Wed, 01 May 2019)
Log Message:
-----------
cluttex (30apr19)

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/checkglobal.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/option.lua
    trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua
    trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua
    trunk/Master/texmf-dist/scripts/cluttex/cluttex.lua

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

Modified: trunk/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua
===================================================================
--- trunk/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Build/source/texk/texlive/linked_scripts/cluttex/cluttex.lua	2019-04-30 22:36:50 UTC (rev 50913)
@@ -344,7 +344,7 @@
 if os.type == "windows" then
 package.preload["texrunner.shellutil"] = function(...)
 --[[
-  Copyright 2016 ARATA Mizuki
+  Copyright 2016,2019 ARATA Mizuki
 
   This file is part of ClutTeX.
 
@@ -363,6 +363,7 @@
 ]]
 
 local string_gsub = string.gsub
+local os_execute = os.execute
 
 -- s: string
 local function escape(s)
@@ -370,14 +371,21 @@
 end
 
 
+local function has_command(name)
+  local result = os_execute("where " .. escape(name) .. " > NUL 2>&1")
+  -- Note that os.execute returns a number on Lua 5.1 or LuaTeX
+  return result == 0 or result == true
+end
+
 return {
   escape = escape,
+  has_command = has_command,
 }
 end
 else
 package.preload["texrunner.shellutil"] = function(...)
 --[[
-  Copyright 2016 ARATA Mizuki
+  Copyright 2016,2019 ARATA Mizuki
 
   This file is part of ClutTeX.
 
@@ -400,6 +408,7 @@
 local table = table
 local table_insert = table.insert
 local table_concat = table.concat
+local os_execute = os.execute
 
 -- s: string
 local function escape(s)
@@ -430,8 +439,15 @@
 end
 
 
+local function has_command(name)
+  local result = os_execute("which " .. escape(name) .. " > /dev/null")
+  -- Note that os.execute returns a number on Lua 5.1 or LuaTeX
+  return result == 0 or result == true
+end
+
 return {
   escape = escape,
+  has_command = has_command,
 }
 end
 end
@@ -542,6 +558,7 @@
 
 -- options_and_params, i = parseoption(arg, options)
 -- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]}
+-- options_and_params[j] = {"option", "value"}
 -- arg[i], arg[i + 1], ..., arg[#arg] are non-options
 local function parseoption(arg, options)
   local i = 1
@@ -581,6 +598,7 @@
           elseif o.boolean and name == "no-" .. o.long then
             -- --no-option
             opt = o
+            param = false
             break
           end
         end
@@ -620,6 +638,7 @@
           elseif o.boolean and name == "no-" .. o.long then
             -- -no-option
             opt = o
+            param = false
             break
           end
         elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then
@@ -2124,7 +2143,424 @@
   info  = info_msg,
 }
 end
+package.preload["texrunner.fswatcher_windows"] = 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 ffi = require "ffi"
+local bitlib = assert(bit32 or bit, "Neither bit32 (Lua 5.2) nor bit (LuaJIT) found") -- Lua 5.2 or LuaJIT
+
+ffi.cdef[[
+typedef int BOOL;
+typedef unsigned int UINT;
+typedef uint32_t DWORD;
+typedef void *HANDLE;
+typedef uintptr_t ULONG_PTR;
+typedef uint16_t WCHAR;
+typedef struct _OVERLAPPED {
+  ULONG_PTR Internal;
+  ULONG_PTR InternalHigh;
+  union {
+    struct {
+      DWORD Offset;
+      DWORD OffsetHigh;
+    };
+    void *Pointer;
+  };
+  HANDLE hEvent;
+} OVERLAPPED;
+typedef struct _FILE_NOTIFY_INFORMATION {
+  DWORD NextEntryOffset;
+  DWORD Action;
+  DWORD FileNameLength;
+  WCHAR FileName[?];
+} FILE_NOTIFY_INFORMATION;
+typedef void (__stdcall *LPOVERLAPPED_COMPLETION_ROUTINE)(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, OVERLAPPED *lpOverlapped);
+DWORD GetLastError();
+BOOL CloseHandle(HANDLE hObject);
+HANDLE CreateFileA(const char *lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, void *lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
+HANDLE CreateIoCompletionPort(HANDLE fileHandle, HANDLE existingCompletionPort, ULONG_PTR completionKey, DWORD numberOfConcurrentThreads);
+BOOL ReadDirectoryChangesW(HANDLE hDirectory, void *lpBuffer, DWORD nBufferLength, BOOL bWatchSubtree, DWORD dwNotifyFilter, DWORD *lpBytesReturned, OVERLAPPED *lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpOverlappedCompletionRoutine);
+BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, DWORD *lpNumberOfBytes, ULONG_PTR *lpCompletionKey, OVERLAPPED **lpOverlapped, DWORD dwMilliseconds);
+int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, const char *lpMultiByteStr, int cbMultiByte, WCHAR *lpWideCharStr, int cchWideChar);
+int WideCharToMultiByte(UINT CodePage, DWORD dwFlags, const WCHAR *lpWideCharStr, int cchWideChar, char *lpMultiByteStr, int cbMultiByte, const char *lpDefaultChar, BOOL *lpUsedDefaultChar);
+DWORD GetFullPathNameA(const char *lpFileName, DWORD nBufferLength, char *lpBuffer, char **lpFilePart);
+uint64_t GetTickCount64();
+]]
+
+-- LuaTeX's FFI does not equate a null pointer with nil.
+-- On LuaJIT, ffi.NULL is just nil.
+local NULL = ffi.NULL
+
+-- GetLastError
+local ERROR_FILE_NOT_FOUND         = 0x0002
+local ERROR_PATH_NOT_FOUND         = 0x0003
+local ERROR_ACCESS_DENIED          = 0x0005
+local ERROR_INVALID_PARAMETER      = 0x0057
+local ERROR_INSUFFICIENT_BUFFER    = 0x007A
+local WAIT_TIMEOUT                 = 0x0102
+local ERROR_ABANDONED_WAIT_0       = 0x02DF
+local ERROR_NOACCESS               = 0x03E6
+local ERROR_INVALID_FLAGS          = 0x03EC
+local ERROR_NOTIFY_ENUM_DIR        = 0x03FE
+local ERROR_NO_UNICODE_TRANSLATION = 0x0459
+local KnownErrors = {
+  [ERROR_FILE_NOT_FOUND] = "ERROR_FILE_NOT_FOUND",
+  [ERROR_PATH_NOT_FOUND] = "ERROR_PATH_NOT_FOUND",
+  [ERROR_ACCESS_DENIED] = "ERROR_ACCESS_DENIED",
+  [ERROR_INVALID_PARAMETER] = "ERROR_INVALID_PARAMETER",
+  [ERROR_INSUFFICIENT_BUFFER] = "ERROR_INSUFFICIENT_BUFFER",
+  [ERROR_ABANDONED_WAIT_0] = "ERROR_ABANDONED_WAIT_0",
+  [ERROR_NOACCESS] = "ERROR_NOACCESS",
+  [ERROR_INVALID_FLAGS] = "ERROR_INVALID_FLAGS",
+  [ERROR_NOTIFY_ENUM_DIR] = "ERROR_NOTIFY_ENUM_DIR",
+  [ERROR_NO_UNICODE_TRANSLATION] = "ERROR_NO_UNICODE_TRANSLATION",
+}
+
+-- CreateFile
+local FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+local FILE_FLAG_OVERLAPPED       = 0x40000000
+local OPEN_EXISTING              = 3
+local FILE_SHARE_READ            = 0x00000001
+local FILE_SHARE_WRITE           = 0x00000002
+local FILE_SHARE_DELETE          = 0x00000004
+local FILE_LIST_DIRECTORY        = 0x1
+local INVALID_HANDLE_VALUE       = ffi.cast("void *", -1)
+
+-- ReadDirectoryChangesW / FILE_NOTIFY_INFORMATION
+local FILE_NOTIFY_CHANGE_FILE_NAME   = 0x00000001
+local FILE_NOTIFY_CHANGE_DIR_NAME    = 0x00000002
+local FILE_NOTIFY_CHANGE_ATTRIBUTES  = 0x00000004
+local FILE_NOTIFY_CHANGE_SIZE        = 0x00000008
+local FILE_NOTIFY_CHANGE_LAST_WRITE  = 0x00000010
+local FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020
+local FILE_NOTIFY_CHANGE_CREATION    = 0x00000040
+local FILE_NOTIFY_CHANGE_SECURITY    = 0x00000100
+local FILE_ACTION_ADDED              = 0x00000001
+local FILE_ACTION_REMOVED            = 0x00000002
+local FILE_ACTION_MODIFIED           = 0x00000003
+local FILE_ACTION_RENAMED_OLD_NAME   = 0x00000004
+local FILE_ACTION_RENAMED_NEW_NAME   = 0x00000005
+
+-- WideCharToMultiByte / MultiByteToWideChar
+local CP_ACP  = 0
+local CP_UTF8 = 65001
+
+local C = ffi.C
+
+local function format_error(name, lasterror, extra)
+  local errorname = KnownErrors[lasterror] or string.format("error code %d", lasterror)
+  if extra then
+    return string.format("%s failed with %s (0x%04x) [%s]", name, errorname, lasterror, extra)
+  else
+    return string.format("%s failed with %s (0x%04x)", name, errorname, lasterror)
+  end
+end
+local function wcs_to_mbs(wstr, wstrlen, codepage)
+  -- wstr: FFI uint16_t[?]
+  -- wstrlen: length of wstr, or -1 if NUL-terminated
+  if wstrlen == 0 then
+    return ""
+  end
+  codepage = codepage or CP_ACP
+  local dwFlags = 0
+  local result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, nil, 0, nil, nil)
+  if result <= 0 then
+    -- Failed
+    local lasterror = C.GetLastError()
+    -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("WideCharToMultiByte", lasterror)
+  end
+  local mbsbuf = ffi.new("char[?]", result)
+  result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, mbsbuf, result, nil, nil)
+  if result <= 0 then
+    -- Failed
+    local lasterror = C.GetLastError()
+    -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("WideCharToMultiByte", lasterror)
+  end
+  return ffi.string(mbsbuf, result)
+end
+local function mbs_to_wcs(str, codepage)
+  -- str: Lua string
+  if str == "" then
+    return ffi.new("WCHAR[0]")
+  end
+  codepage = codepage or CP_ACP
+  local dwFlags = 0
+  local result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, nil, 0)
+  if result <= 0 then
+    local lasterror = C.GetLastError()
+    -- ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("MultiByteToWideChar", lasterror)
+  end
+  local wcsbuf = ffi.new("WCHAR[?]", result)
+  result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, wcsbuf, result)
+  if result <= 0 then
+    local lasterror = C.GetLastError()
+    return nil, format_error("MultiByteToWideChar", lasterror)
+  end
+  return wcsbuf, result
+end
+
+
+local function get_full_path_name(filename)
+  local bufsize = 1024
+  local buffer
+  local filePartPtr = ffi.new("char*[1]")
+  local result
+  repeat
+    buffer = ffi.new("char[?]", bufsize)
+    result = C.GetFullPathNameA(filename, bufsize, buffer, filePartPtr)
+    if result == 0 then
+      local lasterror = C.GetLastError()
+      return nil, format_error("GetFullPathNameA", lasterror, filename)
+    elseif bufsize < result then
+      -- result: buffer size required to hold the path + terminating NUL
+      bufsize = result
+    end
+  until result < bufsize
+  local fullpath = ffi.string(buffer, result)
+  local filePart = ffi.string(filePartPtr[0])
+  local dirPart = ffi.string(buffer, ffi.cast("intptr_t", filePartPtr[0]) - ffi.cast("intptr_t", buffer)) -- LuaTeX's FFI doesn't support pointer subtraction
+  return fullpath, filePart, dirPart
+end
+
+--[[
+  dirwatche.dirname : string
+  dirwatcher._rawhandle : cdata HANDLE
+  dirwatcher._overlapped : cdata OVERLAPPED
+  dirwatcher._buffer : cdata char[?]
+]]
+local dirwatcher_meta = {}
+dirwatcher_meta.__index = dirwatcher_meta
+function dirwatcher_meta:close()
+  if self._rawhandle ~= nil then
+    C.CloseHandle(ffi.gc(self._rawhandle, nil))
+    self._rawhandle = nil
+  end
+end
+local function open_directory(dirname)
+  local dwShareMode = bitlib.bor(FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE)
+  local dwFlagsAndAttributes = bitlib.bor(FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED)
+  local handle = C.CreateFileA(dirname, FILE_LIST_DIRECTORY, dwShareMode, nil, OPEN_EXISTING, dwFlagsAndAttributes, nil)
+  if handle == INVALID_HANDLE_VALUE then
+    local lasterror = C.GetLastError()
+    print("Failed to open "..dirname)
+    return nil, format_error("CreateFileA", lasterror, dirname)
+  end
+  return setmetatable({
+    dirname = dirname,
+    _rawhandle = ffi.gc(handle, C.CloseHandle),
+    _overlapped = ffi.new("OVERLAPPED"),
+    _buffer = ffi.new("char[?]", 1024),
+  }, dirwatcher_meta)
+end
+function dirwatcher_meta:start_watch(watchSubtree)
+  local dwNotifyFilter = bitlib.bor(FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_LAST_ACCESS, FILE_NOTIFY_CHANGE_CREATION, FILE_NOTIFY_CHANGE_SECURITY)
+  local buffer = self._buffer
+  local bufferSize = ffi.sizeof(buffer)
+  local result = C.ReadDirectoryChangesW(self._rawhandle, buffer, bufferSize, watchSubtree, dwNotifyFilter, nil, self._overlapped, nil)
+  if result == 0 then
+    local lasterror = C.GetLastError()
+    return nil, format_error("ReadDirectoryChangesW", lasterror, self.dirname)
+  end
+  return true
+end
+local ActionTable = {
+  [FILE_ACTION_ADDED] = "added",
+  [FILE_ACTION_REMOVED] = "removed",
+  [FILE_ACTION_MODIFIED] = "modified",
+  [FILE_ACTION_RENAMED_OLD_NAME] = "rename_from",
+  [FILE_ACTION_RENAMED_NEW_NAME] = "rename_to",
+}
+function dirwatcher_meta:process(numberOfBytes)
+  -- self._buffer received `numberOfBytes` bytes
+  local buffer = self._buffer
+  numberOfBytes = math.min(numberOfBytes, ffi.sizeof(buffer))
+  local ptr = ffi.cast("char *", buffer)
+  local structSize = ffi.sizeof("FILE_NOTIFY_INFORMATION", 1)
+  local t = {}
+  while numberOfBytes >= structSize do
+    local notifyInfo = ffi.cast("FILE_NOTIFY_INFORMATION*", ptr)
+    local nextEntryOffset = notifyInfo.NextEntryOffset
+    local action = notifyInfo.Action
+    local fileNameLength = notifyInfo.FileNameLength
+    local fileName = notifyInfo.FileName
+    local u = { action = ActionTable[action], filename = wcs_to_mbs(fileName, fileNameLength / 2) }
+    table.insert(t, u)
+    if nextEntryOffset == 0 or numberOfBytes <= nextEntryOffset then
+      break
+    end
+    numberOfBytes = numberOfBytes - nextEntryOffset
+    ptr = ptr + nextEntryOffset
+  end
+  return t
+end
+
+--[[
+  watcher._rawport : cdata HANDLE
+  watcher._pending : array of {
+    action = ..., filename = ...
+  }
+  watcher._directories[dirname] = {
+    dir = directory watcher,
+    dirname = dirname,
+    files = { [filename] = user-supplied path } -- files to watch
+  }
+  watcher[i] = i-th directory (_directories[dirname] for some dirname)
+]]
+
+local fswatcher_meta = {}
+fswatcher_meta.__index = fswatcher_meta
+local function new_watcher()
+  local port = C.CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 0)
+  if port == NULL then
+    local lasterror = C.GetLastError()
+    return nil, format_error("CreateIoCompletionPort", lasterror)
+  end
+  return setmetatable({
+    _rawport = ffi.gc(port, C.CloseHandle), -- ?
+    _pending = {},
+    _directories = {},
+  }, fswatcher_meta)
+end
+local function add_directory(self, dirname)
+  local t = self._directories[dirname]
+  if not t then
+    local dirwatcher, err = open_directory(dirname)
+    if not dirwatcher then
+      return dirwatcher, err
+    end
+    t = { dirwatcher = dirwatcher, dirname = dirname, files = {} }
+    table.insert(self, t)
+    local i = #self
+    local result = C.CreateIoCompletionPort(dirwatcher._rawhandle, self._rawport, i, 0)
+    if result == NULL then
+      local lasterror = C.GetLastError()
+      return nil, format_error("CreateIoCompletionPort", lasterror, dirname)
+    end
+    self._directories[dirname] = t
+    local result, err = dirwatcher:start_watch(false)
+    if not result then
+      return result, err
+    end
+  end
+  return t
+end
+function fswatcher_meta:add_file(path, ...)
+  local fullpath, filename, dirname = get_full_path_name(path)
+  local t, err = add_directory(self, dirname)
+  if not t then
+    return t, err
+  end
+  t.files[filename] = path
+  return true
+end
+local INFINITE = 0xFFFFFFFF
+local function get_queued(self, timeout)
+  local startTime = C.GetTickCount64()
+  local timeout_ms
+  if timeout == nil then
+    timeout_ms = INFINITE
+  else
+    timeout_ms = timeout * 1000
+  end
+  local numberOfBytesPtr = ffi.new("DWORD[1]")
+  local completionKeyPtr = ffi.new("ULONG_PTR[1]")
+  local lpOverlapped = ffi.new("OVERLAPPED*[1]")
+  repeat
+    local result = C.GetQueuedCompletionStatus(self._rawport, numberOfBytesPtr, completionKeyPtr, lpOverlapped, timeout_ms)
+    if result == 0 then
+      local lasterror = C.GetLastError()
+      if lasterror == WAIT_TIMEOUT then
+        return nil, "timeout"
+      else
+        return nil, format_error("GetQueuedCompletionStatus", lasterror)
+      end
+    end
+    local numberOfBytes = numberOfBytesPtr[0]
+    local completionKey = tonumber(completionKeyPtr[0])
+    local dir_t = assert(self[completionKey], "invalid completion key: " .. tostring(completionKey))
+    local t = dir_t.dirwatcher:process(numberOfBytes)
+    dir_t.dirwatcher:start_watch(false)
+    local found = false
+    for i,v in ipairs(t) do
+      local path = dir_t.files[v.filename]
+      if path then
+        found = true
+        table.insert(self._pending, {path = path, action = v.action})
+      end
+    end
+    if found then
+      return true
+    end
+    if timeout_ms ~= INFINITE then
+      local tt = C.GetTickCount64()
+      timeout_ms = timeout_ms - (tt - startTime)
+      startTime = tt
+    end
+  until timeout_ms < 0
+  return nil, "timeout"
+end
+function fswatcher_meta:next(timeout)
+  if #self._pending > 0 then
+    local result = table.remove(self._pending, 1)
+    get_queued(self, 0) -- ignore error
+    return result
+  else
+    local result, err = get_queued(self, timeout)
+    if result == nil then
+      return nil, err
+    end
+    return table.remove(self._pending, 1)
+  end
+end
+function fswatcher_meta:close()
+  if self._rawport ~= nil then
+    for i,v in ipairs(self) do
+      v.dirwatcher:close()
+    end
+    C.CloseHandle(ffi.gc(self._rawport, nil))
+    self._rawport = nil
+  end
+end
+--[[
+local watcher = require("fswatcher_windows").new()
+assert(watcher:add_file("rdc-sync.c"))
+assert(watcher:add_file("sub2/hoge"))
+for i = 1, 10 do
+    local result, err = watcher:next(2)
+    if err == "timeout" then
+        print(os.date(), "timeout")
+    else
+        assert(result, err)
+        print(os.date(), result.path, result.action)
+    end
+end
+watcher:close()
+]]
+return {
+  new = new_watcher,
+}
+end
+--[[
   Copyright 2016,2018-2019 ARATA Mizuki
 
   This file is part of ClutTeX.
@@ -2143,7 +2579,7 @@
   along with ClutTeX.  If not, see <http://www.gnu.org/licenses/>.
 ]]
 
-CLUTTEX_VERSION = "v0.2"
+CLUTTEX_VERSION = "v0.3"
 
 -- Standard libraries
 local coroutine = coroutine
@@ -2165,6 +2601,8 @@
 local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file
 local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options
 
+os.setlocale("", "ctype") -- Workaround for recent Universal CRT
+
 -- arguments: input file name, jobname, etc...
 local function genOutputDirectory(...)
   -- The name of the temporary directory is based on the path of input file.
@@ -2611,6 +3049,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)
@@ -2621,28 +3060,87 @@
       table.insert(input_files_to_watch, fileinfo.abspath)
     end
   end
-  local fswatch_command = {"fswatch", "--event=Updated", "--"}
-  for _,path in ipairs(input_files_to_watch) do
-    table.insert(fswatch_command, shellutil.escape(path))
+  local fswatcherlib
+  if os.type == "windows" then
+    -- Windows: Try built-in filesystem watcher
+    local succ, result = pcall(require, "texrunner.fswatcher_windows")
+    if not succ and CLUTTEX_VERBOSITY >= 1 then
+      message.warn("Failed to load texrunner.fswatcher_windows: " .. result)
+    end
+    fswatcherlib = result
   end
-  if CLUTTEX_VERBOSITY >= 1 then
-    message.exec(table.concat(fswatch_command, " "))
-  end
-  local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r"))
-  for l in fswatch:lines() do
-    local found = false
+  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
-      if l == path then
-        found = true
-        break
+      assert(watcher:add_file(path))
+    end
+    while true do
+      local result = assert(watcher:next())
+      if CLUTTEX_VERBOSITY >= 2 then
+        message.info(string.format("%s %s"), result.action, result.path)
       end
-    end
-    if found then
       local success, status = do_typeset()
       if not success then
         -- Not successful
       end
     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))
+    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
+      end
+      if found then
+        local success, status = do_typeset()
+        if not success then
+          -- Not successful
+        end
+      end
+    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))
+    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
+      end
+      if found then
+        local success, status = do_typeset()
+        if not success then
+          -- Not successful
+        end
+      end
+    end
+  else
+    message.error("Could not watch files because neither `fswatch' nor `inotifywait' was installed.")
+    message.info("See ClutTeX's manual for details.")
+    os.exit(1)
   end
 
 else

Modified: trunk/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/CHANGELOG.md	2019-04-30 22:36:50 UTC (rev 50913)
@@ -1,3 +1,11 @@
+Version 0.3 (2019-04-30)
+-----
+
+Changes:
+
+* Support other methods for watching file system: `inotifywait` for Linux and a built-in one for Windows.
+* Fix `--no-*` options.
+
 Version 0.2 (2019-02-22)
 -----
 

Modified: trunk/Master/texmf-dist/doc/support/cluttex/Makefile
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/Makefile	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/Makefile	2019-04-30 22:36:50 UTC (rev 50913)
@@ -19,6 +19,7 @@
  src/texrunner/handleoption.lua \
  src/texrunner/isatty.lua \
  src/texrunner/message.lua \
+ src/texrunner/fswatcher_windows.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-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/README.md	2019-04-30 22:36:50 UTC (rev 50913)
@@ -62,7 +62,7 @@
   Change the current working directory to the output directory when running TeX.
 * `--watch`
   Watch input files for change.
-  Requires [fswatch](http://emcrisostomo.github.io/fswatch/) program to be installed.
+  Requires [fswatch](http://emcrisostomo.github.io/fswatch/) program or `inotifywait` program to be installed on Unix systems.
 * `--color[=WHEN]`
   Make ClutTeX's message colorful.
   `WHEN` is one of `always`, `auto`, or `never`.

Modified: trunk/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/bin/cluttex.bat	2019-04-30 22:36:50 UTC (rev 50913)
@@ -347,7 +347,7 @@
 if os.type == "windows" then
 package.preload["texrunner.shellutil"] = function(...)
 --[[
-  Copyright 2016 ARATA Mizuki
+  Copyright 2016,2019 ARATA Mizuki
 
   This file is part of ClutTeX.
 
@@ -366,6 +366,7 @@
 ]]
 
 local string_gsub = string.gsub
+local os_execute = os.execute
 
 -- s: string
 local function escape(s)
@@ -373,14 +374,21 @@
 end
 
 
+local function has_command(name)
+  local result = os_execute("where " .. escape(name) .. " > NUL 2>&1")
+  -- Note that os.execute returns a number on Lua 5.1 or LuaTeX
+  return result == 0 or result == true
+end
+
 return {
   escape = escape,
+  has_command = has_command,
 }
 end
 else
 package.preload["texrunner.shellutil"] = function(...)
 --[[
-  Copyright 2016 ARATA Mizuki
+  Copyright 2016,2019 ARATA Mizuki
 
   This file is part of ClutTeX.
 
@@ -403,6 +411,7 @@
 local table = table
 local table_insert = table.insert
 local table_concat = table.concat
+local os_execute = os.execute
 
 -- s: string
 local function escape(s)
@@ -433,8 +442,15 @@
 end
 
 
+local function has_command(name)
+  local result = os_execute("which " .. escape(name) .. " > /dev/null")
+  -- Note that os.execute returns a number on Lua 5.1 or LuaTeX
+  return result == 0 or result == true
+end
+
 return {
   escape = escape,
+  has_command = has_command,
 }
 end
 end
@@ -545,6 +561,7 @@
 
 -- options_and_params, i = parseoption(arg, options)
 -- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]}
+-- options_and_params[j] = {"option", "value"}
 -- arg[i], arg[i + 1], ..., arg[#arg] are non-options
 local function parseoption(arg, options)
   local i = 1
@@ -584,6 +601,7 @@
           elseif o.boolean and name == "no-" .. o.long then
             -- --no-option
             opt = o
+            param = false
             break
           end
         end
@@ -623,6 +641,7 @@
           elseif o.boolean and name == "no-" .. o.long then
             -- -no-option
             opt = o
+            param = false
             break
           end
         elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then
@@ -2127,7 +2146,424 @@
   info  = info_msg,
 }
 end
+package.preload["texrunner.fswatcher_windows"] = 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 ffi = require "ffi"
+local bitlib = assert(bit32 or bit, "Neither bit32 (Lua 5.2) nor bit (LuaJIT) found") -- Lua 5.2 or LuaJIT
+
+ffi.cdef[[
+typedef int BOOL;
+typedef unsigned int UINT;
+typedef uint32_t DWORD;
+typedef void *HANDLE;
+typedef uintptr_t ULONG_PTR;
+typedef uint16_t WCHAR;
+typedef struct _OVERLAPPED {
+  ULONG_PTR Internal;
+  ULONG_PTR InternalHigh;
+  union {
+    struct {
+      DWORD Offset;
+      DWORD OffsetHigh;
+    };
+    void *Pointer;
+  };
+  HANDLE hEvent;
+} OVERLAPPED;
+typedef struct _FILE_NOTIFY_INFORMATION {
+  DWORD NextEntryOffset;
+  DWORD Action;
+  DWORD FileNameLength;
+  WCHAR FileName[?];
+} FILE_NOTIFY_INFORMATION;
+typedef void (__stdcall *LPOVERLAPPED_COMPLETION_ROUTINE)(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, OVERLAPPED *lpOverlapped);
+DWORD GetLastError();
+BOOL CloseHandle(HANDLE hObject);
+HANDLE CreateFileA(const char *lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, void *lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
+HANDLE CreateIoCompletionPort(HANDLE fileHandle, HANDLE existingCompletionPort, ULONG_PTR completionKey, DWORD numberOfConcurrentThreads);
+BOOL ReadDirectoryChangesW(HANDLE hDirectory, void *lpBuffer, DWORD nBufferLength, BOOL bWatchSubtree, DWORD dwNotifyFilter, DWORD *lpBytesReturned, OVERLAPPED *lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpOverlappedCompletionRoutine);
+BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, DWORD *lpNumberOfBytes, ULONG_PTR *lpCompletionKey, OVERLAPPED **lpOverlapped, DWORD dwMilliseconds);
+int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, const char *lpMultiByteStr, int cbMultiByte, WCHAR *lpWideCharStr, int cchWideChar);
+int WideCharToMultiByte(UINT CodePage, DWORD dwFlags, const WCHAR *lpWideCharStr, int cchWideChar, char *lpMultiByteStr, int cbMultiByte, const char *lpDefaultChar, BOOL *lpUsedDefaultChar);
+DWORD GetFullPathNameA(const char *lpFileName, DWORD nBufferLength, char *lpBuffer, char **lpFilePart);
+uint64_t GetTickCount64();
+]]
+
+-- LuaTeX's FFI does not equate a null pointer with nil.
+-- On LuaJIT, ffi.NULL is just nil.
+local NULL = ffi.NULL
+
+-- GetLastError
+local ERROR_FILE_NOT_FOUND         = 0x0002
+local ERROR_PATH_NOT_FOUND         = 0x0003
+local ERROR_ACCESS_DENIED          = 0x0005
+local ERROR_INVALID_PARAMETER      = 0x0057
+local ERROR_INSUFFICIENT_BUFFER    = 0x007A
+local WAIT_TIMEOUT                 = 0x0102
+local ERROR_ABANDONED_WAIT_0       = 0x02DF
+local ERROR_NOACCESS               = 0x03E6
+local ERROR_INVALID_FLAGS          = 0x03EC
+local ERROR_NOTIFY_ENUM_DIR        = 0x03FE
+local ERROR_NO_UNICODE_TRANSLATION = 0x0459
+local KnownErrors = {
+  [ERROR_FILE_NOT_FOUND] = "ERROR_FILE_NOT_FOUND",
+  [ERROR_PATH_NOT_FOUND] = "ERROR_PATH_NOT_FOUND",
+  [ERROR_ACCESS_DENIED] = "ERROR_ACCESS_DENIED",
+  [ERROR_INVALID_PARAMETER] = "ERROR_INVALID_PARAMETER",
+  [ERROR_INSUFFICIENT_BUFFER] = "ERROR_INSUFFICIENT_BUFFER",
+  [ERROR_ABANDONED_WAIT_0] = "ERROR_ABANDONED_WAIT_0",
+  [ERROR_NOACCESS] = "ERROR_NOACCESS",
+  [ERROR_INVALID_FLAGS] = "ERROR_INVALID_FLAGS",
+  [ERROR_NOTIFY_ENUM_DIR] = "ERROR_NOTIFY_ENUM_DIR",
+  [ERROR_NO_UNICODE_TRANSLATION] = "ERROR_NO_UNICODE_TRANSLATION",
+}
+
+-- CreateFile
+local FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+local FILE_FLAG_OVERLAPPED       = 0x40000000
+local OPEN_EXISTING              = 3
+local FILE_SHARE_READ            = 0x00000001
+local FILE_SHARE_WRITE           = 0x00000002
+local FILE_SHARE_DELETE          = 0x00000004
+local FILE_LIST_DIRECTORY        = 0x1
+local INVALID_HANDLE_VALUE       = ffi.cast("void *", -1)
+
+-- ReadDirectoryChangesW / FILE_NOTIFY_INFORMATION
+local FILE_NOTIFY_CHANGE_FILE_NAME   = 0x00000001
+local FILE_NOTIFY_CHANGE_DIR_NAME    = 0x00000002
+local FILE_NOTIFY_CHANGE_ATTRIBUTES  = 0x00000004
+local FILE_NOTIFY_CHANGE_SIZE        = 0x00000008
+local FILE_NOTIFY_CHANGE_LAST_WRITE  = 0x00000010
+local FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020
+local FILE_NOTIFY_CHANGE_CREATION    = 0x00000040
+local FILE_NOTIFY_CHANGE_SECURITY    = 0x00000100
+local FILE_ACTION_ADDED              = 0x00000001
+local FILE_ACTION_REMOVED            = 0x00000002
+local FILE_ACTION_MODIFIED           = 0x00000003
+local FILE_ACTION_RENAMED_OLD_NAME   = 0x00000004
+local FILE_ACTION_RENAMED_NEW_NAME   = 0x00000005
+
+-- WideCharToMultiByte / MultiByteToWideChar
+local CP_ACP  = 0
+local CP_UTF8 = 65001
+
+local C = ffi.C
+
+local function format_error(name, lasterror, extra)
+  local errorname = KnownErrors[lasterror] or string.format("error code %d", lasterror)
+  if extra then
+    return string.format("%s failed with %s (0x%04x) [%s]", name, errorname, lasterror, extra)
+  else
+    return string.format("%s failed with %s (0x%04x)", name, errorname, lasterror)
+  end
+end
+local function wcs_to_mbs(wstr, wstrlen, codepage)
+  -- wstr: FFI uint16_t[?]
+  -- wstrlen: length of wstr, or -1 if NUL-terminated
+  if wstrlen == 0 then
+    return ""
+  end
+  codepage = codepage or CP_ACP
+  local dwFlags = 0
+  local result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, nil, 0, nil, nil)
+  if result <= 0 then
+    -- Failed
+    local lasterror = C.GetLastError()
+    -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("WideCharToMultiByte", lasterror)
+  end
+  local mbsbuf = ffi.new("char[?]", result)
+  result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, mbsbuf, result, nil, nil)
+  if result <= 0 then
+    -- Failed
+    local lasterror = C.GetLastError()
+    -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("WideCharToMultiByte", lasterror)
+  end
+  return ffi.string(mbsbuf, result)
+end
+local function mbs_to_wcs(str, codepage)
+  -- str: Lua string
+  if str == "" then
+    return ffi.new("WCHAR[0]")
+  end
+  codepage = codepage or CP_ACP
+  local dwFlags = 0
+  local result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, nil, 0)
+  if result <= 0 then
+    local lasterror = C.GetLastError()
+    -- ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("MultiByteToWideChar", lasterror)
+  end
+  local wcsbuf = ffi.new("WCHAR[?]", result)
+  result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, wcsbuf, result)
+  if result <= 0 then
+    local lasterror = C.GetLastError()
+    return nil, format_error("MultiByteToWideChar", lasterror)
+  end
+  return wcsbuf, result
+end
+
+
+local function get_full_path_name(filename)
+  local bufsize = 1024
+  local buffer
+  local filePartPtr = ffi.new("char*[1]")
+  local result
+  repeat
+    buffer = ffi.new("char[?]", bufsize)
+    result = C.GetFullPathNameA(filename, bufsize, buffer, filePartPtr)
+    if result == 0 then
+      local lasterror = C.GetLastError()
+      return nil, format_error("GetFullPathNameA", lasterror, filename)
+    elseif bufsize < result then
+      -- result: buffer size required to hold the path + terminating NUL
+      bufsize = result
+    end
+  until result < bufsize
+  local fullpath = ffi.string(buffer, result)
+  local filePart = ffi.string(filePartPtr[0])
+  local dirPart = ffi.string(buffer, ffi.cast("intptr_t", filePartPtr[0]) - ffi.cast("intptr_t", buffer)) -- LuaTeX's FFI doesn't support pointer subtraction
+  return fullpath, filePart, dirPart
+end
+
+--[[
+  dirwatche.dirname : string
+  dirwatcher._rawhandle : cdata HANDLE
+  dirwatcher._overlapped : cdata OVERLAPPED
+  dirwatcher._buffer : cdata char[?]
+]]
+local dirwatcher_meta = {}
+dirwatcher_meta.__index = dirwatcher_meta
+function dirwatcher_meta:close()
+  if self._rawhandle ~= nil then
+    C.CloseHandle(ffi.gc(self._rawhandle, nil))
+    self._rawhandle = nil
+  end
+end
+local function open_directory(dirname)
+  local dwShareMode = bitlib.bor(FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE)
+  local dwFlagsAndAttributes = bitlib.bor(FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED)
+  local handle = C.CreateFileA(dirname, FILE_LIST_DIRECTORY, dwShareMode, nil, OPEN_EXISTING, dwFlagsAndAttributes, nil)
+  if handle == INVALID_HANDLE_VALUE then
+    local lasterror = C.GetLastError()
+    print("Failed to open "..dirname)
+    return nil, format_error("CreateFileA", lasterror, dirname)
+  end
+  return setmetatable({
+    dirname = dirname,
+    _rawhandle = ffi.gc(handle, C.CloseHandle),
+    _overlapped = ffi.new("OVERLAPPED"),
+    _buffer = ffi.new("char[?]", 1024),
+  }, dirwatcher_meta)
+end
+function dirwatcher_meta:start_watch(watchSubtree)
+  local dwNotifyFilter = bitlib.bor(FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_LAST_ACCESS, FILE_NOTIFY_CHANGE_CREATION, FILE_NOTIFY_CHANGE_SECURITY)
+  local buffer = self._buffer
+  local bufferSize = ffi.sizeof(buffer)
+  local result = C.ReadDirectoryChangesW(self._rawhandle, buffer, bufferSize, watchSubtree, dwNotifyFilter, nil, self._overlapped, nil)
+  if result == 0 then
+    local lasterror = C.GetLastError()
+    return nil, format_error("ReadDirectoryChangesW", lasterror, self.dirname)
+  end
+  return true
+end
+local ActionTable = {
+  [FILE_ACTION_ADDED] = "added",
+  [FILE_ACTION_REMOVED] = "removed",
+  [FILE_ACTION_MODIFIED] = "modified",
+  [FILE_ACTION_RENAMED_OLD_NAME] = "rename_from",
+  [FILE_ACTION_RENAMED_NEW_NAME] = "rename_to",
+}
+function dirwatcher_meta:process(numberOfBytes)
+  -- self._buffer received `numberOfBytes` bytes
+  local buffer = self._buffer
+  numberOfBytes = math.min(numberOfBytes, ffi.sizeof(buffer))
+  local ptr = ffi.cast("char *", buffer)
+  local structSize = ffi.sizeof("FILE_NOTIFY_INFORMATION", 1)
+  local t = {}
+  while numberOfBytes >= structSize do
+    local notifyInfo = ffi.cast("FILE_NOTIFY_INFORMATION*", ptr)
+    local nextEntryOffset = notifyInfo.NextEntryOffset
+    local action = notifyInfo.Action
+    local fileNameLength = notifyInfo.FileNameLength
+    local fileName = notifyInfo.FileName
+    local u = { action = ActionTable[action], filename = wcs_to_mbs(fileName, fileNameLength / 2) }
+    table.insert(t, u)
+    if nextEntryOffset == 0 or numberOfBytes <= nextEntryOffset then
+      break
+    end
+    numberOfBytes = numberOfBytes - nextEntryOffset
+    ptr = ptr + nextEntryOffset
+  end
+  return t
+end
+
+--[[
+  watcher._rawport : cdata HANDLE
+  watcher._pending : array of {
+    action = ..., filename = ...
+  }
+  watcher._directories[dirname] = {
+    dir = directory watcher,
+    dirname = dirname,
+    files = { [filename] = user-supplied path } -- files to watch
+  }
+  watcher[i] = i-th directory (_directories[dirname] for some dirname)
+]]
+
+local fswatcher_meta = {}
+fswatcher_meta.__index = fswatcher_meta
+local function new_watcher()
+  local port = C.CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 0)
+  if port == NULL then
+    local lasterror = C.GetLastError()
+    return nil, format_error("CreateIoCompletionPort", lasterror)
+  end
+  return setmetatable({
+    _rawport = ffi.gc(port, C.CloseHandle), -- ?
+    _pending = {},
+    _directories = {},
+  }, fswatcher_meta)
+end
+local function add_directory(self, dirname)
+  local t = self._directories[dirname]
+  if not t then
+    local dirwatcher, err = open_directory(dirname)
+    if not dirwatcher then
+      return dirwatcher, err
+    end
+    t = { dirwatcher = dirwatcher, dirname = dirname, files = {} }
+    table.insert(self, t)
+    local i = #self
+    local result = C.CreateIoCompletionPort(dirwatcher._rawhandle, self._rawport, i, 0)
+    if result == NULL then
+      local lasterror = C.GetLastError()
+      return nil, format_error("CreateIoCompletionPort", lasterror, dirname)
+    end
+    self._directories[dirname] = t
+    local result, err = dirwatcher:start_watch(false)
+    if not result then
+      return result, err
+    end
+  end
+  return t
+end
+function fswatcher_meta:add_file(path, ...)
+  local fullpath, filename, dirname = get_full_path_name(path)
+  local t, err = add_directory(self, dirname)
+  if not t then
+    return t, err
+  end
+  t.files[filename] = path
+  return true
+end
+local INFINITE = 0xFFFFFFFF
+local function get_queued(self, timeout)
+  local startTime = C.GetTickCount64()
+  local timeout_ms
+  if timeout == nil then
+    timeout_ms = INFINITE
+  else
+    timeout_ms = timeout * 1000
+  end
+  local numberOfBytesPtr = ffi.new("DWORD[1]")
+  local completionKeyPtr = ffi.new("ULONG_PTR[1]")
+  local lpOverlapped = ffi.new("OVERLAPPED*[1]")
+  repeat
+    local result = C.GetQueuedCompletionStatus(self._rawport, numberOfBytesPtr, completionKeyPtr, lpOverlapped, timeout_ms)
+    if result == 0 then
+      local lasterror = C.GetLastError()
+      if lasterror == WAIT_TIMEOUT then
+        return nil, "timeout"
+      else
+        return nil, format_error("GetQueuedCompletionStatus", lasterror)
+      end
+    end
+    local numberOfBytes = numberOfBytesPtr[0]
+    local completionKey = tonumber(completionKeyPtr[0])
+    local dir_t = assert(self[completionKey], "invalid completion key: " .. tostring(completionKey))
+    local t = dir_t.dirwatcher:process(numberOfBytes)
+    dir_t.dirwatcher:start_watch(false)
+    local found = false
+    for i,v in ipairs(t) do
+      local path = dir_t.files[v.filename]
+      if path then
+        found = true
+        table.insert(self._pending, {path = path, action = v.action})
+      end
+    end
+    if found then
+      return true
+    end
+    if timeout_ms ~= INFINITE then
+      local tt = C.GetTickCount64()
+      timeout_ms = timeout_ms - (tt - startTime)
+      startTime = tt
+    end
+  until timeout_ms < 0
+  return nil, "timeout"
+end
+function fswatcher_meta:next(timeout)
+  if #self._pending > 0 then
+    local result = table.remove(self._pending, 1)
+    get_queued(self, 0) -- ignore error
+    return result
+  else
+    local result, err = get_queued(self, timeout)
+    if result == nil then
+      return nil, err
+    end
+    return table.remove(self._pending, 1)
+  end
+end
+function fswatcher_meta:close()
+  if self._rawport ~= nil then
+    for i,v in ipairs(self) do
+      v.dirwatcher:close()
+    end
+    C.CloseHandle(ffi.gc(self._rawport, nil))
+    self._rawport = nil
+  end
+end
+--[[
+local watcher = require("fswatcher_windows").new()
+assert(watcher:add_file("rdc-sync.c"))
+assert(watcher:add_file("sub2/hoge"))
+for i = 1, 10 do
+    local result, err = watcher:next(2)
+    if err == "timeout" then
+        print(os.date(), "timeout")
+    else
+        assert(result, err)
+        print(os.date(), result.path, result.action)
+    end
+end
+watcher:close()
+]]
+return {
+  new = new_watcher,
+}
+end
+--[[
   Copyright 2016,2018-2019 ARATA Mizuki
 
   This file is part of ClutTeX.
@@ -2146,7 +2582,7 @@
   along with ClutTeX.  If not, see <http://www.gnu.org/licenses/>.
 ]]
 
-CLUTTEX_VERSION = "v0.2"
+CLUTTEX_VERSION = "v0.3"
 
 -- Standard libraries
 local coroutine = coroutine
@@ -2168,6 +2604,8 @@
 local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file
 local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options
 
+os.setlocale("", "ctype") -- Workaround for recent Universal CRT
+
 -- arguments: input file name, jobname, etc...
 local function genOutputDirectory(...)
   -- The name of the temporary directory is based on the path of input file.
@@ -2614,6 +3052,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)
@@ -2624,28 +3063,87 @@
       table.insert(input_files_to_watch, fileinfo.abspath)
     end
   end
-  local fswatch_command = {"fswatch", "--event=Updated", "--"}
-  for _,path in ipairs(input_files_to_watch) do
-    table.insert(fswatch_command, shellutil.escape(path))
+  local fswatcherlib
+  if os.type == "windows" then
+    -- Windows: Try built-in filesystem watcher
+    local succ, result = pcall(require, "texrunner.fswatcher_windows")
+    if not succ and CLUTTEX_VERBOSITY >= 1 then
+      message.warn("Failed to load texrunner.fswatcher_windows: " .. result)
+    end
+    fswatcherlib = result
   end
-  if CLUTTEX_VERBOSITY >= 1 then
-    message.exec(table.concat(fswatch_command, " "))
-  end
-  local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r"))
-  for l in fswatch:lines() do
-    local found = false
+  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
-      if l == path then
-        found = true
-        break
+      assert(watcher:add_file(path))
+    end
+    while true do
+      local result = assert(watcher:next())
+      if CLUTTEX_VERBOSITY >= 2 then
+        message.info(string.format("%s %s"), result.action, result.path)
       end
-    end
-    if found then
       local success, status = do_typeset()
       if not success then
         -- Not successful
       end
     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))
+    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
+      end
+      if found then
+        local success, status = do_typeset()
+        if not success then
+          -- Not successful
+        end
+      end
+    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))
+    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
+      end
+      if found then
+        local success, status = do_typeset()
+        if not success then
+          -- Not successful
+        end
+      end
+    end
+  else
+    message.error("Could not watch files because neither `fswatch' nor `inotifywait' was installed.")
+    message.info("See ClutTeX's manual for details.")
+    os.exit(1)
   end
 
 else

Modified: trunk/Master/texmf-dist/doc/support/cluttex/build.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/build.lua	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/build.lua	2019-04-30 22:36:50 UTC (rev 50913)
@@ -28,6 +28,7 @@
   table.remove(arg, 1)
 end
 local outfile = arg[1]
+local preserve_location_info = false
 
 local modules = {
   {
@@ -82,6 +83,10 @@
     name = "texrunner.message",
     path = "texrunner/message.lua",
   },
+  {
+    name = "texrunner.fswatcher_windows",
+    path = "texrunner/fswatcher_windows.lua",
+  },
 }
 
 local imported_globals = {"io", "os", "string", "table", "package", "require", "assert", "error", "ipairs", "type", "select", "arg"}
@@ -102,7 +107,13 @@
 end
 
 local function strip_test_code(code)
-  return (code:gsub("%-%- TEST CODE\n(.-)%-%- END TEST CODE\n", ""))
+  if preserve_location_info then
+    return (code:gsub("%-%- TEST CODE\n.-%-%- END TEST CODE\n", function(s)
+      return (s:gsub("[^\n]",""))
+    end))
+  else
+    return (code:gsub("%-%- TEST CODE\n(.-)%-%- END TEST CODE\n", ""))
+  end
 end
 
 local function load_module_code(path)
@@ -133,8 +144,10 @@
   end
 end
 
-table.insert(lines, string.format("local %s = %s\n", table.concat(imported_globals, ", "), table.concat(imported_globals, ", ")))
-table.insert(lines, "local CLUTTEX_VERBOSITY, CLUTTEX_VERSION\n")
+if not preserve_location_info then
+  table.insert(lines, string.format("local %s = %s\n", table.concat(imported_globals, ", "), table.concat(imported_globals, ", ")))
+  table.insert(lines, "local CLUTTEX_VERBOSITY, CLUTTEX_VERSION\n")
+end
 
 if default_os then
   table.insert(lines, string.format("os.type = os.type or %q\n", default_os))
@@ -142,19 +155,34 @@
 
 -- LuajitTeX doesn't seem to set package.loaded table...
 table.insert(lines, "if lfs and not package.loaded['lfs'] then package.loaded['lfs'] = lfs end\n")
-
-for _,m in ipairs(modules) do
-  if m.path_windows or m.path_unix then
-    table.insert(lines, 'if os.type == "windows" then\n')
-    table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path_windows or m.path)))
-    table.insert(lines, 'else\n')
-    table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path_unix or m.path)))
-    table.insert(lines, 'end\n')
-  else
-    table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path)))
+if preserve_location_info then
+  table.insert(lines, "local loadstring = loadstring or load\n")
+  for _,m in ipairs(modules) do
+    if m.path_windows or m.path_unix then
+      table.insert(lines, 'if os.type == "windows" then\n')
+      table.insert(lines, string.format("package.preload[%q] = assert(loadstring(%q, %q))\n", m.name, load_module_code(m.path_windows or m.path), "=" .. (m.path_windows or m.path)))
+      table.insert(lines, 'else\n')
+      table.insert(lines, string.format("package.preload[%q] = assert(loadstring(%q, %q))\n", m.name, load_module_code(m.path_unix or m.path), "=" .. (m.path_unix or m.path)))
+      table.insert(lines, 'end\n')
+    else
+      table.insert(lines, string.format("package.preload[%q] = assert(loadstring(%q, %q))\n", m.name, load_module_code(m.path), "=" .. m.path))
+    end
   end
+  table.insert(lines, string.format("assert(loadstring(%q, %q))(...)\n", main, "=cluttex.lua"))
+else  
+  for _,m in ipairs(modules) do
+    if m.path_windows or m.path_unix then
+      table.insert(lines, 'if os.type == "windows" then\n')
+      table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path_windows or m.path)))
+      table.insert(lines, 'else\n')
+      table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path_unix or m.path)))
+      table.insert(lines, 'end\n')
+    else
+      table.insert(lines, string.format("package.preload[%q] = function(...)\n%send\n", m.name, load_module_code(m.path)))
+    end
+  end
+  table.insert(lines, strip_global_imports(main))
 end
-table.insert(lines, strip_global_imports(main))
 
 if outfile then
   io.output(assert(io.open(outfile, "wb")))

Modified: trunk/Master/texmf-dist/doc/support/cluttex/checkglobal.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/checkglobal.lua	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/checkglobal.lua	2019-04-30 22:36:50 UTC (rev 50913)
@@ -32,6 +32,7 @@
   tostring = true,
   type = true,
   xpcall = true,
+  loadstring = true, -- Lua 5.1
 
   -- Standard modules
   bit32 = true, -- Lua 5.2

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-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/doc/manual-ja.tex	2019-04-30 22:36:50 UTC (rev 50913)
@@ -10,8 +10,9 @@
 \renewcommand\sectionautorefname{セクション}
 \renewcommand\subsectionautorefname{サブセクション}
 
-\title{\ClutTeX{}マニュアル}
+\title{\ClutTeX{}マニュアル\\(バージョン0.3)}
 \author{ARATA Mizuki}
+\date{2019年4月30日}
 
 \begin{document}
 \maketitle
@@ -23,7 +24,7 @@
 \begin{itemize}
 \item 作業ディレクトリを\texttt{.aux}や\texttt{.log}等の「余計な」ファイルで散らかさない
 \item (相互参照の解決などで)複数回処理を行う必要がある場合に、必要な回数だけ自動で処理する
-\item 入力ファイルを監視し、変更があった場合に自動で再処理する(\texttt{--watch}オプション\footnote{別途プログラムが必要})
+\item 入力ファイルを監視し、変更があった場合に自動で再処理する(\texttt{--watch}オプション\footnote{Unix系OSでは、別途プログラムが必要。})
 \item MakeIndex, \BibTeX, Biber等のコマンドを自動で実行する(\texttt{--makeindex}オプション, \texttt{--bibtex}オプション, \texttt{--biber}オプション)
 \item p\TeX 系列の処理系でPDFを生成する場合、別途\texttt{dvipdfmx}を実行する必要がない(自動で\texttt{dvipdfmx}を実行する)
 \end{itemize}
@@ -71,7 +72,7 @@
   デフォルト:3
 \item[\texttt{--watch}]
   入力ファイルを監視する。
-  別途、\texttt{fswatch}プログラムが必要となる。
+  別途、\texttt{fswatch}プログラムまたは\texttt{inotifywait}プログラムが必要となる場合がある。
   詳しくは\autoref{sec:watch-mode}を参照。
 \item[\texttt{--color[=\metavar{WHEN}]}]
   ターミナルへの出力を色付けする。
@@ -145,9 +146,9 @@
 
 \section{監視モード}\label{sec:watch-mode}
 \ClutTeX{}に\texttt{--watch}オプションを指定して起動した場合、文書の処理後に\emph{監視モード}に入る。
-\texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}}プログラムが予めインストールされている必要がある。
 
-\ClutTeX{}の将来のバージョンでは、外部プログラムに頼らない監視モードを実装するかもしれない。
+Windows上では、\ClutTeX{}単体でファイルシステムの監視を行う。
+一方で、それ以外のOS(Unix系)では、\texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}}プログラムまたは\texttt{inotifywait}プログラムが予めインストールされている必要がある。
 
 \section{MakeIndexや\BibTeX}
 MakeIndexや\BibTeX を使って処理を行う場合は、\texttt{--makeindex}や\texttt{--bibtex}等のオプションを指定する。

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-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/doc/manual.tex	2019-04-30 22:36:50 UTC (rev 50913)
@@ -8,8 +8,10 @@
 \newcommand\texpkg[1]{\texttt{#1}}
 \newcommand\metavar[1]{\textnormal{\textsf{#1}}}
 
-\title{\ClutTeX\ manual}
+\title{\ClutTeX\ manual\\(Version 0.3)}
 \author{ARATA Mizuki}
+\date{2019-04-30}
+
 \begin{document}
 \maketitle
 \tableofcontents
@@ -20,7 +22,7 @@
 \begin{itemize}
 \item Does not clutter your working directory with ``extra'' files, like \texttt{.aux} or \texttt{.log}.
 \item If multiple runs are required to generate correct document, do so.
-\item Watch input files, and re-process documents if changes are detected\footnote{needs an external program}.
+\item Watch input files, and re-process documents if changes are detected\footnote{needs an external program if you are on a Unix system}.
 \item Run MakeIndex, \BibTeX, Biber, if requested.
 \item Produces a PDF, even if the engine (e.g.\ p\TeX) does not suport direct PDF generation.
 \end{itemize}
@@ -65,7 +67,7 @@
   Default: 3
 \item[\texttt{--watch}]
   Watch input files for change.
-  Requires \texttt{fswatch} command to be available.
+  May need an external program to be available.
   See \autoref{sec:watch-mode} for details.
 \item[\texttt{--color[=\metavar{WHEN}]}]
   Colorize messages.
@@ -138,9 +140,9 @@
 
 \section{Watch mode}\label{sec:watch-mode}
 If \texttt{--watch} option is given, \ClutTeX\ enters \emph{watch mode} after processing the document.
-An auxiliary program \texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}} needs to be installed for this mode.
 
-A future version of \ClutTeX\ may implement a built-in filesystem watcher.
+On Windows, a built-in filesystem watcher is implemented.
+On other platforms, an auxiliary program \texttt{fswatch}\footnote{\url{http://emcrisostomo.github.io/fswatch/}} or \texttt{inotifywait} needs to be installed.
 
 \section{MakeIndex and \BibTeX}
 If you want to generate index or bibliography, using MakeIndex or \BibTeX, set \texttt{--makeindex}, \texttt{--bibtex}, or \texttt{--biber} option.

Modified: trunk/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/src/cluttex.lua	2019-04-30 22:36:50 UTC (rev 50913)
@@ -18,7 +18,7 @@
   along with ClutTeX.  If not, see <http://www.gnu.org/licenses/>.
 ]]
 
-CLUTTEX_VERSION = "v0.2"
+CLUTTEX_VERSION = "v0.3"
 
 -- Standard libraries
 local table = table
@@ -45,6 +45,8 @@
 local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file
 local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options
 
+os.setlocale("", "ctype") -- Workaround for recent Universal CRT
+
 -- arguments: input file name, jobname, etc...
 local function genOutputDirectory(...)
   -- The name of the temporary directory is based on the path of input file.
@@ -491,6 +493,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)
@@ -501,28 +504,87 @@
       table.insert(input_files_to_watch, fileinfo.abspath)
     end
   end
-  local fswatch_command = {"fswatch", "--event=Updated", "--"}
-  for _,path in ipairs(input_files_to_watch) do
-    table.insert(fswatch_command, shellutil.escape(path))
+  local fswatcherlib
+  if os.type == "windows" then
+    -- Windows: Try built-in filesystem watcher
+    local succ, result = pcall(require, "texrunner.fswatcher_windows")
+    if not succ and CLUTTEX_VERBOSITY >= 1 then
+      message.warn("Failed to load texrunner.fswatcher_windows: " .. result)
+    end
+    fswatcherlib = result
   end
-  if CLUTTEX_VERBOSITY >= 1 then
-    message.exec(table.concat(fswatch_command, " "))
-  end
-  local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r"))
-  for l in fswatch:lines() do
-    local found = false
+  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
-      if l == path then
-        found = true
-        break
+      assert(watcher:add_file(path))
+    end
+    while true do
+      local result = assert(watcher:next())
+      if CLUTTEX_VERBOSITY >= 2 then
+        message.info(string.format("%s %s"), result.action, result.path)
       end
-    end
-    if found then
       local success, status = do_typeset()
       if not success then
         -- Not successful
       end
     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))
+    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
+      end
+      if found then
+        local success, status = do_typeset()
+        if not success then
+          -- Not successful
+        end
+      end
+    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))
+    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
+      end
+      if found then
+        local success, status = do_typeset()
+        if not success then
+          -- Not successful
+        end
+      end
+    end
+  else
+    message.error("Could not watch files because neither `fswatch' nor `inotifywait' was installed.")
+    message.info("See ClutTeX's manual for details.")
+    os.exit(1)
   end
 
 else

Added: trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.lua	                        (rev 0)
+++ trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.lua	2019-04-30 22:36:50 UTC (rev 50913)
@@ -0,0 +1,423 @@
+--[[
+  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 ffi = require "ffi"
+local bitlib = assert(bit32 or bit, "Neither bit32 (Lua 5.2) nor bit (LuaJIT) found") -- Lua 5.2 or LuaJIT
+
+ffi.cdef[[
+typedef int BOOL;
+typedef unsigned int UINT;
+typedef uint32_t DWORD;
+typedef void *HANDLE;
+typedef uintptr_t ULONG_PTR;
+typedef uint16_t WCHAR;
+typedef struct _OVERLAPPED {
+  ULONG_PTR Internal;
+  ULONG_PTR InternalHigh;
+  union {
+    struct {
+      DWORD Offset;
+      DWORD OffsetHigh;
+    };
+    void *Pointer;
+  };
+  HANDLE hEvent;
+} OVERLAPPED;
+typedef struct _FILE_NOTIFY_INFORMATION {
+  DWORD NextEntryOffset;
+  DWORD Action;
+  DWORD FileNameLength;
+  WCHAR FileName[?];
+} FILE_NOTIFY_INFORMATION;
+typedef void (__stdcall *LPOVERLAPPED_COMPLETION_ROUTINE)(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, OVERLAPPED *lpOverlapped);
+DWORD GetLastError();
+BOOL CloseHandle(HANDLE hObject);
+HANDLE CreateFileA(const char *lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, void *lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
+HANDLE CreateIoCompletionPort(HANDLE fileHandle, HANDLE existingCompletionPort, ULONG_PTR completionKey, DWORD numberOfConcurrentThreads);
+BOOL ReadDirectoryChangesW(HANDLE hDirectory, void *lpBuffer, DWORD nBufferLength, BOOL bWatchSubtree, DWORD dwNotifyFilter, DWORD *lpBytesReturned, OVERLAPPED *lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpOverlappedCompletionRoutine);
+BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, DWORD *lpNumberOfBytes, ULONG_PTR *lpCompletionKey, OVERLAPPED **lpOverlapped, DWORD dwMilliseconds);
+int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, const char *lpMultiByteStr, int cbMultiByte, WCHAR *lpWideCharStr, int cchWideChar);
+int WideCharToMultiByte(UINT CodePage, DWORD dwFlags, const WCHAR *lpWideCharStr, int cchWideChar, char *lpMultiByteStr, int cbMultiByte, const char *lpDefaultChar, BOOL *lpUsedDefaultChar);
+DWORD GetFullPathNameA(const char *lpFileName, DWORD nBufferLength, char *lpBuffer, char **lpFilePart);
+uint64_t GetTickCount64();
+]]
+
+-- LuaTeX's FFI does not equate a null pointer with nil.
+-- On LuaJIT, ffi.NULL is just nil.
+local NULL = ffi.NULL
+
+-- GetLastError
+local ERROR_FILE_NOT_FOUND         = 0x0002
+local ERROR_PATH_NOT_FOUND         = 0x0003
+local ERROR_ACCESS_DENIED          = 0x0005
+local ERROR_INVALID_PARAMETER      = 0x0057
+local ERROR_INSUFFICIENT_BUFFER    = 0x007A
+local WAIT_TIMEOUT                 = 0x0102
+local ERROR_ABANDONED_WAIT_0       = 0x02DF
+local ERROR_NOACCESS               = 0x03E6
+local ERROR_INVALID_FLAGS          = 0x03EC
+local ERROR_NOTIFY_ENUM_DIR        = 0x03FE
+local ERROR_NO_UNICODE_TRANSLATION = 0x0459
+local KnownErrors = {
+  [ERROR_FILE_NOT_FOUND] = "ERROR_FILE_NOT_FOUND",
+  [ERROR_PATH_NOT_FOUND] = "ERROR_PATH_NOT_FOUND",
+  [ERROR_ACCESS_DENIED] = "ERROR_ACCESS_DENIED",
+  [ERROR_INVALID_PARAMETER] = "ERROR_INVALID_PARAMETER",
+  [ERROR_INSUFFICIENT_BUFFER] = "ERROR_INSUFFICIENT_BUFFER",
+  [ERROR_ABANDONED_WAIT_0] = "ERROR_ABANDONED_WAIT_0",
+  [ERROR_NOACCESS] = "ERROR_NOACCESS",
+  [ERROR_INVALID_FLAGS] = "ERROR_INVALID_FLAGS",
+  [ERROR_NOTIFY_ENUM_DIR] = "ERROR_NOTIFY_ENUM_DIR",
+  [ERROR_NO_UNICODE_TRANSLATION] = "ERROR_NO_UNICODE_TRANSLATION",
+}
+
+-- CreateFile
+local FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+local FILE_FLAG_OVERLAPPED       = 0x40000000
+local OPEN_EXISTING              = 3
+local FILE_SHARE_READ            = 0x00000001
+local FILE_SHARE_WRITE           = 0x00000002
+local FILE_SHARE_DELETE          = 0x00000004
+local FILE_LIST_DIRECTORY        = 0x1
+local INVALID_HANDLE_VALUE       = ffi.cast("void *", -1)
+
+-- ReadDirectoryChangesW / FILE_NOTIFY_INFORMATION
+local FILE_NOTIFY_CHANGE_FILE_NAME   = 0x00000001
+local FILE_NOTIFY_CHANGE_DIR_NAME    = 0x00000002
+local FILE_NOTIFY_CHANGE_ATTRIBUTES  = 0x00000004
+local FILE_NOTIFY_CHANGE_SIZE        = 0x00000008
+local FILE_NOTIFY_CHANGE_LAST_WRITE  = 0x00000010
+local FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020
+local FILE_NOTIFY_CHANGE_CREATION    = 0x00000040
+local FILE_NOTIFY_CHANGE_SECURITY    = 0x00000100
+local FILE_ACTION_ADDED              = 0x00000001
+local FILE_ACTION_REMOVED            = 0x00000002
+local FILE_ACTION_MODIFIED           = 0x00000003
+local FILE_ACTION_RENAMED_OLD_NAME   = 0x00000004
+local FILE_ACTION_RENAMED_NEW_NAME   = 0x00000005
+
+-- WideCharToMultiByte / MultiByteToWideChar
+local CP_ACP  = 0
+local CP_UTF8 = 65001
+
+local C = ffi.C
+
+local function format_error(name, lasterror, extra)
+  local errorname = KnownErrors[lasterror] or string.format("error code %d", lasterror)
+  if extra then
+    return string.format("%s failed with %s (0x%04x) [%s]", name, errorname, lasterror, extra)
+  else
+    return string.format("%s failed with %s (0x%04x)", name, errorname, lasterror)
+  end
+end
+local function wcs_to_mbs(wstr, wstrlen, codepage)
+  -- wstr: FFI uint16_t[?]
+  -- wstrlen: length of wstr, or -1 if NUL-terminated
+  if wstrlen == 0 then
+    return ""
+  end
+  codepage = codepage or CP_ACP
+  local dwFlags = 0
+  local result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, nil, 0, nil, nil)
+  if result <= 0 then
+    -- Failed
+    local lasterror = C.GetLastError()
+    -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("WideCharToMultiByte", lasterror)
+  end
+  local mbsbuf = ffi.new("char[?]", result)
+  result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, mbsbuf, result, nil, nil)
+  if result <= 0 then
+    -- Failed
+    local lasterror = C.GetLastError()
+    -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("WideCharToMultiByte", lasterror)
+  end
+  return ffi.string(mbsbuf, result)
+end
+local function mbs_to_wcs(str, codepage)
+  -- str: Lua string
+  if str == "" then
+    return ffi.new("WCHAR[0]")
+  end
+  codepage = codepage or CP_ACP
+  local dwFlags = 0
+  local result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, nil, 0)
+  if result <= 0 then
+    local lasterror = C.GetLastError()
+    -- ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("MultiByteToWideChar", lasterror)
+  end
+  local wcsbuf = ffi.new("WCHAR[?]", result)
+  result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, wcsbuf, result)
+  if result <= 0 then
+    local lasterror = C.GetLastError()
+    return nil, format_error("MultiByteToWideChar", lasterror)
+  end
+  return wcsbuf, result
+end
+
+-- TEST CODE
+do
+  local ws = {0x3042}
+  local resultstr = wcs_to_mbs(ffi.new("WCHAR[1]", ws), 1, CP_UTF8)
+  assert(#resultstr == 3)
+  assert(resultstr == "\xE3\x81\x82") -- \u{XXXX} notation is not available on LuaJIT
+end
+-- END TEST CODE
+
+local function get_full_path_name(filename)
+  local bufsize = 1024
+  local buffer
+  local filePartPtr = ffi.new("char*[1]")
+  local result
+  repeat
+    buffer = ffi.new("char[?]", bufsize)
+    result = C.GetFullPathNameA(filename, bufsize, buffer, filePartPtr)
+    if result == 0 then
+      local lasterror = C.GetLastError()
+      return nil, format_error("GetFullPathNameA", lasterror, filename)
+    elseif bufsize < result then
+      -- result: buffer size required to hold the path + terminating NUL
+      bufsize = result
+    end
+  until result < bufsize
+  local fullpath = ffi.string(buffer, result)
+  local filePart = ffi.string(filePartPtr[0])
+  local dirPart = ffi.string(buffer, ffi.cast("intptr_t", filePartPtr[0]) - ffi.cast("intptr_t", buffer)) -- LuaTeX's FFI doesn't support pointer subtraction
+  return fullpath, filePart, dirPart
+end
+
+--[[
+  dirwatche.dirname : string
+  dirwatcher._rawhandle : cdata HANDLE
+  dirwatcher._overlapped : cdata OVERLAPPED
+  dirwatcher._buffer : cdata char[?]
+]]
+local dirwatcher_meta = {}
+dirwatcher_meta.__index = dirwatcher_meta
+function dirwatcher_meta:close()
+  if self._rawhandle ~= nil then
+    C.CloseHandle(ffi.gc(self._rawhandle, nil))
+    self._rawhandle = nil
+  end
+end
+local function open_directory(dirname)
+  local dwShareMode = bitlib.bor(FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE)
+  local dwFlagsAndAttributes = bitlib.bor(FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED)
+  local handle = C.CreateFileA(dirname, FILE_LIST_DIRECTORY, dwShareMode, nil, OPEN_EXISTING, dwFlagsAndAttributes, nil)
+  if handle == INVALID_HANDLE_VALUE then
+    local lasterror = C.GetLastError()
+    print("Failed to open "..dirname)
+    return nil, format_error("CreateFileA", lasterror, dirname)
+  end
+  return setmetatable({
+    dirname = dirname,
+    _rawhandle = ffi.gc(handle, C.CloseHandle),
+    _overlapped = ffi.new("OVERLAPPED"),
+    _buffer = ffi.new("char[?]", 1024),
+  }, dirwatcher_meta)
+end
+function dirwatcher_meta:start_watch(watchSubtree)
+  local dwNotifyFilter = bitlib.bor(FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_LAST_ACCESS, FILE_NOTIFY_CHANGE_CREATION, FILE_NOTIFY_CHANGE_SECURITY)
+  local buffer = self._buffer
+  local bufferSize = ffi.sizeof(buffer)
+  local result = C.ReadDirectoryChangesW(self._rawhandle, buffer, bufferSize, watchSubtree, dwNotifyFilter, nil, self._overlapped, nil)
+  if result == 0 then
+    local lasterror = C.GetLastError()
+    return nil, format_error("ReadDirectoryChangesW", lasterror, self.dirname)
+  end
+  return true
+end
+local ActionTable = {
+  [FILE_ACTION_ADDED] = "added",
+  [FILE_ACTION_REMOVED] = "removed",
+  [FILE_ACTION_MODIFIED] = "modified",
+  [FILE_ACTION_RENAMED_OLD_NAME] = "rename_from",
+  [FILE_ACTION_RENAMED_NEW_NAME] = "rename_to",
+}
+function dirwatcher_meta:process(numberOfBytes)
+  -- self._buffer received `numberOfBytes` bytes
+  local buffer = self._buffer
+  numberOfBytes = math.min(numberOfBytes, ffi.sizeof(buffer))
+  local ptr = ffi.cast("char *", buffer)
+  local structSize = ffi.sizeof("FILE_NOTIFY_INFORMATION", 1)
+  local t = {}
+  while numberOfBytes >= structSize do
+    local notifyInfo = ffi.cast("FILE_NOTIFY_INFORMATION*", ptr)
+    local nextEntryOffset = notifyInfo.NextEntryOffset
+    local action = notifyInfo.Action
+    local fileNameLength = notifyInfo.FileNameLength
+    local fileName = notifyInfo.FileName
+    local u = { action = ActionTable[action], filename = wcs_to_mbs(fileName, fileNameLength / 2) }
+    table.insert(t, u)
+    if nextEntryOffset == 0 or numberOfBytes <= nextEntryOffset then
+      break
+    end
+    numberOfBytes = numberOfBytes - nextEntryOffset
+    ptr = ptr + nextEntryOffset
+  end
+  return t
+end
+
+--[[
+  watcher._rawport : cdata HANDLE
+  watcher._pending : array of {
+    action = ..., filename = ...
+  }
+  watcher._directories[dirname] = {
+    dir = directory watcher,
+    dirname = dirname,
+    files = { [filename] = user-supplied path } -- files to watch
+  }
+  watcher[i] = i-th directory (_directories[dirname] for some dirname)
+]]
+
+local fswatcher_meta = {}
+fswatcher_meta.__index = fswatcher_meta
+local function new_watcher()
+  local port = C.CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 0)
+  if port == NULL then
+    local lasterror = C.GetLastError()
+    return nil, format_error("CreateIoCompletionPort", lasterror)
+  end
+  return setmetatable({
+    _rawport = ffi.gc(port, C.CloseHandle), -- ?
+    _pending = {},
+    _directories = {},
+  }, fswatcher_meta)
+end
+local function add_directory(self, dirname)
+  local t = self._directories[dirname]
+  if not t then
+    local dirwatcher, err = open_directory(dirname)
+    if not dirwatcher then
+      return dirwatcher, err
+    end
+    t = { dirwatcher = dirwatcher, dirname = dirname, files = {} }
+    table.insert(self, t)
+    local i = #self
+    local result = C.CreateIoCompletionPort(dirwatcher._rawhandle, self._rawport, i, 0)
+    if result == NULL then
+      local lasterror = C.GetLastError()
+      return nil, format_error("CreateIoCompletionPort", lasterror, dirname)
+    end
+    self._directories[dirname] = t
+    local result, err = dirwatcher:start_watch(false)
+    if not result then
+      return result, err
+    end
+  end
+  return t
+end
+function fswatcher_meta:add_file(path, ...)
+  local fullpath, filename, dirname = get_full_path_name(path)
+  local t, err = add_directory(self, dirname)
+  if not t then
+    return t, err
+  end
+  t.files[filename] = path
+  return true
+end
+local INFINITE = 0xFFFFFFFF
+local function get_queued(self, timeout)
+  local startTime = C.GetTickCount64()
+  local timeout_ms
+  if timeout == nil then
+    timeout_ms = INFINITE
+  else
+    timeout_ms = timeout * 1000
+  end
+  local numberOfBytesPtr = ffi.new("DWORD[1]")
+  local completionKeyPtr = ffi.new("ULONG_PTR[1]")
+  local lpOverlapped = ffi.new("OVERLAPPED*[1]")
+  repeat
+    local result = C.GetQueuedCompletionStatus(self._rawport, numberOfBytesPtr, completionKeyPtr, lpOverlapped, timeout_ms)
+    if result == 0 then
+      local lasterror = C.GetLastError()
+      if lasterror == WAIT_TIMEOUT then
+        return nil, "timeout"
+      else
+        return nil, format_error("GetQueuedCompletionStatus", lasterror)
+      end
+    end
+    local numberOfBytes = numberOfBytesPtr[0]
+    local completionKey = tonumber(completionKeyPtr[0])
+    local dir_t = assert(self[completionKey], "invalid completion key: " .. tostring(completionKey))
+    local t = dir_t.dirwatcher:process(numberOfBytes)
+    dir_t.dirwatcher:start_watch(false)
+    local found = false
+    for i,v in ipairs(t) do
+      local path = dir_t.files[v.filename]
+      if path then
+        found = true
+        table.insert(self._pending, {path = path, action = v.action})
+      end
+    end
+    if found then
+      return true
+    end
+    if timeout_ms ~= INFINITE then
+      local tt = C.GetTickCount64()
+      timeout_ms = timeout_ms - (tt - startTime)
+      startTime = tt
+    end
+  until timeout_ms < 0
+  return nil, "timeout"
+end
+function fswatcher_meta:next(timeout)
+  if #self._pending > 0 then
+    local result = table.remove(self._pending, 1)
+    get_queued(self, 0) -- ignore error
+    return result
+  else
+    local result, err = get_queued(self, timeout)
+    if result == nil then
+      return nil, err
+    end
+    return table.remove(self._pending, 1)
+  end
+end
+function fswatcher_meta:close()
+  if self._rawport ~= nil then
+    for i,v in ipairs(self) do
+      v.dirwatcher:close()
+    end
+    C.CloseHandle(ffi.gc(self._rawport, nil))
+    self._rawport = nil
+  end
+end
+--[[
+local watcher = require("fswatcher_windows").new()
+assert(watcher:add_file("rdc-sync.c"))
+assert(watcher:add_file("sub2/hoge"))
+for i = 1, 10 do
+    local result, err = watcher:next(2)
+    if err == "timeout" then
+        print(os.date(), "timeout")
+    else
+        assert(result, err)
+        print(os.date(), result.path, result.action)
+    end
+end
+watcher:close()
+]]
+return {
+  new = new_watcher,
+}


Property changes on: trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/fswatcher_windows.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/option.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/option.lua	2019-04-30 22:36:50 UTC (rev 50913)
@@ -19,6 +19,7 @@
 
 -- options_and_params, i = parseoption(arg, options)
 -- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]}
+-- options_and_params[j] = {"option", "value"}
 -- arg[i], arg[i + 1], ..., arg[#arg] are non-options
 local function parseoption(arg, options)
   local i = 1
@@ -58,6 +59,7 @@
           elseif o.boolean and name == "no-" .. o.long then
             -- --no-option
             opt = o
+            param = false
             break
           end
         end
@@ -97,6 +99,7 @@
           elseif o.boolean and name == "no-" .. o.long then
             -- -no-option
             opt = o
+            param = false
             break
           end
         elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then

Modified: trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_unix.lua	2019-04-30 22:36:50 UTC (rev 50913)
@@ -1,5 +1,5 @@
 --[[
-  Copyright 2016 ARATA Mizuki
+  Copyright 2016,2019 ARATA Mizuki
 
   This file is part of ClutTeX.
 
@@ -22,6 +22,7 @@
 local table = table
 local table_insert = table.insert
 local table_concat = table.concat
+local os_execute = os.execute
 
 -- s: string
 local function escape(s)
@@ -57,6 +58,13 @@
 assert(escape([[Hello' world!"]]) == [['Hello'"'"' world!"']])
 -- END TEST CODE
 
+local function has_command(name)
+  local result = os_execute("which " .. escape(name) .. " > /dev/null")
+  -- Note that os.execute returns a number on Lua 5.1 or LuaTeX
+  return result == 0 or result == true
+end
+
 return {
   escape = escape,
+  has_command = has_command,
 }

Modified: trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua
===================================================================
--- trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/doc/support/cluttex/src/texrunner/shellutil_windows.lua	2019-04-30 22:36:50 UTC (rev 50913)
@@ -1,5 +1,5 @@
 --[[
-  Copyright 2016 ARATA Mizuki
+  Copyright 2016,2019 ARATA Mizuki
 
   This file is part of ClutTeX.
 
@@ -18,6 +18,7 @@
 ]]
 
 local string_gsub = string.gsub
+local os_execute = os.execute
 
 -- s: string
 local function escape(s)
@@ -30,6 +31,13 @@
 assert(escape([[Hello\" world!"]]) == [["Hello\\\" world!\""]])
 -- END TEST CODE
 
+local function has_command(name)
+  local result = os_execute("where " .. escape(name) .. " > NUL 2>&1")
+  -- Note that os.execute returns a number on Lua 5.1 or LuaTeX
+  return result == 0 or result == true
+end
+
 return {
   escape = escape,
+  has_command = has_command,
 }

Modified: trunk/Master/texmf-dist/scripts/cluttex/cluttex.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/cluttex/cluttex.lua	2019-04-30 22:36:21 UTC (rev 50912)
+++ trunk/Master/texmf-dist/scripts/cluttex/cluttex.lua	2019-04-30 22:36:50 UTC (rev 50913)
@@ -344,7 +344,7 @@
 if os.type == "windows" then
 package.preload["texrunner.shellutil"] = function(...)
 --[[
-  Copyright 2016 ARATA Mizuki
+  Copyright 2016,2019 ARATA Mizuki
 
   This file is part of ClutTeX.
 
@@ -363,6 +363,7 @@
 ]]
 
 local string_gsub = string.gsub
+local os_execute = os.execute
 
 -- s: string
 local function escape(s)
@@ -370,14 +371,21 @@
 end
 
 
+local function has_command(name)
+  local result = os_execute("where " .. escape(name) .. " > NUL 2>&1")
+  -- Note that os.execute returns a number on Lua 5.1 or LuaTeX
+  return result == 0 or result == true
+end
+
 return {
   escape = escape,
+  has_command = has_command,
 }
 end
 else
 package.preload["texrunner.shellutil"] = function(...)
 --[[
-  Copyright 2016 ARATA Mizuki
+  Copyright 2016,2019 ARATA Mizuki
 
   This file is part of ClutTeX.
 
@@ -400,6 +408,7 @@
 local table = table
 local table_insert = table.insert
 local table_concat = table.concat
+local os_execute = os.execute
 
 -- s: string
 local function escape(s)
@@ -430,8 +439,15 @@
 end
 
 
+local function has_command(name)
+  local result = os_execute("which " .. escape(name) .. " > /dev/null")
+  -- Note that os.execute returns a number on Lua 5.1 or LuaTeX
+  return result == 0 or result == true
+end
+
 return {
   escape = escape,
+  has_command = has_command,
 }
 end
 end
@@ -542,6 +558,7 @@
 
 -- options_and_params, i = parseoption(arg, options)
 -- options[i] = {short = "o", long = "option" [, param = true] [, boolean = true] [, allow_single_hyphen = false]}
+-- options_and_params[j] = {"option", "value"}
 -- arg[i], arg[i + 1], ..., arg[#arg] are non-options
 local function parseoption(arg, options)
   local i = 1
@@ -581,6 +598,7 @@
           elseif o.boolean and name == "no-" .. o.long then
             -- --no-option
             opt = o
+            param = false
             break
           end
         end
@@ -620,6 +638,7 @@
           elseif o.boolean and name == "no-" .. o.long then
             -- -no-option
             opt = o
+            param = false
             break
           end
         elseif o.long and #name >= 2 and (o.long == name or (o.boolean and name == "no-" .. o.long)) then
@@ -2124,7 +2143,424 @@
   info  = info_msg,
 }
 end
+package.preload["texrunner.fswatcher_windows"] = 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 ffi = require "ffi"
+local bitlib = assert(bit32 or bit, "Neither bit32 (Lua 5.2) nor bit (LuaJIT) found") -- Lua 5.2 or LuaJIT
+
+ffi.cdef[[
+typedef int BOOL;
+typedef unsigned int UINT;
+typedef uint32_t DWORD;
+typedef void *HANDLE;
+typedef uintptr_t ULONG_PTR;
+typedef uint16_t WCHAR;
+typedef struct _OVERLAPPED {
+  ULONG_PTR Internal;
+  ULONG_PTR InternalHigh;
+  union {
+    struct {
+      DWORD Offset;
+      DWORD OffsetHigh;
+    };
+    void *Pointer;
+  };
+  HANDLE hEvent;
+} OVERLAPPED;
+typedef struct _FILE_NOTIFY_INFORMATION {
+  DWORD NextEntryOffset;
+  DWORD Action;
+  DWORD FileNameLength;
+  WCHAR FileName[?];
+} FILE_NOTIFY_INFORMATION;
+typedef void (__stdcall *LPOVERLAPPED_COMPLETION_ROUTINE)(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, OVERLAPPED *lpOverlapped);
+DWORD GetLastError();
+BOOL CloseHandle(HANDLE hObject);
+HANDLE CreateFileA(const char *lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, void *lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
+HANDLE CreateIoCompletionPort(HANDLE fileHandle, HANDLE existingCompletionPort, ULONG_PTR completionKey, DWORD numberOfConcurrentThreads);
+BOOL ReadDirectoryChangesW(HANDLE hDirectory, void *lpBuffer, DWORD nBufferLength, BOOL bWatchSubtree, DWORD dwNotifyFilter, DWORD *lpBytesReturned, OVERLAPPED *lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpOverlappedCompletionRoutine);
+BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, DWORD *lpNumberOfBytes, ULONG_PTR *lpCompletionKey, OVERLAPPED **lpOverlapped, DWORD dwMilliseconds);
+int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, const char *lpMultiByteStr, int cbMultiByte, WCHAR *lpWideCharStr, int cchWideChar);
+int WideCharToMultiByte(UINT CodePage, DWORD dwFlags, const WCHAR *lpWideCharStr, int cchWideChar, char *lpMultiByteStr, int cbMultiByte, const char *lpDefaultChar, BOOL *lpUsedDefaultChar);
+DWORD GetFullPathNameA(const char *lpFileName, DWORD nBufferLength, char *lpBuffer, char **lpFilePart);
+uint64_t GetTickCount64();
+]]
+
+-- LuaTeX's FFI does not equate a null pointer with nil.
+-- On LuaJIT, ffi.NULL is just nil.
+local NULL = ffi.NULL
+
+-- GetLastError
+local ERROR_FILE_NOT_FOUND         = 0x0002
+local ERROR_PATH_NOT_FOUND         = 0x0003
+local ERROR_ACCESS_DENIED          = 0x0005
+local ERROR_INVALID_PARAMETER      = 0x0057
+local ERROR_INSUFFICIENT_BUFFER    = 0x007A
+local WAIT_TIMEOUT                 = 0x0102
+local ERROR_ABANDONED_WAIT_0       = 0x02DF
+local ERROR_NOACCESS               = 0x03E6
+local ERROR_INVALID_FLAGS          = 0x03EC
+local ERROR_NOTIFY_ENUM_DIR        = 0x03FE
+local ERROR_NO_UNICODE_TRANSLATION = 0x0459
+local KnownErrors = {
+  [ERROR_FILE_NOT_FOUND] = "ERROR_FILE_NOT_FOUND",
+  [ERROR_PATH_NOT_FOUND] = "ERROR_PATH_NOT_FOUND",
+  [ERROR_ACCESS_DENIED] = "ERROR_ACCESS_DENIED",
+  [ERROR_INVALID_PARAMETER] = "ERROR_INVALID_PARAMETER",
+  [ERROR_INSUFFICIENT_BUFFER] = "ERROR_INSUFFICIENT_BUFFER",
+  [ERROR_ABANDONED_WAIT_0] = "ERROR_ABANDONED_WAIT_0",
+  [ERROR_NOACCESS] = "ERROR_NOACCESS",
+  [ERROR_INVALID_FLAGS] = "ERROR_INVALID_FLAGS",
+  [ERROR_NOTIFY_ENUM_DIR] = "ERROR_NOTIFY_ENUM_DIR",
+  [ERROR_NO_UNICODE_TRANSLATION] = "ERROR_NO_UNICODE_TRANSLATION",
+}
+
+-- CreateFile
+local FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+local FILE_FLAG_OVERLAPPED       = 0x40000000
+local OPEN_EXISTING              = 3
+local FILE_SHARE_READ            = 0x00000001
+local FILE_SHARE_WRITE           = 0x00000002
+local FILE_SHARE_DELETE          = 0x00000004
+local FILE_LIST_DIRECTORY        = 0x1
+local INVALID_HANDLE_VALUE       = ffi.cast("void *", -1)
+
+-- ReadDirectoryChangesW / FILE_NOTIFY_INFORMATION
+local FILE_NOTIFY_CHANGE_FILE_NAME   = 0x00000001
+local FILE_NOTIFY_CHANGE_DIR_NAME    = 0x00000002
+local FILE_NOTIFY_CHANGE_ATTRIBUTES  = 0x00000004
+local FILE_NOTIFY_CHANGE_SIZE        = 0x00000008
+local FILE_NOTIFY_CHANGE_LAST_WRITE  = 0x00000010
+local FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020
+local FILE_NOTIFY_CHANGE_CREATION    = 0x00000040
+local FILE_NOTIFY_CHANGE_SECURITY    = 0x00000100
+local FILE_ACTION_ADDED              = 0x00000001
+local FILE_ACTION_REMOVED            = 0x00000002
+local FILE_ACTION_MODIFIED           = 0x00000003
+local FILE_ACTION_RENAMED_OLD_NAME   = 0x00000004
+local FILE_ACTION_RENAMED_NEW_NAME   = 0x00000005
+
+-- WideCharToMultiByte / MultiByteToWideChar
+local CP_ACP  = 0
+local CP_UTF8 = 65001
+
+local C = ffi.C
+
+local function format_error(name, lasterror, extra)
+  local errorname = KnownErrors[lasterror] or string.format("error code %d", lasterror)
+  if extra then
+    return string.format("%s failed with %s (0x%04x) [%s]", name, errorname, lasterror, extra)
+  else
+    return string.format("%s failed with %s (0x%04x)", name, errorname, lasterror)
+  end
+end
+local function wcs_to_mbs(wstr, wstrlen, codepage)
+  -- wstr: FFI uint16_t[?]
+  -- wstrlen: length of wstr, or -1 if NUL-terminated
+  if wstrlen == 0 then
+    return ""
+  end
+  codepage = codepage or CP_ACP
+  local dwFlags = 0
+  local result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, nil, 0, nil, nil)
+  if result <= 0 then
+    -- Failed
+    local lasterror = C.GetLastError()
+    -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("WideCharToMultiByte", lasterror)
+  end
+  local mbsbuf = ffi.new("char[?]", result)
+  result = C.WideCharToMultiByte(codepage, dwFlags, wstr, wstrlen, mbsbuf, result, nil, nil)
+  if result <= 0 then
+    -- Failed
+    local lasterror = C.GetLastError()
+    -- Candidates: ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("WideCharToMultiByte", lasterror)
+  end
+  return ffi.string(mbsbuf, result)
+end
+local function mbs_to_wcs(str, codepage)
+  -- str: Lua string
+  if str == "" then
+    return ffi.new("WCHAR[0]")
+  end
+  codepage = codepage or CP_ACP
+  local dwFlags = 0
+  local result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, nil, 0)
+  if result <= 0 then
+    local lasterror = C.GetLastError()
+    -- ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_UNICODE_TRANSLATION
+    return nil, format_error("MultiByteToWideChar", lasterror)
+  end
+  local wcsbuf = ffi.new("WCHAR[?]", result)
+  result = C.MultiByteToWideChar(codepage, dwFlags, str, #str, wcsbuf, result)
+  if result <= 0 then
+    local lasterror = C.GetLastError()
+    return nil, format_error("MultiByteToWideChar", lasterror)
+  end
+  return wcsbuf, result
+end
+
+
+local function get_full_path_name(filename)
+  local bufsize = 1024
+  local buffer
+  local filePartPtr = ffi.new("char*[1]")
+  local result
+  repeat
+    buffer = ffi.new("char[?]", bufsize)
+    result = C.GetFullPathNameA(filename, bufsize, buffer, filePartPtr)
+    if result == 0 then
+      local lasterror = C.GetLastError()
+      return nil, format_error("GetFullPathNameA", lasterror, filename)
+    elseif bufsize < result then
+      -- result: buffer size required to hold the path + terminating NUL
+      bufsize = result
+    end
+  until result < bufsize
+  local fullpath = ffi.string(buffer, result)
+  local filePart = ffi.string(filePartPtr[0])
+  local dirPart = ffi.string(buffer, ffi.cast("intptr_t", filePartPtr[0]) - ffi.cast("intptr_t", buffer)) -- LuaTeX's FFI doesn't support pointer subtraction
+  return fullpath, filePart, dirPart
+end
+
+--[[
+  dirwatche.dirname : string
+  dirwatcher._rawhandle : cdata HANDLE
+  dirwatcher._overlapped : cdata OVERLAPPED
+  dirwatcher._buffer : cdata char[?]
+]]
+local dirwatcher_meta = {}
+dirwatcher_meta.__index = dirwatcher_meta
+function dirwatcher_meta:close()
+  if self._rawhandle ~= nil then
+    C.CloseHandle(ffi.gc(self._rawhandle, nil))
+    self._rawhandle = nil
+  end
+end
+local function open_directory(dirname)
+  local dwShareMode = bitlib.bor(FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE)
+  local dwFlagsAndAttributes = bitlib.bor(FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED)
+  local handle = C.CreateFileA(dirname, FILE_LIST_DIRECTORY, dwShareMode, nil, OPEN_EXISTING, dwFlagsAndAttributes, nil)
+  if handle == INVALID_HANDLE_VALUE then
+    local lasterror = C.GetLastError()
+    print("Failed to open "..dirname)
+    return nil, format_error("CreateFileA", lasterror, dirname)
+  end
+  return setmetatable({
+    dirname = dirname,
+    _rawhandle = ffi.gc(handle, C.CloseHandle),
+    _overlapped = ffi.new("OVERLAPPED"),
+    _buffer = ffi.new("char[?]", 1024),
+  }, dirwatcher_meta)
+end
+function dirwatcher_meta:start_watch(watchSubtree)
+  local dwNotifyFilter = bitlib.bor(FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_LAST_ACCESS, FILE_NOTIFY_CHANGE_CREATION, FILE_NOTIFY_CHANGE_SECURITY)
+  local buffer = self._buffer
+  local bufferSize = ffi.sizeof(buffer)
+  local result = C.ReadDirectoryChangesW(self._rawhandle, buffer, bufferSize, watchSubtree, dwNotifyFilter, nil, self._overlapped, nil)
+  if result == 0 then
+    local lasterror = C.GetLastError()
+    return nil, format_error("ReadDirectoryChangesW", lasterror, self.dirname)
+  end
+  return true
+end
+local ActionTable = {
+  [FILE_ACTION_ADDED] = "added",
+  [FILE_ACTION_REMOVED] = "removed",
+  [FILE_ACTION_MODIFIED] = "modified",
+  [FILE_ACTION_RENAMED_OLD_NAME] = "rename_from",
+  [FILE_ACTION_RENAMED_NEW_NAME] = "rename_to",
+}
+function dirwatcher_meta:process(numberOfBytes)
+  -- self._buffer received `numberOfBytes` bytes
+  local buffer = self._buffer
+  numberOfBytes = math.min(numberOfBytes, ffi.sizeof(buffer))
+  local ptr = ffi.cast("char *", buffer)
+  local structSize = ffi.sizeof("FILE_NOTIFY_INFORMATION", 1)
+  local t = {}
+  while numberOfBytes >= structSize do
+    local notifyInfo = ffi.cast("FILE_NOTIFY_INFORMATION*", ptr)
+    local nextEntryOffset = notifyInfo.NextEntryOffset
+    local action = notifyInfo.Action
+    local fileNameLength = notifyInfo.FileNameLength
+    local fileName = notifyInfo.FileName
+    local u = { action = ActionTable[action], filename = wcs_to_mbs(fileName, fileNameLength / 2) }
+    table.insert(t, u)
+    if nextEntryOffset == 0 or numberOfBytes <= nextEntryOffset then
+      break
+    end
+    numberOfBytes = numberOfBytes - nextEntryOffset
+    ptr = ptr + nextEntryOffset
+  end
+  return t
+end
+
+--[[
+  watcher._rawport : cdata HANDLE
+  watcher._pending : array of {
+    action = ..., filename = ...
+  }
+  watcher._directories[dirname] = {
+    dir = directory watcher,
+    dirname = dirname,
+    files = { [filename] = user-supplied path } -- files to watch
+  }
+  watcher[i] = i-th directory (_directories[dirname] for some dirname)
+]]
+
+local fswatcher_meta = {}
+fswatcher_meta.__index = fswatcher_meta
+local function new_watcher()
+  local port = C.CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 0)
+  if port == NULL then
+    local lasterror = C.GetLastError()
+    return nil, format_error("CreateIoCompletionPort", lasterror)
+  end
+  return setmetatable({
+    _rawport = ffi.gc(port, C.CloseHandle), -- ?
+    _pending = {},
+    _directories = {},
+  }, fswatcher_meta)
+end
+local function add_directory(self, dirname)
+  local t = self._directories[dirname]
+  if not t then
+    local dirwatcher, err = open_directory(dirname)
+    if not dirwatcher then
+      return dirwatcher, err
+    end
+    t = { dirwatcher = dirwatcher, dirname = dirname, files = {} }
+    table.insert(self, t)
+    local i = #self
+    local result = C.CreateIoCompletionPort(dirwatcher._rawhandle, self._rawport, i, 0)
+    if result == NULL then
+      local lasterror = C.GetLastError()
+      return nil, format_error("CreateIoCompletionPort", lasterror, dirname)
+    end
+    self._directories[dirname] = t
+    local result, err = dirwatcher:start_watch(false)
+    if not result then
+      return result, err
+    end
+  end
+  return t
+end
+function fswatcher_meta:add_file(path, ...)
+  local fullpath, filename, dirname = get_full_path_name(path)
+  local t, err = add_directory(self, dirname)
+  if not t then
+    return t, err
+  end
+  t.files[filename] = path
+  return true
+end
+local INFINITE = 0xFFFFFFFF
+local function get_queued(self, timeout)
+  local startTime = C.GetTickCount64()
+  local timeout_ms
+  if timeout == nil then
+    timeout_ms = INFINITE
+  else
+    timeout_ms = timeout * 1000
+  end
+  local numberOfBytesPtr = ffi.new("DWORD[1]")
+  local completionKeyPtr = ffi.new("ULONG_PTR[1]")
+  local lpOverlapped = ffi.new("OVERLAPPED*[1]")
+  repeat
+    local result = C.GetQueuedCompletionStatus(self._rawport, numberOfBytesPtr, completionKeyPtr, lpOverlapped, timeout_ms)
+    if result == 0 then
+      local lasterror = C.GetLastError()
+      if lasterror == WAIT_TIMEOUT then
+        return nil, "timeout"
+      else
+        return nil, format_error("GetQueuedCompletionStatus", lasterror)
+      end
+    end
+    local numberOfBytes = numberOfBytesPtr[0]
+    local completionKey = tonumber(completionKeyPtr[0])
+    local dir_t = assert(self[completionKey], "invalid completion key: " .. tostring(completionKey))
+    local t = dir_t.dirwatcher:process(numberOfBytes)
+    dir_t.dirwatcher:start_watch(false)
+    local found = false
+    for i,v in ipairs(t) do
+      local path = dir_t.files[v.filename]
+      if path then
+        found = true
+        table.insert(self._pending, {path = path, action = v.action})
+      end
+    end
+    if found then
+      return true
+    end
+    if timeout_ms ~= INFINITE then
+      local tt = C.GetTickCount64()
+      timeout_ms = timeout_ms - (tt - startTime)
+      startTime = tt
+    end
+  until timeout_ms < 0
+  return nil, "timeout"
+end
+function fswatcher_meta:next(timeout)
+  if #self._pending > 0 then
+    local result = table.remove(self._pending, 1)
+    get_queued(self, 0) -- ignore error
+    return result
+  else
+    local result, err = get_queued(self, timeout)
+    if result == nil then
+      return nil, err
+    end
+    return table.remove(self._pending, 1)
+  end
+end
+function fswatcher_meta:close()
+  if self._rawport ~= nil then
+    for i,v in ipairs(self) do
+      v.dirwatcher:close()
+    end
+    C.CloseHandle(ffi.gc(self._rawport, nil))
+    self._rawport = nil
+  end
+end
+--[[
+local watcher = require("fswatcher_windows").new()
+assert(watcher:add_file("rdc-sync.c"))
+assert(watcher:add_file("sub2/hoge"))
+for i = 1, 10 do
+    local result, err = watcher:next(2)
+    if err == "timeout" then
+        print(os.date(), "timeout")
+    else
+        assert(result, err)
+        print(os.date(), result.path, result.action)
+    end
+end
+watcher:close()
+]]
+return {
+  new = new_watcher,
+}
+end
+--[[
   Copyright 2016,2018-2019 ARATA Mizuki
 
   This file is part of ClutTeX.
@@ -2143,7 +2579,7 @@
   along with ClutTeX.  If not, see <http://www.gnu.org/licenses/>.
 ]]
 
-CLUTTEX_VERSION = "v0.2"
+CLUTTEX_VERSION = "v0.3"
 
 -- Standard libraries
 local coroutine = coroutine
@@ -2165,6 +2601,8 @@
 local extract_bibtex_from_aux_file = require "texrunner.auxfile".extract_bibtex_from_aux_file
 local handle_cluttex_options = require "texrunner.handleoption".handle_cluttex_options
 
+os.setlocale("", "ctype") -- Workaround for recent Universal CRT
+
 -- arguments: input file name, jobname, etc...
 local function genOutputDirectory(...)
   -- The name of the temporary directory is based on the path of input file.
@@ -2611,6 +3049,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)
@@ -2621,28 +3060,87 @@
       table.insert(input_files_to_watch, fileinfo.abspath)
     end
   end
-  local fswatch_command = {"fswatch", "--event=Updated", "--"}
-  for _,path in ipairs(input_files_to_watch) do
-    table.insert(fswatch_command, shellutil.escape(path))
+  local fswatcherlib
+  if os.type == "windows" then
+    -- Windows: Try built-in filesystem watcher
+    local succ, result = pcall(require, "texrunner.fswatcher_windows")
+    if not succ and CLUTTEX_VERBOSITY >= 1 then
+      message.warn("Failed to load texrunner.fswatcher_windows: " .. result)
+    end
+    fswatcherlib = result
   end
-  if CLUTTEX_VERBOSITY >= 1 then
-    message.exec(table.concat(fswatch_command, " "))
-  end
-  local fswatch = assert(io.popen(table.concat(fswatch_command, " "), "r"))
-  for l in fswatch:lines() do
-    local found = false
+  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
-      if l == path then
-        found = true
-        break
+      assert(watcher:add_file(path))
+    end
+    while true do
+      local result = assert(watcher:next())
+      if CLUTTEX_VERBOSITY >= 2 then
+        message.info(string.format("%s %s"), result.action, result.path)
       end
-    end
-    if found then
       local success, status = do_typeset()
       if not success then
         -- Not successful
       end
     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))
+    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
+      end
+      if found then
+        local success, status = do_typeset()
+        if not success then
+          -- Not successful
+        end
+      end
+    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))
+    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
+      end
+      if found then
+        local success, status = do_typeset()
+        if not success then
+          -- Not successful
+        end
+      end
+    end
+  else
+    message.error("Could not watch files because neither `fswatch' nor `inotifywait' was installed.")
+    message.info("See ClutTeX's manual for details.")
+    os.exit(1)
   end
 
 else



More information about the tex-live-commits mailing list