#!/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: