#!/usr/bin/env texlua
-- $Id$ -*-Lua-*-
--[[
Copyright 2008, 2009 Manuel Pégourié-Gonnard.
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see .
Previous work in the public domain:
- Contributions from Reinhard Kotucha (2008).
- First texlua versions by Frank Küster (2007).
- Original shell script by Thomas Esser, David Aspinall, and Simon Wilkinson.
--]]
--------------------------------------------------------------------------------
------------------ global constants and general functions ------------------
--------------------------------------------------------------------------------
----------------------------- global constants -----------------------------
-- progname and version
progname = 'texdoc'
version = '0.45'
if string.sub(version, -1) == '+' then
local svnrev = string.match('$Rev$', '%$Rev:%s*(%d*)%s*%$');
version = version..' svn r'..svnrev
end
-- make sure to update setup_config_from_cl() accordingly
-- and set a default value in setup_config_from_defaults() if relevant
usage_msg = [[
texdoc tries to find appropriate TeX documentation for the specified NAME(s).
With no NAME, it can print configuration information (-f, --files);
the usual --help and --version options are also accepted.
Usage: texdoc [OPTIONS]... [NAME]...
-f, --files Print the name of the config files being used.
-e, --extensions=L Require file extensions to be in the list L.
-w, --view Use view mode: start a viewer.
-m, --mixed Use mixed mode (view or list).
-l, --list Use list mode: don't start a viewer.
-s, --search Search for name as a substring.
-r, --regex Search for name as a lua regex.
-a, --alias Use the alias table.
-A, --noalias Don't use the alias table.
-i, --interact Use interactive menus.
-I, --nointeract Use plain lists, no interaction required.
-v, --verbosity=N Set verbosity level to N.
-d, --debug Set verbosity level to maximum.
Environment: PAGER, BROWSER, PDFVIEWER, PSVIEWER, DVIVIEWER.
Files: /texdoc/texdoc.cnf files, see the -f option.
Homepage: http://tug.org/texdoc/
Manual: displayed by `texdoc texdoc'.]]
error_msg = [[
Try `texdoc --help' for a short help, `texdoc texdoc' for the user manual.]]
notfound_msg = [[
Sorry, no documentation found for PKGNAME.
If you are unsure about the name, try searching CTAN's TeX catalogue at
http://ctan.org/search.html#byDescription.]]
known_options = {
'viewer_.*',
'mode',
'interact_switch', 'alias_switch',
'ext_list',
'verbosity_level',
'lastfile_switch',
'rm_dir',
'rm_file',
'unzip_bz2',
'unzip_gz',
}
err_priority = {
error = 1,
warning = 2,
info = 3,
debug1 = 4,
debug2 = 5,
debug3 = 6,
}
err_max = 6
place_holder = '%%s' -- used for viewer commands
-- zip/gz support
--
-- optionally, texdoc can support compressed documentation, but this is
-- system-dependant (commands for unzipping, temporary files, etc).
-- Since TeX Live doesn't ship compressed doc, downstream distributors who
-- want to ship zipped doc should change support_zipped to true *and* make sure
-- everything works for them (look for support_zipped in the code).
-- If you use this feature, please let us know: if nobody uses it,
-- we'll drop it at some point.
support_zipped = false
-------------------------- general-use functions ---------------------------
-- Remark: we always assume our tables have no hole (that is, no nil value
-- followed by a non-nil value). So we use the simple iterator below, and
-- the # operator sometimes (a bit faster than table.getn).
function list (t)
local i = 0
return function ()
i = i + 1
return t[i]
end
end
-- remove the 'abc/../' components in a path
function simplify_path (path)
local res = string.gsub (path, '/[^/]+/%.%./', '/')
return res -- get rid of gsub's 2nd return value
end
-- change '/' to '\' on windows, removine 'abc/../' components first
if os.type == "windows" then
function win32_hook (path)
local res = string.gsub (simplify_path(path), '/', '\\')
return res -- get rid of gsub's 2nd return value
end
else
function win32_hook (path)
return simplify_path(path)
end
end
--------------------------------------------------------------------------------
-------------------------- functions for searching -------------------------
--------------------------------------------------------------------------------
------------------ exploring trees (kpse-style functions) ------------------
-- global variables:
-- exact_docfiles: list of "exact matches"
-- rel_docfiles: list of "non-exact matches"
do -- begin scope of doc_roots, a lua version of kpse's TEXDOCS
local doc_roots
-- structure of the doc_roots variable:
-- doc_roots[i] = {
-- path = ,
-- index_mandatory =
-- recursion_allowed = ,
-- }
-- set the doc_roots list from kpse's $TEXDOCS
function get_texdocs ()
doc_roots = {}
local sep = (os.type == 'windows') and ';' or ':'
local kpse_texdocs = kpse.expand_var("$TEXDOCS")
-- expand the path and turn it into a lua list
local raw_doc_roots = string.explode(kpse.expand_braces(kpse_texdocs), sep)
err_print('Search paths:', 'debug3')
for i, dir in ipairs(raw_doc_roots) do
doc_roots[i] = {}
local n
dir, n = string.gsub (dir, '//$', '')
doc_roots[i].recursion_allowed = (n == 1)
doc_roots[i].path, n = string.gsub (dir, '^!!', '')
doc_roots[i].index_mandatory = (n == 1)
err_print(string.format('%s (index_mandatory=%s, recursion_allowed=%s)',
doc_roots[i].path,
doc_roots[i].index_mandatory and 'true' or 'false',
doc_roots[i].recursion_allowed and 'true' or 'false'),
'debug3')
end
end
-- once get_texdocs() is done, roots are represented by their index in doc_roots
-- this is usefull to avoid fake matches and also for the sort routine
-- conversions are done by real_path() below and code_path() later
-- decode a path as given in *_docfiles into a real path
function real_path(fake)
local code, file = string.match(fake, '^(.-):(.*)$')
code = tonumber(code)
return win32_hook(doc_roots[code].path..'/'..file)
end
-- find docfiles "matching" pattern
function populate_docfiles (pattern)
pattern = normalize_pattern(pattern)
rel_docfiles, exact_docfiles = {}, {} -- global
is_dir = {} -- global; is_dir[path] = true iff path is a dir, see scan_lsr
local curr_dir = lfs.currentdir()
for code, doc_root in ipairs (doc_roots) do
root, shift = lsr_root (doc_root.path)
if root and shift and doc_root.recursion_allowed then
assert(lfs.chdir(root))
scan_lsr(code, shift, pattern)
elseif (not doc_root.index_mandatory)
and lfs.isdir(doc_root.path) then
assert(lfs.chdir(doc_root.path))
scan_tree(code, '.', pattern, doc_root.recursion_allowed)
end
end
assert(lfs.chdir(curr_dir))
exact_docfiles = rmdirs (exact_docfiles)
rel_docfiles = rmdirs (rel_docfiles)
end
end -- scope of doc_roots
-- encode the base path on two digits and concatenate with filename
function code_path (code, file)
local padding = (code > 9) and '' or '0'
return padding..code..':'..file
end
-- merge two components of a path, taking care of empty components
function merge_path (a, b)
return ((a == '') or (b == '')) and a..b or a..'/'..b
end
-- scan a tree without ls-R file
function scan_tree (code, path, pattern, recurse)
for file in lfs.dir(path) do
if file ~= '.' and file ~= '..' then
local f = (path == '.') and file or path..'/'..file
if lfs.isdir(f) then
if recurse then scan_tree(code, f, pattern, recurse) end
else
process_file(file, f, code, pattern, true)
end
end
end
end
-- finds a ls-R file in a parent directory an return it or nil
function lsr_root (path)
if not lfs.isdir (path) then return end
local root, shift = path, ''
if string.sub(root, -1) == '/' then root = string.sub(root, 1, -2) end
while string.find(root, '/', 1, true) do
if lfs.isfile(root..'/ls-R') then
return root, shift
end
local last_comp = string.match(root, '^.*/(.*)$')
-- /!\ cannot put last_comp in a regex: can contain special char
root = string.sub(root, 1, - (#last_comp + 2))
shift = last_comp..'/'..shift
end
end
-- scan a ls-R file
function scan_lsr (code, shift, pattern)
local isdoc = false
local current_dir
local l = #shift
local lsr = assert(io.open('ls-R', 'r'))
local _ = lsr:read('*line') -- throw away first line (comment)
local maybe_dir = true -- next line may be a directory
while true do
local line = lsr:read('*line')
while line == '' do line, maybe_dir = lsr:read('*line'), true end
if line == nil then break end -- EOF
local dir_line = maybe_dir and string.match (line, '^%./(.*):$')
if dir_line then
maybe_dir = false -- next line may not be a dir
if string.sub (dir_line, 1, l) == shift then
isdoc = true
current_dir = string.sub (dir_line, l+1)
is_dir[code_path(code, current_dir)] = true
elseif isdoc then
break -- we're exiting the ./doc (or shift) dir, so it's over
end
elseif isdoc then
process_file (line, merge_path(current_dir, line), code, pattern)
end
end
lsr:close()
end
-- remove directories from a list
function rmdirs (files)
local res = {}
for f in list (files) do
if not is_dir[f] then table.insert(res, f) end
end
return res
end
-- like populate_docfiles, but rel replaces exact if exact is empty
function mixed_populate_docfiles (pattern)
populate_docfiles (pattern)
if not exact_docfiles[1] then
if not string.find (pattern, '/') then
err_print ("No exact match, trying full search mode.", "info")
end
exact_docfiles = rel_docfiles
end
end
-- for sty files, we obviously don't want to look in TEXDOCS...
-- and we don't need a list since those are not duplicated (ahem...)
function populate_docfiles_sty (styname)
exact_docfiles = { kpse.find_file (styname) }
rel_docfiles = {}
end
---------------------------- selecting results -----------------------------
-- says if ext is 'good' according to ext_list
function is_good_ext (ext)
for e in list (config.ext_list) do
if e == '*' then
return true
elseif (e == '') and (not ext) then
return true
elseif ext == e then
return true
end
end
return false
end
-- include a file in the *_docfiles lists if it "matches"
function process_file (file, pathfile, code, pattern)
local base, ext = string.match(string.lower(file), '^(.*)%.(.*)$')
if string.find(string.lower(pathfile), pattern, 1, no_regex)
and is_good_ext (ext) then
if (base == pattern) or (file == pattern) then
table.insert(exact_docfiles, code_path (code, pathfile))
else
table.insert(rel_docfiles, code_path (code, pathfile))
end
end
end
-- for now, just make lowercase, but may do more later
function normalize_pattern (pattern)
return string.lower(pattern)
end
----------------------------- sorting results ------------------------------
-- compare two filenames with the following rule:
-- 1. extensions are ordered as in ext_list first,
-- 2. then filenames lexicographically.
function file_order (a, b)
local ext_a = string.match (a, '^.*%.(.*)$')
local ext_b = string.match (b, '^.*%.(.*)$')
ext_pos_a = config.ext_list_inv[ext_a] or (config.ext_list_max+1)
ext_pos_b = config.ext_list_inv[ext_b] or (config.ext_list_max+1)
if ext_pos_a < ext_pos_b then
return true
elseif ext_pos_a > ext_pos_b then
return false
else
return (a < b)
end
end
--------------------------------------------------------------------------------
---------------- functions to set config values and aliases ----------------
--------------------------------------------------------------------------------
---------------------------- general functions -----------------------------
-- set a config parameter, but don't overwrite it if already set
-- three special types: *_list (list), *_switch (boolean), *_level (number)
function set_config_element (key, value, context)
local is_known = false -- is key a valid option?
for option in list(known_options) do
if string.match(key, option) then is_known = true break end
end
-- warn and exit if key is not a known option
if not is_known then config_warn(key, nil, context) return end
-- exit if key is already set (/!\ must test for nil, not false)
if not (config[key] == nil) then return nil end
if string.match(key, '_list$') then -- coma-separated list
local values = string.explode(value, ',')
local inverse = {}
for i, j in ipairs(values) do -- sanitize values...
values[i] = string.gsub(j, '%s*$', '')
values[i] = string.gsub(j, '^%s*', '')
inverse[j] = i -- ... and build inverse mapping on the way
end
config[key] = values
config[key..'_inv'] = inverse
config[key..'_max'] = #values
elseif string.find (key, '_switch$') then -- boolean
if value == 'true' then
config[key] = true
elseif value == 'false' then
config[key] = false
else
config_warn (key, value, context)
end
elseif string.find (key, '_level$') then -- integer
local val = tonumber (value)
if val then
config[key] = val
else
config_warn (key, value, context)
end
else -- string
config[key] = value
end
-- special case: if we just set verbosity_level, print version info now
if key == 'verbosity_level' then
err_print(arg[0]..' version '..version, 'debug1')
end
-- now tell what we have just done, for debugging
err_print('Setting "'..key..'='..value..'" '
..context_to_string(context)..'.', 'debug2')
end
-- a helper function for warning messages in the above
function config_warn (key, value, context)
local begin = value and 'Illegal value "'..value..'" for option "'..key..'"'
or 'Unknown option "'..key..'"'
local ending = '. Skipping.'
err_print (begin..'\n '..context_to_string(context)..ending, 'warning')
end
-- interpreting 'context' for the previous functions
function context_to_string(context)
if not context then return '(no context)' end
if context.src == 'cl' then
return 'from command line option "'..context.name..'"'
elseif context.src == 'env' then
return 'from environment variable "'..context.name..'"'
elseif context.src == 'file' then
return 'in file "'..context.file..'" on line '..context.line
elseif context.src == 'def' then
return 'from built-in defaults'
else
return 'from unkown source (should not happen, please report)'
end
end
-- set a whole list, also whithout overwriting
function set_config_list (conf, context)
for key, value in pairs(conf) do
set_config_element (key, value, context)
end
end
-- set an alias (w/o overwriting)
function set_alias (key, value)
if alias[key] == nil then
alias[key] = value
end
end
------------------------ options from command line -------------------------
-- set config from the command line
-- Please make sure to update usage_msg accordingly
-- and set a default value in setup_config_from_defaults() if relevant.
-- TODO: should use some getopt_long()-like mechanism some day
function setup_config_from_cl ()
local curr_arg
local function set_config_elt(key, val)
set_config_element(key, val, {src='cl', name=curr_arg})
end
while arg[1] and string.match(arg[1],'^%-') do
curr_arg = table.remove(arg,1)
if (curr_arg == '-h') or (curr_arg == '--help') then
print (usage_msg)
os.exit(0)
elseif (curr_arg == '-V') or (curr_arg == '--version') then
print (progname .. ' ' .. version )
os.exit(0)
elseif (curr_arg == '-f') or (curr_arg == '--files') then
print (progname .. ' ' .. version )
setup_config_from_files ()
show_config_files (print, true)
os.exit(0)
elseif (curr_arg == '-w') or (curr_arg == '--view') then
set_config_elt('mode', 'view')
elseif (curr_arg == '-m') or (curr_arg == '--mixed') then
set_config_elt('mode', 'mixed')
elseif (curr_arg == '-l') or (curr_arg == '--list') then
set_config_elt('mode', 'list')
elseif (curr_arg == '-s') or (curr_arg == '--search') then
set_config_element ('mode', 'search', {src='cl', name=curr_arg})
elseif (curr_arg == '-r') or (curr_arg == '--regex') then
set_config_element ('mode', 'regex', {src='cl', name=curr_arg})
elseif (curr_arg == '-I') or (curr_arg == '--nointeract') then
set_config_elt('interact_switch', 'false')
elseif (curr_arg == '-i') or (curr_arg == '--interact') then
set_config_elt('interact_switch', 'true')
elseif (curr_arg == '-A') or (curr_arg == '--noalias') then
set_config_elt('alias_switch', 'false')
elseif (curr_arg == '-a') or (curr_arg == '--alias') then
set_config_elt('alias_switch', 'true')
elseif (curr_arg == '-d') or (curr_arg == '--debug') then
set_config_elt('verbosity_level', err_max)
elseif string.match(curr_arg, '^%-v') then
local value = string.gsub(curr_arg, '^%-v=?', '')
set_config_elt('verbosity_level', value)
elseif string.match(curr_arg, '^%-%-verbosity') then
local value = string.gsub(curr_arg, '^%-%-verbosity=?', '')
set_config_elt('verbosity_level', value)
elseif string.match(curr_arg, '^%-e') then
local value = string.gsub(curr_arg, '^%-e=?', '')
set_config_elt('ext_list', value)
elseif string.match(curr_arg, '^%-%-extensions') then
local value = string.gsub(curr_arg, '^%-%-extensions=?', '')
set_config_elt('ext_list', value)
else
err_print ("unknown option: "..curr_arg, "error")
print (error_msg)
os.exit(1)
end
end
end
------------------------- config from environment --------------------------
-- set config from environment if available
function setup_config_from_env ()
local function set_config_elt_from_vars(key, vars)
for var in list(vars) do
local value = os.getenv(var)
if value then
set_config_element(key, value, {src='env', name=var})
end
end
end
set_config_elt_from_vars('viewer_pdf',
{"PDFVIEWER_texdoc", "TEXDOCVIEW_pdf", "TEXDOC_VIEWER_PDF", "PDFVIEWER"})
set_config_elt_from_vars('viewer_ps',
{"PSVIEWER_texdoc", "TEXDOCVIEW_ps", "TEXDOC_VIEWER_PS", "PSVIEWER"})
set_config_elt_from_vars('viewer_dvi',
{"DVIVIEWER_texdoc", "TEXDOCVIEW_dvi", "TEXDOC_VIEWER_DVI", "DVIVIEWER"})
set_config_elt_from_vars('viewer_html',
{"BROWSER_texdoc", "TEXDOCVIEW_html", "TEXDOC_VIEWER_HTML", "BROWSER"})
set_config_elt_from_vars('viewer_txt',
{"PAGER_texdoc", "TEXDOCVIEW_txt", "TEXDOC_VIEWER_TXT", "PAGER"})
end
---------------------- options and aliases from files ----------------------
-- set config+aliases from a particular config file assumed to exist
function read_config_file(configfile)
local cnf = assert(io.open(configfile, 'r'))
local lineno = 0
while true do
local key, val
local line=cnf:read('*line')
lineno = lineno + 1
if line == nil then break end -- EOF
line = string.gsub(line, '%s*#.*$', '') -- comments begin with #
line = string.gsub(line, '%s*$', '') -- remove trailing spaces
line = string.gsub(line, '^%s*', '') -- remove leading spaces
key, val = string.match(line, '^([%a%d_]+)%s*=%s*(.+)')
if key and val then
set_config_element(key, val, {
src='file', file=configfile, line=lineno})
else
key, val = string.match(line, '^alias%s+([%a%d_-]+)%s*=%s*(.+)')
if key and val then
set_alias(key, val)
else
if (not string.match (line, '^%s*$')) then
err_print ('syntax error in '..configfile..
' at line '..lineno..'.', 'warning')
end
end
end
end
cnf:close()
end
-- return a table with config file and if they exist
function get_config_files ()
local platform = string.match (kpse.var_value ('SELFAUTOLOC'), '.*/(.*)$')
local TEXMFHOME = kpse.var_value ('TEXMFHOME')
local TEXMFLOCAL = kpse.var_value ('TEXMFLOCAL')
local TEXMFMAIN = kpse.var_value ('TEXMFMAIN')
return {
TEXMFHOME .. '/texdoc/texdoc-'..platform..'.cnf',
TEXMFHOME .. '/texdoc/texdoc.cnf',
TEXMFHOME .. '/texdoc/texdoc-dist.cnf',
TEXMFLOCAL .. '/texdoc/texdoc-'..platform..'.cnf',
TEXMFLOCAL .. '/texdoc/texdoc.cnf',
TEXMFMAIN .. '/texdoc/texdoc.cnf'
}
end
-- the config_files table is shared by the next two functions
do
local config_files = {}
-- set config/aliases from all config files
function setup_config_from_files ()
for i, file in ipairs (get_config_files ()) do
local found = lfs.isfile(file)
config_files[i] = {
path = file,
status = found and (config.lastfile_switch
and 'disabled' or 'active') or 'absent',
}
if config_files[i].status == 'active' then
read_config_file (file)
end
end
end
-- now a special information function (see -f,--file option)
function show_config_files (print_fun, prefix)
print_fun("Configuration files are:")
for i, file in ipairs (config_files) do
local home = prefix and
((i==2) and "(*) " or " ") -- home conffile is the 2nd
or ''
print_fun (home..file.status..'\t'..win32_hook(file.path))
end
if prefix then
print("(*) This is the recommended configuration file "
.. "for your personal preferences.")
end
end
end -- scope of config_files
---------------------- options from built-in defaults ----------------------
-- for default viewer on general Unix, we have a list; the following two
-- functions are used to check in the path which program is available
-- check if "name" is the name of a file in the path
-- Warning: to be used only on Unix! (separators, and PATH irrelevant on win32)
function is_in_path(name)
local path_list = string.explode(os.getenv("PATH"), ':')
for _, path in ipairs(path_list) do
if lfs.isfile(path..'/'..name) then return true end
end
return false
end
-- return the first element of "list" whose name is found in path, or nil
function first_in_path(cmds)
for _, cmd in ipairs(cmds) do
if is_in_path(cmd[1]) then return cmd[2] end
end
return nil
end
-- set some fall-back default values if no previous value is set
function setup_config_from_defaults()
local function set_config_ls(ls) set_config_list(ls, {src='def'}) end
local function set_config_elt(key, val)
set_config_element(key, val, {src='def'})
end
if (os.type == "windows") then
set_config_ls {
-- Use 'start' to get file associations.
-- We need to quote the filenames, but the first quoted argument
-- is considered as the title by start, so we provide a dummy title.
-- Also, since the command line parser removes quotes if there
-- is no space inside, the dummy title must contain spaces.
viewer_dvi = 'start "texdoc dvi viewer"',
viewer_html = 'start "texdoc html viewer"',
viewer_pdf = 'start "texdoc pdf viewer"',
viewer_ps = 'start "texdoc ps viewer"',
-- 'more' is always available.
-- However, we can't assume texdoc is called from a cmd.exe window
-- (it can be run from the start->run menu), hence we make sure
-- to open a new window if needed.
viewer_txt = 'start cmd /k more',
}
elseif (os.name == 'macosx') then
set_config_ls {
viewer_dvi = 'open',
viewer_html = 'open',
viewer_pdf = 'open',
viewer_ps = 'open',
viewer_txt = 'less',
}
else -- generic Unix
set_config_ls {
viewer_dvi = first_in_path {
{'gnome-open', '(gnome-open %s) &'}, -- gnome
{'kde-open', '(kde-open %s) &'}, -- kde 4
{'kfmclient', '(kfmclient exec %s) &'}, -- older kde
{'exo-open', '(exo-open %s) &'}, -- xfce
{'xdg-open', '(xdg-open %s) &'}, -- freedesktop.org
{'evince', '(evince %s) &'},
{'okular', '(okular %s) &'},
{'kdvi', '(kdvi %s) &'},
{'xgdvi', '(xgdvi %s) &'},
{'spawg', '(spawg %s) &'},
{'spawx11', '(spawx11 %s) &'},
{'tkdvi', '(tkdvi %s) &'},
{'dvilx', '(dvilx %s) &'},
{'advi', '(advi %s) &'},
{'xdvik-ja', '(xdvik-ja %s) &'},
{'xdvi', '(xdvi %s) &'},
{'see', '(see %s) &'}
},
viewer_html = first_in_path {
{'gnome-open', '(gnome-open %s) &'}, -- gnome
{'kde-open', '(kde-open %s) &'}, -- kde 4
{'kfmclient', '(kfmclient exec %s) &'}, -- older kde
{'exo-open', '(exo-open %s) &'}, -- xfce
{'xdg-open', '(xdg-open %s) &'}, -- freedesktop.org
{'firefox', '(firefox %s) &'},
{'seamonkey', '(seamonkey %s) &'},
{'mozilla', '(mozilla %s) &'},
{'konqueror', '(konqueror %s) &'},
{'epiphany', '(epiphany %s) &'},
{'opera', '(opera %s) &'},
{'w3m', 'w3m'},
{'links', 'links'},
{'lynx', 'lynx'},
{'see', 'see'}
},
viewer_pdf = first_in_path {
{'gnome-open', '(gnome-open %s) &'}, -- gnome
{'kde-open', '(kde-open %s) &'}, -- kde 4
{'kfmclient', '(kfmclient exec %s) &'}, -- older kde
{'exo-open', '(exo-open %s) &'}, -- xfce
{'xdg-open', '(xdg-open %s) &'}, -- freedesktop.org
{'evince', '(evince %s) &'},
{'okular', '(okular %s) &'},
{'kpdf', '(kpdf %s) &'},
{'xpdf', '(xpdf %s) &'},
{'acroread', '(xpdf %s) &'},
{'see', '(see %s) &'}
},
viewer_ps = first_in_path {
{'gnome-open', '(gnome-open %s) &'}, -- gnome
{'kde-open', '(kde-open %s) &'}, -- kde 4
{'kfmclient', '(kfmclient exec %s) &'}, -- older kde
{'exo-open', '(exo-open %s) &'}, -- xfce
{'xdg-open', '(xdg-open %s) &'}, -- freedesktop.org
{'evince', '(evince %s) &'},
{'okular', '(okular %s) &'},
{'kghostview', '(kghostview %s) &'},
{'gv', '(gv %s) &'},
{'see', '(see %s) &'}
},
viewer_txt = first_in_path {
{'most', 'most'},
{'less', 'less'},
{'more', 'more'}
}
}
end
-- then various, platform independant, stuff
set_config_ls {
mode = 'view',
interact_switch = 'true',
verbosity_level = '3',
}
-- must be set after mode!
set_config_elt ('alias_switch', alias_from_mode(config.mode))
-- now a particular case for config.ext_list and zip-related stuff
if support_zipped then
set_config_elt('ext_list',
'pdf,pdf.gz,pdf.bz2, html,html.gz,html.bz2, txt,txt.gz,txt.bz2,'..
'dvi,dvi.gz,dvi.bz2, ps,ps.gz,ps.bz2, ,gz,bz2')
set_config_ls {
unzip_gz = 'gzip -d -c ',
unzip_bz2 = 'bzip -d -c ',
rm_file = 'rm -f',
rm_dir = 'rmdir'
}
else
set_config_elt('ext_list', 'pdf, html, txt, dvi, ps, ')
end
end
-- the default value of config.alias_switch depends on the mode as follows
function alias_from_mode (mode) -- /!\ returns a string!
if (mode == 'view') or (mode == 'mixed') or (mode == 'list') then
return 'true'
else
return 'false'
end
end
--------------------------------------------------------------------------------
--------------- functions for viewing/displaying the results ---------------
--------------------------------------------------------------------------------
--------------------------------- viewing ----------------------------------
-- prepare for viewing: returns and
-- is either:
-- 1. the filename, quoted with "
-- 2. the filename, quoted with " followed by some rm commands
-- The second case happens when the doc was zipped. In the case, this function
-- unzips it in a tempdir so that the viewer command can use the unzipped file.
function how_to_view (filename)
filename = real_path(filename) -- TODO: if not filename then ...
if support_zipped then
viewext,zipext = string.match(filename, '.*%.([^.]*)%.([^.]*)$')
if viewext and zipext then
unzip_command = config['unzip_'..zipext]
local basename_pattern = '.*/(.*%.' .. viewext .. ')'
basename = string.match(filename,basename_pattern)
tmpdir = os.tmpdir("/tmp/texdoc.XXXXXX")
unzip_commandline = unzip_command .. filename .. " > "
.. tmpdir .. "/" .. basename
if os.execute(unzip_commandline) then
filename = tmpdir .. "/" .. basename
else
print("Error executing \n" .. unzip_commandline)
end
viewer_replacement = '"' .. filename .. '"; '
.. config.rm_file .. ' ' .. filename .. '; '
.. config.rm_dir .. ' ' .. tmpdir
end
end
-- if viewext wasn't set zipped way, then try the normal way
if not viewext then
viewer_replacement = '"' .. filename .. '"'
-- files without extension are assumed to be text
viewext = string.match(filename,'.*%.(.*)$') or 'txt'
if not config['viewer_'..viewext] then
err_print ("cannot determine type of file\n\t"
..filename.."Assuming text. Set the `viewer_"..viewext..
"' variable in texdoc.cnf to avoid this.", "warning")
viewext = 'txt'
if not config['viewer_'..viewext] then
err_print ("text viewer not found. This "..
"should not happen, sorry. Skipping\n\t"..filename, "error")
end
end -- viewer for ext
end -- zipped or not
return config['viewer_'..viewext], viewer_replacement
end
-- view a file, if possible
function try_viewing (view_command, viewer_replacement)
if not view_command then
view_result = false
else
if string.match (view_command, place_holder) then
view_command = string.gsub(
view_command, place_holder, viewer_replacement)
else
view_command = view_command..' '..viewer_replacement
end
err_print(view_command, 'debug1')
view_result = os.execute(view_command)
if not view_result then
err_print ("the following command failed\n\t"
.. view_command, "error")
end
end
return view_result
end
-------------------------------- displaying --------------------------------
-- display a table, sorted, numbered with given offset (0 by default),
-- with real path
function display_table (t, offset)
offset = offset or 0
table.sort(t, file_order)
for i, val in ipairs (t) do
print(string.format('%2d %s', i+offset, real_path(val)))
end
end
-- print a list of files as a menu (with an optional complementary list)
function print_menu (files, comp)
comp = comp or {}
max_lines = tonumber (config.max_lines) or 20
local f = #files
if config.interact_switch then
local n = f + #comp
if n > max_lines then
io.write (n, " results. Display them all? (y/N) ")
local ans = io.read('*line')
if not ((ans == 'y') or (ans == 'Y')
-- io.read is quite strange wrt windows line endings :-(
or (ans == '\ry') or (ans == '\rY')) then return end
end
end
display_table (files)
display_table (comp, f)
if config.interact_switch then
io.write ("Please enter the number of the file to view, ",
"anything else to skip: ")
local num = tonumber(io.read('*line'))
if num and (num <= f) and files[num] then
try_viewing (how_to_view (files[num]))
elseif num and comp[num-f] then
try_viewing (how_to_view (comp[num-f]))
end
end
end
--------------------------------------------------------------------------------
----------------------- functions for error handling -----------------------
--------------------------------------------------------------------------------
-- exit codes (probably make sense only with a single argument)
-- 0 OK
-- 1 Usage
-- 2 No doc found for at least one arg
-- ? Should do something for viewer problems etc
-- apologize/complain if something went wrong
function apologize (reason, name)
if reason == 'notfound' then
exit_code = 2
msg = string.gsub (notfound_msg, 'PKGNAME', name)
print (msg) -- to get rid of gsub's 2nd value
else
exit_code = 255
err_print ('Oops, this should not happen'..
' (unknown error code). Sorry.', 'error')
end
end
-- check that arg list is not empty
function assert_arg_not_empty ()
if not arg[1] then
print (usage_msg)
os.exit(1)
end
end
-- generic error display function (see the error_priority constant)
function err_print (msg, lvl)
-- be careful: maybe config.verbosity_level is not set yet
local verbosity_level = config.verbosity_level or 2
if err_priority[lvl] <= verbosity_level then
io.stderr:write ("texdoc "..lvl..": "..msg.."\n")
end
end
--------------------------------------------------------------------------------
--------------------------- main code execution ----------------------------
--------------------------------------------------------------------------------
----------------------------- initialisations ------------------------------
-- initialize kpathsea
kpse.set_program_name(arg[-1], "texdoc")
-- config options from command line, env, conf files or defaults
config = {} -- everything is stored in this table ...
alias = {} -- ... except aliases
assert_arg_not_empty ()
setup_config_from_cl ()
assert_arg_not_empty ()
setup_config_from_env ()
setup_config_from_files ()
setup_config_from_defaults ()
-- now that config.verbosity_level is known...
show_config_files(function(s) err_print(s, 'debug1') end)
get_texdocs()
------------------------ looping over the arguments ------------------------
-- initialising and saving a few values
exit_code = 0
no_regex = true
real_populate_docfiles = populate_docfiles
real_mixed_populate_docfiles = mixed_populate_docfiles
real_real_path = real_path
-- the actual loop
for docname in list (arg) do
-- inform the user which arg beeing treated if more than one was provided
if arg[2] then
print ("*** Results for: "..docname.." ***")
end
-- applying alias if relevant
if config.alias_switch and alias[docname] then
err_print (docname.." aliased to "..alias[docname], 'info')
docname = alias[docname]
end
-- exceptions for arguments with extension given
if config.mode ~= 'regex' then
docname_base, docname_ext = string.match (docname, '^(.*)%.(.*)$')
if docname_ext == 'sty' then
err_print ("using special search mode for sty files", 'info')
populate_docfiles = populate_docfiles_sty
mixed_populate_docfiles = populate_docfiles_sty
real_path = function (arg) return arg end
end
end
-- main "ifcase mode" construct
if (config.mode == 'regex') then
no_regex = false
populate_docfiles(docname)
if rel_docfiles[1] then
print_menu (rel_docfiles)
else
apologize ('notfound', docname)
end
elseif (config.mode == 'search') then
populate_docfiles(docname)
if exact_docfiles[1] or rel_docfiles[1] then
print_menu (exact_docfiles, rel_docfiles)
else
apologize ('notfound', docname)
end
elseif (config.mode == 'list') then
mixed_populate_docfiles (docname)
if exact_docfiles[1] then
print_menu (exact_docfiles)
else
apologize ('notfound', docname)
end
elseif (config.mode == 'view') then
mixed_populate_docfiles (docname)
if exact_docfiles[1] then
table.sort(exact_docfiles, file_order)
try_viewing (how_to_view(exact_docfiles[1]))
else
apologize ('notfound', docname)
end
elseif (config.mode == 'mixed') then
mixed_populate_docfiles (docname)
if (not exact_docfiles[1]) then -- no results
apologize ('notfound', docname)
elseif (not exact_docfiles[2]) then -- 1 result
local ok = try_viewing (how_to_view(exact_docfiles[1]))
if not ok then apologize ('oops') end
else -- 2 or more results
print_menu (exact_docfiles)
end
end
-- restoring possibly diverted values
populate_docfiles = real_populate_docfiles
mixed_populate_docfiles = real_mixed_populate_docfiles
real_path = real_real_path
end
os.exit(exit_code)
-- Local Variables:
-- lua-indent-level: 4
-- tab-width: 4
-- indent-tabs-mode: nil
-- End:
-- vim:sw=4 ts=4 expandtab: