#!/usr/bin/env texlua
-- $Id: texdoc.tlu 10633 2008-09-18 09:43:54Z mpg $ -*-Lua-*-
--[[Revised 2008 by Manuel Pgouri-Gonnard 
    with contributions from Reinhard Kotucha.
    First texlua versions by Frank Kster (2007). 
    Origial shell script by Thomas Esser, David Aspinall, and Simon Wilkinson.
    Public domain.]]
--[[ Changelog
  0.4 2008-07
  - moved configuration from texmf.cnf to texdoc.cnf
  - added an 'alias' feature
  - new modes 'mixed' and 'regex'
  - file lists are now menus (select number to view results)
  - changed the search modes to get only the more relevant results (hopefully)
  - /!\ zip support disabled by default (not portable) (see comments below)

  0.3 2007-06-28
  - added changelog
  - better OS detection for default viewer settings
  - removed some debugging code
  - -s now works in dirs without ls-R, too
  
  0.2 2007-06-28
  - implemented reading of configuration from texmf.cnf
  - fixed "-s" option
  
  0.1
  - initial public release 
]]

-- some constants 
progname = 'texdoc'
version = '0.4'
-- make sure to update setup_config_from_cl() accordingly
-- and set a default value in setup_config_from_defaults() if relevant
usage_msg = [[
Usage:  texdoc  -h, --help | -v, --version | -f, --files | [option(s)] name(s)
    -h, --help              Show this short help.
    -v, --version           Print the version of the program.
    -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.
    -n, --noise-level=N     Set verbosity level to N.
Environment: 
    PAGER, BROWSER, PDFVIEWER, PSVIEWER, DVIVIEWER.
Files: 
    <texmf>/texdoc/texdoc.cnf files, see the -f option.
Homepage: 
    http://tug.org/texdoc/
The full manual is accessed via `texdoc texdoc'.]]
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.]]
place_holder = '%%s' -- used for viewer commands
err_priority = {
    error   = 1,
    warning = 2,
    info    = 3,
    debug1  = 4,
}

-- 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 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 = true


-- BEGIN function definitions (till the 'END' mark)
---=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

-- a general-use function
function list (t)
    local i = 0
    return function ()
        i = i + 1
        return t[i]
    end
end

-- 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 above, and 
-- the # operator sometimes (bit faster than table.getn).

-- functions for the search options
-----------------------------------

-- BEGIN kpse-like

-- get global texdocs, do_recurse and must_have_lsr lists from kpse's $TEXDOCS
function get_texdocs ()
    local sep = (os.type == 'windows') and ';' or ':'
    texdocs = kpse.expand_var ("$TEXDOCS")
    -- expand the path and turn it into a lua list
    texdocs = string.explode (kpse.expand_braces (texdocs), sep)
    do_recurse, must_have_lsr = {}, {} -- global
    for i, dir in ipairs (texdocs) do
        local n
        dir, n = string.gsub (dir, '//$', '')
        do_recurse[i] = (n == 1)
        texdocs[i], n = string.gsub (dir, '^!!', '')
        must_have_lsr[i] = (n == 1)
    end
end

-- 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 lists if it should be
function process_file (file, pathfile, code, pattern)
    local base, ext = string.match(file, '^(.*)%.(.*)$')
    if string.find(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

-- encodes the base path on two digits and concatenate with filename
-- see real_path() for decoding
function code_path (code, file)
    local padding = (code > 9) and '' or '0'
    return padding..code..':'..file
end

-- scan a tree
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 root with ls-R above path and 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

-- 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 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

-- find dirfiles according to pattern
function populate_docfiles (pattern)
    if not texdocs then get_texdocs() end
    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, docdir in ipairs (texdocs) do
        root, shift = lsr_root (docdir)
        if root and shift and do_recurse[code] then  
            assert(lfs.chdir(root))
            scan_lsr(code, shift, pattern)
        elseif (not must_have_lsr[code]) and lfs.isdir(docdir) then
            assert(lfs.chdir(docdir))
            scan_tree(code, '.', pattern, do_recurse[code])
        end
    end
    assert(lfs.chdir(curr_dir))
    exact_docfiles = rmdirs (exact_docfiles)
    rel_docfiles = rmdirs (rel_docfiles)
end

-- END kpse-like

-- 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

-- translate a path as given by populate_docfiles into a real path
-- should never be called before get_texdocs()
function real_path (fake)
    local code, file = string.match (fake, '^(.-):(.*)$')
    code = tonumber (code)
    return win32_hook (texdocs[code]..'/'..file)
end

-- now a special function that does nothing on Unix
if os.type == "windows" then
    function win32_hook (path)
        local res = string.gsub (path, '/', '\\')
        return res -- get rid of gsub's 2nd arg
    end
else
    function win32_hook (path)
        return path
    end
end

-- remove directories from the files 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

-- 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


-- functions to set config values and aliases
---------------------------------------------

-- set a value without overwriting if already set and
-- using to special types: *_list and *_switch
function set_config_element (key, value, file, line)
    if config[key] == nil then -- must explicitly test for nil, not false
        if string.match(key, '_list$') then
            -- this is actually a coma-separated list of values
            local values = string.explode(value, ',')
            local inverse = {}
            for i, j in ipairs(values) do
                values[i] = string.gsub(j, '%s*$', '')
                values[i] = string.gsub(j, '^%s*', '')
                inverse[j] = i
            end
            config[key] = values
            config[key..'_inv'] = inverse
            config[key..'_max'] = #values
        elseif string.find (key, '_switch$') then
            if value == 'true' then
                config[key] = true
            elseif value == 'false' then
                config[key] = false
            else
                config_warn (key, value, file, line)
            end
        elseif string.find (key, '_level$') then
            local val = tonumber (value)
            if val then 
                config[key] = val
            else
                config_warn (key, value, file, line)
            end
        else
            config[key] = value
        end
    end
end

-- a helper  function for warning messages in the above
function config_warn (key, value, file, line)
    local begin = 'illegal value '..value..' for key '..key
    local ending = '.  Skipping.'
    if file and line then
        err_print (begin..'\nin '..file..' line '..line..ending, 'warning')
    else
        err_print (begin..ending, 'warning')
    end
end


-- set a whole list, also whithout overwriting
function set_config_list (conf)
    for key, value in pairs(conf) do
        set_config_element (key, value)
    end
end

-- set an alias (w/o overwriting)
function set_alias (key, value)
    if alias[key] == nil then
        alias[key] = value
    end
end

-- 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.
function setup_config_from_cl ()
    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: ' .. version )
            os.exit(0)
        elseif (curr_arg == '-f') or (curr_arg == '--files') then
            print_config_files ()
            os.exit(0)
        elseif (curr_arg == '-w') or (curr_arg == '--view') then
            set_config_element('mode', 'view')
        elseif (curr_arg == '-m') or (curr_arg == '--mixed') then
            set_config_element('mode', 'mixed')
        elseif (curr_arg == '-l') or (curr_arg == '--list') then
            set_config_element('mode', 'list')
        elseif (curr_arg == '-s') or (curr_arg == '--search') then
            set_config_element ('mode', 'search')
        elseif (curr_arg == '-r') or (curr_arg == '--regex') then
            set_config_element ('mode', 'regex')
        elseif (curr_arg == '-I') or (curr_arg == '--nointeract') then
            set_config_element('interact_switch', 'false')
        elseif (curr_arg == '-i') or (curr_arg == '--interact') then
            set_config_element('interact_switch', 'true')
        elseif (curr_arg == '-A') or (curr_arg == '--noalias') then
            set_config_element('alias_switch', 'false')
        elseif (curr_arg == '-a') or (curr_arg == '--alias') then
            set_config_element('alias_switch', 'true')
        elseif string.match(curr_arg, '^%-n') then 
            set_config_element('noise_level', 
            string.gsub(curr_arg, '^%-n=?', ''))
        elseif string.match(curr_arg, '^%-%-noise%-level') then
            set_config_element('noise_level', 
            string.gsub(curr_arg, '^%-%-noise%-level=?', ''))
        elseif string.match(curr_arg, '^%-e') then 
            set_config_element('ext_list', 
            string.gsub(curr_arg, '^%-e=?', ''))
        elseif string.match(curr_arg, '^%-%-extensions') then
            set_config_element('ext_list', 
            string.gsub(curr_arg, '^%-%-extensions=?', ''))
        else
            err_print ("unknow option: "..curr_arg, "error")
            print (usage_msg)
            os.exit(1)
        end
    end
end

-- the default value af config.alias_switch depends on the mode as follows
function alias_from_mode (mode)
    if (mode == 'view') or (mode == 'mixed') or (mode == 'list') then 
        return 'true'
    else
        return 'false'
    end
end

-- set config from environment if available
function setup_config_from_env ()
    set_config_list {
        viewer_pdf  = os.getenv ("PDFVIEWER_texdoc") 
        or os.getenv ("TEXDOCVIEW_pdf") or os.getenv ("TEXDOC_VIEWER_PDF")
        or os.getenv ("PDFVIEWER"),
        viewer_ps   = os.getenv ("PSVIEWER_texdoc")
        or os.getenv ("TEXDOCVIEW_ps") or os.getenv ("TEXDOC_VIEWER_PS")
        or os.getenv ("PSVIEWER"),
        viewer_dvi  = os.getenv ("DVIVIEWER_texdoc")
        or os.getenv ("TEXDOCVIEW_dvi") or os.getenv ("TEXDOC_VIEWER_DVI")
        or os.getenv ("DVIVIEWER"),
        viewer_html = os.getenv ("BROWSER_texdoc")
        or os.getenv ("TEXDOCVIEW_html") or os.getenv ("TEXDOC_VIEWER_HTML")
        or os.getenv ("BROWSER"),
        viewer_txt  = os.getenv ("PAGER_texdoc")
        or os.getenv ("TEXDOCVIEW_txt") or os.getenv ("TEXDOC_VIEWER_TXT")
        or os.getenv ("PAGER"),
    }
end

-- 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, configfile, 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')
    local config_files = {}
    return {
        TEXMFHOME  .. '/texdoc/texdoc-'..platform..'.cnf',
        TEXMFHOME  .. '/texdoc/texdoc.cnf',
        TEXMFLOCAL .. '/texdoc/texdoc-'..platform..'.cnf',
        TEXMFLOCAL .. '/texdoc/texdoc.cnf',
        TEXMFMAIN  .. '/texdoc/texdoc.cnf'
    } 
end

-- set config/aliases from all config files 
function setup_config_from_files ()
    for file in list (get_config_files ()) do
        if lfs.isfile(file) then read_config_file (file) end
    end
end

-- now a special information function (see -f,--file option)
function print_config_files ()
    print "Configuration files are:"
    for i, file in ipairs (get_config_files ()) do
        local found = lfs.isfile(file) and "(active)" or "(not found)"
        local home = (i==2) and "* " or "" -- home conffile is the 2nd
        print (home..found, win32_hook(file))
    end
end

-- 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()
    if (os.type == "windows") then
        -- probably Windows (or OS/2)
        -- which commands should we use for unzipping?
        set_config_list {
            viewer_dvi    = 'start ""',
            viewer_html   = 'start ""',
            viewer_pdf    = 'start ""',
            viewer_ps     = 'start ""',
            viewer_txt    = 'start ""',
        }
    else -- since we don't support msdos, if os.type is not windows, it's unix
        if (os.name == 'macosx') then
            set_config_list {
                viewer_dvi    = 'open',
                viewer_html   = 'open',
                viewer_pdf    = 'open',
                viewer_ps     = 'open',
                viewer_txt    = 'less',
            }
        else
            set_config_list {
                viewer_dvi      = first_in_path {
                    {'evince',  '(evince %s) &'},
                    {'okular',  '(okular %s) &'},
                    {'kdvi',    '(kdvi %s) &'},
                    {'xdvi',    '(xdvi %s) &'},
                    {'see',     '(see %s) &'}
                },
                viewer_html     = first_in_path {
                    {'firefox',     '(firefox %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 {
                    {'evince',  '(evince %s) &'},
                    {'okular',  '(okular %s) &'},
                    {'kpdf',    '(kpdf %s) &'},
                    {'xpdf',    '(xpdf %s) &'},
                    {'see',     '(see %s) &'}
                },
                viewer_ps       = first_in_path {
                    {'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
    end
    -- then various stuff
    set_config_list {
        mode                = 'view',
        interact_switch  = 'true',
        noise_level         = '3',
    }
    -- must be set after mode!
    set_config_element ('alias_switch', alias_from_mode(config.mode))
    -- now a particular case for config.ext_list and zip-related stuff
    -- Note: removed texdoc_formats/zip_formats, gives simpler & generic code
    if support_zipped then
        set_config_element('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_list {
            unzip_gz    = 'gzip -d -c ',
            unzip_bz2   = 'bzip2 -d -c ',
            rm_file     = 'rm -f',
            rm_dir      = 'rmdir'
        }
    else
        set_config_element('ext_list', 'pdf, html, txt, dvi, ps, ')
    end
end


-- functions for viewing/displaying the results
-----------------------------------------------

-- prepare for viewing: set viewer_replacement and viewer_ext
-- may uncompress if support_zipped is set (giving the complete filename on the
-- command line is unsupported for compressed files by the way)
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]
            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 then no zipext was found, so start again
    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 the returned sting contains ; then we are working on 
        -- some unzipped file in a tmp dir and the first part is the
        -- file to be viewed and the rest is some shell thingy
        first, rest = string.match (viewer_replacement, '^([^;]*);(.*)$')
        if not first then
            first = viewer_replacement
        end
        if string.match (view_command, place_holder) then
            replacement = '"'..first..'"'
            if rest then
                replacement = replacement .. ";" .. rest
            end
            view_command = string.gsub(view_command, place_holder, replacement)
        else
            view_command = view_command..' "'..first..'"'
            if rest then
                view_command = view_command .. ";" .. rest
            end
        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

-- compare two filenames with the following rule:
-- 1. extensions are ordered as in ext_list first,
-- 2. then filenames lexicograhpicaly.
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

-- 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
        if config.interact_switch then 
            print (i+offset, real_path(val))
        else
            print (real_path(val))
        end
    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')) 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

-- 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.noise_level is not set yet
    local noise_level = config.noise_level or 10000
    if err_priority[lvl] <= noise_level then
        io.stderr:write ("texdoc "..lvl..": "..msg.."\n")
    end
end


-- END of function definitions: here starts the 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 ()

-- the main loop over the args
------------------------------

-- 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:
