--- reads configuration files into a Lua table.
-- Understands INI files, classic Unix config files, and simple
-- delimited columns of values.
-- See the Guide for further discussion
-- @class module
-- @name pl.config
local stringx = require ('pl.stringx')
local split,strip = stringx.split,stringx.strip
local type,tonumber,ipairs,io = type,tonumber,ipairs,io
local utils = require 'pl.utils'
local raise = utils.raise
module ('pl.config',utils._module)
-- @class table
-- @name configuration
-- @field variablilize make names into valid Lua identifiers (default true)
-- @field convert_numbers try to convert values into numbers (default true)
-- @field trim_space ensure that there is no starting or trailing whitespace with values (default true)
-- @field list_delim delimiter to use when separating columns (default ',')
--- 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
function lines(file)
local f,openf,err
local line = ''
if type(file) == 'string' then
f,err = io.open(file,'r')
if not f then return raise(err) end
openf = true
else
f = file or io.stdin
if not file.read then return raise 'not a file-like object' end
end
if not f then return raise'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
-- @return nil,error_msg in case of an error, otherwise a table containing items
function read(file,cnfg)
local f,openf,err
if not cnfg then
cnfg = {variablilize = true, convert_numbers = true,
trim_space = true, list_delim=','
}
end
local t = {}
local top_t = t
local variablilize = cnfg.variablilize
local list_delim = cnfg.list_delim
local convert_numbers = cnfg.convert_numbers
local trim_space = cnfg.trim_space
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 = tonumber(value)
if val then value = val end
end
if trim_space and type(value) == 'string' then
value = strip(value)
end
return value
end
local iter,err = lines(file)
if not iter then return raise(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
local i1,i2 = line:find('%s*=%s*')
if i1 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