--- Reads configuration files into a Lua table. -- Understands INI files, classic Unix config files, and simple -- delimited columns of values. -- -- # test.config -- # Read timeout in seconds -- read.timeout=10 -- # Write timeout in seconds -- write.timeout=5 -- #acceptable ports -- ports = 1002,1003,1004 -- -- -- readconfig.lua -- require 'pl' -- local t = config.read 'test.config' -- print(pretty.write(t)) -- -- ### output ##### -- { -- ports = { -- 1002, -- 1003, -- 1004 -- }, -- write_timeout = 5, -- read_timeout = 10 -- } -- -- See the Guide for further @{06-data.md.Reading_Configuration_Files|discussion} -- -- Dependencies: none -- @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 -- 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 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 -- @param cnfg a configuration table that may contain these fields: -- -- * `variablilize` make names into valid Lua identifiers (default `true`) -- * `convert_numbers` function to convert values into numbers (default `tonumber`) -- * `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 ',') -- * `ignore_assign` ignore any key-pair assignments (default `false`) -- * `kepsep` use this as key-pair separator (default '=') -- -- @return a table containing items, or nil -- @return error message (same as @{config.lines}) function config.read(file,cnfg) local f,openf,err cnfg = cnfg or {} local function check_cnfg (var,def) local val = cnfg[var] if val == nil then return def else return val end end local t = {} local top_t = t local variablilize = check_cnfg ('variabilize',true) local list_delim = check_cnfg('list_delim',',') local convert_numbers = check_cnfg('convert_numbers',tonumber) if convert_numbers==true then convert_numbers = tonumber end 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*' local function process_name(key) if variablilize 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('^[%d%+%-]') then local val = convert_numbers(value) if val then value = val end end if type(value) == 'string' then if trim_space then value = strip(value) end if trim_quotes then value = strip_quotes(value) end end return value end local iter,err = config.lines(file) if not iter then return nil,err end for line in iter do -- strips comments local ci = line:find('%s*[#;]') if ci then line = line:sub(1,ci-1) end -- and ignore blank lines if line:find('^%s*$') then elseif 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 end return top_t end return config