local z,e,K,D,O=_G.type,_G.tonumber,_G.ipairs,_G.io,_G.table local function n(z,e)local K={}local D=O.insert e="\091\094"..(e.."\093\043")for z in z:gmatch(e)do D(K,z)end return K end local function t(z)return(z:gsub("\094\037\115\043","")):gsub("\037\115\043\036","")end local function b(z)return z:gsub("\091\'\"\093\040\046\042\041\091\'\"\093","\037\049")end local P={}function P.lines(e)local K,O,n local t=""if z(e)=="\115\116\114\105\110\103"then K,n=D.open(e,"\114")if not K then return nil,n end O=true else K=e or D.stdin if not e.read then return nil,"\110\111\116 \097 \102\105\108\101-\108\105\107\101 \111\098\106\101\099\116"end end if not K then return nil,"\102\105\108\101 \105\115 \110\105\108"end return function()local z=K:read()while z do if z:match("\037\083")and not z:match("\094\037\115\042\091\059\035\093")then local e=z:find("\092\037\115\042\036")if e then t=t..z:sub(1,e-1)elseif t==""then return z else z=t..z t=""return z end end z=K:read()end if O then K:close()end end end function P.read(D,O)local A local c,f=P.lines(D)if not c then return nil,f end local a=c()O=O or{}if O.smart then A=true if a:match("\094\091\094\061\093\043\061")then O.keysep="\061"elseif a:match("\094\091\094\058\093\043\058")then O.keysep="\058"O.list_delim="\058"elseif a:match("\094\037\083\043\037\115\043")then O.keysep=" "if a:match("\094\037\083\043\037\115\043\037\083\043\037\115\043\037\083\043")then O.list_delim=" "end O.variabilize=false end end local function U(z,e)local K=O[z]if K==nil then return e else return K end end local T="\094\091\037\100\037\043\037-\093"local p={}local i=p local B=U("\118\097\114\105\097\098\105\108\105\122\101",true)local X=U("\108\105\115\116\095\100\101\108\105\109","\044")local F=U("\099\111\110\118\101\114\116\095\110\117\109\098\101\114\115",true)local Y=U("\099\111\110\118\101\114\116\095\098\111\111\108\101\097\110",false)local V=U("\116\114\105\109\095\115\112\097\099\101",true)local k=U("\116\114\105\109\095\113\117\111\116\101\115",false)local l=U("\105\103\110\111\114\101\095\097\115\115\105\103\110",false)local y=U("\107\101\121\115\101\112","\061")local N=y==" "and"\037\115\043"or"\037\115\042"..(y.."\037\115\042")if X==" "then X="\037\115\043"end local function R(z)if B then z=z:gsub("\091\094\037\119\093","\095")end return z end local function Q(D)if X and D:find(X)then D=n(D,X)for z,e in K(D)do D[z]=Q(e)end elseif F and D:find(T)then local z=e(D)if not z and D:match(" \107\066\036")then D=D:gsub(" \107\066","")z=e(D)end if z then D=z end elseif Y and D=="\116\114\117\101"then return true elseif Y and D=="\102\097\108\115\101"then return false end if z(D)=="\115\116\114\105\110\103"then if V then D=t(D)end if not k and(A and D:match("\094\""))then k=true end if k then D=b(D)end end return D end while a do if a:find("\094\037\091")then local z=R(a:match("\037\091\040\091\094\037\093\093\043\041\037\093"))p=i p[z]={}p=p[z]else a=a:gsub("\094\037\115\042","")local z,e=a:find(N)if z and not l then local K=R(a:sub(1,z-1))local D=Q(a:sub(e+1))p[K]=D else p[#p+1]=Q(a)end end a=c()end return i end return P
--- Reads configuration files into a Lua table.
--  Understands INI files, classic Unix config files, and simple
-- delimited columns of values. See @{06-data.md.Reading_Configuration_Files|the Guide}
--
--    # test.config
--    # Read timeout in seconds
--    read.timeout=10
--    # Write timeout in seconds
--    write.timeout=5
--    #acceptable ports
--    ports = 1002,1003,1004
--
--    -- readconfig.lua
--    local config = require 'config'
--    local t = config.read 'test.config'
--    print(pretty.write(t))
--
--    ### output #####
--    {
--      ports = {
--        1002,
--        1003,
--        1004
--      },
--      write_timeout = 5,
--      read_timeout = 10
--    }
--
-- @module pl.config

local type,tonumber,ipairs,io, table = _G.type,_G.tonumber,_G.ipairs,_G.io,_G.table

local function split(s,re)
    local res = {}
    local t_insert = table.insert
    re = '[^'..re..']+'
    for k in s:gmatch(re) do t_insert(res,k) end
    return res
end

local function strip(s)
    return s:gsub('^%s+',''):gsub('%s+$','')
end

local function strip_quotes (s)
    return s:gsub("['\"](.*)['\"]",'%1')
end

local config = {}

--- like io.lines(), but allows for lines to be continued with '\'.
-- @param file a file-like object (anything where read() returns the next line) or a filename.
-- Defaults to stardard input.
-- @return an iterator over the lines, or nil
-- @return error 'not a file-like object' or 'file is nil'
function config.lines(file)
    local f,openf,err
    local line = ''
    if type(file) == 'string' then
        f,err = io.open(file,'r')
        if not f then return nil,err end
        openf = true
    else
        f = file or io.stdin
        if not file.read then return nil, 'not a file-like object' end
    end
    if not f then return nil, 'file is nil' end
    return function()
        local l = f:read()
        while l do
            -- only for non-blank lines that don't begin with either ';' or '#'
            if l:match '%S' and not l:match '^%s*[;#]' then
                -- does the line end with '\'?
                local i = l:find '\\%s*$'
                if i then -- if so,
                    line = line..l:sub(1,i-1)
                elseif line == '' then
                    return l
                else
                    l = line..l
                    line = ''
                    return l
                end
            end
            l = f:read()
        end
        if openf then f:close() end
    end
end

--- read a configuration file into a table
-- @param file either a file-like object or a string, which must be a filename
-- @tab[opt] cnfg a configuration table that may contain these fields:
--
--  * `smart`  try to deduce what kind of config file we have (default false)
--  * `variabilize` make names into valid Lua identifiers (default true)
--  * `convert_numbers` try to convert values into numbers (default true)
--  * `trim_space` ensure that there is no starting or trailing whitespace with values (default true)
--  * `trim_quotes` remove quotes from strings (default false)
--  * `list_delim` delimiter to use when separating columns (default ',')
--  * `keysep` separator between key and value pairs (default '=')
--
-- @return a table containing items, or `nil`
-- @return error message (same as @{config.lines}
function config.read(file,cnfg)
    local auto

    local iter,err = config.lines(file)
    if not iter then return nil,err end
    local line = iter()
    cnfg = cnfg or {}
    if cnfg.smart then
        auto = true
        if line:match '^[^=]+=' then
            cnfg.keysep = '='
        elseif line:match '^[^:]+:' then
            cnfg.keysep = ':'
            cnfg.list_delim = ':'
        elseif line:match '^%S+%s+' then
            cnfg.keysep = ' '
            -- more than two columns assume that it's a space-delimited list
            -- cf /etc/fstab with /etc/ssh/ssh_config
            if line:match '^%S+%s+%S+%s+%S+' then
                cnfg.list_delim = ' '
            end
            cnfg.variabilize = false
        end
    end


    local function check_cnfg (var,def)
        local val = cnfg[var]
        if val == nil then return def else return val end
    end

    local initial_digits = '^[%d%+%-]'
    local t = {}
    local top_t = t
    local variabilize = check_cnfg ('variabilize',true)
    local list_delim = check_cnfg('list_delim',',')
    local convert_numbers = check_cnfg('convert_numbers',true)
    local convert_boolean = check_cnfg('convert_boolean',false)
    local trim_space = check_cnfg('trim_space',true)
    local trim_quotes = check_cnfg('trim_quotes',false)
    local ignore_assign = check_cnfg('ignore_assign',false)
    local keysep = check_cnfg('keysep','=')
    local keypat = keysep == ' ' and '%s+' or '%s*'..keysep..'%s*'
    if list_delim == ' ' then list_delim = '%s+' end

    local function process_name(key)
        if variabilize then
            key = key:gsub('[^%w]','_')
        end
        return key
    end

    local function process_value(value)
        if list_delim and value:find(list_delim) then
            value = split(value,list_delim)
            for i,v in ipairs(value) do
                value[i] = process_value(v)
            end
        elseif convert_numbers and value:find(initial_digits) then
            local val = tonumber(value)
            if not val and value:match ' kB$' then
                value = value:gsub(' kB','')
                val = tonumber(value)
            end
            if val then value = val end
        elseif convert_boolean and value == 'true' then
           return true
        elseif convert_boolean and value == 'false' then
           return false
        end
        if type(value) == 'string' then
           if trim_space then value = strip(value) end
           if not trim_quotes and auto and value:match '^"' then
                trim_quotes = true
            end
           if trim_quotes then value = strip_quotes(value) end
        end
        return value
    end

    while line do
        if line:find('^%[') then -- section!
            local section = process_name(line:match('%[([^%]]+)%]'))
            t = top_t
            t[section] = {}
            t = t[section]
        else
            line = line:gsub('^%s*','')
            local i1,i2 = line:find(keypat)
            if i1 and not ignore_assign then -- key,value assignment
                local key = process_name(line:sub(1,i1-1))
                local value = process_value(line:sub(i2+1))
                t[key] = value
            else -- a plain list of values...
                t[#t+1] = process_value(line)
            end
        end
        line = iter()
    end
    return top_t
end

return config
