texlive[67027] trunk: texfindpkg (5may23)

commits+karl at tug.org commits+karl at tug.org
Fri May 5 23:25:43 CEST 2023


Revision: 67027
          http://tug.org/svn/texlive?view=revision&revision=67027
Author:   karl
Date:     2023-05-05 23:25:43 +0200 (Fri, 05 May 2023)
Log Message:
-----------
texfindpkg (5may23)

Modified Paths:
--------------
    trunk/Build/source/texk/texlive/linked_scripts/texfindpkg/texfindpkg.lua
    trunk/Master/texmf-dist/doc/man/man1/texfindpkg.1
    trunk/Master/texmf-dist/doc/man/man1/texfindpkg.man1.pdf
    trunk/Master/texmf-dist/doc/support/texfindpkg/README.md
    trunk/Master/texmf-dist/scripts/texfindpkg/texfindpkg.lua
    trunk/Master/texmf-dist/tex/latex/texfindpkg/texfindpkg.json.gz
    trunk/Master/tlpkg/libexec/ctan2tds

Added Paths:
-----------
    trunk/Master/texmf-dist/source/support/texfindpkg/
    trunk/Master/texmf-dist/source/support/texfindpkg/tfpbuild.lua

Modified: trunk/Build/source/texk/texlive/linked_scripts/texfindpkg/texfindpkg.lua
===================================================================
--- trunk/Build/source/texk/texlive/linked_scripts/texfindpkg/texfindpkg.lua	2023-05-05 21:23:26 UTC (rev 67026)
+++ trunk/Build/source/texk/texlive/linked_scripts/texfindpkg/texfindpkg.lua	2023-05-05 21:25:43 UTC (rev 67027)
@@ -5,16 +5,29 @@
 -- Repository: https://github.com/lvjr/texfindpkg
 -- License: GNU General Public License v3.0
 
-local tfpversion = "2023D"
-local tfpdate = "2023-04-05"
+local tfp = tfp or {}
 
+tfp.version = "2023E"
+tfp.date = "2023-05-05"
+
+local building = tfp.building
+local tfpresult = ""
+
 ------------------------------------------------------------
 --> \section{Some variables and functions}
 ------------------------------------------------------------
 
 local lfs = require("lfs")
+
 local insert = table.insert
-local match = string.match
+local remove = table.remove
+local concat = table.concat
+local gmatch = string.gmatch
+local match  = string.match
+local find   = string.find
+local gsub   = string.gsub
+local sub    = string.sub
+local rep    = string.rep
 
 local lookup = kpse.lookup
 kpse.set_program_name("kpsewhich")
@@ -24,9 +37,20 @@
 local gzip = gzip           -- for gzip.compress and gzip.decompress
 
 local function tfpPrint(msg)
-  print("[tfp] " .. msg)
+  msg = "[tfp] " .. msg
+  if building then
+    tfpresult = tfpresult .. msg .. "\n"
+  else
+    print(msg)
+  end
 end
 
+local function tfpRealPrint(msg)
+  if not building then
+    print("[tfp] " .. msg)
+  end
+end
+
 local showdbg = false
 
 local function dbgPrint(msg)
@@ -85,6 +109,7 @@
 ------------------------------------------------------------
 
 local tlpkgtext
+local tlinspkgtext
 
 local function tlReadPackageDB()
   local tlroot = kpse.var_value("TEXMFROOT")
@@ -97,7 +122,7 @@
   if #list > 0 then
     tlpkgtext = fileRead(tlroot .. "/" .. list[1])
     if not tlpkgtext then
-      tfpPrint("error in reading texlive package database!")
+      tfpPrint("error in reading texlive.tlpdb.main file!")
     end
   else
     -- no texlive.tlpdb.main file in a fresh TeX live
@@ -104,21 +129,26 @@
     tfpPrint("error in finding texlive package database!")
     tfpPrint("please run 'tlmgr update --self' first.")
   end
+  tlinspkgtext = fileRead(tlroot .. "/texlive.tlpdb")
+  if not tlinspkgtext then
+    tfpPrint("error in reading texlive.tlpdb file!")
+  end
 end
 
 local tlpkgdata = {}
+local tlinspkgdata = {}
 
 local function tlExtractFiles(name, desc)
   -- ignore binary packages
   -- also ignore latex-dev packages
-  if name:find("%.") or name:find("^latex%-[%a]-%-dev") then
+  if find(name, "%.") or find(name, "^latex%-[%a]-%-dev") then
     --print(name)
     return
   end
   -- ignore package files in doc folder
-  desc = desc:match("\nrunfiles .+") or ""
-  for base, ext in desc:gmatch("/([%a%d%-]+)%.([%a%d]+)\n") do
-    if ext == "sty" or ext == "cls" then
+  desc = match(desc, "\nrunfiles .+") or ""
+  for base, ext in gmatch(desc, "/([%a%d%-%.]+)%.([%a%d]+)\n") do
+    if ext == "sty" or ext == "cls" or ext == "tex" or ext == "ltx" then
       dbgPrint(name, base .. "." .. ext)
       tlpkgdata[base .. "." .. ext] = name
     end
@@ -125,40 +155,56 @@
   end
 end
 
-local function tlParsePackageDB()
-  tlpkgtext:gsub("name (.-)\n(.-)\n\n", tlExtractFiles)
+local function tlExtractPackages(name, desc)
+  tlinspkgdata[name] = true
 end
 
+local function tlParsePackageDB(tlpkgtext)
+  gsub(tlpkgtext, "name (.-)\n(.-)\n\n", tlExtractFiles)
+  return tlpkgdata
+end
+
+local function tlParseTwoPackageDB()
+  gsub(tlpkgtext, "name (.-)\n(.-)\n\n", tlExtractFiles)
+  -- texlive.tlpdb might use different eol characters
+  gsub(tlinspkgtext, "name (.-)\r?\n(.-)\r?\n\r?\n", tlExtractPackages)
+end
+
 ------------------------------------------------------------
 --> \section{Handle MiKTeX package database}
 ------------------------------------------------------------
 
 local mtpkgtext
+local mtinspkgtext
 
 local function mtReadPackageDB()
-  local mtvar = kpse.var_value("TEXMFVAR")
+  local mtvar = kpse.var_value("TEXMFDIST")
   if mtvar then
-    mtpdb = mtvar .. "/miktex/cache/packages/miktex-zzdb3-2.9/package-manifests.ini"
+    mtpkgtext = fileRead(mtvar .. "/miktex/config/package-manifests.ini")
+    if not mtpkgtext then
+      tfpPrint("error in reading package-manifests.ini file!")
+    end
+    mtinspkgtext = fileRead(mtvar .. "/miktex/config/packages.ini")
+    if not mtinspkgtext then
+      tfpPrint("error in reading packages.ini file!")
+    end
   else
     tfpPrint("error in finding texmf root!")
   end
-  mtpkgtext = fileRead(mtpdb)
-  if not mtpkgtext then
-    tfpPrint("error in reading miktex package database!")
-  end
 end
 
 local mtpkgdata = {}
+local mtinspkgdata = {}
 
 local function mtExtractFiles(name, desc)
   -- ignore package files in source or doc folders
   -- also ignore latex-dev packages
-  if name:find("_") or name:find("^latex%-[%a]-%-dev") then
+  if find(name, "_") or find(name, "^latex%-[%a]-%-dev") then
     --print(name)
     return
   end
-  for base, ext in desc:gmatch("/([%a%d%-]+)%.([%a%d]+)\r?\n") do
-    if ext == "sty" or ext == "cls" then
+  for base, ext in gmatch(desc, "/([%a%d%-%.]+)%.([%a%d]+)\r?\n") do
+    if ext == "sty" or ext == "cls" or ext == "tex" or ext == "ltx" then
       dbgPrint(name, base .. "." .. ext)
       mtpkgdata[base .. "." .. ext] = name
     end
@@ -165,145 +211,29 @@
   end
 end
 
-local function mtParsePackageDB()
-  -- package-manifests.ini might use different eol characters
-  mtpkgtext:gsub("%[(.-)%]\r?\n(.-)\r?\n\r?\n", mtExtractFiles)
+local function mtExtractPackages(name, desc)
+  mtinspkgdata[name] = true
 end
 
-------------------------------------------------------------
---> \section{Compare TeX Live and MiKTeX packages}
-------------------------------------------------------------
-
-local tlpkgname = "download/texlive.tlpdb"
-local mtpkgname = "download/package-manifests.ini"
-
-local function compareDistributions()
-  tlpkgtext = fileRead(tlpkgname)
-  if tlpkgtext then
-    tlParsePackageDB()
-  else
-    tfpPrint("error in reading texlive package database!")
-  end
-  mtpkgtext = fileRead(mtpkgname)
-  if mtpkgtext then
-    mtParsePackageDB()
-  else
-    tfpPrint("error in reading miktex package database!")
-  end
-  local tlmissing, mkmissing = {}, {}
-  for k, vt in pairs(tlpkgdata) do
-    local vm = mtpkgdata[k] -- or "[none]"
-    if vm then
-      mtpkgdata[k] = nil -- remove it
-      if vm ~= vt then
-        print("texlive->" .. vt, "miktex->" .. vm, k)
-      end
-    else
-      insert(mkmissing, {vt, k})
-    end
-  end
-  for k, v in pairs(mtpkgdata) do
-    print("texlive doesn't include " .. v .. " -> " .. k)
-  end
-  for _, v in ipairs(mkmissing) do
-    print("miktex doesn't include " .. v[1] .. " -> " .. v[2])
-  end
+local function mtParsePackageDB(mtpkgtext)
+  -- package-manifests.ini might use different eol characters
+  gsub(mtpkgtext, "%[(.-)%]\r?\n(.-)\r?\n\r?\n", mtExtractFiles)
+  return mtpkgdata
 end
 
-------------------------------------------------------------
---> \section{Generate json file from cwl files}
-------------------------------------------------------------
-
-local function insertNewValue(tbl, val)
-  if not valueExists(tbl, val) then
-    insert(tbl, val)
-  end
+local function mtParseTwoPackageDB()
+  -- package-manifests.ini and packages.ini might use different eol characters
+  gsub(mtpkgtext, "%[(.-)%]\r?\n(.-)\r?\n\r?\n", mtExtractFiles)
+  gsub(mtinspkgtext, "%[(.-)%]\r?\n(.-)\r?\n\r?\n", mtExtractPackages)
 end
 
-local function extractFileData(cwl)
-  -- the cwl files have different eol characters
-  local deps = {}
-  for d in cwl:gmatch("\n#include:(.-)[\r\n]") do
-    --dbgPrint(d)
-    n = d:match("^class-(.+)$")
-    if n then
-      insertNewValue(deps, n .. ".cls")
-    else
-      insertNewValue(deps, d .. ".sty")
-    end
-  end
-  local envs = {}
-  for b, e in cwl:gmatch("\n\\begin{(.-)}.-\n\\end{(.-)}") do
-    if b == e then
-      --dbgPrint("{" .. e .. "}")
-      insert(envs, e)
-    end
-  end
-  local cmds = {}
-  for c in cwl:gmatch("\n\\(%a+)") do
-    if c ~= "begin" and c ~= "end" then
-      --dbgPrint("\\" .. c)
-      if not valueExists(cmds, c) then
-        insert(cmds, c)
-      end
-    end
-  end
-  --return {deps, envs, cmds}
-  return {deps = deps, envs = envs, cmds = cmds}
-end
-
-local function writeJson(cwldata)
-  tfpPrint("writing json database to file...")
-  local tbl1 = {}
-  for k, v in pairs(cwldata) do
-    table.insert(tbl1, {k, v})
-  end
-  table.sort(tbl1, function(a, b)
-    if a[1] < b[1] then return true end
-  end)
-  local tbl2 = {}
-  for _, v in ipairs(tbl1) do
-    local item = '"' .. v[1] .. '":' .. json.tostring(v[2])
-    table.insert(tbl2, item)
-  end
-  local text = "{\n" .. table.concat(tbl2, "\n,\n") .. "\n}"
-  fileWrite(text, "texfindpkg.json")
-  fileWrite(gzip.compress(text), "texfindpkg.json.gz")
-end
-
-local cwlpath = "completion"
-
-local function generateJsonData()
-  local list = getFiles(cwlpath, "%.cwl$")
-  local fname
-  local cwldata = {}
-  for _, v in ipairs(list) do
-    local a, b = match(v, "^([^%-]-)%-(.+)%.cwl")
-    if a == "class" then
-      fname = b .. ".cls"
-    else
-      b = match(v, "^(.+)%.cwl")
-      fname = b .. ".sty"
-    end
-    --print(fname)
-    local cwl = fileRead(cwlpath .. "/" .. v)
-    --print(cwl)
-    if cwl then
-      local item = extractFileData(cwl)
-      dbgPrint(item)
-      cwldata[fname] = item
-    else
-      tfpPrint("error in reading " .. v)
-    end
-  end
-  writeJson(cwldata)
-end
-
 ------------------------------------------------------------
 --> \section{Install packages in current TeX distribution}
 ------------------------------------------------------------
 
-local dist -- name of current tex distribution
+local dist               -- name of current tex distribution
+local totalinslist = {}  -- list of all missing packages
+local filecount = 0      -- total number of files found
 
 local function initPackageDB()
   dist = testDistribution()
@@ -310,10 +240,10 @@
   tfpPrint("you are using " .. dist)
   if dist == "texlive" then
     tlReadPackageDB()
-    tlParsePackageDB()
+    tlParseTwoPackageDB()
   else
     mtReadPackageDB()
-    mtParsePackageDB()
+    mtParseTwoPackageDB()
   end
 end
 
@@ -325,32 +255,86 @@
   end
 end
 
-local function tfpExecute(c)
-  if os.type == "windows" then
-    os.execute(c)
+local function checkInsPakage(pkg)
+  if dist == "texlive" then
+    return tlinspkgdata[pkg]
   else
-    os.execute('sudo env "PATH=$PATH" ' .. c)
+    return mtinspkgdata[pkg]
   end
 end
 
+local function tfpExecute(c)
+  if not building then
+    if os.type == "windows" then
+      os.execute(c)
+    else
+      os.execute('sudo env "PATH=$PATH" ' .. c)
+    end
+  end
+end
+
 local function installSomePackages(list)
   if not list then return end
   if dist == "texlive" then
-    local p = table.concat(list, " ")
-    tfpPrint("installing package " .. p)
-    tfpExecute("tlmgr install " .. p)
+    local pkgs = concat(list, " ")
+    if #list > 1 then
+      tfpRealPrint("installing texlive packages: " .. pkgs)
+    else
+      tfpRealPrint("installing texlive package: " .. pkgs)
+    end
+    tfpExecute("tlmgr install " .. pkgs)
   else
     for _, p in ipairs(list) do
-      tfpPrint("installing package " .. p)
+      tfpRealPrint("installing miktex package: " .. p)
       tfpExecute("miktex packages install " .. p)
     end
   end
 end
 
+local function updateTotalInsList(inslist)
+  if #totalinslist == 0 then
+    totalinslist = inslist
+  else
+    for _, pkg in ipairs(inslist) do
+      if not valueExists(totalinslist, pkg) then
+        insert(totalinslist, pkg)
+      end
+    end
+  end
+end
+
 local function listSomePackages(list)
-  if not list then return end
-  local p = table.concat(list, " ")
-  tfpPrint("please install " .. dist .. " package " .. p)
+  if not list then return {} end
+  if #list > 0 then
+    filecount = filecount + 1
+  end
+  local pkgs = concat(list, " ")
+  if #list == 1 then
+    tfpPrint(dist .. " package needed: " .. pkgs)
+  else
+    tfpPrint(dist .. " packages needed: " .. pkgs)
+  end
+  local inslist = {}
+  for _, p in ipairs(list) do
+    if not checkInsPakage(p) then
+      insert(inslist, p)
+    end
+  end
+  if #inslist == 0 then
+    if #list == 1 then
+      tfpRealPrint("this package is already installed")
+    else
+      tfpRealPrint("these packages are already installed")
+    end
+  else
+    local pkgs = concat(inslist, " ")
+    if #inslist == 1 then
+      tfpRealPrint(dist .. " package not yet installed: " .. pkgs)
+    else
+      tfpRealPrint(dist .. " packages not yet installed: " .. pkgs)
+    end
+  end
+  updateTotalInsList(inslist)
 end
 
 ------------------------------------------------------------
@@ -359,7 +343,8 @@
 
 local tfptext = ""  -- the json text
 local tfpdata = {}  -- the lua object
-local fnlist = {}  -- file name list
+local fnlist  = {}  -- file name list
+local pkglist = {}  -- package name list
 
 local function initDependencyDB()
   local ziptext = fileRead(lookup("texfindpkg.json.gz"))
@@ -372,67 +357,87 @@
   end
 end
 
-local function findDependencies(fname)
+local function printDependency(fname, level)
+  local msg = fname
+  local pkg = findOnePackage(fname)
+  if pkg then
+    msg = msg .. " (from " .. pkg .. ")"
+    if not valueExists(pkglist, pkg) then
+      insert(pkglist, pkg)
+    end
+  else
+    msg = msg .. " (not found)"
+  end
+  if level == 0 then
+    tfpPrint(msg)
+  else
+    tfpPrint(rep("   ", level - 1) .. "|- " .. msg)
+  end
+end
+
+local function findDependencies(fname, level)
   --print(fname)
   if valueExists(fnlist, fname) then return end
   local item = tfpdata[fname]
   if not item then
-    tfpPrint("no dependency info for " .. fname)
+    -- no dependency info for fname
+    printDependency(fname, level)
     return
   end
-  tfpPrint("finding dependencies for " .. fname)
-  table.insert(fnlist, fname)
+  -- finding dependencies for fname
+  printDependency(fname, level)
+  insert(fnlist, fname)
   local deps = item.deps
   if deps then
     for _, dname in ipairs(deps) do
-      findDependencies(dname)
+      findDependencies(dname, level + 1)
     end
   end
 end
 
 local function queryByFileName(fname)
-  fnlist = {} -- reset the list
-  findDependencies(fname)
+  fnlist, pkglist = {}, {} -- reset the list
+  tfpPrint("building dependency tree for " .. fname .. ":")
+  tfpPrint(rep("-", 24))
+  findDependencies(fname, 0)
+  tfpPrint(rep("-", 24))
   if #fnlist == 0 then
     tfpPrint("could not find any package with file " .. fname)
     return
   end
-  local pkglist = {}
-  for _, fn in ipairs(fnlist) do
-    local pkg = findOnePackage(fn)
-    --print(fn, pkg)
-    if pkg then
-      table.insert(pkglist, pkg)
-    end
-  end
-  if not pkglist then
+  if #pkglist == 0 then
     tfpPrint("error in finding package in " .. dist)
     return
   end
-  return pkglist
+  listSomePackages(pkglist)
 end
 
 local function getFileNameFromCmdEnvName(cmdenv, name)
   --print(name)
-  for line in tfptext:gmatch("(.-)\n[,}]") do
-    if line:find('"' .. name .. '"') then
+  local flist = {}
+  for line in gmatch(tfptext, "(.-)\n[,}]") do
+    if find(line, '"' .. name .. '"') then
       --print(line)
-      local fname, fspec = line:match('"(.-)":(.+)')
+      local fname, fspec = match(line, '"(.-)":(.+)')
       --print(fname, fspec)
       local item = json.tolua(fspec)
-      if valueExists(item[cmdenv], name) then
-        tfpPrint("found package file " .. fname)
-        return fname
+      if item[cmdenv] and valueExists(item[cmdenv], name) then
+        insert(flist, fname)
       end
     end
   end
+  return flist
 end
 
 local function queryByCommandName(cname)
   --print(cname)
-  local fname = getFileNameFromCmdEnvName("cmds", cname)
-  if fname then
-    return queryByFileName(fname)
+  local flist = getFileNameFromCmdEnvName("cmds", cname)
+  if #flist > 0 then
+    for _, fname in ipairs(flist) do
+      tfpPrint(rep("=", 48))
+      tfpPrint("found package file " .. fname .. " with command \\" .. cname)
+      queryByFileName(fname)
+    end
   else
     tfpPrint("could not find any package with command \\" .. cname)
   end
@@ -440,83 +445,187 @@
 
 local function queryByEnvironmentName(ename)
   --print(ename)
-  local fname = getFileNameFromCmdEnvName("envs", ename)
-  if fname then
-    return queryByFileName(fname)
+  local flist = getFileNameFromCmdEnvName("envs", ename)
+  if #flist > 0 then
+    for _, fname in ipairs(flist) do
+      tfpPrint(rep("=", 48))
+      tfpPrint("found package file " .. fname .. " with environment {" .. ename .. "}")
+      queryByFileName(fname)
+    end
   else
     tfpPrint("could not find any package with environment {" .. ename .. "}")
   end
 end
 
-local function query(name)
-  local h = name:sub(1,1)
+local function queryOne(t, name)
+  if t == "cmd" then
+    queryByCommandName(name)
+  elseif t == "env" then
+    queryByEnvironmentName(name)
+  elseif t == "file" then
+    tfpPrint(rep("=", 48))
+    queryByFileName(name)
+  else
+    -- do something for t == "pkg"
+  end
+end
+
+local function query(namelist)
+  for _, v in ipairs(namelist) do
+    queryOne(v[1], v[2])
+  end
+  if filecount > 1 then
+    tfpRealPrint(rep("=", 48))
+    local pkgs = concat(totalinslist, " ")
+    if #totalinslist == 1 then
+      tfpRealPrint(dist .. " package not yet installed in total: " .. pkgs)
+    else
+      tfpRealPrint(dist .. " packages not yet installed in total: " .. pkgs)
+    end
+  end
+end
+
+local function install(namelist)
+  query(namelist)
+  if #totalinslist > 0 then
+    installSomePackages(totalinslist)
+  end
+end
+
+------------------------------------------------------------
+--> \section{Parse query or install arguments}
+------------------------------------------------------------
+
+local function parseName(name)
+  local h = sub(name, 1, 1)
   if h == "\\" then
-    local b = name:sub(2)
-    return queryByCommandName(b)
+    local b = sub(name, 2)
+    return({"cmd", b})
   elseif h == "{" then
-    if name:sub(-1) == "}" then
-      local b = name:sub(2,-2)
-      return queryByEnvironmentName(b)
+    if sub(name, -1) == "}" then
+      local b = sub(name, 2, -2)
+      return({"env", b})
     else
-      tfpPrint("invalid input " .. name)
+      error("invalid name '" .. name .. "'")
     end
+  elseif find(name, "%.") then
+    return({"file", name})
   else
-    return queryByFileName(name)
+    return({"pkg", name})
   end
 end
 
-local function install(name)
-  local list = query(name)
-  installSomePackages(list)
+local function parseArgList(arglist)
+  local namelist = {}
+  local nametype = nil
+  for _, v in ipairs(arglist) do
+    if v == "-f" then
+      nametype = "file"
+    elseif v == "-c" then
+      nametype = "cmd"
+    elseif v == "-e" then
+      nametype = "env"
+    else
+      if nametype then
+        insert(namelist, {nametype, v})
+      else
+        insert(namelist, parseName(v))
+      end
+    end
+  end
+  if #namelist == 0 then
+    error("missing the name of file/cmd/env!")
+  else
+    return namelist
+  end
 end
 
+local function doQuery(arglist)
+  local namelist = parseArgList(arglist)
+  initPackageDB()
+  initDependencyDB()
+  query(namelist)
+end
+
+local function doInstall(arglist)
+  local namelist = parseArgList(arglist)
+  initPackageDB()
+  initDependencyDB()
+  install(namelist)
+end
+
 ------------------------------------------------------------
---> \section{Respond to user input}
+--> \section{Print help or version text}
 ------------------------------------------------------------
 
 local helptext = [[
 usage: texfindpkg <action> [<options>] [<name>]
 
-Valid actions are:
+valid actions are:
    install      Install some package and its dependencies
    query        Query dependencies for some package
+   help         Print this message and exit
+   version      Print version information and exit
 
-Valid options are:
-   --help       Print this message and exit
-   --version    Print version information and exit
+valid options are:
+   -f           Query or install by file name
+   -c           Query or install by command name
+   -e           Query or install by environment name
+
+please report bug at https://github.com/lvjr/texfindpkg
 ]]
 
-local function main()
-  if arg[1] == nil then
-    print(helptext)
-  elseif arg[1] == "--help" then
-    print(helptext)
-  elseif arg[1] == "--version" then
-    print("TeXFindPkg Version " .. tfpversion .. " (" .. tfpdate .. ")\n")
-  elseif arg[1] == "install" then
-    if arg[2] then
-      initPackageDB()
-      initDependencyDB()
-      install(arg[2])
-    else
-      tfpPrint("missing the name of file/cmd/env!")
-    end
-  elseif arg[1] == "query" then
-    if arg[2] then
-      initPackageDB()
-      initDependencyDB()
-      local list = query(arg[2])
-      listSomePackages(list)
-    else
-      tfpPrint("missing the name of file/cmd/env!")
-    end
-  elseif arg[1] == "generate" then
-    generateJsonData()
-  elseif arg[1] == "compare" then
-    compareDistributions()
+local function help()
+  print(helptext)
+end
+
+local function version()
+  print("TeXFindPkg Version " .. tfp.version .. " (" .. tfp.date .. ")\n")
+end
+
+------------------------------------------------------------
+--> \section{Respond to user input}
+------------------------------------------------------------
+
+local function tfpMain(tfparg)
+  tfpresult = ""
+  if tfparg[1] == nil then return help() end
+  local action = remove(tfparg, 1)
+  action = match(action, "^%-*(.*)$") -- remove leading dashes
+  --print(action)
+  if action == "query" then
+    doQuery(tfparg)
+  elseif action == "install" then
+    doInstall(tfparg)
+  elseif action == "help" then
+    help()
+  elseif action == "version" then
+    version()
   else
-    tfpPrint("unknown option " .. arg[1])
+    tfpPrint("unknown action '" .. action .. "'")
+    help()
   end
+  return tfpresult
 end
 
-main()
+local function main()
+  tfpMain(arg)
+end
+
+if building then
+  tfp.tfpMain          = tfpMain
+  tfp.showdbg          = showdbg
+  tfp.dbgPrint         = dbgPrint
+  tfp.tfpPrint         = tfpPrint
+  tfp.fileRead         = fileRead
+  tfp.fileWrite        = fileWrite
+  tfp.getFiles         = getFiles
+  tfp.valueExists      = valueExists
+  tfp.json             = json
+  tfp.gzip             = gzip
+  tfp.tlParsePackageDB = tlParsePackageDB
+  tfp.mtParsePackageDB = mtParsePackageDB
+  return tfp
+else
+  main()
+end

Modified: trunk/Master/texmf-dist/doc/man/man1/texfindpkg.1
===================================================================
--- trunk/Master/texmf-dist/doc/man/man1/texfindpkg.1	2023-05-05 21:23:26 UTC (rev 67026)
+++ trunk/Master/texmf-dist/doc/man/man1/texfindpkg.1	2023-05-05 21:25:43 UTC (rev 67027)
@@ -1,26 +1,29 @@
-.TH texfindpkg 1 "2023-04-05" "2023D"
+.TH texfindpkg 1 "2023-05-05" "2023E"
 .SH NAME
-TexFindPkg \- Install TeX packages and their dependencies
+TexFindPkg \- Query or install TeX packages and their dependencies
 .SH SYNOPSIS
-texfindpkg <action> [<options>] [<name>]
+texfindpkg <action> [<option>] [<name>]
 .SH DESCRIPTION
-TeXFindPkg makes it easy to install TeX packages and their dependencies
-by file names, command names or environment names.
+TeXFindPkg tool makes it easy to query or install some TeX packages and
+their dependencies by file names, command names or environment names.
 .PP
 The most commonly used TeXFindPkg actions are:
-.IP install
+.IP "query, \-\-query" 6
+Query dependencies for some package
+.IP "install, \-\-install" 6
 Install some package and its dependencies
-.IP query
-Query dependencies for some package
+.IP "help, \-\-help" 6
+Print a help message and exit
+.IP "version, \-\-version" 6
+Print version information and exit
 .SH OPTIONS
 Various options apply
-.IP --help
-Print a help message and exit
-.IP --version
-Print version information and exit
+.IP -f 6
+Query or install packages by file name
+.IP -c 6
+Query or install packages by command name
+.IP -e 6
+Query or install packages by environment name
 .SH BUGS
-.SH AUTHOR
-Jianrui Lyu <tolvjr at 163.com>
-.PP
-Please log issues on the GitHub homepage:
-https://github.com/lvjr/texfindpkg/issues.
+Please log issues on
+https://github.com/lvjr/texfindpkg/issues

Modified: trunk/Master/texmf-dist/doc/man/man1/texfindpkg.man1.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/support/texfindpkg/README.md
===================================================================
--- trunk/Master/texmf-dist/doc/support/texfindpkg/README.md	2023-05-05 21:23:26 UTC (rev 67026)
+++ trunk/Master/texmf-dist/doc/support/texfindpkg/README.md	2023-05-05 21:25:43 UTC (rev 67027)
@@ -1,27 +1,32 @@
-# TeXFindPkg tool for installing TeX packages
+# TeXFindPkg tool for querying or installing TeX packages
 
 ```
-Description: Install TeX packages and their dependencies
+Description: Query or Install TeX packages and their dependencies
 Copyright: 2023 (c) Jianrui Lyu <tolvjr at 163.com>
 Repository: https://github.com/lvjr/texfindpkg
 License: GNU General Public License v3.0
 ```
 
-## Introduction
+## 1\. Introduction
 
-TeXFindPkg makes it easy to install TeX packages and their dependencies by file names, command names or environment names.
+TeXFindPkg makes it easy to query or install TeX packages and their dependencies by file names, command names or environment names.
+It supports both TeXLive and MiKTeX distributions. At present it focuses mainly on LaTeX packages,
+but may extend to ConTeXt packages if anyone would like to contribute.
 
-- To install a package by its file name you can run `texfindpkg install array.sty`;
-- To install a package by some command name you can run `texfindpkg install \fakeverb`;
-- To install a package by some environment name you can run `texfindpkg install {frame}`.
+## 2\. Installation
 
-TeXFindPkg supports both TeXLive and MiKTeX distributions. At present it focuses mainly on LaTeX packages,
-but may extend to ConTeXt packages if anyone would like to contribute.
+Normally your TeX distribution will copy TeXFindPkg files and create a binary file `texfindpkg` when you install this package.
+If a manual installation is needed, you could copy TeXFindPkg files to TEXMF tree as follows:
 
-## Installation
+| Package file       | Where to install it |
+| :------            | :------ |
+| texfindpkg.1       | TEXMF/doc/man/man1/texfindpkg.1 |
+| README.md          | TEXMF/doc/support/texfindpkg/README.md |
+| texfindpkg.lua     | TEXMF/scripts/texfindpkg/texfindpkg.lua |
+| texfindpkg.json.gz | TEXMF/scripts/texfindpkg/texfindpkg.json.gz |
+| tfpbuild.lua       | TEXMF/source/texfindpkg/tfpbuild.lua |
 
-Your TeX distribution should have created a binary file `texfindpkg` when you install this package.
-If not, you could create a symbolic link from `/usr/local/bin/texfindpkg` to `texfindpkg.lua` on Linux or MacOS,
+Then you could create a symbolic link from `/usr/local/bin/texfindpkg` to `path/to/texfindpkg.lua` on Linux or MacOS,
 or create a batch file `texfindpkg.bat` in binary folder of the TeX distribution with these lines on Windows:
 
 ```
@@ -29,30 +34,37 @@
 texlua path\to\texfindpkg.lua %*
 ```
 
-## Usage
+## 3\. Usage
 
+After installing TeXFindPkg, you can run it by executing
+
 ```
-texfindpkg <action> [<options>] [<name>]
+texfindpkg <action> [<option>] [<name>]
 ```
 
-where `<action>` could be `install` or `query`, and `<name>` could be file name, command name or environment name.
-For example:
+The `<action>` could be `query`, `install`, `help` or `version`.
+The leading dashes in any `<action>` will be removed first.
 
-```
-texfindpkg install array.sty
-texfindpkg install \fakeverb
-texfindpkg install {frame}
-texfindpkg query array.sty
-texfindpkg query \fakeverb
-texfindpkg query {frame}
-```
+For `query` action, you pass `-f`, `-c` or `-e` as `<option>`
+to indicate the following `<name>` is a file name, command name or environment name.
+For example,
 
-## Building
+- `texfindpkg query -f array.sty` means querying package with file `array.sty`;
+- `texfindpkg query -c fakeverb` means querying package with command `\fakeverb`;
+- `texfindpkg query -e frame` means querying package with environment `{frame}`.
 
+The only difference between `query` and `install` actions is that
+with `install` action TeXFindPkg will install missing TeXLive or MiKTeX packages at the end.
+
+With `help` action, TeXFindPkg prints help message and exit.
+With `version` action, TeXFindPkg prints version information and exit.
+
+## 4\. Building
+
 TeXFindPkg uses completion files of TeXstudio editor which are in `completion` folder of TeXstudio [repository](https://github.com/texstudio-org/texstudio).
 
-After putting `completion` folder into current folder, you can run `texlua texfindpkg.lua generate` to generate `texfindpkg.json` and `texfindpkg.json.gz` files.
+After putting `completion` folder into current folder, you can execute `texlua tfpbuild.lua generate` to generate `texfindpkg.json` and `texfindpkg.json.gz` files.
 
-## Contributing
+## 5\. Contributing
 
 Any updates of dependencies, commands or environments for packages should be contributed directly to TeXstudio project.

Modified: trunk/Master/texmf-dist/scripts/texfindpkg/texfindpkg.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/texfindpkg/texfindpkg.lua	2023-05-05 21:23:26 UTC (rev 67026)
+++ trunk/Master/texmf-dist/scripts/texfindpkg/texfindpkg.lua	2023-05-05 21:25:43 UTC (rev 67027)
@@ -5,16 +5,29 @@
 -- Repository: https://github.com/lvjr/texfindpkg
 -- License: GNU General Public License v3.0
 
-local tfpversion = "2023D"
-local tfpdate = "2023-04-05"
+local tfp = tfp or {}
 
+tfp.version = "2023E"
+tfp.date = "2023-05-05"
+
+local building = tfp.building
+local tfpresult = ""
+
 ------------------------------------------------------------
 --> \section{Some variables and functions}
 ------------------------------------------------------------
 
 local lfs = require("lfs")
+
 local insert = table.insert
-local match = string.match
+local remove = table.remove
+local concat = table.concat
+local gmatch = string.gmatch
+local match  = string.match
+local find   = string.find
+local gsub   = string.gsub
+local sub    = string.sub
+local rep    = string.rep
 
 local lookup = kpse.lookup
 kpse.set_program_name("kpsewhich")
@@ -24,9 +37,20 @@
 local gzip = gzip           -- for gzip.compress and gzip.decompress
 
 local function tfpPrint(msg)
-  print("[tfp] " .. msg)
+  msg = "[tfp] " .. msg
+  if building then
+    tfpresult = tfpresult .. msg .. "\n"
+  else
+    print(msg)
+  end
 end
 
+local function tfpRealPrint(msg)
+  if not building then
+    print("[tfp] " .. msg)
+  end
+end
+
 local showdbg = false
 
 local function dbgPrint(msg)
@@ -85,6 +109,7 @@
 ------------------------------------------------------------
 
 local tlpkgtext
+local tlinspkgtext
 
 local function tlReadPackageDB()
   local tlroot = kpse.var_value("TEXMFROOT")
@@ -97,7 +122,7 @@
   if #list > 0 then
     tlpkgtext = fileRead(tlroot .. "/" .. list[1])
     if not tlpkgtext then
-      tfpPrint("error in reading texlive package database!")
+      tfpPrint("error in reading texlive.tlpdb.main file!")
     end
   else
     -- no texlive.tlpdb.main file in a fresh TeX live
@@ -104,21 +129,26 @@
     tfpPrint("error in finding texlive package database!")
     tfpPrint("please run 'tlmgr update --self' first.")
   end
+  tlinspkgtext = fileRead(tlroot .. "/texlive.tlpdb")
+  if not tlinspkgtext then
+    tfpPrint("error in reading texlive.tlpdb file!")
+  end
 end
 
 local tlpkgdata = {}
+local tlinspkgdata = {}
 
 local function tlExtractFiles(name, desc)
   -- ignore binary packages
   -- also ignore latex-dev packages
-  if name:find("%.") or name:find("^latex%-[%a]-%-dev") then
+  if find(name, "%.") or find(name, "^latex%-[%a]-%-dev") then
     --print(name)
     return
   end
   -- ignore package files in doc folder
-  desc = desc:match("\nrunfiles .+") or ""
-  for base, ext in desc:gmatch("/([%a%d%-]+)%.([%a%d]+)\n") do
-    if ext == "sty" or ext == "cls" then
+  desc = match(desc, "\nrunfiles .+") or ""
+  for base, ext in gmatch(desc, "/([%a%d%-%.]+)%.([%a%d]+)\n") do
+    if ext == "sty" or ext == "cls" or ext == "tex" or ext == "ltx" then
       dbgPrint(name, base .. "." .. ext)
       tlpkgdata[base .. "." .. ext] = name
     end
@@ -125,40 +155,56 @@
   end
 end
 
-local function tlParsePackageDB()
-  tlpkgtext:gsub("name (.-)\n(.-)\n\n", tlExtractFiles)
+local function tlExtractPackages(name, desc)
+  tlinspkgdata[name] = true
 end
 
+local function tlParsePackageDB(tlpkgtext)
+  gsub(tlpkgtext, "name (.-)\n(.-)\n\n", tlExtractFiles)
+  return tlpkgdata
+end
+
+local function tlParseTwoPackageDB()
+  gsub(tlpkgtext, "name (.-)\n(.-)\n\n", tlExtractFiles)
+  -- texlive.tlpdb might use different eol characters
+  gsub(tlinspkgtext, "name (.-)\r?\n(.-)\r?\n\r?\n", tlExtractPackages)
+end
+
 ------------------------------------------------------------
 --> \section{Handle MiKTeX package database}
 ------------------------------------------------------------
 
 local mtpkgtext
+local mtinspkgtext
 
 local function mtReadPackageDB()
-  local mtvar = kpse.var_value("TEXMFVAR")
+  local mtvar = kpse.var_value("TEXMFDIST")
   if mtvar then
-    mtpdb = mtvar .. "/miktex/cache/packages/miktex-zzdb3-2.9/package-manifests.ini"
+    mtpkgtext = fileRead(mtvar .. "/miktex/config/package-manifests.ini")
+    if not mtpkgtext then
+      tfpPrint("error in reading package-manifests.ini file!")
+    end
+    mtinspkgtext = fileRead(mtvar .. "/miktex/config/packages.ini")
+    if not mtinspkgtext then
+      tfpPrint("error in reading packages.ini file!")
+    end
   else
     tfpPrint("error in finding texmf root!")
   end
-  mtpkgtext = fileRead(mtpdb)
-  if not mtpkgtext then
-    tfpPrint("error in reading miktex package database!")
-  end
 end
 
 local mtpkgdata = {}
+local mtinspkgdata = {}
 
 local function mtExtractFiles(name, desc)
   -- ignore package files in source or doc folders
   -- also ignore latex-dev packages
-  if name:find("_") or name:find("^latex%-[%a]-%-dev") then
+  if find(name, "_") or find(name, "^latex%-[%a]-%-dev") then
     --print(name)
     return
   end
-  for base, ext in desc:gmatch("/([%a%d%-]+)%.([%a%d]+)\r?\n") do
-    if ext == "sty" or ext == "cls" then
+  for base, ext in gmatch(desc, "/([%a%d%-%.]+)%.([%a%d]+)\r?\n") do
+    if ext == "sty" or ext == "cls" or ext == "tex" or ext == "ltx" then
       dbgPrint(name, base .. "." .. ext)
       mtpkgdata[base .. "." .. ext] = name
     end
@@ -165,145 +211,29 @@
   end
 end
 
-local function mtParsePackageDB()
-  -- package-manifests.ini might use different eol characters
-  mtpkgtext:gsub("%[(.-)%]\r?\n(.-)\r?\n\r?\n", mtExtractFiles)
+local function mtExtractPackages(name, desc)
+  mtinspkgdata[name] = true
 end
 
-------------------------------------------------------------
---> \section{Compare TeX Live and MiKTeX packages}
-------------------------------------------------------------
-
-local tlpkgname = "download/texlive.tlpdb"
-local mtpkgname = "download/package-manifests.ini"
-
-local function compareDistributions()
-  tlpkgtext = fileRead(tlpkgname)
-  if tlpkgtext then
-    tlParsePackageDB()
-  else
-    tfpPrint("error in reading texlive package database!")
-  end
-  mtpkgtext = fileRead(mtpkgname)
-  if mtpkgtext then
-    mtParsePackageDB()
-  else
-    tfpPrint("error in reading miktex package database!")
-  end
-  local tlmissing, mkmissing = {}, {}
-  for k, vt in pairs(tlpkgdata) do
-    local vm = mtpkgdata[k] -- or "[none]"
-    if vm then
-      mtpkgdata[k] = nil -- remove it
-      if vm ~= vt then
-        print("texlive->" .. vt, "miktex->" .. vm, k)
-      end
-    else
-      insert(mkmissing, {vt, k})
-    end
-  end
-  for k, v in pairs(mtpkgdata) do
-    print("texlive doesn't include " .. v .. " -> " .. k)
-  end
-  for _, v in ipairs(mkmissing) do
-    print("miktex doesn't include " .. v[1] .. " -> " .. v[2])
-  end
+local function mtParsePackageDB(mtpkgtext)
+  -- package-manifests.ini might use different eol characters
+  gsub(mtpkgtext, "%[(.-)%]\r?\n(.-)\r?\n\r?\n", mtExtractFiles)
+  return mtpkgdata
 end
 
-------------------------------------------------------------
---> \section{Generate json file from cwl files}
-------------------------------------------------------------
-
-local function insertNewValue(tbl, val)
-  if not valueExists(tbl, val) then
-    insert(tbl, val)
-  end
+local function mtParseTwoPackageDB()
+  -- package-manifests.ini and packages.ini might use different eol characters
+  gsub(mtpkgtext, "%[(.-)%]\r?\n(.-)\r?\n\r?\n", mtExtractFiles)
+  gsub(mtinspkgtext, "%[(.-)%]\r?\n(.-)\r?\n\r?\n", mtExtractPackages)
 end
 
-local function extractFileData(cwl)
-  -- the cwl files have different eol characters
-  local deps = {}
-  for d in cwl:gmatch("\n#include:(.-)[\r\n]") do
-    --dbgPrint(d)
-    n = d:match("^class-(.+)$")
-    if n then
-      insertNewValue(deps, n .. ".cls")
-    else
-      insertNewValue(deps, d .. ".sty")
-    end
-  end
-  local envs = {}
-  for b, e in cwl:gmatch("\n\\begin{(.-)}.-\n\\end{(.-)}") do
-    if b == e then
-      --dbgPrint("{" .. e .. "}")
-      insert(envs, e)
-    end
-  end
-  local cmds = {}
-  for c in cwl:gmatch("\n\\(%a+)") do
-    if c ~= "begin" and c ~= "end" then
-      --dbgPrint("\\" .. c)
-      if not valueExists(cmds, c) then
-        insert(cmds, c)
-      end
-    end
-  end
-  --return {deps, envs, cmds}
-  return {deps = deps, envs = envs, cmds = cmds}
-end
-
-local function writeJson(cwldata)
-  tfpPrint("writing json database to file...")
-  local tbl1 = {}
-  for k, v in pairs(cwldata) do
-    table.insert(tbl1, {k, v})
-  end
-  table.sort(tbl1, function(a, b)
-    if a[1] < b[1] then return true end
-  end)
-  local tbl2 = {}
-  for _, v in ipairs(tbl1) do
-    local item = '"' .. v[1] .. '":' .. json.tostring(v[2])
-    table.insert(tbl2, item)
-  end
-  local text = "{\n" .. table.concat(tbl2, "\n,\n") .. "\n}"
-  fileWrite(text, "texfindpkg.json")
-  fileWrite(gzip.compress(text), "texfindpkg.json.gz")
-end
-
-local cwlpath = "completion"
-
-local function generateJsonData()
-  local list = getFiles(cwlpath, "%.cwl$")
-  local fname
-  local cwldata = {}
-  for _, v in ipairs(list) do
-    local a, b = match(v, "^([^%-]-)%-(.+)%.cwl")
-    if a == "class" then
-      fname = b .. ".cls"
-    else
-      b = match(v, "^(.+)%.cwl")
-      fname = b .. ".sty"
-    end
-    --print(fname)
-    local cwl = fileRead(cwlpath .. "/" .. v)
-    --print(cwl)
-    if cwl then
-      local item = extractFileData(cwl)
-      dbgPrint(item)
-      cwldata[fname] = item
-    else
-      tfpPrint("error in reading " .. v)
-    end
-  end
-  writeJson(cwldata)
-end
-
 ------------------------------------------------------------
 --> \section{Install packages in current TeX distribution}
 ------------------------------------------------------------
 
-local dist -- name of current tex distribution
+local dist               -- name of current tex distribution
+local totalinslist = {}  -- list of all missing packages
+local filecount = 0      -- total number of files found
 
 local function initPackageDB()
   dist = testDistribution()
@@ -310,10 +240,10 @@
   tfpPrint("you are using " .. dist)
   if dist == "texlive" then
     tlReadPackageDB()
-    tlParsePackageDB()
+    tlParseTwoPackageDB()
   else
     mtReadPackageDB()
-    mtParsePackageDB()
+    mtParseTwoPackageDB()
   end
 end
 
@@ -325,32 +255,86 @@
   end
 end
 
-local function tfpExecute(c)
-  if os.type == "windows" then
-    os.execute(c)
+local function checkInsPakage(pkg)
+  if dist == "texlive" then
+    return tlinspkgdata[pkg]
   else
-    os.execute('sudo env "PATH=$PATH" ' .. c)
+    return mtinspkgdata[pkg]
   end
 end
 
+local function tfpExecute(c)
+  if not building then
+    if os.type == "windows" then
+      os.execute(c)
+    else
+      os.execute('sudo env "PATH=$PATH" ' .. c)
+    end
+  end
+end
+
 local function installSomePackages(list)
   if not list then return end
   if dist == "texlive" then
-    local p = table.concat(list, " ")
-    tfpPrint("installing package " .. p)
-    tfpExecute("tlmgr install " .. p)
+    local pkgs = concat(list, " ")
+    if #list > 1 then
+      tfpRealPrint("installing texlive packages: " .. pkgs)
+    else
+      tfpRealPrint("installing texlive package: " .. pkgs)
+    end
+    tfpExecute("tlmgr install " .. pkgs)
   else
     for _, p in ipairs(list) do
-      tfpPrint("installing package " .. p)
+      tfpRealPrint("installing miktex package: " .. p)
       tfpExecute("miktex packages install " .. p)
     end
   end
 end
 
+local function updateTotalInsList(inslist)
+  if #totalinslist == 0 then
+    totalinslist = inslist
+  else
+    for _, pkg in ipairs(inslist) do
+      if not valueExists(totalinslist, pkg) then
+        insert(totalinslist, pkg)
+      end
+    end
+  end
+end
+
 local function listSomePackages(list)
-  if not list then return end
-  local p = table.concat(list, " ")
-  tfpPrint("please install " .. dist .. " package " .. p)
+  if not list then return {} end
+  if #list > 0 then
+    filecount = filecount + 1
+  end
+  local pkgs = concat(list, " ")
+  if #list == 1 then
+    tfpPrint(dist .. " package needed: " .. pkgs)
+  else
+    tfpPrint(dist .. " packages needed: " .. pkgs)
+  end
+  local inslist = {}
+  for _, p in ipairs(list) do
+    if not checkInsPakage(p) then
+      insert(inslist, p)
+    end
+  end
+  if #inslist == 0 then
+    if #list == 1 then
+      tfpRealPrint("this package is already installed")
+    else
+      tfpRealPrint("these packages are already installed")
+    end
+  else
+    local pkgs = concat(inslist, " ")
+    if #inslist == 1 then
+      tfpRealPrint(dist .. " package not yet installed: " .. pkgs)
+    else
+      tfpRealPrint(dist .. " packages not yet installed: " .. pkgs)
+    end
+  end
+  updateTotalInsList(inslist)
 end
 
 ------------------------------------------------------------
@@ -359,7 +343,8 @@
 
 local tfptext = ""  -- the json text
 local tfpdata = {}  -- the lua object
-local fnlist = {}  -- file name list
+local fnlist  = {}  -- file name list
+local pkglist = {}  -- package name list
 
 local function initDependencyDB()
   local ziptext = fileRead(lookup("texfindpkg.json.gz"))
@@ -372,67 +357,87 @@
   end
 end
 
-local function findDependencies(fname)
+local function printDependency(fname, level)
+  local msg = fname
+  local pkg = findOnePackage(fname)
+  if pkg then
+    msg = msg .. " (from " .. pkg .. ")"
+    if not valueExists(pkglist, pkg) then
+      insert(pkglist, pkg)
+    end
+  else
+    msg = msg .. " (not found)"
+  end
+  if level == 0 then
+    tfpPrint(msg)
+  else
+    tfpPrint(rep("   ", level - 1) .. "|- " .. msg)
+  end
+end
+
+local function findDependencies(fname, level)
   --print(fname)
   if valueExists(fnlist, fname) then return end
   local item = tfpdata[fname]
   if not item then
-    tfpPrint("no dependency info for " .. fname)
+    -- no dependency info for fname
+    printDependency(fname, level)
     return
   end
-  tfpPrint("finding dependencies for " .. fname)
-  table.insert(fnlist, fname)
+  -- finding dependencies for fname
+  printDependency(fname, level)
+  insert(fnlist, fname)
   local deps = item.deps
   if deps then
     for _, dname in ipairs(deps) do
-      findDependencies(dname)
+      findDependencies(dname, level + 1)
     end
   end
 end
 
 local function queryByFileName(fname)
-  fnlist = {} -- reset the list
-  findDependencies(fname)
+  fnlist, pkglist = {}, {} -- reset the list
+  tfpPrint("building dependency tree for " .. fname .. ":")
+  tfpPrint(rep("-", 24))
+  findDependencies(fname, 0)
+  tfpPrint(rep("-", 24))
   if #fnlist == 0 then
     tfpPrint("could not find any package with file " .. fname)
     return
   end
-  local pkglist = {}
-  for _, fn in ipairs(fnlist) do
-    local pkg = findOnePackage(fn)
-    --print(fn, pkg)
-    if pkg then
-      table.insert(pkglist, pkg)
-    end
-  end
-  if not pkglist then
+  if #pkglist == 0 then
     tfpPrint("error in finding package in " .. dist)
     return
   end
-  return pkglist
+  listSomePackages(pkglist)
 end
 
 local function getFileNameFromCmdEnvName(cmdenv, name)
   --print(name)
-  for line in tfptext:gmatch("(.-)\n[,}]") do
-    if line:find('"' .. name .. '"') then
+  local flist = {}
+  for line in gmatch(tfptext, "(.-)\n[,}]") do
+    if find(line, '"' .. name .. '"') then
       --print(line)
-      local fname, fspec = line:match('"(.-)":(.+)')
+      local fname, fspec = match(line, '"(.-)":(.+)')
       --print(fname, fspec)
       local item = json.tolua(fspec)
-      if valueExists(item[cmdenv], name) then
-        tfpPrint("found package file " .. fname)
-        return fname
+      if item[cmdenv] and valueExists(item[cmdenv], name) then
+        insert(flist, fname)
       end
     end
   end
+  return flist
 end
 
 local function queryByCommandName(cname)
   --print(cname)
-  local fname = getFileNameFromCmdEnvName("cmds", cname)
-  if fname then
-    return queryByFileName(fname)
+  local flist = getFileNameFromCmdEnvName("cmds", cname)
+  if #flist > 0 then
+    for _, fname in ipairs(flist) do
+      tfpPrint(rep("=", 48))
+      tfpPrint("found package file " .. fname .. " with command \\" .. cname)
+      queryByFileName(fname)
+    end
   else
     tfpPrint("could not find any package with command \\" .. cname)
   end
@@ -440,83 +445,187 @@
 
 local function queryByEnvironmentName(ename)
   --print(ename)
-  local fname = getFileNameFromCmdEnvName("envs", ename)
-  if fname then
-    return queryByFileName(fname)
+  local flist = getFileNameFromCmdEnvName("envs", ename)
+  if #flist > 0 then
+    for _, fname in ipairs(flist) do
+      tfpPrint(rep("=", 48))
+      tfpPrint("found package file " .. fname .. " with environment {" .. ename .. "}")
+      queryByFileName(fname)
+    end
   else
     tfpPrint("could not find any package with environment {" .. ename .. "}")
   end
 end
 
-local function query(name)
-  local h = name:sub(1,1)
+local function queryOne(t, name)
+  if t == "cmd" then
+    queryByCommandName(name)
+  elseif t == "env" then
+    queryByEnvironmentName(name)
+  elseif t == "file" then
+    tfpPrint(rep("=", 48))
+    queryByFileName(name)
+  else
+    -- do something for t == "pkg"
+  end
+end
+
+local function query(namelist)
+  for _, v in ipairs(namelist) do
+    queryOne(v[1], v[2])
+  end
+  if filecount > 1 then
+    tfpRealPrint(rep("=", 48))
+    local pkgs = concat(totalinslist, " ")
+    if #totalinslist == 1 then
+      tfpRealPrint(dist .. " package not yet installed in total: " .. pkgs)
+    else
+      tfpRealPrint(dist .. " packages not yet installed in total: " .. pkgs)
+    end
+  end
+end
+
+local function install(namelist)
+  query(namelist)
+  if #totalinslist > 0 then
+    installSomePackages(totalinslist)
+  end
+end
+
+------------------------------------------------------------
+--> \section{Parse query or install arguments}
+------------------------------------------------------------
+
+local function parseName(name)
+  local h = sub(name, 1, 1)
   if h == "\\" then
-    local b = name:sub(2)
-    return queryByCommandName(b)
+    local b = sub(name, 2)
+    return({"cmd", b})
   elseif h == "{" then
-    if name:sub(-1) == "}" then
-      local b = name:sub(2,-2)
-      return queryByEnvironmentName(b)
+    if sub(name, -1) == "}" then
+      local b = sub(name, 2, -2)
+      return({"env", b})
     else
-      tfpPrint("invalid input " .. name)
+      error("invalid name '" .. name .. "'")
     end
+  elseif find(name, "%.") then
+    return({"file", name})
   else
-    return queryByFileName(name)
+    return({"pkg", name})
   end
 end
 
-local function install(name)
-  local list = query(name)
-  installSomePackages(list)
+local function parseArgList(arglist)
+  local namelist = {}
+  local nametype = nil
+  for _, v in ipairs(arglist) do
+    if v == "-f" then
+      nametype = "file"
+    elseif v == "-c" then
+      nametype = "cmd"
+    elseif v == "-e" then
+      nametype = "env"
+    else
+      if nametype then
+        insert(namelist, {nametype, v})
+      else
+        insert(namelist, parseName(v))
+      end
+    end
+  end
+  if #namelist == 0 then
+    error("missing the name of file/cmd/env!")
+  else
+    return namelist
+  end
 end
 
+local function doQuery(arglist)
+  local namelist = parseArgList(arglist)
+  initPackageDB()
+  initDependencyDB()
+  query(namelist)
+end
+
+local function doInstall(arglist)
+  local namelist = parseArgList(arglist)
+  initPackageDB()
+  initDependencyDB()
+  install(namelist)
+end
+
 ------------------------------------------------------------
---> \section{Respond to user input}
+--> \section{Print help or version text}
 ------------------------------------------------------------
 
 local helptext = [[
 usage: texfindpkg <action> [<options>] [<name>]
 
-Valid actions are:
+valid actions are:
    install      Install some package and its dependencies
    query        Query dependencies for some package
+   help         Print this message and exit
+   version      Print version information and exit
 
-Valid options are:
-   --help       Print this message and exit
-   --version    Print version information and exit
+valid options are:
+   -f           Query or install by file name
+   -c           Query or install by command name
+   -e           Query or install by environment name
+
+please report bug at https://github.com/lvjr/texfindpkg
 ]]
 
-local function main()
-  if arg[1] == nil then
-    print(helptext)
-  elseif arg[1] == "--help" then
-    print(helptext)
-  elseif arg[1] == "--version" then
-    print("TeXFindPkg Version " .. tfpversion .. " (" .. tfpdate .. ")\n")
-  elseif arg[1] == "install" then
-    if arg[2] then
-      initPackageDB()
-      initDependencyDB()
-      install(arg[2])
-    else
-      tfpPrint("missing the name of file/cmd/env!")
-    end
-  elseif arg[1] == "query" then
-    if arg[2] then
-      initPackageDB()
-      initDependencyDB()
-      local list = query(arg[2])
-      listSomePackages(list)
-    else
-      tfpPrint("missing the name of file/cmd/env!")
-    end
-  elseif arg[1] == "generate" then
-    generateJsonData()
-  elseif arg[1] == "compare" then
-    compareDistributions()
+local function help()
+  print(helptext)
+end
+
+local function version()
+  print("TeXFindPkg Version " .. tfp.version .. " (" .. tfp.date .. ")\n")
+end
+
+------------------------------------------------------------
+--> \section{Respond to user input}
+------------------------------------------------------------
+
+local function tfpMain(tfparg)
+  tfpresult = ""
+  if tfparg[1] == nil then return help() end
+  local action = remove(tfparg, 1)
+  action = match(action, "^%-*(.*)$") -- remove leading dashes
+  --print(action)
+  if action == "query" then
+    doQuery(tfparg)
+  elseif action == "install" then
+    doInstall(tfparg)
+  elseif action == "help" then
+    help()
+  elseif action == "version" then
+    version()
   else
-    tfpPrint("unknown option " .. arg[1])
+    tfpPrint("unknown action '" .. action .. "'")
+    help()
   end
+  return tfpresult
 end
 
-main()
+local function main()
+  tfpMain(arg)
+end
+
+if building then
+  tfp.tfpMain          = tfpMain
+  tfp.showdbg          = showdbg
+  tfp.dbgPrint         = dbgPrint
+  tfp.tfpPrint         = tfpPrint
+  tfp.fileRead         = fileRead
+  tfp.fileWrite        = fileWrite
+  tfp.getFiles         = getFiles
+  tfp.valueExists      = valueExists
+  tfp.json             = json
+  tfp.gzip             = gzip
+  tfp.tlParsePackageDB = tlParsePackageDB
+  tfp.mtParsePackageDB = mtParsePackageDB
+  return tfp
+else
+  main()
+end

Added: trunk/Master/texmf-dist/source/support/texfindpkg/tfpbuild.lua
===================================================================
--- trunk/Master/texmf-dist/source/support/texfindpkg/tfpbuild.lua	                        (rev 0)
+++ trunk/Master/texmf-dist/source/support/texfindpkg/tfpbuild.lua	2023-05-05 21:25:43 UTC (rev 67027)
@@ -0,0 +1,312 @@
+#!/usr/bin/env texlua
+
+-- Description: Install TeX packages and their dependencies
+-- Copyright: 2023 (c) Jianrui Lyu <tolvjr at 163.com>
+-- Repository: https://github.com/lvjr/texfindpkg
+-- License: GNU General Public License v3.0
+
+tfp = { building = true }
+
+kpse.set_program_name("kpsewhich")
+local tfp = require(kpse.lookup("texfindpkg.lua"))
+
+------------------------------------------------------------
+--> \section{Some variables and functions}
+------------------------------------------------------------
+
+local tfpMain          = tfp.tfpMain
+local showdbg          = tfp.showdbg
+local dbgPrint         = tfp.dbgPrint
+local fileRead         = tfp.fileRead
+local fileWrite        = tfp.fileWrite
+local getFiles         = tfp.getFiles
+local valueExists      = tfp.valueExists
+local json             = tfp.json
+local gzip             = tfp.gzip
+local tlParsePackageDB = tfp.tlParsePackageDB
+local mtParsePackageDB = tfp.mtParsePackageDB
+
+local insert = table.insert
+local remove = table.remove
+local concat = table.concat
+local gmatch = string.gmatch
+local match  = string.match
+
+------------------------------------------------------------
+--> \section{Compare TeX Live and MiKTeX packages}
+------------------------------------------------------------
+
+local tlpkgname = "download/texlive.tlpdb"
+local mtpkgname = "download/package-manifests.ini"
+
+local function compareDistributions()
+  tlpkgtext = fileRead(tlpkgname)
+  if tlpkgtext then
+    tlpkgdata = tlParsePackageDB(tlpkgtext)
+  else
+    print("error in reading texlive package database!")
+  end
+  mtpkgtext = fileRead(mtpkgname)
+  if mtpkgtext then
+    mtpkgdata = mtParsePackageDB(mtpkgtext)
+  else
+    print("error in reading miktex package database!")
+  end
+  local tlmissing, mkmissing = {}, {}
+  for k, vt in pairs(tlpkgdata) do
+    local vm = mtpkgdata[k] -- or "[none]"
+    if vm then
+      mtpkgdata[k] = nil -- remove it
+      if vm ~= vt then
+        print("texlive->" .. vt, "miktex->" .. vm, k)
+      end
+    else
+      insert(mkmissing, {vt, k})
+    end
+  end
+  for k, v in pairs(mtpkgdata) do
+    print("texlive doesn't include " .. v .. " -> " .. k)
+  end
+  for _, v in ipairs(mkmissing) do
+    print("miktex doesn't include " .. v[1] .. " -> " .. v[2])
+  end
+end
+
+------------------------------------------------------------
+--> \section{Generate json file from cwl files}
+------------------------------------------------------------
+
+local function insertNewValue(tbl, val)
+  if not valueExists(tbl, val) then
+    insert(tbl, val)
+  end
+end
+
+local latexcwls = {"tex", "xetex", "luatex", "plaintex", "latex-document", "latex-dev"}
+local islatexcwl = false
+
+local function cwlToRealName(base)
+  if valueExists(latexcwls, base) then return end
+  local n = match(base, "^tikzlibrary(.+)$")
+  if n then return base .. ".code.tex" end
+  local n = match(base, "^tcolorboxlibrary(.+)$")
+  if n then
+    if n == "many" or n == "most" or n == "all" then
+      return nil
+    else
+      return "tcb" .. n .. ".code.tex"
+    end
+  end
+  n = match(base, "^class%-(.+)$")
+  if n then return n .. ".cls" end
+  return base .. ".sty"
+end
+
+local cwldata = {}
+
+local function extractFileData(cwl)
+  -- the cwl files have different eol characters
+  local deps = {}
+  for base in gmatch(cwl, "\n#include:(.-)[\r\n]") do
+    --dbgPrint(base)
+    local n = cwlToRealName(base)
+    if n then insertNewValue(deps, n) end
+  end
+  local envs = {}
+  if islatexcwl and cwldata["latex.ltx"] then
+    envs = cwldata["latex.ltx"].envs or {}
+  end
+  for e in gmatch(cwl, "\n\\begin{(.-)}") do
+    --dbgPrint("{" .. e .. "}")
+    insertNewValue(envs, e)
+  end
+  local cmds = {}
+  if islatexcwl and cwldata["latex.ltx"] then
+    cmds = cwldata["latex.ltx"].cmds or {}
+  end
+  for c in gmatch(cwl, "\n\\(%a+)") do
+    if c ~= "begin" and c ~= "end" then
+      --dbgPrint("\\" .. c)
+      if not valueExists(cmds, c) then
+        insert(cmds, c)
+      end
+    end
+  end
+  --return {deps, envs, cmds}
+  if #deps == 0 then deps = nil end
+  if #cmds == 0 then cmds = nil end
+  if #envs == 0 then envs = nil end
+  return {deps = deps, envs = envs, cmds = cmds}
+end
+
+local function writeJson()
+  print("writing json database to file...")
+  local tbl1 = {}
+  for k, v in pairs(cwldata) do
+    insert(tbl1, {k, v})
+  end
+  table.sort(tbl1, function(a, b)
+    -- make latex.ltx the first item in the json file
+    local aa, bb
+    if a[1] == "latex.ltx" then aa = "001" else aa = a[1] end
+    if b[1] == "latex.ltx" then bb = "001" else bb = b[1] end
+    return aa < bb
+  end)
+  local tbl2 = {}
+  for _, v in ipairs(tbl1) do
+    local item = '"' .. v[1] .. '":' .. json.tostring(v[2])
+    insert(tbl2, item)
+  end
+  local text = "{\n" .. concat(tbl2, "\n,\n") .. "\n}"
+  fileWrite(text, "texfindpkg.json")
+  fileWrite(gzip.compress(text), "texfindpkg.json.gz")
+end
+
+local cwlpath = "completion"
+
+local ignoredcwls = {"optex"}
+
+local function generateJsonData()
+  local list = getFiles(cwlpath, "%.cwl$")
+  local fname
+  for _, v in ipairs(list) do
+    local base = match(v, "^(.+)%.cwl")
+    if not valueExists(ignoredcwls, base) then
+      if valueExists(latexcwls, base) then
+        islatexcwl = true
+        fname = "latex.ltx"
+      else
+        islatexcwl = false
+        fname = cwlToRealName(base)
+      end
+      if fname then
+        --print(fname)
+        local cwl = fileRead(cwlpath .. "/" .. v)
+        --print(cwl)
+        if cwl then
+          local item = extractFileData(cwl)
+          dbgPrint(item)
+          cwldata[fname] = item
+        else
+          print("error in reading " .. v)
+        end
+      end
+    end
+  end
+  writeJson()
+end
+
+------------------------------------------------------------
+--> \section{Print help or version text}
+------------------------------------------------------------
+
+local helptext = [[
+usage: texfindpkg <action> [<options>] [<name>]
+
+valid actions are:
+   generate     Generate texfindpkg.json and texfindpkg.json.gz
+   compare      Compare packages in TeXLive and MiKTeX
+   check        Check test files and report the results
+   save         Check test files and save the results
+   help         Print this message and exit
+   version      Print version information and exit
+
+please report bug at https://github.com/lvjr/texfindpkg
+]]
+
+local function help()
+  print(helptext)
+end
+
+local function version()
+  print("TeXFindPkg Version " .. tfp.version .. " (" .. tfp.date .. ")\n")
+end
+
+------------------------------------------------------------
+--> \section{Check or save test files}
+------------------------------------------------------------
+
+local function cmdLineToArgList(cmdline)
+  local tfparg = {}
+  for v in gmatch(cmdline, "([^ ]+)") do
+    insert(tfparg, v)
+  end
+  remove(tfparg, 1)
+  return tfparg
+end
+
+local testpath = "test"
+
+local function readTestFiles(v)
+  local tests = {}
+  local text = fileRead(testpath .. "/" .. v)
+  for i, o in gmatch(text, "<<<<+\n(.-)\n%-%-%-%-+\n(.-)\n>>>>+") do
+    --print(i, o)
+    insert(tests, {i, o})
+  end
+  return tests
+end
+
+local function checkTestFiles()
+  local files = getFiles(testpath, "%.tfp$")
+  for _, v in ipairs(files) do
+    print("checking test file " .. v)
+    local tests = readTestFiles(v)
+    for _, v in ipairs(tests) do
+      local i, o = v[1], v[2] .. "\n"
+      local result = tfpMain(cmdLineToArgList(i))
+      if o == result then
+        dbgPrint("test passed for '" .. i .. "'")
+      else
+        print("test failed for '" .. i .. "'")
+      end
+    end
+  end
+end
+
+local function saveTestFiles()
+  local files = getFiles(testpath, "%.tfp$")
+  for _, v in ipairs(files) do
+    print("reading test file " .. v)
+    local tests = readTestFiles(v)
+    local text = ""
+    for _, v in ipairs(tests) do
+      local i = v[1]
+      dbgPrint("running test '" .. i .. "'")
+      local o = tfpMain(cmdLineToArgList(i))
+      local sect = "<<<<<<<<\n" .. i .. "\n--------\n" .. o .. ">>>>>>>>\n"
+      text = text .. sect
+    end
+    print("saving test file " .. v)
+    fileWrite(text, testpath .. "/" .. v)
+  end
+end
+
+------------------------------------------------------------
+--> \section{Respond to user input}
+------------------------------------------------------------
+
+local function main()
+  if arg[1] == nil then return help() end
+  local action = remove(arg, 1)
+  action = match(action, "^%-*(.*)$") -- remove leading dashes
+  --print(action)
+  if action == "help" then
+    help()
+  elseif action == "version" then
+    version()
+  elseif action == "generate" then
+    generateJsonData()
+  elseif action == "compare" then
+    compareDistributions()
+  elseif action == "check" then
+    checkTestFiles()
+  elseif action == "save" then
+    saveTestFiles()
+  else
+    print("unknown action '" .. action .. "'")
+    help()
+  end
+end
+
+main()


Property changes on: trunk/Master/texmf-dist/source/support/texfindpkg/tfpbuild.lua
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/tex/latex/texfindpkg/texfindpkg.json.gz
===================================================================
(Binary files differ)

Modified: trunk/Master/tlpkg/libexec/ctan2tds
===================================================================
--- trunk/Master/tlpkg/libexec/ctan2tds	2023-05-05 21:23:26 UTC (rev 67026)
+++ trunk/Master/tlpkg/libexec/ctan2tds	2023-05-05 21:25:43 UTC (rev 67027)
@@ -3006,6 +3006,7 @@
  'tex-vpat',		'NULL',
  'texaccents',		'\.inc',
  'texdraw',		'NULL',
+ 'texfindpkg',		'tfpbuild.lua',
  'texosquery',		'java|\.batch$|' . $standardsource,
  'thailatex',           'NULL',                 # no gain in splitting up
  'tikztosvg',		'NULL',			# not Makefile



More information about the tex-live-commits mailing list.