+ if not end_of_flags and (match('--$S{long}',theArg,res) or match('-$S{short}',theArg,res)) then
+ if res.long then -- long option
+ parm = check_parm(res.long)
+ elseif #res.short == 1 or is_flag(res.short) then
+ parm = res.short
+ else
+ local parmstr,eq = check_parm(res.short)
+ if not eq then
+ parm = at(parmstr,1)
+ local flag = is_flag(parm)
+ if flag and flag.type ~= 'boolean' then
+ --if isdigit(at(parmstr,2)) then
+ -- a short option followed by a digit is an exception (for AW;))
+ -- push ahead into the arg array
+ tinsert(arg,i+1,parmstr:sub(2))
+ else
+ -- push multiple flags into the arg array!
+ for k = 2,#parmstr do
+ tinsert(arg,i+k-1,'-'..at(parmstr,k))
+ end
+ end
+ else
+ parm = parmstr
+ end
+ end
+ if aliases[parm] then parm = aliases[parm] end
+ if not parms[parm] and (parm == 'h' or parm == 'help') then
+ lapp.quit()
+ end
+ else -- a parameter
+ parm = parmlist[iparm]
+ if not parm then
+ -- extra unnamed parameters are indexed starting at 1
+ parm = iextra
+ ps = { type = 'string' }
+ parms[parm] = ps
+ iextra = iextra + 1
+ else
+ ps = parms[parm]
+ end
+ if not ps.varargs then
+ iparm = iparm + 1
+ end
+ val = theArg
+ end
+ ps = parms[parm]
+ if not ps then lapp.error("unrecognized parameter: "..parm) end
+ if ps.type ~= 'boolean' then -- we need a value! This should follow
+ if not val then
+ i = i + 1
+ val = arg[i]
+ theArg = val
+ end
+ lapp.assert(val,parm.." was expecting a value")
+ else -- toggle boolean flags (usually false -> true)
+ val = not ps.defval
+ end
+ ps.used = true
+ val = convert_parameter(ps,val)
+ set_result(ps,parm,val)
+ if builtin_types[ps.type] == 'file' then
+ set_result(ps,parm..'_name',theArg)
+ end
+ if lapp.callback then
+ lapp.callback(parm,theArg,res)
+ end
+ i = i + 1
+ val = nil
+ end
+ -- check unused parms, set defaults and check if any required parameters were missed
+ for parm,ps in pairs(parms) do
+ if not ps.used then
+ if ps.required then lapp.error("missing required parameter: "..parm) end
+ set_result(ps,parm,ps.defval)
+ end
+ end
+ return results
+end
+
+if arg then
+ script = arg[0]
+ script = script or rawget(_G,"LAPP_SCRIPT") or "unknown"
+ -- strip dir and extension to get current script name
+ script = script:gsub('.+[\\/]',''):gsub('%.%a+$','')
+else
+ script = "inter"
+end
+
+
+setmetatable(lapp, {
+ __call = function(tbl,str,args) return lapp.process_options_string(str,args) end,
+})
+
+
+return lapp
+
+
diff --git a/lualibs/pl/lexer.lua b/lualibs/pl/lexer.lua
new file mode 100644
index 0000000000..071a62bef5
--- /dev/null
+++ b/lualibs/pl/lexer.lua
@@ -0,0 +1,488 @@
+--- Lexical scanner for creating a sequence of tokens from text.
+-- `lexer.scan(s)` returns an iterator over all tokens found in the
+-- string `s`. This iterator returns two values, a token type string
+-- (such as 'string' for quoted string, 'iden' for identifier) and the value of the
+-- token.
+--
+-- Versions specialized for Lua and C are available; these also handle block comments
+-- and classify keywords as 'keyword' tokens. For example:
+--
+-- > s = 'for i=1,n do'
+-- > for t,v in lexer.lua(s) do print(t,v) end
+-- keyword for
+-- iden i
+-- = =
+-- number 1
+-- , ,
+-- iden n
+-- keyword do
+--
+-- See the Guide for further @{06-data.md.Lexical_Scanning|discussion}
+-- @module pl.lexer
+
+local yield,wrap = coroutine.yield,coroutine.wrap
+local strfind = string.find
+local strsub = string.sub
+local append = table.insert
+
+local function assert_arg(idx,val,tp)
+ if type(val) ~= tp then
+ error("argument "..idx.." must be "..tp, 2)
+ end
+end
+
+local lexer = {}
+
+local NUMBER1 = '^[%+%-]?%d+%.?%d*[eE][%+%-]?%d+'
+local NUMBER2 = '^[%+%-]?%d+%.?%d*'
+local NUMBER3 = '^0x[%da-fA-F]+'
+local NUMBER4 = '^%d+%.?%d*[eE][%+%-]?%d+'
+local NUMBER5 = '^%d+%.?%d*'
+local IDEN = '^[%a_][%w_]*'
+local WSPACE = '^%s+'
+local STRING1 = "^(['\"])%1" -- empty string
+local STRING2 = [[^(['"])(\*)%2%1]]
+local STRING3 = [[^(['"]).-[^\](\*)%2%1]]
+local CHAR1 = "^''"
+local CHAR2 = [[^'(\*)%1']]
+local CHAR3 = [[^'.-[^\](\*)%1']]
+local PREPRO = '^#.-[^\\]\n'
+
+local plain_matches,lua_matches,cpp_matches,lua_keyword,cpp_keyword
+
+local function tdump(tok)
+ return yield(tok,tok)
+end
+
+local function ndump(tok,options)
+ if options and options.number then
+ tok = tonumber(tok)
+ end
+ return yield("number",tok)
+end
+
+-- regular strings, single or double quotes; usually we want them
+-- without the quotes
+local function sdump(tok,options)
+ if options and options.string then
+ tok = tok:sub(2,-2)
+ end
+ return yield("string",tok)
+end
+
+-- long Lua strings need extra work to get rid of the quotes
+local function sdump_l(tok,options,findres)
+ if options and options.string then
+ local quotelen = 3
+ if findres[3] then
+ quotelen = quotelen + findres[3]:len()
+ end
+ tok = tok:sub(quotelen, -quotelen)
+ if tok:sub(1, 1) == "\n" then
+ tok = tok:sub(2)
+ end
+ end
+ return yield("string",tok)
+end
+
+local function chdump(tok,options)
+ if options and options.string then
+ tok = tok:sub(2,-2)
+ end
+ return yield("char",tok)
+end
+
+local function cdump(tok)
+ return yield('comment',tok)
+end
+
+local function wsdump (tok)
+ return yield("space",tok)
+end
+
+local function pdump (tok)
+ return yield('prepro',tok)
+end
+
+local function plain_vdump(tok)
+ return yield("iden",tok)
+end
+
+local function lua_vdump(tok)
+ if lua_keyword[tok] then
+ return yield("keyword",tok)
+ else
+ return yield("iden",tok)
+ end
+end
+
+local function cpp_vdump(tok)
+ if cpp_keyword[tok] then
+ return yield("keyword",tok)
+ else
+ return yield("iden",tok)
+ end
+end
+
+--- create a plain token iterator from a string or file-like object.
+-- @tparam string|file s a string or a file-like object with `:read()` method returning lines.
+-- @tab matches an optional match table - array of token descriptions.
+-- A token is described by a `{pattern, action}` pair, where `pattern` should match
+-- token body and `action` is a function called when a token of described type is found.
+-- @tab[opt] filter a table of token types to exclude, by default `{space=true}`
+-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
+-- which means convert numbers and strip string quotes.
+function lexer.scan(s,matches,filter,options)
+ local file = type(s) ~= 'string' and s
+ filter = filter or {space=true}
+ options = options or {number=true,string=true}
+ if filter then
+ if filter.space then filter[wsdump] = true end
+ if filter.comments then
+ filter[cdump] = true
+ end
+ end
+ if not matches then
+ if not plain_matches then
+ plain_matches = {
+ {WSPACE,wsdump},
+ {NUMBER3,ndump},
+ {IDEN,plain_vdump},
+ {NUMBER1,ndump},
+ {NUMBER2,ndump},
+ {STRING1,sdump},
+ {STRING2,sdump},
+ {STRING3,sdump},
+ {'^.',tdump}
+ }
+ end
+ matches = plain_matches
+ end
+ local function lex(first_arg)
+ local line_nr = 0
+ local next_line = file and file:read()
+ local sz = file and 0 or #s
+ local idx = 1
+
+ -- res is the value used to resume the coroutine.
+ local function handle_requests(res)
+ while res do
+ local tp = type(res)
+ -- insert a token list
+ if tp == 'table' then
+ res = yield('','')
+ for _,t in ipairs(res) do
+ res = yield(t[1],t[2])
+ end
+ elseif tp == 'string' then -- or search up to some special pattern
+ local i1,i2 = strfind(s,res,idx)
+ if i1 then
+ local tok = strsub(s,i1,i2)
+ idx = i2 + 1
+ res = yield('',tok)
+ else
+ res = yield('','')
+ idx = sz + 1
+ end
+ else
+ res = yield(line_nr,idx)
+ end
+ end
+ end
+
+ handle_requests(first_arg)
+ if not file then line_nr = 1 end
+
+ while true do
+ if idx > sz then
+ if file then
+ if not next_line then return end
+ s = next_line
+ line_nr = line_nr + 1
+ next_line = file:read()
+ if next_line then
+ s = s .. '\n'
+ end
+ idx, sz = 1, #s
+ else
+ while true do
+ handle_requests(yield())
+ end
+ end
+ end
+
+ for _,m in ipairs(matches) do
+ local pat = m[1]
+ local fun = m[2]
+ local findres = {strfind(s,pat,idx)}
+ local i1, i2 = findres[1], findres[2]
+ if i1 then
+ local tok = strsub(s,i1,i2)
+ idx = i2 + 1
+ local res
+ if not (filter and filter[fun]) then
+ lexer.finished = idx > sz
+ res = fun(tok, options, findres)
+ end
+ if not file and tok:find("\n") then
+ -- Update line number.
+ local _, newlines = tok:gsub("\n", {})
+ line_nr = line_nr + newlines
+ end
+ handle_requests(res)
+ break
+ end
+ end
+ end
+ end
+ return wrap(lex)
+end
+
+local function isstring (s)
+ return type(s) == 'string'
+end
+
+--- insert tokens into a stream.
+-- @param tok a token stream
+-- @param a1 a string is the type, a table is a token list and
+-- a function is assumed to be a token-like iterator (returns type & value)
+-- @string a2 a string is the value
+function lexer.insert (tok,a1,a2)
+ if not a1 then return end
+ local ts
+ if isstring(a1) and isstring(a2) then
+ ts = {{a1,a2}}
+ elseif type(a1) == 'function' then
+ ts = {}
+ for t,v in a1() do
+ append(ts,{t,v})
+ end
+ else
+ ts = a1
+ end
+ tok(ts)
+end
+
+--- get everything in a stream upto a newline.
+-- @param tok a token stream
+-- @return a string
+function lexer.getline (tok)
+ local t,v = tok('.-\n')
+ return v
+end
+
+--- get current line number.
+-- @param tok a token stream
+-- @return the line number.
+-- if the input source is a file-like object,
+-- also return the column.
+function lexer.lineno (tok)
+ return tok(0)
+end
+
+--- get the rest of the stream.
+-- @param tok a token stream
+-- @return a string
+function lexer.getrest (tok)
+ local t,v = tok('.+')
+ return v
+end
+
+--- get the Lua keywords as a set-like table.
+-- So `res["and"]` etc would be `true`.
+-- @return a table
+function lexer.get_keywords ()
+ if not lua_keyword then
+ lua_keyword = {
+ ["and"] = true, ["break"] = true, ["do"] = true,
+ ["else"] = true, ["elseif"] = true, ["end"] = true,
+ ["false"] = true, ["for"] = true, ["function"] = true,
+ ["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true,
+ ["not"] = true, ["or"] = true, ["repeat"] = true,
+ ["return"] = true, ["then"] = true, ["true"] = true,
+ ["until"] = true, ["while"] = true
+ }
+ end
+ return lua_keyword
+end
+
+--- create a Lua token iterator from a string or file-like object.
+-- Will return the token type and value.
+-- @string s the string
+-- @tab[opt] filter a table of token types to exclude, by default `{space=true,comments=true}`
+-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
+-- which means convert numbers and strip string quotes.
+function lexer.lua(s,filter,options)
+ filter = filter or {space=true,comments=true}
+ lexer.get_keywords()
+ if not lua_matches then
+ lua_matches = {
+ {WSPACE,wsdump},
+ {NUMBER3,ndump},
+ {IDEN,lua_vdump},
+ {NUMBER4,ndump},
+ {NUMBER5,ndump},
+ {STRING1,sdump},
+ {STRING2,sdump},
+ {STRING3,sdump},
+ {'^%-%-%[(=*)%[.-%]%1%]',cdump},
+ {'^%-%-.-\n',cdump},
+ {'^%[(=*)%[.-%]%1%]',sdump_l},
+ {'^==',tdump},
+ {'^~=',tdump},
+ {'^<=',tdump},
+ {'^>=',tdump},
+ {'^%.%.%.',tdump},
+ {'^%.%.',tdump},
+ {'^.',tdump}
+ }
+ end
+ return lexer.scan(s,lua_matches,filter,options)
+end
+
+--- create a C/C++ token iterator from a string or file-like object.
+-- Will return the token type type and value.
+-- @string s the string
+-- @tab[opt] filter a table of token types to exclude, by default `{space=true,comments=true}`
+-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
+-- which means convert numbers and strip string quotes.
+function lexer.cpp(s,filter,options)
+ filter = filter or {space=true,comments=true}
+ if not cpp_keyword then
+ cpp_keyword = {
+ ["class"] = true, ["break"] = true, ["do"] = true, ["sizeof"] = true,
+ ["else"] = true, ["continue"] = true, ["struct"] = true,
+ ["false"] = true, ["for"] = true, ["public"] = true, ["void"] = true,
+ ["private"] = true, ["protected"] = true, ["goto"] = true,
+ ["if"] = true, ["static"] = true, ["const"] = true, ["typedef"] = true,
+ ["enum"] = true, ["char"] = true, ["int"] = true, ["bool"] = true,
+ ["long"] = true, ["float"] = true, ["true"] = true, ["delete"] = true,
+ ["double"] = true, ["while"] = true, ["new"] = true,
+ ["namespace"] = true, ["try"] = true, ["catch"] = true,
+ ["switch"] = true, ["case"] = true, ["extern"] = true,
+ ["return"] = true,["default"] = true,['unsigned'] = true,['signed'] = true,
+ ["union"] = true, ["volatile"] = true, ["register"] = true,["short"] = true,
+ }
+ end
+ if not cpp_matches then
+ cpp_matches = {
+ {WSPACE,wsdump},
+ {PREPRO,pdump},
+ {NUMBER3,ndump},
+ {IDEN,cpp_vdump},
+ {NUMBER4,ndump},
+ {NUMBER5,ndump},
+ {CHAR1,chdump},
+ {CHAR2,chdump},
+ {CHAR3,chdump},
+ {STRING1,sdump},
+ {STRING2,sdump},
+ {STRING3,sdump},
+ {'^//.-\n',cdump},
+ {'^/%*.-%*/',cdump},
+ {'^==',tdump},
+ {'^!=',tdump},
+ {'^<=',tdump},
+ {'^>=',tdump},
+ {'^->',tdump},
+ {'^&&',tdump},
+ {'^||',tdump},
+ {'^%+%+',tdump},
+ {'^%-%-',tdump},
+ {'^%+=',tdump},
+ {'^%-=',tdump},
+ {'^%*=',tdump},
+ {'^/=',tdump},
+ {'^|=',tdump},
+ {'^%^=',tdump},
+ {'^::',tdump},
+ {'^.',tdump}
+ }
+ end
+ return lexer.scan(s,cpp_matches,filter,options)
+end
+
+--- get a list of parameters separated by a delimiter from a stream.
+-- @param tok the token stream
+-- @string[opt=')'] endtoken end of list. Can be '\n'
+-- @string[opt=','] delim separator
+-- @return a list of token lists.
+function lexer.get_separated_list(tok,endtoken,delim)
+ endtoken = endtoken or ')'
+ delim = delim or ','
+ local parm_values = {}
+ local level = 1 -- used to count ( and )
+ local tl = {}
+ local function tappend (tl,t,val)
+ val = val or t
+ append(tl,{t,val})
+ end
+ local is_end
+ if endtoken == '\n' then
+ is_end = function(t,val)
+ return t == 'space' and val:find '\n'
+ end
+ else
+ is_end = function (t)
+ return t == endtoken
+ end
+ end
+ local token,value
+ while true do
+ token,value=tok()
+ if not token then return nil,'EOS' end -- end of stream is an error!
+ if is_end(token,value) and level == 1 then
+ append(parm_values,tl)
+ break
+ elseif token == '(' then
+ level = level + 1
+ tappend(tl,'(')
+ elseif token == ')' then
+ level = level - 1
+ if level == 0 then -- finished with parm list
+ append(parm_values,tl)
+ break
+ else
+ tappend(tl,')')
+ end
+ elseif token == delim and level == 1 then
+ append(parm_values,tl) -- a new parm
+ tl = {}
+ else
+ tappend(tl,token,value)
+ end
+ end
+ return parm_values,{token,value}
+end
+
+--- get the next non-space token from the stream.
+-- @param tok the token stream.
+function lexer.skipws (tok)
+ local t,v = tok()
+ while t == 'space' do
+ t,v = tok()
+ end
+ return t,v
+end
+
+local skipws = lexer.skipws
+
+--- get the next token, which must be of the expected type.
+-- Throws an error if this type does not match!
+-- @param tok the token stream
+-- @string expected_type the token type
+-- @bool no_skip_ws whether we should skip whitespace
+function lexer.expecting (tok,expected_type,no_skip_ws)
+ assert_arg(1,tok,'function')
+ assert_arg(2,expected_type,'string')
+ local t,v
+ if no_skip_ws then
+ t,v = tok()
+ else
+ t,v = skipws(tok)
+ end
+ if t ~= expected_type then error ("expecting "..expected_type,2) end
+ return v
+end
+
+return lexer
diff --git a/lualibs/pl/luabalanced.lua b/lualibs/pl/luabalanced.lua
new file mode 100644
index 0000000000..a75f6fda13
--- /dev/null
+++ b/lualibs/pl/luabalanced.lua
@@ -0,0 +1,263 @@
+--- Extract delimited Lua sequences from strings.
+-- Inspired by Damian Conway's Text::Balanced in Perl.
+--
+-- - [1] Lua Wiki Page
+-- - [2] http://search.cpan.org/dist/Text-Balanced/lib/Text/Balanced.pm
+--
+--
+-- local lb = require "pl.luabalanced"
+-- --Extract Lua expression starting at position 4.
+-- print(lb.match_expression("if x^2 + x > 5 then print(x) end", 4))
+-- --> x^2 + x > 5 16
+-- --Extract Lua string starting at (default) position 1.
+-- print(lb.match_string([["test\"123" .. "more"]]))
+-- --> "test\"123" 12
+--
+-- (c) 2008, David Manura, Licensed under the same terms as Lua (MIT license).
+-- @class module
+-- @name pl.luabalanced
+
+local M = {}
+
+local assert = assert
+
+-- map opening brace <-> closing brace.
+local ends = { ['('] = ')', ['{'] = '}', ['['] = ']' }
+local begins = {}; for k,v in pairs(ends) do begins[v] = k end
+
+
+-- Match Lua string in string starting at position .
+-- Returns , , where is the matched
+-- string (or nil on no match) and is the character
+-- following the match (or on no match).
+-- Supports all Lua string syntax: "...", '...', [[...]], [=[...]=], etc.
+local function match_string(s, pos)
+ pos = pos or 1
+ local posa = pos
+ local c = s:sub(pos,pos)
+ if c == '"' or c == "'" then
+ pos = pos + 1
+ while 1 do
+ pos = assert(s:find("[" .. c .. "\\]", pos), 'syntax error')
+ if s:sub(pos,pos) == c then
+ local part = s:sub(posa, pos)
+ return part, pos + 1
+ else
+ pos = pos + 2
+ end
+ end
+ else
+ local sc = s:match("^%[(=*)%[", pos)
+ if sc then
+ local _; _, pos = s:find("%]" .. sc .. "%]", pos)
+ assert(pos)
+ local part = s:sub(posa, pos)
+ return part, pos + 1
+ else
+ return nil, pos
+ end
+ end
+end
+M.match_string = match_string
+
+
+-- Match bracketed Lua expression, e.g. "(...)", "{...}", "[...]", "[[...]]",
+-- [=[...]=], etc.
+-- Function interface is similar to match_string.
+local function match_bracketed(s, pos)
+ pos = pos or 1
+ local posa = pos
+ local ca = s:sub(pos,pos)
+ if not ends[ca] then
+ return nil, pos
+ end
+ local stack = {}
+ while 1 do
+ pos = s:find('[%(%{%[%)%}%]\"\']', pos)
+ assert(pos, 'syntax error: unbalanced')
+ local c = s:sub(pos,pos)
+ if c == '"' or c == "'" then
+ local part; part, pos = match_string(s, pos)
+ assert(part)
+ elseif ends[c] then -- open
+ local mid, posb
+ if c == '[' then mid, posb = s:match('^%[(=*)%[()', pos) end
+ if mid then
+ pos = s:match('%]' .. mid .. '%]()', posb)
+ assert(pos, 'syntax error: long string not terminated')
+ if #stack == 0 then
+ local part = s:sub(posa, pos-1)
+ return part, pos
+ end
+ else
+ stack[#stack+1] = c
+ pos = pos + 1
+ end
+ else -- close
+ assert(stack[#stack] == assert(begins[c]), 'syntax error: unbalanced')
+ stack[#stack] = nil
+ if #stack == 0 then
+ local part = s:sub(posa, pos)
+ return part, pos+1
+ end
+ pos = pos + 1
+ end
+ end
+end
+M.match_bracketed = match_bracketed
+
+
+-- Match Lua comment, e.g. "--...\n", "--[[...]]", "--[=[...]=]", etc.
+-- Function interface is similar to match_string.
+local function match_comment(s, pos)
+ pos = pos or 1
+ if s:sub(pos, pos+1) ~= '--' then
+ return nil, pos
+ end
+ pos = pos + 2
+ local partt, post = match_string(s, pos)
+ if partt then
+ return '--' .. partt, post
+ end
+ local part; part, pos = s:match('^([^\n]*\n?)()', pos)
+ return '--' .. part, pos
+end
+
+
+-- Match Lua expression, e.g. "a + b * c[e]".
+-- Function interface is similar to match_string.
+local wordop = {['and']=true, ['or']=true, ['not']=true}
+local is_compare = {['>']=true, ['<']=true, ['~']=true}
+local function match_expression(s, pos)
+ pos = pos or 1
+ local posa = pos
+ local lastident
+ local poscs, posce
+ while pos do
+ local c = s:sub(pos,pos)
+ if c == '"' or c == "'" or c == '[' and s:find('^[=%[]', pos+1) then
+ local part; part, pos = match_string(s, pos)
+ assert(part, 'syntax error')
+ elseif c == '-' and s:sub(pos+1,pos+1) == '-' then
+ -- note: handle adjacent comments in loop to properly support
+ -- backtracing (poscs/posce).
+ poscs = pos
+ while s:sub(pos,pos+1) == '--' do
+ local part; part, pos = match_comment(s, pos)
+ assert(part)
+ pos = s:match('^%s*()', pos)
+ posce = pos
+ end
+ elseif c == '(' or c == '{' or c == '[' then
+ local part; part, pos = match_bracketed(s, pos)
+ elseif c == '=' and s:sub(pos+1,pos+1) == '=' then
+ pos = pos + 2 -- skip over two-char op containing '='
+ elseif c == '=' and is_compare[s:sub(pos-1,pos-1)] then
+ pos = pos + 1 -- skip over two-char op containing '='
+ elseif c:match'^[%)%}%];,=]' then
+ local part = s:sub(posa, pos-1)
+ return part, pos
+ elseif c:match'^[%w_]' then
+ local newident,newpos = s:match('^([%w_]+)()', pos)
+ if pos ~= posa and not wordop[newident] then -- non-first ident
+ local pose = ((posce == pos) and poscs or pos) - 1
+ while s:match('^%s', pose) do pose = pose - 1 end
+ local ce = s:sub(pose,pose)
+ if ce:match'[%)%}\'\"%]]' or
+ ce:match'[%w_]' and not wordop[lastident]
+ then
+ local part = s:sub(posa, pos-1)
+ return part, pos
+ end
+ end
+ lastident, pos = newident, newpos
+ else
+ pos = pos + 1
+ end
+ pos = s:find('[%(%{%[%)%}%]\"\';,=%w_%-]', pos)
+ end
+ local part = s:sub(posa, #s)
+ return part, #s+1
+end
+M.match_expression = match_expression
+
+
+-- Match name list (zero or more names). E.g. "a,b,c"
+-- Function interface is similar to match_string,
+-- but returns array as match.
+local function match_namelist(s, pos)
+ pos = pos or 1
+ local list = {}
+ while 1 do
+ local c = #list == 0 and '^' or '^%s*,%s*'
+ local item, post = s:match(c .. '([%a_][%w_]*)%s*()', pos)
+ if item then pos = post else break end
+ list[#list+1] = item
+ end
+ return list, pos
+end
+M.match_namelist = match_namelist
+
+
+-- Match expression list (zero or more expressions). E.g. "a+b,b*c".
+-- Function interface is similar to match_string,
+-- but returns array as match.
+local function match_explist(s, pos)
+ pos = pos or 1
+ local list = {}
+ while 1 do
+ if #list ~= 0 then
+ local post = s:match('^%s*,%s*()', pos)
+ if post then pos = post else break end
+ end
+ local item; item, pos = match_expression(s, pos)
+ assert(item, 'syntax error')
+ list[#list+1] = item
+ end
+ return list, pos
+end
+M.match_explist = match_explist
+
+
+-- Replace snippets of code in Lua code string
+-- using replacement function f(u,sin) --> sout.
+-- is the type of snippet ('c' = comment, 's' = string,
+-- 'e' = any other code).
+-- Snippet is replaced with (unless is nil or false, in
+-- which case the original snippet is kept)
+-- This is somewhat analogous to string.gsub .
+local function gsub(s, f)
+ local pos = 1
+ local posa = 1
+ local sret = ''
+ while 1 do
+ pos = s:find('[%-\'\"%[]', pos)
+ if not pos then break end
+ if s:match('^%-%-', pos) then
+ local exp = s:sub(posa, pos-1)
+ if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
+ local comment; comment, pos = match_comment(s, pos)
+ sret = sret .. (f('c', assert(comment)) or comment)
+ posa = pos
+ else
+ local posb = s:find('^[\'\"%[]', pos)
+ local str
+ if posb then str, pos = match_string(s, posb) end
+ if str then
+ local exp = s:sub(posa, posb-1)
+ if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
+ sret = sret .. (f('s', str) or str)
+ posa = pos
+ else
+ pos = pos + 1
+ end
+ end
+ end
+ local exp = s:sub(posa)
+ if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
+ return sret
+end
+M.gsub = gsub
+
+
+return M
diff --git a/lualibs/pl/operator.lua b/lualibs/pl/operator.lua
new file mode 100644
index 0000000000..60eaffd10a
--- /dev/null
+++ b/lualibs/pl/operator.lua
@@ -0,0 +1,209 @@
+--- Lua operators available as functions.
+--
+-- (similar to the Python module of the same name)
+--
+-- There is a module field `optable` which maps the operator strings
+-- onto these functions, e.g. `operator.optable['()']==operator.call`
+--
+-- Operator strings like '>' and '{}' can be passed to most Penlight functions
+-- expecting a function argument.
+--
+-- @module pl.operator
+
+local strfind = string.find
+
+local operator = {}
+
+--- apply function to some arguments **()**
+-- @param fn a function or callable object
+-- @param ... arguments
+function operator.call(fn,...)
+ return fn(...)
+end
+
+--- get the indexed value from a table **[]**
+-- @param t a table or any indexable object
+-- @param k the key
+function operator.index(t,k)
+ return t[k]
+end
+
+--- returns true if arguments are equal **==**
+-- @param a value
+-- @param b value
+function operator.eq(a,b)
+ return a==b
+end
+
+--- returns true if arguments are not equal **~=**
+ -- @param a value
+-- @param b value
+function operator.neq(a,b)
+ return a~=b
+end
+
+--- returns true if a is less than b **<**
+-- @param a value
+-- @param b value
+function operator.lt(a,b)
+ return a < b
+end
+
+--- returns true if a is less or equal to b **<=**
+-- @param a value
+-- @param b value
+function operator.le(a,b)
+ return a <= b
+end
+
+--- returns true if a is greater than b **>**
+-- @param a value
+-- @param b value
+function operator.gt(a,b)
+ return a > b
+end
+
+--- returns true if a is greater or equal to b **>=**
+-- @param a value
+-- @param b value
+function operator.ge(a,b)
+ return a >= b
+end
+
+--- returns length of string or table **#**
+-- @param a a string or a table
+function operator.len(a)
+ return #a
+end
+
+--- add two values **+**
+-- @param a value
+-- @param b value
+function operator.add(a,b)
+ return a+b
+end
+
+--- subtract b from a **-**
+-- @param a value
+-- @param b value
+function operator.sub(a,b)
+ return a-b
+end
+
+--- multiply two values __*__
+-- @param a value
+-- @param b value
+function operator.mul(a,b)
+ return a*b
+end
+
+--- divide first value by second **/**
+-- @param a value
+-- @param b value
+function operator.div(a,b)
+ return a/b
+end
+
+--- raise first to the power of second **^**
+-- @param a value
+-- @param b value
+function operator.pow(a,b)
+ return a^b
+end
+
+--- modulo; remainder of a divided by b **%**
+-- @param a value
+-- @param b value
+function operator.mod(a,b)
+ return a%b
+end
+
+--- concatenate two values (either strings or `__concat` defined) **..**
+-- @param a value
+-- @param b value
+function operator.concat(a,b)
+ return a..b
+end
+
+--- return the negative of a value **-**
+-- @param a value
+function operator.unm(a)
+ return -a
+end
+
+--- false if value evaluates as true **not**
+-- @param a value
+function operator.lnot(a)
+ return not a
+end
+
+--- true if both values evaluate as true **and**
+-- @param a value
+-- @param b value
+function operator.land(a,b)
+ return a and b
+end
+
+--- true if either value evaluate as true **or**
+-- @param a value
+-- @param b value
+function operator.lor(a,b)
+ return a or b
+end
+
+--- make a table from the arguments **{}**
+-- @param ... non-nil arguments
+-- @return a table
+function operator.table (...)
+ return {...}
+end
+
+--- match two strings **~**.
+-- uses @{string.find}
+function operator.match (a,b)
+ return strfind(a,b)~=nil
+end
+
+--- the null operation.
+-- @param ... arguments
+-- @return the arguments
+function operator.nop (...)
+ return ...
+end
+
+---- Map from operator symbol to function.
+-- Most of these map directly from operators;
+-- But note these extras
+--
+-- * __'()'__ `call`
+-- * __'[]'__ `index`
+-- * __'{}'__ `table`
+-- * __'~'__ `match`
+--
+-- @table optable
+-- @field operator
+ operator.optable = {
+ ['+']=operator.add,
+ ['-']=operator.sub,
+ ['*']=operator.mul,
+ ['/']=operator.div,
+ ['%']=operator.mod,
+ ['^']=operator.pow,
+ ['..']=operator.concat,
+ ['()']=operator.call,
+ ['[]']=operator.index,
+ ['<']=operator.lt,
+ ['<=']=operator.le,
+ ['>']=operator.gt,
+ ['>=']=operator.ge,
+ ['==']=operator.eq,
+ ['~=']=operator.neq,
+ ['#']=operator.len,
+ ['and']=operator.land,
+ ['or']=operator.lor,
+ ['{}']=operator.table,
+ ['~']=operator.match,
+ ['']=operator.nop,
+}
+
+return operator
diff --git a/lualibs/pl/path.lua b/lualibs/pl/path.lua
new file mode 100644
index 0000000000..f8f3d786e1
--- /dev/null
+++ b/lualibs/pl/path.lua
@@ -0,0 +1,441 @@
+--- Path manipulation and file queries.
+--
+-- This is modelled after Python's os.path library (10.1); see @{04-paths.md|the Guide}.
+--
+-- Dependencies: `pl.utils`, `lfs`
+-- @module pl.path
+
+-- imports and locals
+local _G = _G
+local sub = string.sub
+local getenv = os.getenv
+local tmpnam = os.tmpname
+local attributes, currentdir, link_attrib
+local package = package
+local append, concat, remove = table.insert, table.concat, table.remove
+local utils = require 'pl.utils'
+local assert_string,raise = utils.assert_string,utils.raise
+
+local attrib
+local path = {}
+
+local res,lfs = _G.pcall(_G.require,'lfs')
+if res then
+ attributes = lfs.attributes
+ currentdir = lfs.currentdir
+ link_attrib = lfs.symlinkattributes
+else
+ error("pl.path requires LuaFileSystem")
+end
+
+attrib = attributes
+path.attrib = attrib
+path.link_attrib = link_attrib
+
+--- Lua iterator over the entries of a given directory.
+-- Behaves like `lfs.dir`
+path.dir = lfs.dir
+
+--- Creates a directory.
+path.mkdir = lfs.mkdir
+
+--- Removes a directory.
+path.rmdir = lfs.rmdir
+
+---- Get the working directory.
+path.currentdir = currentdir
+
+--- Changes the working directory.
+path.chdir = lfs.chdir
+
+
+--- is this a directory?
+-- @string P A file path
+function path.isdir(P)
+ assert_string(1,P)
+ if P:match("\\$") then
+ P = P:sub(1,-2)
+ end
+ return attrib(P,'mode') == 'directory'
+end
+
+--- is this a file?.
+-- @string P A file path
+function path.isfile(P)
+ assert_string(1,P)
+ return attrib(P,'mode') == 'file'
+end
+
+-- is this a symbolic link?
+-- @string P A file path
+function path.islink(P)
+ assert_string(1,P)
+ if link_attrib then
+ return link_attrib(P,'mode')=='link'
+ else
+ return false
+ end
+end
+
+--- return size of a file.
+-- @string P A file path
+function path.getsize(P)
+ assert_string(1,P)
+ return attrib(P,'size')
+end
+
+--- does a path exist?.
+-- @string P A file path
+-- @return the file path if it exists, nil otherwise
+function path.exists(P)
+ assert_string(1,P)
+ return attrib(P,'mode') ~= nil and P
+end
+
+--- Return the time of last access as the number of seconds since the epoch.
+-- @string P A file path
+function path.getatime(P)
+ assert_string(1,P)
+ return attrib(P,'access')
+end
+
+--- Return the time of last modification
+-- @string P A file path
+function path.getmtime(P)
+ return attrib(P,'modification')
+end
+
+---Return the system's ctime.
+-- @string P A file path
+function path.getctime(P)
+ assert_string(1,P)
+ return path.attrib(P,'change')
+end
+
+
+local function at(s,i)
+ return sub(s,i,i)
+end
+
+path.is_windows = utils.is_windows
+
+local other_sep
+-- !constant sep is the directory separator for this platform.
+if path.is_windows then
+ path.sep = '\\'; other_sep = '/'
+ path.dirsep = ';'
+else
+ path.sep = '/'
+ path.dirsep = ':'
+end
+local sep,dirsep = path.sep,path.dirsep
+
+--- are we running Windows?
+-- @class field
+-- @name path.is_windows
+
+--- path separator for this platform.
+-- @class field
+-- @name path.sep
+
+--- separator for PATH for this platform
+-- @class field
+-- @name path.dirsep
+
+--- given a path, return the directory part and a file part.
+-- if there's no directory part, the first value will be empty
+-- @string P A file path
+function path.splitpath(P)
+ assert_string(1,P)
+ local i = #P
+ local ch = at(P,i)
+ while i > 0 and ch ~= sep and ch ~= other_sep do
+ i = i - 1
+ ch = at(P,i)
+ end
+ if i == 0 then
+ return '',P
+ else
+ return sub(P,1,i-1), sub(P,i+1)
+ end
+end
+
+--- return an absolute path.
+-- @string P A file path
+-- @string[opt] pwd optional start path to use (default is current dir)
+function path.abspath(P,pwd)
+ assert_string(1,P)
+ if pwd then assert_string(2,pwd) end
+ local use_pwd = pwd ~= nil
+ if not use_pwd and not currentdir then return P end
+ P = P:gsub('[\\/]$','')
+ pwd = pwd or currentdir()
+ if not path.isabs(P) then
+ P = path.join(pwd,P)
+ elseif path.is_windows and not use_pwd and at(P,2) ~= ':' and at(P,2) ~= '\\' then
+ P = pwd:sub(1,2)..P -- attach current drive to path like '\\fred.txt'
+ end
+ return path.normpath(P)
+end
+
+--- given a path, return the root part and the extension part.
+-- if there's no extension part, the second value will be empty
+-- @string P A file path
+-- @treturn string root part
+-- @treturn string extension part (maybe empty)
+function path.splitext(P)
+ assert_string(1,P)
+ local i = #P
+ local ch = at(P,i)
+ while i > 0 and ch ~= '.' do
+ if ch == sep or ch == other_sep then
+ return P,''
+ end
+ i = i - 1
+ ch = at(P,i)
+ end
+ if i == 0 then
+ return P,''
+ else
+ return sub(P,1,i-1),sub(P,i)
+ end
+end
+
+--- return the directory part of a path
+-- @string P A file path
+function path.dirname(P)
+ assert_string(1,P)
+ local p1,p2 = path.splitpath(P)
+ return p1
+end
+
+--- return the file part of a path
+-- @string P A file path
+function path.basename(P)
+ assert_string(1,P)
+ local p1,p2 = path.splitpath(P)
+ return p2
+end
+
+--- get the extension part of a path.
+-- @string P A file path
+function path.extension(P)
+ assert_string(1,P)
+ local p1,p2 = path.splitext(P)
+ return p2
+end
+
+--- is this an absolute path?.
+-- @string P A file path
+function path.isabs(P)
+ assert_string(1,P)
+ if path.is_windows then
+ return at(P,1) == '/' or at(P,1)=='\\' or at(P,2)==':'
+ else
+ return at(P,1) == '/'
+ end
+end
+
+--- return the path resulting from combining the individual paths.
+-- if the second (or later) path is absolute, we return the last absolute path (joined with any non-absolute paths following).
+-- empty elements (except the last) will be ignored.
+-- @string p1 A file path
+-- @string p2 A file path
+-- @string ... more file paths
+function path.join(p1,p2,...)
+ assert_string(1,p1)
+ assert_string(2,p2)
+ if select('#',...) > 0 then
+ local p = path.join(p1,p2)
+ local args = {...}
+ for i = 1,#args do
+ assert_string(i,args[i])
+ p = path.join(p,args[i])
+ end
+ return p
+ end
+ if path.isabs(p2) then return p2 end
+ local endc = at(p1,#p1)
+ if endc ~= path.sep and endc ~= other_sep and endc ~= "" then
+ p1 = p1..path.sep
+ end
+ return p1..p2
+end
+
+--- normalize the case of a pathname. On Unix, this returns the path unchanged;
+-- for Windows, it converts the path to lowercase, and it also converts forward slashes
+-- to backward slashes.
+-- @string P A file path
+function path.normcase(P)
+ assert_string(1,P)
+ if path.is_windows then
+ return (P:lower():gsub('/','\\'))
+ else
+ return P
+ end
+end
+
+--- normalize a path name.
+-- A//B, A/./B and A/foo/../B all become A/B.
+-- @string P a file path
+function path.normpath(P)
+ assert_string(1,P)
+ -- Split path into anchor and relative path.
+ local anchor = ''
+ if path.is_windows then
+ if P:match '^\\\\' then -- UNC
+ anchor = '\\\\'
+ P = P:sub(3)
+ elseif at(P, 1) == '/' or at(P, 1) == '\\' then
+ anchor = '\\'
+ P = P:sub(2)
+ elseif at(P, 2) == ':' then
+ anchor = P:sub(1, 2)
+ P = P:sub(3)
+ if at(P, 1) == '/' or at(P, 1) == '\\' then
+ anchor = anchor..'\\'
+ P = P:sub(2)
+ end
+ end
+ P = P:gsub('/','\\')
+ else
+ -- According to POSIX, in path start '//' and '/' are distinct,
+ -- but '///+' is equivalent to '/'.
+ if P:match '^//' and at(P, 3) ~= '/' then
+ anchor = '//'
+ P = P:sub(3)
+ elseif at(P, 1) == '/' then
+ anchor = '/'
+ P = P:match '^/*(.*)$'
+ end
+ end
+ local parts = {}
+ for part in P:gmatch('[^'..sep..']+') do
+ if part == '..' then
+ if #parts ~= 0 and parts[#parts] ~= '..' then
+ remove(parts)
+ else
+ append(parts, part)
+ end
+ elseif part ~= '.' then
+ append(parts, part)
+ end
+ end
+ P = anchor..concat(parts, sep)
+ if P == '' then P = '.' end
+ return P
+end
+
+local function ATS (P)
+ if at(P,#P) ~= path.sep then
+ P = P..path.sep
+ end
+ return path.normcase(P)
+end
+
+--- relative path from current directory or optional start point
+-- @string P a path
+-- @string[opt] start optional start point (default current directory)
+function path.relpath (P,start)
+ assert_string(1,P)
+ if start then assert_string(2,start) end
+ local split,normcase,min,append = utils.split, path.normcase, math.min, table.insert
+ P = normcase(path.abspath(P,start))
+ start = start or currentdir()
+ start = normcase(start)
+ local startl, Pl = split(start,sep), split(P,sep)
+ local n = min(#startl,#Pl)
+ if path.is_windows and n > 0 and at(Pl[1],2) == ':' and Pl[1] ~= startl[1] then
+ return P
+ end
+ local k = n+1 -- default value if this loop doesn't bail out!
+ for i = 1,n do
+ if startl[i] ~= Pl[i] then
+ k = i
+ break
+ end
+ end
+ local rell = {}
+ for i = 1, #startl-k+1 do rell[i] = '..' end
+ if k <= #Pl then
+ for i = k,#Pl do append(rell,Pl[i]) end
+ end
+ return table.concat(rell,sep)
+end
+
+
+--- Replace a starting '~' with the user's home directory.
+-- In windows, if HOME isn't set, then USERPROFILE is used in preference to
+-- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows.
+-- @string P A file path
+function path.expanduser(P)
+ assert_string(1,P)
+ if at(P,1) == '~' then
+ local home = getenv('HOME')
+ if not home then -- has to be Windows
+ home = getenv 'USERPROFILE' or (getenv 'HOMEDRIVE' .. getenv 'HOMEPATH')
+ end
+ return home..sub(P,2)
+ else
+ return P
+ end
+end
+
+
+---Return a suitable full path to a new temporary file name.
+-- unlike os.tmpnam(), it always gives you a writeable path (uses TEMP environment variable on Windows)
+function path.tmpname ()
+ local res = tmpnam()
+ -- On Windows if Lua is compiled using MSVC14 os.tmpname
+ -- already returns an absolute path within TEMP env variable directory,
+ -- no need to prepend it.
+ if path.is_windows and not res:find(':') then
+ res = getenv('TEMP')..res
+ end
+ return res
+end
+
+--- return the largest common prefix path of two paths.
+-- @string path1 a file path
+-- @string path2 a file path
+function path.common_prefix (path1,path2)
+ assert_string(1,path1)
+ assert_string(2,path2)
+ path1, path2 = path.normcase(path1), path.normcase(path2)
+ -- get them in order!
+ if #path1 > #path2 then path2,path1 = path1,path2 end
+ for i = 1,#path1 do
+ local c1 = at(path1,i)
+ if c1 ~= at(path2,i) then
+ local cp = path1:sub(1,i-1)
+ if at(path1,i-1) ~= sep then
+ cp = path.dirname(cp)
+ end
+ return cp
+ end
+ end
+ if at(path2,#path1+1) ~= sep then path1 = path.dirname(path1) end
+ return path1
+ --return ''
+end
+
+--- return the full path where a particular Lua module would be found.
+-- Both package.path and package.cpath is searched, so the result may
+-- either be a Lua file or a shared library.
+-- @string mod name of the module
+-- @return on success: path of module, lua or binary
+-- @return on error: nil,error string
+function path.package_path(mod)
+ assert_string(1,mod)
+ local res
+ mod = mod:gsub('%.',sep)
+ res = package.searchpath(mod,package.path)
+ if res then return res,true end
+ res = package.searchpath(mod,package.cpath)
+ if res then return res,false end
+ return raise 'cannot find module on path'
+end
+
+
+---- finis -----
+return path
diff --git a/lualibs/pl/permute.lua b/lualibs/pl/permute.lua
new file mode 100644
index 0000000000..56d1a8dc97
--- /dev/null
+++ b/lualibs/pl/permute.lua
@@ -0,0 +1,63 @@
+--- Permutation operations.
+--
+-- Dependencies: `pl.utils`, `pl.tablex`
+-- @module pl.permute
+local tablex = require 'pl.tablex'
+local utils = require 'pl.utils'
+local copy = tablex.deepcopy
+local append = table.insert
+local coroutine = coroutine
+local resume = coroutine.resume
+local assert_arg = utils.assert_arg
+
+
+local permute = {}
+
+-- PiL, 9.3
+
+local permgen
+permgen = function (a, n, fn)
+ if n == 0 then
+ fn(a)
+ else
+ for i=1,n do
+ -- put i-th element as the last one
+ a[n], a[i] = a[i], a[n]
+
+ -- generate all permutations of the other elements
+ permgen(a, n - 1, fn)
+
+ -- restore i-th element
+ a[n], a[i] = a[i], a[n]
+
+ end
+ end
+end
+
+--- an iterator over all permutations of the elements of a list.
+-- Please note that the same list is returned each time, so do not keep references!
+-- @param a list-like table
+-- @return an iterator which provides the next permutation as a list
+function permute.iter (a)
+ assert_arg(1,a,'table')
+ local n = #a
+ local co = coroutine.create(function () permgen(a, n, coroutine.yield) end)
+ return function () -- iterator
+ local code, res = resume(co)
+ return res
+ end
+end
+
+--- construct a table containing all the permutations of a list.
+-- @param a list-like table
+-- @return a table of tables
+-- @usage permute.table {1,2,3} --> {{2,3,1},{3,2,1},{3,1,2},{1,3,2},{2,1,3},{1,2,3}}
+function permute.table (a)
+ assert_arg(1,a,'table')
+ local res = {}
+ local n = #a
+ permgen(a,n,function(t) append(res,copy(t)) end)
+ return res
+end
+
+return permute
diff --git a/lualibs/pl/pretty.lua b/lualibs/pl/pretty.lua
new file mode 100644
index 0000000000..7365ad3c5f
--- /dev/null
+++ b/lualibs/pl/pretty.lua
@@ -0,0 +1,338 @@
+--- Pretty-printing Lua tables.
+-- Also provides a sandboxed Lua table reader and
+-- a function to present large numbers in human-friendly format.
+--
+-- Dependencies: `pl.utils`, `pl.lexer`, `pl.stringx`, `debug`
+-- @module pl.pretty
+
+local append = table.insert
+local concat = table.concat
+local mfloor, mhuge, mtype = math.floor, math.huge, math.type
+local utils = require 'pl.utils'
+local lexer = require 'pl.lexer'
+local debug = require 'debug'
+local quote_string = require'pl.stringx'.quote_string
+local assert_arg = utils.assert_arg
+
+local original_tostring = tostring
+
+-- Patch tostring to format numbers with better precision
+-- and to produce cross-platform results for
+-- infinite values and NaN.
+local function tostring(value)
+ if type(value) ~= "number" then
+ return original_tostring(value)
+ elseif value ~= value then
+ return "NaN"
+ elseif value == mhuge then
+ return "Inf"
+ elseif value == -mhuge then
+ return "-Inf"
+ elseif (_VERSION ~= "Lua 5.3" or mtype(value) == "integer") and mfloor(value) == value then
+ return ("%d"):format(value)
+ else
+ local res = ("%.14g"):format(value)
+ if _VERSION == "Lua 5.3" and mtype(value) == "float" and not res:find("%.") then
+ -- Number is internally a float but looks like an integer.
+ -- Insert ".0" after first run of digits.
+ res = res:gsub("%d+", "%0.0", 1)
+ end
+ return res
+ end
+end
+
+local pretty = {}
+
+local function save_global_env()
+ local env = {}
+ env.hook, env.mask, env.count = debug.gethook()
+
+ -- env.hook is "external hook" if is a C hook function
+ if env.hook~="external hook" then
+ debug.sethook()
+ end
+
+ env.string_mt = getmetatable("")
+ debug.setmetatable("", nil)
+ return env
+end
+
+local function restore_global_env(env)
+ if env then
+ debug.setmetatable("", env.string_mt)
+ if env.hook~="external hook" then
+ debug.sethook(env.hook, env.mask, env.count)
+ end
+ end
+end
+
+--- Read a string representation of a Lua table.
+-- This function loads and runs the string as Lua code, but bails out
+-- if it contains a function definition.
+-- Loaded string is executed in an empty environment.
+-- @string s string to read in `{...}` format, possibly with some whitespace
+-- before or after the curly braces. A single line comment may be present
+-- at the beginning.
+-- @return a table in case of success.
+-- If loading the string failed, return `nil` and error message.
+-- If executing loaded string failed, return `nil` and the error it raised.
+function pretty.read(s)
+ assert_arg(1,s,'string')
+ if s:find '^%s*%-%-' then -- may start with a comment..
+ s = s:gsub('%-%-.-\n','')
+ end
+ if not s:find '^%s*{' then return nil,"not a Lua table" end
+ if s:find '[^\'"%w_]function[^\'"%w_]' then
+ local tok = lexer.lua(s)
+ for t,v in tok do
+ if t == 'keyword' and v == 'function' then
+ return nil,"cannot have functions in table definition"
+ end
+ end
+ end
+ s = 'return '..s
+ local chunk,err = utils.load(s,'tbl','t',{})
+ if not chunk then return nil,err end
+ local global_env = save_global_env()
+ local ok,ret = pcall(chunk)
+ restore_global_env(global_env)
+ if ok then return ret
+ else
+ return nil,ret
+ end
+end
+
+--- Read a Lua chunk.
+-- @string s Lua code.
+-- @tab[opt] env environment used to run the code, empty by default.
+-- @bool[opt] paranoid abort loading if any looping constructs a found in the code
+-- and disable string methods.
+-- @return the environment in case of success or `nil` and syntax or runtime error
+-- if something went wrong.
+function pretty.load (s, env, paranoid)
+ env = env or {}
+ if paranoid then
+ local tok = lexer.lua(s)
+ for t,v in tok do
+ if t == 'keyword'
+ and (v == 'for' or v == 'repeat' or v == 'function' or v == 'goto')
+ then
+ return nil,"looping not allowed"
+ end
+ end
+ end
+ local chunk,err = utils.load(s,'tbl','t',env)
+ if not chunk then return nil,err end
+ local global_env = paranoid and save_global_env()
+ local ok,err = pcall(chunk)
+ restore_global_env(global_env)
+ if not ok then return nil,err end
+ return env
+end
+
+local function quote_if_necessary (v)
+ if not v then return ''
+ else
+ --AAS
+ if v:find ' ' then v = quote_string(v) end
+ end
+ return v
+end
+
+local keywords
+
+local function is_identifier (s)
+ return type(s) == 'string' and s:find('^[%a_][%w_]*$') and not keywords[s]
+end
+
+local function quote (s)
+ if type(s) == 'table' then
+ return pretty.write(s,'')
+ else
+ --AAS
+ return quote_string(s)-- ('%q'):format(tostring(s))
+ end
+end
+
+local function index (numkey,key)
+ --AAS
+ if not numkey then
+ key = quote(key)
+ key = key:find("^%[") and (" " .. key .. " ") or key
+ end
+ return '['..key..']'
+end
+
+
+--- Create a string representation of a Lua table.
+-- This function never fails, but may complain by returning an
+-- extra value. Normally puts out one item per line, using
+-- the provided indent; set the second parameter to an empty string
+-- if you want output on one line.
+-- @tab tbl Table to serialize to a string.
+-- @string[opt] space The indent to use.
+-- Defaults to two spaces; pass an empty string for no indentation.
+-- @bool[opt] not_clever Pass `true` for plain output, e.g `{['key']=1}`.
+-- Defaults to `false`.
+-- @return a string
+-- @return an optional error message
+function pretty.write (tbl,space,not_clever)
+ if type(tbl) ~= 'table' then
+ local res = tostring(tbl)
+ if type(tbl) == 'string' then return quote(tbl) end
+ return res, 'not a table'
+ end
+ if not keywords then
+ keywords = lexer.get_keywords()
+ end
+ local set = ' = '
+ if space == '' then set = '=' end
+ space = space or ' '
+ local lines = {}
+ local line = ''
+ local tables = {}
+
+
+ local function put(s)
+ if #s > 0 then
+ line = line..s
+ end
+ end
+
+ local function putln (s)
+ if #line > 0 then
+ line = line..s
+ append(lines,line)
+ line = ''
+ else
+ append(lines,s)
+ end
+ end
+
+ local function eat_last_comma ()
+ local n,lastch = #lines
+ local lastch = lines[n]:sub(-1,-1)
+ if lastch == ',' then
+ lines[n] = lines[n]:sub(1,-2)
+ end
+ end
+
+
+ local writeit
+ writeit = function (t,oldindent,indent)
+ local tp = type(t)
+ if tp ~= 'string' and tp ~= 'table' then
+ putln(quote_if_necessary(tostring(t))..',')
+ elseif tp == 'string' then
+ -- if t:find('\n') then
+ -- putln('[[\n'..t..']],')
+ -- else
+ -- putln(quote(t)..',')
+ -- end
+ --AAS
+ putln(quote_string(t) ..",")
+ elseif tp == 'table' then
+ if tables[t] then
+ putln(',')
+ return
+ end
+ tables[t] = true
+ local newindent = indent..space
+ putln('{')
+ local used = {}
+ if not not_clever then
+ for i,val in ipairs(t) do
+ put(indent)
+ writeit(val,indent,newindent)
+ used[i] = true
+ end
+ end
+ for key,val in pairs(t) do
+ local tkey = type(key)
+ local numkey = tkey == 'number'
+ if not_clever then
+ key = tostring(key)
+ put(indent..index(numkey,key)..set)
+ writeit(val,indent,newindent)
+ else
+ if not numkey or not used[key] then -- non-array indices
+ if tkey ~= 'string' then
+ key = tostring(key)
+ end
+ if numkey or not is_identifier(key) then
+ key = index(numkey,key)
+ end
+ put(indent..key..set)
+ writeit(val,indent,newindent)
+ end
+ end
+ end
+ tables[t] = nil
+ eat_last_comma()
+ putln(oldindent..'},')
+ else
+ putln(tostring(t)..',')
+ end
+ end
+ writeit(tbl,'',space)
+ eat_last_comma()
+ return concat(lines,#space > 0 and '\n' or '')
+end
+
+--- Dump a Lua table out to a file or stdout.
+-- @tab t The table to write to a file or stdout.
+-- @string[opt] filename File name to write too. Defaults to writing
+-- to stdout.
+function pretty.dump (t, filename)
+ if not filename then
+ print(pretty.write(t))
+ return true
+ else
+ return utils.writefile(filename, pretty.write(t))
+ end
+end
+
+local memp,nump = {'B','KiB','MiB','GiB'},{'','K','M','B'}
+
+local function comma (val)
+ local thou = math.floor(val/1000)
+ if thou > 0 then return comma(thou)..','.. tostring(val % 1000)
+ else return tostring(val) end
+end
+
+--- Format large numbers nicely for human consumption.
+-- @number num a number.
+-- @string[opt] kind one of `'M'` (memory in `KiB`, `MiB`, etc.),
+-- `'N'` (postfixes are `'K'`, `'M'` and `'B'`),
+-- or `'T'` (use commas as thousands separator), `'N'` by default.
+-- @int[opt] prec number of digits to use for `'M'` and `'N'`, `1` by default.
+function pretty.number (num,kind,prec)
+ local fmt = '%.'..(prec or 1)..'f%s'
+ if kind == 'T' then
+ return comma(num)
+ else
+ local postfixes, fact
+ if kind == 'M' then
+ fact = 1024
+ postfixes = memp
+ else
+ fact = 1000
+ postfixes = nump
+ end
+ local div = fact
+ local k = 1
+ while num >= div and k <= #postfixes do
+ div = div * fact
+ k = k + 1
+ end
+ div = div / fact
+ if k > #postfixes then k = k - 1; div = div/fact end
+ if k > 1 then
+ return fmt:format(num/div,postfixes[k] or 'duh')
+ else
+ return num..postfixes[1]
+ end
+ end
+end
+
+return pretty
diff --git a/lualibs/pl/seq.lua b/lualibs/pl/seq.lua
new file mode 100644
index 0000000000..c867add15a
--- /dev/null
+++ b/lualibs/pl/seq.lua
@@ -0,0 +1,545 @@
+--- Manipulating iterators as sequences.
+-- See @{07-functional.md.Sequences|The Guide}
+--
+-- Dependencies: `pl.utils`, `pl.types`, `debug`
+-- @module pl.seq
+
+local next,assert,pairs,tonumber,type,setmetatable = next,assert,pairs,tonumber,type,setmetatable
+local strfind,format = string.find,string.format
+local mrandom = math.random
+local tsort,tappend = table.sort,table.insert
+local io = io
+local utils = require 'pl.utils'
+local callable = require 'pl.types'.is_callable
+local function_arg = utils.function_arg
+local assert_arg = utils.assert_arg
+local debug = require 'debug'
+
+local seq = {}
+
+-- given a number, return a function(y) which returns true if y > x
+-- @param x a number
+function seq.greater_than(x)
+ return function(v)
+ return tonumber(v) > x
+ end
+end
+
+-- given a number, returns a function(y) which returns true if y < x
+-- @param x a number
+function seq.less_than(x)
+ return function(v)
+ return tonumber(v) < x
+ end
+end
+
+-- given any value, return a function(y) which returns true if y == x
+-- @param x a value
+function seq.equal_to(x)
+ if type(x) == "number" then
+ return function(v)
+ return tonumber(v) == x
+ end
+ else
+ return function(v)
+ return v == x
+ end
+ end
+end
+
+--- given a string, return a function(y) which matches y against the string.
+-- @param s a string
+function seq.matching(s)
+ return function(v)
+ return strfind(v,s)
+ end
+end
+
+local nexti
+
+--- sequence adaptor for a table. Note that if any generic function is
+-- passed a table, it will automatically use seq.list()
+-- @param t a list-like table
+-- @usage sum(list(t)) is the sum of all elements of t
+-- @usage for x in list(t) do...end
+function seq.list(t)
+ assert_arg(1,t,'table')
+ if not nexti then
+ nexti = ipairs{}
+ end
+ local key,value = 0
+ return function()
+ key,value = nexti(t,key)
+ return value
+ end
+end
+
+--- return the keys of the table.
+-- @param t an arbitrary table
+-- @return iterator over keys
+function seq.keys(t)
+ assert_arg(1,t,'table')
+ local key,value
+ return function()
+ key,value = next(t,key)
+ return key
+ end
+end
+
+local list = seq.list
+local function default_iter(iter)
+ if type(iter) == 'table' then return list(iter)
+ else return iter end
+end
+
+seq.iter = default_iter
+
+--- create an iterator over a numerical range. Like the standard Python function xrange.
+-- @param start a number
+-- @param finish a number greater than start
+function seq.range(start,finish)
+ local i = start - 1
+ return function()
+ i = i + 1
+ if i > finish then return nil
+ else return i end
+ end
+end
+
+-- count the number of elements in the sequence which satisfy the predicate
+-- @param iter a sequence
+-- @param condn a predicate function (must return either true or false)
+-- @param optional argument to be passed to predicate as second argument.
+-- @return count
+function seq.count(iter,condn,arg)
+ local i = 0
+ seq.foreach(iter,function(val)
+ if condn(val,arg) then i = i + 1 end
+ end)
+ return i
+end
+
+--- return the minimum and the maximum value of the sequence.
+-- @param iter a sequence
+-- @return minimum value
+-- @return maximum value
+function seq.minmax(iter)
+ local vmin,vmax = 1e70,-1e70
+ for v in default_iter(iter) do
+ v = tonumber(v)
+ if v < vmin then vmin = v end
+ if v > vmax then vmax = v end
+ end
+ return vmin,vmax
+end
+
+--- return the sum and element count of the sequence.
+-- @param iter a sequence
+-- @param fn an optional function to apply to the values
+function seq.sum(iter,fn)
+ local s = 0
+ local i = 0
+ for v in default_iter(iter) do
+ if fn then v = fn(v) end
+ s = s + v
+ i = i + 1
+ end
+ return s,i
+end
+
+--- create a table from the sequence. (This will make the result a List.)
+-- @param iter a sequence
+-- @return a List
+-- @usage copy(list(ls)) is equal to ls
+-- @usage copy(list {1,2,3}) == List{1,2,3}
+function seq.copy(iter)
+ local res,k = {},1
+ for v in default_iter(iter) do
+ res[k] = v
+ k = k + 1
+ end
+ setmetatable(res, require('pl.List'))
+ return res
+end
+
+--- create a table of pairs from the double-valued sequence.
+-- @param iter a double-valued sequence
+-- @param i1 used to capture extra iterator values
+-- @param i2 as with pairs & ipairs
+-- @usage copy2(ipairs{10,20,30}) == {{1,10},{2,20},{3,30}}
+-- @return a list-like table
+function seq.copy2 (iter,i1,i2)
+ local res,k = {},1
+ for v1,v2 in iter,i1,i2 do
+ res[k] = {v1,v2}
+ k = k + 1
+ end
+ return res
+end
+
+--- create a table of 'tuples' from a multi-valued sequence.
+-- A generalization of copy2 above
+-- @param iter a multiple-valued sequence
+-- @return a list-like table
+function seq.copy_tuples (iter)
+ iter = default_iter(iter)
+ local res = {}
+ local row = {iter()}
+ while #row > 0 do
+ tappend(res,row)
+ row = {iter()}
+ end
+ return res
+end
+
+--- return an iterator of random numbers.
+-- @param n the length of the sequence
+-- @param l same as the first optional argument to math.random
+-- @param u same as the second optional argument to math.random
+-- @return a sequence
+function seq.random(n,l,u)
+ local rand
+ assert(type(n) == 'number')
+ if u then
+ rand = function() return mrandom(l,u) end
+ elseif l then
+ rand = function() return mrandom(l) end
+ else
+ rand = mrandom
+ end
+
+ return function()
+ if n == 0 then return nil
+ else
+ n = n - 1
+ return rand()
+ end
+ end
+end
+
+--- return an iterator to the sorted elements of a sequence.
+-- @param iter a sequence
+-- @param comp an optional comparison function (comp(x,y) is true if x < y)
+function seq.sort(iter,comp)
+ local t = seq.copy(iter)
+ tsort(t,comp)
+ return list(t)
+end
+
+--- return an iterator which returns elements of two sequences.
+-- @param iter1 a sequence
+-- @param iter2 a sequence
+-- @usage for x,y in seq.zip(ls1,ls2) do....end
+function seq.zip(iter1,iter2)
+ iter1 = default_iter(iter1)
+ iter2 = default_iter(iter2)
+ return function()
+ return iter1(),iter2()
+ end
+end
+
+--- Makes a table where the key/values are the values and value counts of the sequence.
+-- This version works with 'hashable' values like strings and numbers.
+-- `pl.tablex.count_map` is more general.
+-- @param iter a sequence
+-- @return a map-like table
+-- @return a table
+-- @see pl.tablex.count_map
+function seq.count_map(iter)
+ local t = {}
+ local v
+ for s in default_iter(iter) do
+ v = t[s]
+ if v then t[s] = v + 1
+ else t[s] = 1 end
+ end
+ return setmetatable(t, require('pl.Map'))
+end
+
+-- given a sequence, return all the unique values in that sequence.
+-- @param iter a sequence
+-- @param returns_table true if we return a table, not a sequence
+-- @return a sequence or a table; defaults to a sequence.
+function seq.unique(iter,returns_table)
+ local t = seq.count_map(iter)
+ local res,k = {},1
+ for key in pairs(t) do res[k] = key; k = k + 1 end
+ table.sort(res)
+ if returns_table then
+ return res
+ else
+ return list(res)
+ end
+end
+
+--- print out a sequence iter with a separator.
+-- @param iter a sequence
+-- @param sep the separator (default space)
+-- @param nfields maximum number of values per line (default 7)
+-- @param fmt optional format function for each value
+function seq.printall(iter,sep,nfields,fmt)
+ local write = io.write
+ if not sep then sep = ' ' end
+ if not nfields then
+ if sep == '\n' then nfields = 1e30
+ else nfields = 7 end
+ end
+ if fmt then
+ local fstr = fmt
+ fmt = function(v) return format(fstr,v) end
+ end
+ local k = 1
+ for v in default_iter(iter) do
+ if fmt then v = fmt(v) end
+ if k < nfields then
+ write(v,sep)
+ k = k + 1
+ else
+ write(v,'\n')
+ k = 1
+ end
+ end
+ write '\n'
+end
+
+-- return an iterator running over every element of two sequences (concatenation).
+-- @param iter1 a sequence
+-- @param iter2 a sequence
+function seq.splice(iter1,iter2)
+ iter1 = default_iter(iter1)
+ iter2 = default_iter(iter2)
+ local iter = iter1
+ return function()
+ local ret = iter()
+ if ret == nil then
+ if iter == iter1 then
+ iter = iter2
+ return iter()
+ else return nil end
+ else
+ return ret
+ end
+ end
+end
+
+--- return a sequence where every element of a sequence has been transformed
+-- by a function. If you don't supply an argument, then the function will
+-- receive both values of a double-valued sequence, otherwise behaves rather like
+-- tablex.map.
+-- @param fn a function to apply to elements; may take two arguments
+-- @param iter a sequence of one or two values
+-- @param arg optional argument to pass to function.
+function seq.map(fn,iter,arg)
+ fn = function_arg(1,fn)
+ iter = default_iter(iter)
+ return function()
+ local v1,v2 = iter()
+ if v1 == nil then return nil end
+ return fn(v1,arg or v2) or false
+ end
+end
+
+--- filter a sequence using a predicate function.
+-- @param iter a sequence of one or two values
+-- @param pred a boolean function; may take two arguments
+-- @param arg optional argument to pass to function.
+function seq.filter (iter,pred,arg)
+ pred = function_arg(2,pred)
+ return function ()
+ local v1,v2
+ while true do
+ v1,v2 = iter()
+ if v1 == nil then return nil end
+ if pred(v1,arg or v2) then return v1,v2 end
+ end
+ end
+end
+
+--- 'reduce' a sequence using a binary function.
+-- @func fn a function of two arguments
+-- @param iter a sequence
+-- @param initval optional initial value
+-- @usage seq.reduce(operator.add,seq.list{1,2,3,4}) == 10
+-- @usage seq.reduce('-',{1,2,3,4,5}) == -13
+function seq.reduce (fn,iter,initval)
+ fn = function_arg(1,fn)
+ iter = default_iter(iter)
+ local val = initval or iter()
+ if val == nil then return nil end
+ for v in iter do
+ val = fn(val,v)
+ end
+ return val
+end
+
+--- take the first n values from the sequence.
+-- @param iter a sequence of one or two values
+-- @param n number of items to take
+-- @return a sequence of at most n items
+function seq.take (iter,n)
+ iter = default_iter(iter)
+ return function()
+ if n < 1 then return end
+ local val1,val2 = iter()
+ if not val1 then return end
+ n = n - 1
+ return val1,val2
+ end
+end
+
+--- skip the first n values of a sequence
+-- @param iter a sequence of one or more values
+-- @param n number of items to skip
+function seq.skip (iter,n)
+ n = n or 1
+ for i = 1,n do
+ if iter() == nil then return list{} end
+ end
+ return iter
+end
+
+--- a sequence with a sequence count and the original value.
+-- enum(copy(ls)) is a roundabout way of saying ipairs(ls).
+-- @param iter a single or double valued sequence
+-- @return sequence of (i,v), i = 1..n and v is from iter.
+function seq.enum (iter)
+ local i = 0
+ iter = default_iter(iter)
+ return function ()
+ local val1,val2 = iter()
+ if not val1 then return end
+ i = i + 1
+ return i,val1,val2
+ end
+end
+
+--- map using a named method over a sequence.
+-- @param iter a sequence
+-- @param name the method name
+-- @param arg1 optional first extra argument
+-- @param arg2 optional second extra argument
+function seq.mapmethod (iter,name,arg1,arg2)
+ iter = default_iter(iter)
+ return function()
+ local val = iter()
+ if not val then return end
+ local fn = val[name]
+ if not fn then error(type(val).." does not have method "..name) end
+ return fn(val,arg1,arg2)
+ end
+end
+
+--- a sequence of (last,current) values from another sequence.
+-- This will return S(i-1),S(i) if given S(i)
+-- @param iter a sequence
+function seq.last (iter)
+ iter = default_iter(iter)
+ local val, l = iter(), nil
+ if val == nil then return list{} end
+ return function ()
+ val,l = iter(),val
+ if val == nil then return nil end
+ return val,l
+ end
+end
+
+--- call the function on each element of the sequence.
+-- @param iter a sequence with up to 3 values
+-- @param fn a function
+function seq.foreach(iter,fn)
+ fn = function_arg(2,fn)
+ for i1,i2,i3 in default_iter(iter) do fn(i1,i2,i3) end
+end
+
+---------------------- Sequence Adapters ---------------------
+
+local SMT
+
+local function SW (iter,...)
+ if callable(iter) then
+ return setmetatable({iter=iter},SMT)
+ else
+ return iter,...
+ end
+end
+
+
+-- can't directly look these up in seq because of the wrong argument order...
+local map,reduce,mapmethod = seq.map, seq.reduce, seq.mapmethod
+local overrides = {
+ map = function(self,fun,arg)
+ return map(fun,self,arg)
+ end,
+ reduce = function(self,fun,initval)
+ return reduce(fun,self,initval)
+ end
+}
+
+SMT = {
+ __index = function (tbl,key)
+ local fn = overrides[key] or seq[key]
+ if fn then
+ return function(sw,...) return SW(fn(sw.iter,...)) end
+ else
+ return function(sw,...) return SW(mapmethod(sw.iter,key,...)) end
+ end
+ end,
+ __call = function (sw)
+ return sw.iter()
+ end,
+}
+
+setmetatable(seq,{
+ __call = function(tbl,iter,extra)
+ if not callable(iter) then
+ if type(iter) == 'table' then iter = seq.list(iter)
+ else return iter
+ end
+ end
+ if extra then
+ return setmetatable({iter=function()
+ return iter(extra)
+ end},SMT)
+ else
+ return setmetatable({iter=iter},SMT)
+ end
+ end
+})
+
+--- create a wrapped iterator over all lines in the file.
+-- @param f either a filename, file-like object, or 'STDIN' (for standard input)
+-- @param ... for Lua 5.2 only, optional format specifiers, as in `io.read`.
+-- @return a sequence wrapper
+function seq.lines (f,...)
+ local n = select('#',...)
+ local iter,obj
+ if f == 'STDIN' then
+ f = io.stdin
+ elseif type(f) == 'string' then
+ iter,obj = io.lines(f,...)
+ elseif not f.read then
+ error("Pass either a string or a file-like object",2)
+ end
+ if not iter then
+ iter,obj = f:lines(...)
+ end
+ if obj then -- LuaJIT version returns a function operating on a file
+ local lines,file = iter,obj
+ iter = function() return lines(file) end
+ end
+ return SW(iter)
+end
+
+function seq.import ()
+ debug.setmetatable(function() end,{
+ __index = function(tbl,key)
+ local s = overrides[key] or seq[key]
+ if s then return s
+ else
+ return function(s,...) return seq.mapmethod(s,key,...) end
+ end
+ end
+ })
+end
+
+return seq
diff --git a/lualibs/pl/sip.lua b/lualibs/pl/sip.lua
new file mode 100644
index 0000000000..5d1f2aa0cc
--- /dev/null
+++ b/lualibs/pl/sip.lua
@@ -0,0 +1,337 @@
+--- Simple Input Patterns (SIP).
+-- SIP patterns start with '$', then a
+-- one-letter type, and then an optional variable in curly braces.
+--
+-- sip.match('$v=$q','name="dolly"',res)
+-- ==> res=={'name','dolly'}
+-- sip.match('($q{first},$q{second})','("john","smith")',res)
+-- ==> res=={second='smith',first='john'}
+--
+-- Type names:
+--
+-- v identifier
+-- i integer
+-- f floating-point
+-- q quoted string
+-- ([{< match up to closing bracket
+--
+-- See @{08-additional.md.Simple_Input_Patterns|the Guide}
+--
+-- @module pl.sip
+
+local loadstring = rawget(_G,'loadstring') or load
+local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
+
+local append,concat = table.insert,table.concat
+local ipairs,type = ipairs,type
+local io,_G = io,_G
+local print,rawget = print,rawget
+
+local patterns = {
+ FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
+ INTEGER = '[+%-%d]%d*',
+ IDEN = '[%a_][%w_]*',
+ OPTION = '[%a_][%w_%-]*',
+}
+
+local function assert_arg(idx,val,tp)
+ if type(val) ~= tp then
+ error("argument "..idx.." must be "..tp, 2)
+ end
+end
+
+local sip = {}
+
+local brackets = {['<'] = '>', ['('] = ')', ['{'] = '}', ['['] = ']' }
+local stdclasses = {a=1,c=0,d=1,l=1,p=0,u=1,w=1,x=1,s=0}
+
+local function group(s)
+ return '('..s..')'
+end
+
+-- escape all magic characters except $, which has special meaning
+-- Also, un-escape any characters after $, so $( and $[ passes through as is.
+local function escape (spec)
+ return (spec:gsub('[%-%.%+%[%]%(%)%^%%%?%*]','%%%0'):gsub('%$%%(%S)','$%1'))
+end
+
+-- Most spaces within patterns can match zero or more spaces.
+-- Spaces between alphanumeric characters or underscores or between
+-- patterns that can match these characters, however, must match at least
+-- one space. Otherwise '$v $v' would match 'abcd' as {'abc', 'd'}.
+-- This function replaces continuous spaces within a pattern with either
+-- '%s*' or '%s+' according to this rule. The pattern has already
+-- been stripped of pattern names by now.
+local function compress_spaces(patt)
+ return (patt:gsub("()%s+()", function(i1, i2)
+ local before = patt:sub(i1 - 2, i1 - 1)
+ if before:match('%$[vifadxlu]') or before:match('^[^%$]?[%w_]$') then
+ local after = patt:sub(i2, i2 + 1)
+ if after:match('%$[vifadxlu]') or after:match('^[%w_]') then
+ return '%s+'
+ end
+ end
+ return '%s*'
+ end))
+end
+
+local pattern_map = {
+ v = group(patterns.IDEN),
+ i = group(patterns.INTEGER),
+ f = group(patterns.FLOAT),
+ o = group(patterns.OPTION),
+ r = '(%S.*)',
+ p = '([%a]?[:]?[\\/%.%w_]+)'
+}
+
+function sip.custom_pattern(flag,patt)
+ pattern_map[flag] = patt
+end
+
+--- convert a SIP pattern into the equivalent Lua string pattern.
+-- @param spec a SIP pattern
+-- @param options a table; only the at_start
field is
+-- currently meaningful and ensures that the pattern is anchored
+-- at the start of the string.
+-- @return a Lua string pattern.
+function sip.create_pattern (spec,options)
+ assert_arg(1,spec,'string')
+ local fieldnames,fieldtypes = {},{}
+
+ if type(spec) == 'string' then
+ spec = escape(spec)
+ else
+ local res = {}
+ for i,s in ipairs(spec) do
+ res[i] = escape(s)
+ end
+ spec = concat(res,'.-')
+ end
+
+ local kount = 1
+
+ local function addfield (name,type)
+ name = name or kount
+ append(fieldnames,name)
+ fieldtypes[name] = type
+ kount = kount + 1
+ end
+
+ local named_vars = spec:find('{%a+}')
+
+ if options and options.at_start then
+ spec = '^'..spec
+ end
+ if spec:sub(-1,-1) == '$' then
+ spec = spec:sub(1,-2)..'$r'
+ if named_vars then spec = spec..'{rest}' end
+ end
+
+ local names
+
+ if named_vars then
+ names = {}
+ spec = spec:gsub('{(%a+)}',function(name)
+ append(names,name)
+ return ''
+ end)
+ end
+ spec = compress_spaces(spec)
+
+ local k = 1
+ local err
+ local r = (spec:gsub('%$%S',function(s)
+ local type,name
+ type = s:sub(2,2)
+ if names then name = names[k]; k=k+1 end
+ -- this kludge is necessary because %q generates two matches, and
+ -- we want to ignore the first. Not a problem for named captures.
+ if not names and type == 'q' then
+ addfield(nil,'Q')
+ else
+ addfield(name,type)
+ end
+ local res
+ if pattern_map[type] then
+ res = pattern_map[type]
+ elseif type == 'q' then
+ -- some Lua pattern matching voodoo; we want to match '...' as
+ -- well as "...", and can use the fact that %n will match a
+ -- previous capture. Adding the extra field above comes from needing
+ -- to accommodate the extra spurious match (which is either ' or ")
+ addfield(name,type)
+ res = '(["\'])(.-)%'..(kount-2)
+ else
+ local endbracket = brackets[type]
+ if endbracket then
+ res = '(%b'..type..endbracket..')'
+ elseif stdclasses[type] or stdclasses[type:lower()] then
+ res = '(%'..type..'+)'
+ else
+ err = "unknown format type or character class"
+ end
+ end
+ return res
+ end))
+
+ if err then
+ return nil,err
+ else
+ return r,fieldnames,fieldtypes
+ end
+end
+
+
+local function tnumber (s)
+ return s == 'd' or s == 'i' or s == 'f'
+end
+
+function sip.create_spec_fun(spec,options)
+ local fieldtypes,fieldnames
+ local ls = {}
+ spec,fieldnames,fieldtypes = sip.create_pattern(spec,options)
+ if not spec then return spec,fieldnames end
+ local named_vars = type(fieldnames[1]) == 'string'
+ for i = 1,#fieldnames do
+ append(ls,'mm'..i)
+ end
+ ls[1] = ls[1] or "mm1" -- behave correctly if there are no patterns
+ local fun = ('return (function(s,res)\n\tlocal %s = s:match(%q)\n'):format(concat(ls,','),spec)
+ fun = fun..'\tif not mm1 then return false end\n'
+ local k=1
+ for i,f in ipairs(fieldnames) do
+ if f ~= '_' then
+ local var = 'mm'..i
+ if tnumber(fieldtypes[f]) then
+ var = 'tonumber('..var..')'
+ elseif brackets[fieldtypes[f]] then
+ var = var..':sub(2,-2)'
+ end
+ if named_vars then
+ fun = ('%s\tres.%s = %s\n'):format(fun,f,var)
+ else
+ if fieldtypes[f] ~= 'Q' then -- we skip the string-delim capture
+ fun = ('%s\tres[%d] = %s\n'):format(fun,k,var)
+ k = k + 1
+ end
+ end
+ end
+ end
+ return fun..'\treturn true\nend)\n', named_vars
+end
+
+--- convert a SIP pattern into a matching function.
+-- The returned function takes two arguments, the line and an empty table.
+-- If the line matched the pattern, then this function returns true
+-- and the table is filled with field-value pairs.
+-- @param spec a SIP pattern
+-- @param options optional table; {at_start=true} ensures that the pattern
+-- is anchored at the start of the string.
+-- @return a function if successful, or nil,error
+function sip.compile(spec,options)
+ assert_arg(1,spec,'string')
+ local fun,names = sip.create_spec_fun(spec,options)
+ if not fun then return nil,names end
+ if rawget(_G,'_DEBUG') then print(fun) end
+ local chunk,err = loadstring(fun,'tmp')
+ if err then return nil,err end
+ return chunk(),names
+end
+
+local cache = {}
+
+--- match a SIP pattern against a string.
+-- @param spec a SIP pattern
+-- @param line a string
+-- @param res a table to receive values
+-- @param options (optional) option table
+-- @return true or false
+function sip.match (spec,line,res,options)
+ assert_arg(1,spec,'string')
+ assert_arg(2,line,'string')
+ assert_arg(3,res,'table')
+ if not cache[spec] then
+ cache[spec] = sip.compile(spec,options)
+ end
+ return cache[spec](line,res)
+end
+
+--- match a SIP pattern against the start of a string.
+-- @param spec a SIP pattern
+-- @param line a string
+-- @param res a table to receive values
+-- @return true or false
+function sip.match_at_start (spec,line,res)
+ return sip.match(spec,line,res,{at_start=true})
+end
+
+--- given a pattern and a file object, return an iterator over the results
+-- @param spec a SIP pattern
+-- @param f a file-like object.
+function sip.fields (spec,f)
+ assert_arg(1,spec,'string')
+ if not f then return nil,"no file object" end
+ local fun,err = sip.compile(spec)
+ if not fun then return nil,err end
+ local res = {}
+ return function()
+ while true do
+ local line = f:read()
+ if not line then return end
+ if fun(line,res) then
+ local values = res
+ res = {}
+ return unpack(values)
+ end
+ end
+ end
+end
+
+local read_patterns = {}
+
+--- register a match which will be used in the read function.
+-- @string spec a SIP pattern
+-- @func fun a function to be called with the results of the match
+-- @see read
+function sip.pattern (spec,fun)
+ assert_arg(1,spec,'string')
+ local pat,named = sip.compile(spec)
+ append(read_patterns,{pat=pat,named=named,callback=fun})
+end
+
+--- enter a loop which applies all registered matches to the input file.
+-- @param f a file-like object
+-- @array matches optional list of `{spec,fun}` pairs, as for `pattern` above.
+function sip.read (f,matches)
+ local owned,err
+ if not f then return nil,"no file object" end
+ if type(f) == 'string' then
+ f,err = io.open(f)
+ if not f then return nil,err end
+ owned = true
+ end
+ if matches then
+ for _,p in ipairs(matches) do
+ sip.pattern(p[1],p[2])
+ end
+ end
+ local res = {}
+ for line in f:lines() do
+ for _,item in ipairs(read_patterns) do
+ if item.pat(line,res) then
+ if item.callback then
+ if item.named then
+ item.callback(res)
+ else
+ item.callback(unpack(res))
+ end
+ end
+ res = {}
+ break
+ end
+ end
+ end
+ if owned then f:close() end
+end
+
+return sip
diff --git a/lualibs/pl/strict.lua b/lualibs/pl/strict.lua
new file mode 100644
index 0000000000..d4e0fc729f
--- /dev/null
+++ b/lualibs/pl/strict.lua
@@ -0,0 +1,125 @@
+--- Checks uses of undeclared global variables.
+-- All global variables must be 'declared' through a regular assignment
+-- (even assigning `nil` will do) in a main chunk before being used
+-- anywhere or assigned to inside a function. Existing metatables `__newindex` and `__index`
+-- metamethods are respected.
+--
+-- You can set any table to have strict behaviour using `strict.module`. Creating a new
+-- module with `strict.closed_module` makes the module immune to monkey-patching, if
+-- you don't wish to encourage monkey business.
+--
+-- If the global `PENLIGHT_NO_GLOBAL_STRICT` is defined, then this module won't make the
+-- global environment strict - if you just want to explicitly set table strictness.
+--
+-- @module pl.strict
+
+require 'debug' -- for Lua 5.2
+local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget
+local strict = {}
+
+local function what ()
+ local d = getinfo(3, "S")
+ return d and d.what or "C"
+end
+
+--- make an existing table strict.
+-- @string name name of table (optional)
+-- @tab[opt] mod table - if `nil` then we'll return a new table
+-- @tab[opt] predeclared - table of variables that are to be considered predeclared.
+-- @return the given table, or a new table
+function strict.module (name,mod,predeclared)
+ local mt, old_newindex, old_index, old_index_type, global, closed
+ if predeclared then
+ global = predeclared.__global
+ closed = predeclared.__closed
+ end
+ if type(mod) == 'table' then
+ mt = getmetatable(mod)
+ if mt and rawget(mt,'__declared') then return end -- already patched...
+ else
+ mod = {}
+ end
+ if mt == nil then
+ mt = {}
+ setmetatable(mod, mt)
+ else
+ old_newindex = mt.__newindex
+ old_index = mt.__index
+ old_index_type = type(old_index)
+ end
+ mt.__declared = predeclared or {}
+ mt.__newindex = function(t, n, v)
+ if old_newindex then
+ old_newindex(t, n, v)
+ if rawget(t,n)~=nil then return end
+ end
+ if not mt.__declared[n] then
+ if global then
+ local w = what()
+ if w ~= "main" and w ~= "C" then
+ error("assign to undeclared global '"..n.."'", 2)
+ end
+ end
+ mt.__declared[n] = true
+ end
+ rawset(t, n, v)
+ end
+ mt.__index = function(t,n)
+ if not mt.__declared[n] and what() ~= "C" then
+ if old_index then
+ if old_index_type == "table" then
+ local fallback = old_index[n]
+ if fallback ~= nil then
+ return fallback
+ end
+ else
+ local res = old_index(t, n)
+ if res ~= nil then
+ return res
+ end
+ end
+ end
+ local msg = "variable '"..n.."' is not declared"
+ if name then
+ msg = msg .. " in '"..name.."'"
+ end
+ error(msg, 2)
+ end
+ return rawget(t, n)
+ end
+ return mod
+end
+
+--- make all tables in a table strict.
+-- So `strict.make_all_strict(_G)` prevents monkey-patching
+-- of any global table
+-- @tab T
+function strict.make_all_strict (T)
+ for k,v in pairs(T) do
+ if type(v) == 'table' and v ~= T then
+ strict.module(k,v)
+ end
+ end
+end
+
+--- make a new module table which is closed to further changes.
+function strict.closed_module (mod,name)
+ local M = {}
+ mod = mod or {}
+ local mt = getmetatable(mod)
+ if not mt then
+ mt = {}
+ setmetatable(mod,mt)
+ end
+ mt.__newindex = function(t,k,v)
+ M[k] = v
+ end
+ return strict.module(name,M)
+end
+
+if not rawget(_G,'PENLIGHT_NO_GLOBAL_STRICT') then
+ strict.module(nil,_G,{_PROMPT=true,__global=true})
+end
+
+return strict
+
diff --git a/lualibs/pl/stringio.lua b/lualibs/pl/stringio.lua
new file mode 100644
index 0000000000..10fd102fcb
--- /dev/null
+++ b/lualibs/pl/stringio.lua
@@ -0,0 +1,158 @@
+--- Reading and writing strings using file-like objects.
+--
+-- f = stringio.open(text)
+-- l1 = f:read() -- read first line
+-- n,m = f:read ('*n','*n') -- read two numbers
+-- for line in f:lines() do print(line) end -- iterate over all lines
+-- f = stringio.create()
+-- f:write('hello')
+-- f:write('dolly')
+-- assert(f:value(),'hellodolly')
+--
+-- See @{03-strings.md.File_style_I_O_on_Strings|the Guide}.
+-- @module pl.stringio
+
+local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
+local tonumber = tonumber
+local concat,append = table.concat,table.insert
+
+local stringio = {}
+
+-- Writer class
+local SW = {}
+SW.__index = SW
+
+local function xwrite(self,...)
+ local args = {...} --arguments may not be nil!
+ for i = 1, #args do
+ append(self.tbl,args[i])
+ end
+end
+
+function SW:write(arg1,arg2,...)
+ if arg2 then
+ xwrite(self,arg1,arg2,...)
+ else
+ append(self.tbl,arg1)
+ end
+end
+
+function SW:writef(fmt,...)
+ self:write(fmt:format(...))
+end
+
+function SW:value()
+ return concat(self.tbl)
+end
+
+function SW:__tostring()
+ return self:value()
+end
+
+function SW:close() -- for compatibility only
+end
+
+function SW:seek()
+end
+
+-- Reader class
+local SR = {}
+SR.__index = SR
+
+function SR:_read(fmt)
+ local i,str = self.i,self.str
+ local sz = #str
+ if i > sz then return nil end
+ local res
+ if fmt == '*l' or fmt == '*L' then
+ local idx = str:find('\n',i) or (sz+1)
+ res = str:sub(i,fmt == '*l' and idx-1 or idx)
+ self.i = idx+1
+ elseif fmt == '*a' then
+ res = str:sub(i)
+ self.i = sz
+ elseif fmt == '*n' then
+ local _,i2,i2,idx
+ _,idx = str:find ('%s*%d+',i)
+ _,i2 = str:find ('^%.%d+',idx+1)
+ if i2 then idx = i2 end
+ _,i2 = str:find ('^[eE][%+%-]*%d+',idx+1)
+ if i2 then idx = i2 end
+ local val = str:sub(i,idx)
+ res = tonumber(val)
+ self.i = idx+1
+ elseif type(fmt) == 'number' then
+ res = str:sub(i,i+fmt-1)
+ self.i = i + fmt
+ else
+ error("bad read format",2)
+ end
+ return res
+end
+
+function SR:read(...)
+ if select('#',...) == 0 then
+ return self:_read('*l')
+ else
+ local res, fmts = {},{...}
+ for i = 1, #fmts do
+ res[i] = self:_read(fmts[i])
+ end
+ return unpack(res)
+ end
+end
+
+function SR:seek(whence,offset)
+ local base
+ whence = whence or 'cur'
+ offset = offset or 0
+ if whence == 'set' then
+ base = 1
+ elseif whence == 'cur' then
+ base = self.i
+ elseif whence == 'end' then
+ base = #self.str
+ end
+ self.i = base + offset
+ return self.i
+end
+
+function SR:lines(...)
+ local n, args = select('#',...)
+ if n > 0 then
+ args = {...}
+ end
+ return function()
+ if n == 0 then
+ return self:_read '*l'
+ else
+ return self:read(unpack(args))
+ end
+ end
+end
+
+function SR:close() -- for compatibility only
+end
+
+--- create a file-like object which can be used to construct a string.
+-- The resulting object has an extra `value()` method for
+-- retrieving the string value. Implements `file:write`, `file:seek`, `file:lines`,
+-- plus an extra `writef` method which works like `utils.printf`.
+-- @usage f = create(); f:write('hello, dolly\n'); print(f:value())
+function stringio.create()
+ return setmetatable({tbl={}},SW)
+end
+
+--- create a file-like object for reading from a given string.
+-- Implements `file:read`.
+-- @string s The input string.
+-- @usage fs = open '20 10'; x,y = f:read ('*n','*n'); assert(x == 20 and y == 10)
+function stringio.open(s)
+ return setmetatable({str=s,i=1},SR)
+end
+
+function stringio.lines(s,...)
+ return stringio.open(s):lines(...)
+end
+
+return stringio
diff --git a/lualibs/pl/stringx.lua b/lualibs/pl/stringx.lua
new file mode 100644
index 0000000000..adf1070add
--- /dev/null
+++ b/lualibs/pl/stringx.lua
@@ -0,0 +1,548 @@
+--- Python-style extended string library.
+--
+-- see 3.6.1 of the Python reference.
+-- If you want to make these available as string methods, then say
+-- `stringx.import()` to bring them into the standard `string` table.
+--
+-- See @{03-strings.md|the Guide}
+--
+-- Dependencies: `pl.utils`
+-- @module pl.stringx
+local utils = require 'pl.utils'
+local string = string
+local find = string.find
+local type,setmetatable,ipairs = type,setmetatable,ipairs
+local error = error
+local gsub = string.gsub
+local rep = string.rep
+local sub = string.sub
+local concat = table.concat
+local append = table.insert
+local escape = utils.escape
+local ceil, max = math.ceil, math.max
+local assert_arg,usplit = utils.assert_arg,utils.split
+local lstrip
+
+local function assert_string (n,s)
+ assert_arg(n,s,'string')
+end
+
+local function non_empty(s)
+ return #s > 0
+end
+
+local function assert_nonempty_string(n,s)
+ assert_arg(n,s,'string',non_empty,'must be a non-empty string')
+end
+
+local function makelist(l)
+ return setmetatable(l, require('pl.List'))
+end
+
+local stringx = {}
+
+------------------
+-- String Predicates
+-- @section predicates
+
+--- does s only contain alphabetic characters?
+-- @string s a string
+function stringx.isalpha(s)
+ assert_string(1,s)
+ return find(s,'^%a+$') == 1
+end
+
+--- does s only contain digits?
+-- @string s a string
+function stringx.isdigit(s)
+ assert_string(1,s)
+ return find(s,'^%d+$') == 1
+end
+
+--- does s only contain alphanumeric characters?
+-- @string s a string
+function stringx.isalnum(s)
+ assert_string(1,s)
+ return find(s,'^%w+$') == 1
+end
+
+--- does s only contain spaces?
+-- @string s a string
+function stringx.isspace(s)
+ assert_string(1,s)
+ return find(s,'^%s+$') == 1
+end
+
+--- does s only contain lower case characters?
+-- @string s a string
+function stringx.islower(s)
+ assert_string(1,s)
+ return find(s,'^[%l%s]+$') == 1
+end
+
+--- does s only contain upper case characters?
+-- @string s a string
+function stringx.isupper(s)
+ assert_string(1,s)
+ return find(s,'^[%u%s]+$') == 1
+end
+
+local function raw_startswith(s, prefix)
+ return find(s,prefix,1,true) == 1
+end
+
+local function raw_endswith(s, suffix)
+ return #s >= #suffix and find(s, suffix, #s-#suffix+1, true) and true or false
+end
+
+local function test_affixes(s, affixes, fn)
+ if type(affixes) == 'string' then
+ return fn(s,affixes)
+ elseif type(affixes) == 'table' then
+ for _,affix in ipairs(affixes) do
+ if fn(s,affix) then return true end
+ end
+ return false
+ else
+ error(("argument #2 expected a 'string' or a 'table', got a '%s'"):format(type(affixes)))
+ end
+end
+
+--- does s start with prefix or one of prefixes?
+-- @string s a string
+-- @param prefix a string or an array of strings
+function stringx.startswith(s,prefix)
+ assert_string(1,s)
+ return test_affixes(s,prefix,raw_startswith)
+end
+
+--- does s end with suffix or one of suffixes?
+-- @string s a string
+-- @param suffix a string or an array of strings
+function stringx.endswith(s,suffix)
+ assert_string(1,s)
+ return test_affixes(s,suffix,raw_endswith)
+end
+
+--- Strings and Lists
+-- @section lists
+
+--- concatenate the strings using this string as a delimiter.
+-- @string s the string
+-- @param seq a table of strings or numbers
+-- @usage (' '):join {1,2,3} == '1 2 3'
+function stringx.join(s,seq)
+ assert_string(1,s)
+ return concat(seq,s)
+end
+
+--- Split a string into a list of lines.
+-- `"\r"`, `"\n"`, and `"\r\n"` are considered line ends.
+-- They are not included in the lines unless `keepends` is passed.
+-- Terminal line end does not produce an extra line.
+-- Splitting an empty string results in an empty list.
+-- @string s the string.
+-- @bool[opt] keep_ends include line ends.
+function stringx.splitlines(s, keep_ends)
+ assert_string(1, s)
+ local res = {}
+ local pos = 1
+ while true do
+ local line_end_pos = find(s, '[\r\n]', pos)
+ if not line_end_pos then
+ break
+ end
+
+ local line_end = sub(s, line_end_pos, line_end_pos)
+ if line_end == '\r' and sub(s, line_end_pos + 1, line_end_pos + 1) == '\n' then
+ line_end = '\r\n'
+ end
+
+ local line = sub(s, pos, line_end_pos - 1)
+ if keep_ends then
+ line = line .. line_end
+ end
+ append(res, line)
+
+ pos = line_end_pos + #line_end
+ end
+
+ if pos <= #s then
+ append(res, sub(s, pos))
+ end
+ return makelist(res)
+end
+
+--- split a string into a list of strings using a delimiter.
+-- @function split
+-- @string s the string
+-- @string[opt] re a delimiter (defaults to whitespace)
+-- @int[opt] n maximum number of results
+-- @usage #(('one two'):split()) == 2
+-- @usage ('one,two,three'):split(',') == List{'one','two','three'}
+-- @usage ('one,two,three'):split(',',2) == List{'one','two,three'}
+function stringx.split(s,re,n)
+ assert_string(1,s)
+ local plain = true
+ if not re then -- default spaces
+ s = lstrip(s)
+ plain = false
+ end
+ local res = usplit(s,re,plain,n)
+ if re and re ~= '' and find(s,re,-#re,true) then
+ res[#res+1] = ""
+ end
+ return makelist(res)
+end
+
+--- replace all tabs in s with tabsize spaces. If not specified, tabsize defaults to 8.
+-- with 0.9.5 this now correctly expands to the next tab stop (if you really
+-- want to just replace tabs, use :gsub('\t',' ') etc)
+-- @string s the string
+-- @int tabsize[opt=8] number of spaces to expand each tab
+function stringx.expandtabs(s,tabsize)
+ assert_string(1,s)
+ tabsize = tabsize or 8
+ return (s:gsub("([^\t\r\n]*)\t", function(before_tab)
+ return before_tab .. (" "):rep(tabsize - #before_tab % tabsize)
+ end))
+end
+
+--- Finding and Replacing
+-- @section find
+
+local function _find_all(s,sub,first,last)
+ first = first or 1
+ last = last or #s
+ if sub == '' then return last+1,last-first+1 end
+ local i1,i2 = find(s,sub,first,true)
+ local res
+ local k = 0
+ while i1 do
+ if last and i2 > last then break end
+ res = i1
+ k = k + 1
+ i1,i2 = find(s,sub,i2+1,true)
+ end
+ return res,k
+end
+
+--- find index of first instance of sub in s from the left.
+-- @string s the string
+-- @string sub substring
+-- @int[opt] first first index
+-- @int[opt] last last index
+function stringx.lfind(s,sub,first,last)
+ assert_string(1,s)
+ assert_string(2,sub)
+ local i1, i2 = find(s,sub,first,true)
+
+ if i1 and (not last or i2 <= last) then
+ return i1
+ else
+ return nil
+ end
+end
+
+--- find index of first instance of sub in s from the right.
+-- @string s the string
+-- @string sub substring
+-- @int[opt] first first index
+-- @int[opt] last last index
+function stringx.rfind(s,sub,first,last)
+ assert_string(1,s)
+ assert_string(2,sub)
+ return (_find_all(s,sub,first,last))
+end
+
+--- replace up to n instances of old by new in the string s.
+-- if n is not present, replace all instances.
+-- @string s the string
+-- @string old the target substring
+-- @string new the substitution
+-- @int[opt] n optional maximum number of substitutions
+-- @return result string
+function stringx.replace(s,old,new,n)
+ assert_string(1,s)
+ assert_string(2,old)
+ assert_string(3,new)
+ return (gsub(s,escape(old),new:gsub('%%','%%%%'),n))
+end
+
+--- count all instances of substring in string.
+-- @string s the string
+-- @string sub substring
+function stringx.count(s,sub)
+ assert_string(1,s)
+ local i,k = _find_all(s,sub,1)
+ return k
+end
+
+--- Stripping and Justifying
+-- @section strip
+
+local function _just(s,w,ch,left,right)
+ local n = #s
+ if w > n then
+ if not ch then ch = ' ' end
+ local f1,f2
+ if left and right then
+ local rn = ceil((w-n)/2)
+ local ln = w - n - rn
+ f1 = rep(ch,ln)
+ f2 = rep(ch,rn)
+ elseif right then
+ f1 = rep(ch,w-n)
+ f2 = ''
+ else
+ f2 = rep(ch,w-n)
+ f1 = ''
+ end
+ return f1..s..f2
+ else
+ return s
+ end
+end
+
+--- left-justify s with width w.
+-- @string s the string
+-- @int w width of justification
+-- @string[opt=' '] ch padding character
+function stringx.ljust(s,w,ch)
+ assert_string(1,s)
+ assert_arg(2,w,'number')
+ return _just(s,w,ch,true,false)
+end
+
+--- right-justify s with width w.
+-- @string s the string
+-- @int w width of justification
+-- @string[opt=' '] ch padding character
+function stringx.rjust(s,w,ch)
+ assert_string(1,s)
+ assert_arg(2,w,'number')
+ return _just(s,w,ch,false,true)
+end
+
+--- center-justify s with width w.
+-- @string s the string
+-- @int w width of justification
+-- @string[opt=' '] ch padding character
+function stringx.center(s,w,ch)
+ assert_string(1,s)
+ assert_arg(2,w,'number')
+ return _just(s,w,ch,true,true)
+end
+
+local function _strip(s,left,right,chrs)
+ if not chrs then
+ chrs = '%s'
+ else
+ chrs = '['..escape(chrs)..']'
+ end
+ if left then
+ local i1,i2 = find(s,'^'..chrs..'*')
+ if i2 >= i1 then
+ s = sub(s,i2+1)
+ end
+ end
+ if right then
+ local i1,i2 = find(s,chrs..'*$')
+ if i2 >= i1 then
+ s = sub(s,1,i1-1)
+ end
+ end
+ return s
+end
+
+--- trim any whitespace on the left of s.
+-- @string s the string
+-- @string[opt='%s'] chrs default any whitespace character,
+-- but can be a string of characters to be trimmed
+function stringx.lstrip(s,chrs)
+ assert_string(1,s)
+ return _strip(s,true,false,chrs)
+end
+lstrip = stringx.lstrip
+
+--- trim any whitespace on the right of s.
+-- @string s the string
+-- @string[opt='%s'] chrs default any whitespace character,
+-- but can be a string of characters to be trimmed
+function stringx.rstrip(s,chrs)
+ assert_string(1,s)
+ return _strip(s,false,true,chrs)
+end
+
+--- trim any whitespace on both left and right of s.
+-- @string s the string
+-- @string[opt='%s'] chrs default any whitespace character,
+-- but can be a string of characters to be trimmed
+function stringx.strip(s,chrs)
+ assert_string(1,s)
+ return _strip(s,true,true,chrs)
+end
+
+--- Partioning Strings
+-- @section partioning
+
+--- split a string using a pattern. Note that at least one value will be returned!
+-- @string s the string
+-- @string[opt='%s'] re a Lua string pattern (defaults to whitespace)
+-- @return the parts of the string
+-- @usage a,b = line:splitv('=')
+function stringx.splitv(s,re)
+ assert_string(1,s)
+ return utils.splitv(s,re)
+end
+
+-- The partition functions split a string using a delimiter into three parts:
+-- the part before, the delimiter itself, and the part afterwards
+local function _partition(p,delim,fn)
+ local i1,i2 = fn(p,delim)
+ if not i1 or i1 == -1 then
+ return p,'',''
+ else
+ if not i2 then i2 = i1 end
+ return sub(p,1,i1-1),sub(p,i1,i2),sub(p,i2+1)
+ end
+end
+
+--- partition the string using first occurance of a delimiter
+-- @string s the string
+-- @string ch delimiter
+-- @return part before ch
+-- @return ch
+-- @return part after ch
+function stringx.partition(s,ch)
+ assert_string(1,s)
+ assert_nonempty_string(2,ch)
+ return _partition(s,ch,stringx.lfind)
+end
+
+--- partition the string p using last occurance of a delimiter
+-- @string s the string
+-- @string ch delimiter
+-- @return part before ch
+-- @return ch
+-- @return part after ch
+function stringx.rpartition(s,ch)
+ assert_string(1,s)
+ assert_nonempty_string(2,ch)
+ return _partition(s,ch,stringx.rfind)
+end
+
+--- return the 'character' at the index.
+-- @string s the string
+-- @int idx an index (can be negative)
+-- @return a substring of length 1 if successful, empty string otherwise.
+function stringx.at(s,idx)
+ assert_string(1,s)
+ assert_arg(2,idx,'number')
+ return sub(s,idx,idx)
+end
+
+--- Miscelaneous
+-- @section misc
+
+--- return an iterator over all lines in a string
+-- @string s the string
+-- @return an iterator
+function stringx.lines(s)
+ assert_string(1,s)
+ if not s:find '\n$' then s = s..'\n' end
+ return s:gmatch('([^\n]*)\n')
+end
+
+--- iniital word letters uppercase ('title case').
+-- Here 'words' mean chunks of non-space characters.
+-- @string s the string
+-- @return a string with each word's first letter uppercase
+function stringx.title(s)
+ assert_string(1,s)
+ return (s:gsub('(%S)(%S*)',function(f,r)
+ return f:upper()..r:lower()
+ end))
+end
+
+stringx.capitalize = stringx.title
+
+local ellipsis = '...'
+local n_ellipsis = #ellipsis
+
+--- Return a shortened version of a string.
+-- Fits string within w characters. Removed characters are marked with ellipsis.
+-- @string s the string
+-- @int w the maxinum size allowed
+-- @bool tail true if we want to show the end of the string (head otherwise)
+-- @usage ('1234567890'):shorten(8) == '12345...'
+-- @usage ('1234567890'):shorten(8, true) == '...67890'
+-- @usage ('1234567890'):shorten(20) == '1234567890'
+function stringx.shorten(s,w,tail)
+ assert_string(1,s)
+ if #s > w then
+ if w < n_ellipsis then return ellipsis:sub(1,w) end
+ if tail then
+ local i = #s - w + 1 + n_ellipsis
+ return ellipsis .. s:sub(i)
+ else
+ return s:sub(1,w-n_ellipsis) .. ellipsis
+ end
+ end
+ return s
+end
+
+--- Utility function that finds any patterns that match a long string's an open or close.
+-- Note that having this function use the least number of equal signs that is possible is a harder algorithm to come up with.
+-- Right now, it simply returns the greatest number of them found.
+-- @param s The string
+-- @return 'nil' if not found. If found, the maximum number of equal signs found within all matches.
+local function has_lquote(s)
+ local lstring_pat = '([%[%]])(=*)%1'
+ local equals
+ local start, finish, bracket, new_equals = nil, 1, nil, nil
+
+ repeat
+ start, finish, bracket, new_equals = s:find(lstring_pat, finish)
+ if new_equals then
+ equals = max(equals or 0, #new_equals)
+ end
+ until not new_equals
+
+ return equals
+end
+
+--- Quote the given string and preserve any control or escape characters, such that reloading the string in Lua returns the same result.
+-- @param s The string to be quoted.
+-- @return The quoted string.
+function stringx.quote_string(s)
+ assert_string(1,s)
+ -- Find out if there are any embedded long-quote sequences that may cause issues.
+ -- This is important when strings are embedded within strings, like when serializing.
+ -- Append a closing bracket to catch unfinished long-quote sequences at the end of the string.
+ local equal_signs = has_lquote(s .. "]")
+
+ -- Note that strings containing "\r" can't be quoted using long brackets
+ -- as Lua lexer converts all newlines to "\n" within long strings.
+ if (s:find("\n") or equal_signs) and not s:find("\r") then
+ -- If there is an embedded sequence that matches a long quote, then
+ -- find the one with the maximum number of = signs and add one to that number.
+ equal_signs = ("="):rep((equal_signs or -1) + 1)
+ -- Long strings strip out leading newline. We want to retain that, when quoting.
+ if s:find("^\n") then s = "\n" .. s end
+ local lbracket, rbracket =
+ "[" .. equal_signs .. "[",
+ "]" .. equal_signs .. "]"
+ s = lbracket .. s .. rbracket
+ else
+ -- Escape funny stuff. Lua 5.1 does not handle "\r" correctly.
+ s = ("%q"):format(s):gsub("\r", "\\r")
+ end
+ return s
+end
+
+function stringx.import()
+ utils.import(stringx,string)
+end
+
+return stringx
diff --git a/lualibs/pl/tablex.lua b/lualibs/pl/tablex.lua
new file mode 100644
index 0000000000..008da15405
--- /dev/null
+++ b/lualibs/pl/tablex.lua
@@ -0,0 +1,927 @@
+--- Extended operations on Lua tables.
+--
+-- See @{02-arrays.md.Useful_Operations_on_Tables|the Guide}
+--
+-- Dependencies: `pl.utils`, `pl.types`
+-- @module pl.tablex
+local utils = require ('pl.utils')
+local types = require ('pl.types')
+local getmetatable,setmetatable,require = getmetatable,setmetatable,require
+local tsort,append,remove = table.sort,table.insert,table.remove
+local min = math.min
+local pairs,type,unpack,select,tostring = pairs,type,utils.unpack,select,tostring
+local function_arg = utils.function_arg
+local assert_arg = utils.assert_arg
+
+local tablex = {}
+
+-- generally, functions that make copies of tables try to preserve the metatable.
+-- However, when the source has no obvious type, then we attach appropriate metatables
+-- like List, Map, etc to the result.
+local function setmeta (res,tbl,pl_class)
+ local mt = getmetatable(tbl) or pl_class and require('pl.' .. pl_class)
+ return mt and setmetatable(res, mt) or res
+end
+
+local function makelist(l)
+ return setmetatable(l, require('pl.List'))
+end
+
+local function makemap(m)
+ return setmetatable(m, require('pl.Map'))
+end
+
+local function complain (idx,msg)
+ error(('argument %d is not %s'):format(idx,msg),3)
+end
+
+local function assert_arg_indexable (idx,val)
+ if not types.is_indexable(val) then
+ complain(idx,"indexable")
+ end
+end
+
+local function assert_arg_iterable (idx,val)
+ if not types.is_iterable(val) then
+ complain(idx,"iterable")
+ end
+end
+
+local function assert_arg_writeable (idx,val)
+ if not types.is_writeable(val) then
+ complain(idx,"writeable")
+ end
+end
+
+--- copy a table into another, in-place.
+-- @within Copying
+-- @tab t1 destination table
+-- @tab t2 source (actually any iterable object)
+-- @return first table
+function tablex.update (t1,t2)
+ assert_arg_writeable(1,t1)
+ assert_arg_iterable(2,t2)
+ for k,v in pairs(t2) do
+ t1[k] = v
+ end
+ return t1
+end
+
+--- total number of elements in this table.
+-- Note that this is distinct from `#t`, which is the number
+-- of values in the array part; this value will always
+-- be greater or equal. The difference gives the size of
+-- the hash part, for practical purposes. Works for any
+-- object with a __pairs metamethod.
+-- @tab t a table
+-- @return the size
+function tablex.size (t)
+ assert_arg_iterable(1,t)
+ local i = 0
+ for k in pairs(t) do i = i + 1 end
+ return i
+end
+
+--- make a shallow copy of a table
+-- @within Copying
+-- @tab t an iterable source
+-- @return new table
+function tablex.copy (t)
+ assert_arg_iterable(1,t)
+ local res = {}
+ for k,v in pairs(t) do
+ res[k] = v
+ end
+ return res
+end
+
+--- make a deep copy of a table, recursively copying all the keys and fields.
+-- This will also set the copied table's metatable to that of the original.
+-- @within Copying
+-- @tab t A table
+-- @return new table
+function tablex.deepcopy(t)
+ if type(t) ~= 'table' then return t end
+ assert_arg_iterable(1,t)
+ local mt = getmetatable(t)
+ local res = {}
+ for k,v in pairs(t) do
+ if type(v) == 'table' then
+ v = tablex.deepcopy(v)
+ end
+ res[k] = v
+ end
+ setmetatable(res,mt)
+ return res
+end
+
+local abs, deepcompare = math.abs
+
+--- compare two values.
+-- if they are tables, then compare their keys and fields recursively.
+-- @within Comparing
+-- @param t1 A value
+-- @param t2 A value
+-- @bool[opt] ignore_mt if true, ignore __eq metamethod (default false)
+-- @number[opt] eps if defined, then used for any number comparisons
+-- @return true or false
+function tablex.deepcompare(t1,t2,ignore_mt,eps)
+ local ty1 = type(t1)
+ local ty2 = type(t2)
+ if ty1 ~= ty2 then return false end
+ -- non-table types can be directly compared
+ if ty1 ~= 'table' then
+ if ty1 == 'number' and eps then return abs(t1-t2) < eps end
+ return t1 == t2
+ end
+ -- as well as tables which have the metamethod __eq
+ local mt = getmetatable(t1)
+ if not ignore_mt and mt and mt.__eq then return t1 == t2 end
+ for k1 in pairs(t1) do
+ if t2[k1]==nil then return false end
+ end
+ for k2 in pairs(t2) do
+ if t1[k2]==nil then return false end
+ end
+ for k1,v1 in pairs(t1) do
+ local v2 = t2[k1]
+ if not deepcompare(v1,v2,ignore_mt,eps) then return false end
+ end
+
+ return true
+end
+
+deepcompare = tablex.deepcompare
+
+--- compare two arrays using a predicate.
+-- @within Comparing
+-- @array t1 an array
+-- @array t2 an array
+-- @func cmp A comparison function
+function tablex.compare (t1,t2,cmp)
+ assert_arg_indexable(1,t1)
+ assert_arg_indexable(2,t2)
+ if #t1 ~= #t2 then return false end
+ cmp = function_arg(3,cmp)
+ for k = 1,#t1 do
+ if not cmp(t1[k],t2[k]) then return false end
+ end
+ return true
+end
+
+--- compare two list-like tables using an optional predicate, without regard for element order.
+-- @within Comparing
+-- @array t1 a list-like table
+-- @array t2 a list-like table
+-- @param cmp A comparison function (may be nil)
+function tablex.compare_no_order (t1,t2,cmp)
+ assert_arg_indexable(1,t1)
+ assert_arg_indexable(2,t2)
+ if cmp then cmp = function_arg(3,cmp) end
+ if #t1 ~= #t2 then return false end
+ local visited = {}
+ for i = 1,#t1 do
+ local val = t1[i]
+ local gotcha
+ for j = 1,#t2 do if not visited[j] then
+ local match
+ if cmp then match = cmp(val,t2[j]) else match = val == t2[j] end
+ if match then
+ gotcha = j
+ break
+ end
+ end end
+ if not gotcha then return false end
+ visited[gotcha] = true
+ end
+ return true
+end
+
+
+--- return the index of a value in a list.
+-- Like string.find, there is an optional index to start searching,
+-- which can be negative.
+-- @within Finding
+-- @array t A list-like table
+-- @param val A value
+-- @int idx index to start; -1 means last element,etc (default 1)
+-- @return index of value or nil if not found
+-- @usage find({10,20,30},20) == 2
+-- @usage find({'a','b','a','c'},'a',2) == 3
+function tablex.find(t,val,idx)
+ assert_arg_indexable(1,t)
+ idx = idx or 1
+ if idx < 0 then idx = #t + idx + 1 end
+ for i = idx,#t do
+ if t[i] == val then return i end
+ end
+ return nil
+end
+
+--- return the index of a value in a list, searching from the end.
+-- Like string.find, there is an optional index to start searching,
+-- which can be negative.
+-- @within Finding
+-- @array t A list-like table
+-- @param val A value
+-- @param idx index to start; -1 means last element,etc (default 1)
+-- @return index of value or nil if not found
+-- @usage rfind({10,10,10},10) == 3
+function tablex.rfind(t,val,idx)
+ assert_arg_indexable(1,t)
+ idx = idx or #t
+ if idx < 0 then idx = #t + idx + 1 end
+ for i = idx,1,-1 do
+ if t[i] == val then return i end
+ end
+ return nil
+end
+
+
+--- return the index (or key) of a value in a table using a comparison function.
+-- @within Finding
+-- @tab t A table
+-- @func cmp A comparison function
+-- @param arg an optional second argument to the function
+-- @return index of value, or nil if not found
+-- @return value returned by comparison function
+function tablex.find_if(t,cmp,arg)
+ assert_arg_iterable(1,t)
+ cmp = function_arg(2,cmp)
+ for k,v in pairs(t) do
+ local c = cmp(v,arg)
+ if c then return k,c end
+ end
+ return nil
+end
+
+--- return a list of all values in a table indexed by another list.
+-- @tab tbl a table
+-- @array idx an index table (a list of keys)
+-- @return a list-like table
+-- @usage index_by({10,20,30,40},{2,4}) == {20,40}
+-- @usage index_by({one=1,two=2,three=3},{'one','three'}) == {1,3}
+function tablex.index_by(tbl,idx)
+ assert_arg_indexable(1,tbl)
+ assert_arg_indexable(2,idx)
+ local res = {}
+ for i = 1,#idx do
+ res[i] = tbl[idx[i]]
+ end
+ return setmeta(res,tbl,'List')
+end
+
+--- apply a function to all values of a table.
+-- This returns a table of the results.
+-- Any extra arguments are passed to the function.
+-- @within MappingAndFiltering
+-- @func fun A function that takes at least one argument
+-- @tab t A table
+-- @param ... optional arguments
+-- @usage map(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900,fred=4}
+function tablex.map(fun,t,...)
+ assert_arg_iterable(1,t)
+ fun = function_arg(1,fun)
+ local res = {}
+ for k,v in pairs(t) do
+ res[k] = fun(v,...)
+ end
+ return setmeta(res,t)
+end
+
+--- apply a function to all values of a list.
+-- This returns a table of the results.
+-- Any extra arguments are passed to the function.
+-- @within MappingAndFiltering
+-- @func fun A function that takes at least one argument
+-- @array t a table (applies to array part)
+-- @param ... optional arguments
+-- @return a list-like table
+-- @usage imap(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900}
+function tablex.imap(fun,t,...)
+ assert_arg_indexable(1,t)
+ fun = function_arg(1,fun)
+ local res = {}
+ for i = 1,#t do
+ res[i] = fun(t[i],...) or false
+ end
+ return setmeta(res,t,'List')
+end
+
+--- apply a named method to values from a table.
+-- @within MappingAndFiltering
+-- @string name the method name
+-- @array t a list-like table
+-- @param ... any extra arguments to the method
+function tablex.map_named_method (name,t,...)
+ utils.assert_string(1,name)
+ assert_arg_indexable(2,t)
+ local res = {}
+ for i = 1,#t do
+ local val = t[i]
+ local fun = val[name]
+ res[i] = fun(val,...)
+ end
+ return setmeta(res,t,'List')
+end
+
+--- apply a function to all values of a table, in-place.
+-- Any extra arguments are passed to the function.
+-- @func fun A function that takes at least one argument
+-- @tab t a table
+-- @param ... extra arguments
+function tablex.transform (fun,t,...)
+ assert_arg_iterable(1,t)
+ fun = function_arg(1,fun)
+ for k,v in pairs(t) do
+ t[k] = fun(v,...)
+ end
+end
+
+--- generate a table of all numbers in a range.
+-- This is consistent with a numerical for loop.
+-- @int start number
+-- @int finish number
+-- @int[opt=1] step make this negative for start < finish
+function tablex.range (start,finish,step)
+ local res
+ step = step or 1
+ if start == finish then
+ res = {start}
+ elseif (start > finish and step > 0) or (finish > start and step < 0) then
+ res = {}
+ else
+ local k = 1
+ res = {}
+ for i=start,finish,step do res[k]=i; k=k+1 end
+ end
+ return makelist(res)
+end
+
+--- apply a function to values from two tables.
+-- @within MappingAndFiltering
+-- @func fun a function of at least two arguments
+-- @tab t1 a table
+-- @tab t2 a table
+-- @param ... extra arguments
+-- @return a table
+-- @usage map2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23,m=44}
+function tablex.map2 (fun,t1,t2,...)
+ assert_arg_iterable(1,t1)
+ assert_arg_iterable(2,t2)
+ fun = function_arg(1,fun)
+ local res = {}
+ for k,v in pairs(t1) do
+ res[k] = fun(v,t2[k],...)
+ end
+ return setmeta(res,t1,'List')
+end
+
+--- apply a function to values from two arrays.
+-- The result will be the length of the shortest array.
+-- @within MappingAndFiltering
+-- @func fun a function of at least two arguments
+-- @array t1 a list-like table
+-- @array t2 a list-like table
+-- @param ... extra arguments
+-- @usage imap2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23}
+function tablex.imap2 (fun,t1,t2,...)
+ assert_arg_indexable(2,t1)
+ assert_arg_indexable(3,t2)
+ fun = function_arg(1,fun)
+ local res,n = {},math.min(#t1,#t2)
+ for i = 1,n do
+ res[i] = fun(t1[i],t2[i],...)
+ end
+ return res
+end
+
+--- 'reduce' a list using a binary function.
+-- @func fun a function of two arguments
+-- @array t a list-like table
+-- @array memo optional initial memo value. Defaults to first value in table.
+-- @return the result of the function
+-- @usage reduce('+',{1,2,3,4}) == 10
+function tablex.reduce (fun,t,memo)
+ assert_arg_indexable(2,t)
+ fun = function_arg(1,fun)
+ local n = #t
+ if n == 0 then
+ return memo
+ end
+ local res = memo and fun(memo, t[1]) or t[1]
+ for i = 2,n do
+ res = fun(res,t[i])
+ end
+ return res
+end
+
+--- apply a function to all elements of a table.
+-- The arguments to the function will be the value,
+-- the key and _finally_ any extra arguments passed to this function.
+-- Note that the Lua 5.0 function table.foreach passed the _key_ first.
+-- @within Iterating
+-- @tab t a table
+-- @func fun a function with at least one argument
+-- @param ... extra arguments
+function tablex.foreach(t,fun,...)
+ assert_arg_iterable(1,t)
+ fun = function_arg(2,fun)
+ for k,v in pairs(t) do
+ fun(v,k,...)
+ end
+end
+
+--- apply a function to all elements of a list-like table in order.
+-- The arguments to the function will be the value,
+-- the index and _finally_ any extra arguments passed to this function
+-- @within Iterating
+-- @array t a table
+-- @func fun a function with at least one argument
+-- @param ... optional arguments
+function tablex.foreachi(t,fun,...)
+ assert_arg_indexable(1,t)
+ fun = function_arg(2,fun)
+ for i = 1,#t do
+ fun(t[i],i,...)
+ end
+end
+
+--- Apply a function to a number of tables.
+-- A more general version of map
+-- The result is a table containing the result of applying that function to the
+-- ith value of each table. Length of output list is the minimum length of all the lists
+-- @within MappingAndFiltering
+-- @func fun a function of n arguments
+-- @tab ... n tables
+-- @usage mapn(function(x,y,z) return x+y+z end, {1,2,3},{10,20,30},{100,200,300}) is {111,222,333}
+-- @usage mapn(math.max, {1,20,300},{10,2,3},{100,200,100}) is {100,200,300}
+-- @param fun A function that takes as many arguments as there are tables
+function tablex.mapn(fun,...)
+ fun = function_arg(1,fun)
+ local res = {}
+ local lists = {...}
+ local minn = 1e40
+ for i = 1,#lists do
+ minn = min(minn,#(lists[i]))
+ end
+ for i = 1,minn do
+ local args,k = {},1
+ for j = 1,#lists do
+ args[k] = lists[j][i]
+ k = k + 1
+ end
+ res[#res+1] = fun(unpack(args))
+ end
+ return res
+end
+
+--- call the function with the key and value pairs from a table.
+-- The function can return a value and a key (note the order!). If both
+-- are not nil, then this pair is inserted into the result: if the key already exists, we convert the value for that
+-- key into a table and append into it. If only value is not nil, then it is appended to the result.
+-- @within MappingAndFiltering
+-- @func fun A function which will be passed each key and value as arguments, plus any extra arguments to pairmap.
+-- @tab t A table
+-- @param ... optional arguments
+-- @usage pairmap(function(k,v) return v end,{fred=10,bonzo=20}) is {10,20} _or_ {20,10}
+-- @usage pairmap(function(k,v) return {k,v},k end,{one=1,two=2}) is {one={'one',1},two={'two',2}}
+function tablex.pairmap(fun,t,...)
+ assert_arg_iterable(1,t)
+ fun = function_arg(1,fun)
+ local res = {}
+ for k,v in pairs(t) do
+ local rv,rk = fun(k,v,...)
+ if rk then
+ if res[rk] then
+ if type(res[rk]) == 'table' then
+ table.insert(res[rk],rv)
+ else
+ res[rk] = {res[rk], rv}
+ end
+ else
+ res[rk] = rv
+ end
+ else
+ res[#res+1] = rv
+ end
+ end
+ return res
+end
+
+local function keys_op(i,v) return i end
+
+--- return all the keys of a table in arbitrary order.
+-- @within Extraction
+-- @tab t A table
+function tablex.keys(t)
+ assert_arg_iterable(1,t)
+ return makelist(tablex.pairmap(keys_op,t))
+end
+
+local function values_op(i,v) return v end
+
+--- return all the values of the table in arbitrary order
+-- @within Extraction
+-- @tab t A table
+function tablex.values(t)
+ assert_arg_iterable(1,t)
+ return makelist(tablex.pairmap(values_op,t))
+end
+
+local function index_map_op (i,v) return i,v end
+
+--- create an index map from a list-like table. The original values become keys,
+-- and the associated values are the indices into the original list.
+-- @array t a list-like table
+-- @return a map-like table
+function tablex.index_map (t)
+ assert_arg_indexable(1,t)
+ return makemap(tablex.pairmap(index_map_op,t))
+end
+
+local function set_op(i,v) return true,v end
+
+--- create a set from a list-like table. A set is a table where the original values
+-- become keys, and the associated values are all true.
+-- @array t a list-like table
+-- @return a set (a map-like table)
+function tablex.makeset (t)
+ assert_arg_indexable(1,t)
+ return setmetatable(tablex.pairmap(set_op,t),require('pl.Set'))
+end
+
+--- combine two tables, either as union or intersection. Corresponds to
+-- set operations for sets () but more general. Not particularly
+-- useful for list-like tables.
+-- @within Merging
+-- @tab t1 a table
+-- @tab t2 a table
+-- @bool dup true for a union, false for an intersection.
+-- @usage merge({alice=23,fred=34},{bob=25,fred=34}) is {fred=34}
+-- @usage merge({alice=23,fred=34},{bob=25,fred=34},true) is {bob=25,fred=34,alice=23}
+-- @see tablex.index_map
+function tablex.merge (t1,t2,dup)
+ assert_arg_iterable(1,t1)
+ assert_arg_iterable(2,t2)
+ local res = {}
+ for k,v in pairs(t1) do
+ if dup or t2[k] then res[k] = v end
+ end
+ if dup then
+ for k,v in pairs(t2) do
+ res[k] = v
+ end
+ end
+ return setmeta(res,t1,'Map')
+end
+
+--- the union of two map-like tables.
+-- If there are duplicate keys, the second table wins.
+-- @tab t1 a table
+-- @tab t2 a table
+-- @treturn tab
+-- @see tablex.merge
+function tablex.union(t1, t2)
+ return tablex.merge(t1, t2, true)
+end
+
+--- the intersection of two map-like tables.
+-- @tab t1 a table
+-- @tab t2 a table
+-- @treturn tab
+-- @see tablex.merge
+function tablex.intersection(t1, t2)
+ return tablex.merge(t1, t2, false)
+end
+
+--- a new table which is the difference of two tables.
+-- With sets (where the values are all true) this is set difference and
+-- symmetric difference depending on the third parameter.
+-- @within Merging
+-- @tab s1 a map-like table or set
+-- @tab s2 a map-like table or set
+-- @bool symm symmetric difference (default false)
+-- @return a map-like table or set
+function tablex.difference (s1,s2,symm)
+ assert_arg_iterable(1,s1)
+ assert_arg_iterable(2,s2)
+ local res = {}
+ for k,v in pairs(s1) do
+ if s2[k] == nil then res[k] = v end
+ end
+ if symm then
+ for k,v in pairs(s2) do
+ if s1[k] == nil then res[k] = v end
+ end
+ end
+ return setmeta(res,s1,'Map')
+end
+
+--- A table where the key/values are the values and value counts of the table.
+-- @array t a list-like table
+-- @func cmp a function that defines equality (otherwise uses ==)
+-- @return a map-like table
+-- @see seq.count_map
+function tablex.count_map (t,cmp)
+ assert_arg_indexable(1,t)
+ local res,mask = {},{}
+ cmp = function_arg(2,cmp or '==')
+ local n = #t
+ for i = 1,#t do
+ local v = t[i]
+ if not mask[v] then
+ mask[v] = true
+ -- check this value against all other values
+ res[v] = 1 -- there's at least one instance
+ for j = i+1,n do
+ local w = t[j]
+ local ok = cmp(v,w)
+ if ok then
+ res[v] = res[v] + 1
+ mask[w] = true
+ end
+ end
+ end
+ end
+ return makemap(res)
+end
+
+--- filter an array's values using a predicate function
+-- @within MappingAndFiltering
+-- @array t a list-like table
+-- @func pred a boolean function
+-- @param arg optional argument to be passed as second argument of the predicate
+function tablex.filter (t,pred,arg)
+ assert_arg_indexable(1,t)
+ pred = function_arg(2,pred)
+ local res,k = {},1
+ for i = 1,#t do
+ local v = t[i]
+ if pred(v,arg) then
+ res[k] = v
+ k = k + 1
+ end
+ end
+ return setmeta(res,t,'List')
+end
+
+--- return a table where each element is a table of the ith values of an arbitrary
+-- number of tables. It is equivalent to a matrix transpose.
+-- @within Merging
+-- @usage zip({10,20,30},{100,200,300}) is {{10,100},{20,200},{30,300}}
+-- @array ... arrays to be zipped
+function tablex.zip(...)
+ return tablex.mapn(function(...) return {...} end,...)
+end
+
+local _copy
+function _copy (dest,src,idest,isrc,nsrc,clean_tail)
+ idest = idest or 1
+ isrc = isrc or 1
+ local iend
+ if not nsrc then
+ nsrc = #src
+ iend = #src
+ else
+ iend = isrc + min(nsrc-1,#src-isrc)
+ end
+ if dest == src then -- special case
+ if idest > isrc and iend >= idest then -- overlapping ranges
+ src = tablex.sub(src,isrc,nsrc)
+ isrc = 1; iend = #src
+ end
+ end
+ for i = isrc,iend do
+ dest[idest] = src[i]
+ idest = idest + 1
+ end
+ if clean_tail then
+ tablex.clear(dest,idest)
+ end
+ return dest
+end
+
+--- copy an array into another one, clearing `dest` after `idest+nsrc`, if necessary.
+-- @within Copying
+-- @array dest a list-like table
+-- @array src a list-like table
+-- @int[opt=1] idest where to start copying values into destination
+-- @int[opt=1] isrc where to start copying values from source
+-- @int[opt=#src] nsrc number of elements to copy from source
+function tablex.icopy (dest,src,idest,isrc,nsrc)
+ assert_arg_indexable(1,dest)
+ assert_arg_indexable(2,src)
+ return _copy(dest,src,idest,isrc,nsrc,true)
+end
+
+--- copy an array into another one.
+-- @within Copying
+-- @array dest a list-like table
+-- @array src a list-like table
+-- @int[opt=1] idest where to start copying values into destination
+-- @int[opt=1] isrc where to start copying values from source
+-- @int[opt=#src] nsrc number of elements to copy from source
+function tablex.move (dest,src,idest,isrc,nsrc)
+ assert_arg_indexable(1,dest)
+ assert_arg_indexable(2,src)
+ return _copy(dest,src,idest,isrc,nsrc,false)
+end
+
+function tablex._normalize_slice(self,first,last)
+ local sz = #self
+ if not first then first=1 end
+ if first<0 then first=sz+first+1 end
+ -- make the range _inclusive_!
+ if not last then last=sz end
+ if last < 0 then last=sz+1+last end
+ return first,last
+end
+
+--- Extract a range from a table, like 'string.sub'.
+-- If first or last are negative then they are relative to the end of the list
+-- eg. sub(t,-2) gives last 2 entries in a list, and
+-- sub(t,-4,-2) gives from -4th to -2nd
+-- @within Extraction
+-- @array t a list-like table
+-- @int first An index
+-- @int last An index
+-- @return a new List
+function tablex.sub(t,first,last)
+ assert_arg_indexable(1,t)
+ first,last = tablex._normalize_slice(t,first,last)
+ local res={}
+ for i=first,last do append(res,t[i]) end
+ return setmeta(res,t,'List')
+end
+
+--- set an array range to a value. If it's a function we use the result
+-- of applying it to the indices.
+-- @array t a list-like table
+-- @param val a value
+-- @int[opt=1] i1 start range
+-- @int[opt=#t] i2 end range
+function tablex.set (t,val,i1,i2)
+ assert_arg_indexable(1,t)
+ i1,i2 = i1 or 1,i2 or #t
+ if types.is_callable(val) then
+ for i = i1,i2 do
+ t[i] = val(i)
+ end
+ else
+ for i = i1,i2 do
+ t[i] = val
+ end
+ end
+end
+
+--- create a new array of specified size with initial value.
+-- @int n size
+-- @param val initial value (can be `nil`, but don't expect `#` to work!)
+-- @return the table
+function tablex.new (n,val)
+ local res = {}
+ tablex.set(res,val,1,n)
+ return res
+end
+
+--- clear out the contents of a table.
+-- @array t a list
+-- @param istart optional start position
+function tablex.clear(t,istart)
+ istart = istart or 1
+ for i = istart,#t do remove(t) end
+end
+
+--- insert values into a table.
+-- similar to `table.insert` but inserts values from given table `values`,
+-- not the object itself, into table `t` at position `pos`.
+-- @within Copying
+-- @array t the list
+-- @int[opt] position (default is at end)
+-- @array values
+function tablex.insertvalues(t, ...)
+ assert_arg(1,t,'table')
+ local pos, values
+ if select('#', ...) == 1 then
+ pos,values = #t+1, ...
+ else
+ pos,values = ...
+ end
+ if #values > 0 then
+ for i=#t,pos,-1 do
+ t[i+#values] = t[i]
+ end
+ local offset = 1 - pos
+ for i=pos,pos+#values-1 do
+ t[i] = values[i + offset]
+ end
+ end
+ return t
+end
+
+--- remove a range of values from a table.
+-- End of range may be negative.
+-- @array t a list-like table
+-- @int i1 start index
+-- @int i2 end index
+-- @return the table
+function tablex.removevalues (t,i1,i2)
+ assert_arg(1,t,'table')
+ i1,i2 = tablex._normalize_slice(t,i1,i2)
+ for i = i1,i2 do
+ remove(t,i1)
+ end
+ return t
+end
+
+local _find
+_find = function (t,value,tables)
+ for k,v in pairs(t) do
+ if v == value then return k end
+ end
+ for k,v in pairs(t) do
+ if not tables[v] and type(v) == 'table' then
+ tables[v] = true
+ local res = _find(v,value,tables)
+ if res then
+ res = tostring(res)
+ if type(k) ~= 'string' then
+ return '['..k..']'..res
+ else
+ return k..'.'..res
+ end
+ end
+ end
+ end
+end
+
+--- find a value in a table by recursive search.
+-- @within Finding
+-- @tab t the table
+-- @param value the value
+-- @array[opt] exclude any tables to avoid searching
+-- @usage search(_G,math.sin,{package.path}) == 'math.sin'
+-- @return a fieldspec, e.g. 'a.b' or 'math.sin'
+function tablex.search (t,value,exclude)
+ assert_arg_iterable(1,t)
+ local tables = {[t]=true}
+ if exclude then
+ for _,v in pairs(exclude) do tables[v] = true end
+ end
+ return _find(t,value,tables)
+end
+
+--- return an iterator to a table sorted by its keys
+-- @within Iterating
+-- @tab t the table
+-- @func f an optional comparison function (f(x,y) is true if x < y)
+-- @usage for k,v in tablex.sort(t) do print(k,v) end
+-- @return an iterator to traverse elements sorted by the keys
+function tablex.sort(t,f)
+ local keys = {}
+ for k in pairs(t) do keys[#keys + 1] = k end
+ tsort(keys,f)
+ local i = 0
+ return function()
+ i = i + 1
+ return keys[i], t[keys[i]]
+ end
+end
+
+--- return an iterator to a table sorted by its values
+-- @within Iterating
+-- @tab t the table
+-- @func f an optional comparison function (f(x,y) is true if x < y)
+-- @usage for k,v in tablex.sortv(t) do print(k,v) end
+-- @return an iterator to traverse elements sorted by the values
+function tablex.sortv(t,f)
+ f = function_arg(2, f or '<')
+ local keys = {}
+ for k in pairs(t) do keys[#keys + 1] = k end
+ tsort(keys,function(x, y) return f(t[x], t[y]) end)
+ local i = 0
+ return function()
+ i = i + 1
+ return keys[i], t[keys[i]]
+ end
+end
+
+--- modifies a table to be read only.
+-- This only offers weak protection. Tables can still be modified with
+-- `table.insert` and `rawset`.
+-- @tab t the table
+-- @return the table read only.
+function tablex.readonly(t)
+ local mt = {
+ __index=t,
+ __newindex=function(t, k, v) error("Attempt to modify read-only table", 2) end,
+ __pairs=function() return pairs(t) end,
+ __ipairs=function() return ipairs(t) end,
+ __len=function() return #t end,
+ __metatable=false
+ }
+ return setmetatable({}, mt)
+end
+
+return tablex
diff --git a/lualibs/pl/template.lua b/lualibs/pl/template.lua
new file mode 100644
index 0000000000..3de5edae4d
--- /dev/null
+++ b/lualibs/pl/template.lua
@@ -0,0 +1,200 @@
+--- A template preprocessor.
+-- Originally by [Ricki Lake](http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor)
+--
+-- There are two rules:
+--
+-- * lines starting with # are Lua
+-- * otherwise, `$(expr)` is the result of evaluating `expr`
+--
+-- Example:
+--
+-- # for i = 1,3 do
+-- $(i) Hello, Word!
+-- # end
+-- ===>
+-- 1 Hello, Word!
+-- 2 Hello, Word!
+-- 3 Hello, Word!
+--
+-- Other escape characters can be used, when the defaults conflict
+-- with the output language.
+--
+-- > for _,n in pairs{'one','two','three'} do
+-- static int l_${n} (luaState *state);
+-- > end
+--
+-- See @{03-strings.md.Another_Style_of_Template|the Guide}.
+--
+-- Dependencies: `pl.utils`
+-- @module pl.template
+
+local utils = require 'pl.utils'
+
+local append,format,strsub,strfind,strgsub = table.insert,string.format,string.sub,string.find,string.gsub
+
+local APPENDER = "\n__R_size = __R_size + 1; __R_table[__R_size] = "
+
+local function parseDollarParen(pieces, chunk, exec_pat, newline)
+ local s = 1
+ for term, executed, e in chunk:gmatch(exec_pat) do
+ executed = '('..strsub(executed,2,-2)..')'
+ append(pieces, APPENDER..format("%q", strsub(chunk,s, term - 1)))
+ append(pieces, APPENDER..format("(%s or '')", executed))
+ s = e
+ end
+ local r
+ if newline then
+ r = format("%q", strgsub(strsub(chunk,s),"\n",""))
+ else
+ r = format("%q", strsub(chunk,s))
+ end
+ if r ~= '""' then
+ append(pieces, APPENDER..r)
+ end
+end
+
+local function parseHashLines(chunk,inline_escape,brackets,esc,newline)
+ local exec_pat = "()"..inline_escape.."(%b"..brackets..")()"
+
+ local esc_pat = esc.."+([^\n]*\n?)"
+ local esc_pat1, esc_pat2 = "^"..esc_pat, "\n"..esc_pat
+ local pieces, s = {"return function()\nlocal __R_size, __R_table = 0, {}", n = 1}, 1
+ while true do
+ local ss, e, lua = strfind(chunk,esc_pat1, s)
+ if not e then
+ ss, e, lua = strfind(chunk,esc_pat2, s)
+ parseDollarParen(pieces, strsub(chunk,s, ss), exec_pat, newline)
+ if not e then break end
+ end
+ append(pieces, "\n"..lua)
+ s = e + 1
+ end
+ append(pieces, "\nreturn __R_table\nend")
+
+ -- let's check for a special case where there is nothing to template, but it's
+ -- just a single static string
+ local short = false
+ if (#pieces == 3) and (pieces[2]:find(APPENDER, 1, true) == 1) then
+ pieces = { "return " .. pieces[2]:sub(#APPENDER+1,-1) }
+ short = true
+ end
+ -- if short == true, the generated function will not return a table of strings,
+ -- but a single string
+ return table.concat(pieces), short
+end
+
+local template = {}
+
+--- expand the template using the specified environment.
+-- This function will compile and render the template. For more performant
+-- recurring usage use the two step approach by using `compile` and `ct:render`.
+-- There are six special fields in the environment table `env`
+--
+-- * `_parent`: continue looking up in this table (e.g. `_parent=_G`).
+-- * `_brackets`: bracket pair that wraps inline Lua expressions, default is '()'.
+-- * `_escape`: character marking Lua lines, default is '#'
+-- * `_inline_escape`: character marking inline Lua expression, default is '$'.
+-- * `_chunk_name`: chunk name for loaded templates, used if there
+-- is an error in Lua code. Default is 'TMP'.
+-- * `_debug`: if truthy, the generated code will be printed upon a render error
+--
+-- @string str the template string
+-- @tab[opt] env the environment
+-- @return `rendered template + nil + source_code`, or `nil + error + source_code`. The last
+-- return value (`source_code`) is only returned if the debug option is used.
+function template.substitute(str,env)
+ env = env or {}
+ local t, err = template.compile(str, {
+ chunk_name = rawget(env,"_chunk_name"),
+ escape = rawget(env,"_escape"),
+ inline_escape = rawget(env,"_inline_escape"),
+ inline_brackets = rawget(env,"_brackets"),
+ newline = nil,
+ debug = rawget(env,"_debug")
+ })
+ if not t then return t, err end
+
+ return t:render(env, rawget(env,"_parent"), rawget(env,"_debug"))
+end
+
+--- executes the previously compiled template and renders it.
+-- @function ct:render
+-- @tab[opt] env the environment.
+-- @tab[opt] parent continue looking up in this table (e.g. `parent=_G`).
+-- @bool[opt] db if thruthy, it will print the code upon a render error
+-- (provided the template was compiled with the debug option).
+-- @return `rendered template + nil + source_code`, or `nil + error + source_code`. The last return value
+-- (`source_code`) is only returned if the template was compiled with the debug option.
+-- @usage
+-- local ct, err = template.compile(my_template)
+-- local rendered , err = ct:render(my_env, parent)
+local render = function(self, env, parent, db)
+ env = env or {}
+ if parent then -- parent is a bit silly, but for backward compatibility retained
+ setmetatable(env, {__index = parent})
+ end
+ setmetatable(self.env, {__index = env})
+
+ local res, out = xpcall(self.fn, debug.traceback)
+ if not res then
+ if self.code and db then print(self.code) end
+ return nil, out, self.code
+ end
+ return table.concat(out), nil, self.code
+end
+
+--- compiles the template.
+-- Returns an object that can repeatedly be rendered without parsing/compiling
+-- the template again.
+-- The options passed in the `opts` table support the following options:
+--
+-- * `chunk_name`: chunk name for loaded templates, used if there
+-- is an error in Lua code. Default is 'TMP'.
+-- * `escape`: character marking Lua lines, default is '#'
+-- * `inline_escape`: character marking inline Lua expression, default is '$'.
+-- * `inline_brackets`: bracket pair that wraps inline Lua expressions, default is '()'.
+-- * `newline`: string to replace newline characters, default is `nil` (not replacing newlines).
+-- * `debug`: if truthy, the generated source code will be retained within the compiled template object, default is `nil`.
+--
+-- @string str the template string
+-- @tab[opt] opts the compilation options to use
+-- @return template object, or `nil + error + source_code`
+-- @usage
+-- local ct, err = template.compile(my_template)
+-- local rendered , err = ct:render(my_env, parent)
+function template.compile(str, opts)
+ opts = opts or {}
+ local chunk_name = opts.chunk_name or 'TMP'
+ local escape = opts.escape or '#'
+ local inline_escape = opts.inline_escape or '$'
+ local inline_brackets = opts.inline_brackets or '()'
+
+ local code, short = parseHashLines(str,inline_escape,inline_brackets,escape,opts.newline)
+ local env = {}
+ local fn, err = utils.load(code, chunk_name,'t',env)
+ if not fn then return nil, err, code end
+
+ if short then
+ -- the template returns a single constant string, let's optimize for that
+ local constant_string = fn()
+ return {
+ fn = fn(),
+ env = env,
+ render = function(self) -- additional params can be ignored
+ -- skip the metatable magic and error handling in the render
+ -- function above for this special case
+ return constant_string, nil, self.code
+ end,
+ code = opts.debug and code or nil,
+ }
+ end
+
+ return {
+ fn = fn(),
+ env = env,
+ render = render,
+ code = opts.debug and code or nil,
+ }
+end
+
+return template
diff --git a/lualibs/pl/test.lua b/lualibs/pl/test.lua
new file mode 100644
index 0000000000..c77eba7cb5
--- /dev/null
+++ b/lualibs/pl/test.lua
@@ -0,0 +1,164 @@
+--- Useful test utilities.
+--
+-- test.asserteq({1,2},{1,2}) -- can compare tables
+-- test.asserteq(1.2,1.19,0.02) -- compare FP numbers within precision
+-- T = test.tuple -- used for comparing multiple results
+-- test.asserteq(T(string.find(" me","me")),T(2,3))
+--
+-- Dependencies: `pl.utils`, `pl.tablex`, `pl.pretty`, `pl.path`, `debug`
+-- @module pl.test
+
+local tablex = require 'pl.tablex'
+local utils = require 'pl.utils'
+local pretty = require 'pl.pretty'
+local path = require 'pl.path'
+local type,unpack = type,utils.pack
+local clock = os.clock
+local debug = require 'debug'
+local io = io
+
+local function dump(x)
+ if type(x) == 'table' and not (getmetatable(x) and getmetatable(x).__tostring) then
+ return pretty.write(x,' ',true)
+ elseif type(x) == 'string' then
+ return '"'..x..'"'
+ else
+ return tostring(x)
+ end
+end
+
+local test = {}
+
+---- error handling for test results.
+-- By default, this writes to stderr and exits the program.
+-- Re-define this function to raise an error and/or redirect output
+function test.error_handler(file,line,got_text, needed_text,msg)
+ local err = io.stderr
+ err:write(path.basename(file)..':'..line..': assertion failed\n')
+ err:write("got:\t",got_text,'\n')
+ err:write("needed:\t",needed_text,'\n')
+ utils.quit(1,msg or "these values were not equal")
+end
+
+local function complain (x,y,msg,where)
+ local i = debug.getinfo(3 + (where or 0))
+ test.error_handler(i.short_src,i.currentline,dump(x),dump(y),msg)
+end
+
+--- general test complain message.
+-- Useful for composing new test functions (see tests/tablex.lua for an example)
+-- @param x a value
+-- @param y value to compare first value against
+-- @param msg message
+-- @param where extra level offset for errors
+-- @function complain
+test.complain = complain
+
+--- like assert, except takes two arguments that must be equal and can be tables.
+-- If they are plain tables, it will use tablex.deepcompare.
+-- @param x any value
+-- @param y a value equal to x
+-- @param eps an optional tolerance for numerical comparisons
+-- @param where extra level offset
+function test.asserteq (x,y,eps,where)
+ local res = x == y
+ if not res then
+ res = tablex.deepcompare(x,y,true,eps)
+ end
+ if not res then
+ complain(x,y,nil,where)
+ end
+end
+
+--- assert that the first string matches the second.
+-- @param s1 a string
+-- @param s2 a string
+-- @param where extra level offset
+function test.assertmatch (s1,s2,where)
+ if not s1:match(s2) then
+ complain (s1,s2,"these strings did not match",where)
+ end
+end
+
+--- assert that the function raises a particular error.
+-- @param fn a function or a table of the form {function,arg1,...}
+-- @param e a string to match the error against
+-- @param where extra level offset
+function test.assertraise(fn,e,where)
+ local ok, err
+ if type(fn) == 'table' then
+ ok, err = pcall(unpack(fn))
+ else
+ ok, err = pcall(fn)
+ end
+ if not err or err:match(e)==nil then
+ complain (err,e,"these errors did not match",where)
+ end
+end
+
+--- a version of asserteq that takes two pairs of values.
+-- x1==y1 and x2==y2
must be true. Useful for functions that naturally
+-- return two values.
+-- @param x1 any value
+-- @param x2 any value
+-- @param y1 any value
+-- @param y2 any value
+-- @param where extra level offset
+function test.asserteq2 (x1,x2,y1,y2,where)
+ if x1 ~= y1 then complain(x1,y1,nil,where) end
+ if x2 ~= y2 then complain(x2,y2,nil,where) end
+end
+
+-- tuple type --
+
+local tuple_mt = {
+ unpack = table.unpack
+}
+tuple_mt.__index = tuple_mt
+
+function tuple_mt.__tostring(self)
+ local ts = {}
+ for i=1, self.n do
+ local s = self[i]
+ ts[i] = type(s) == 'string' and ('%q'):format(s) or tostring(s)
+ end
+ return 'tuple(' .. table.concat(ts, ', ') .. ')'
+end
+
+function tuple_mt.__eq(a, b)
+ if a.n ~= b.n then return false end
+ for i=1, a.n do
+ if a[i] ~= b[i] then return false end
+ end
+ return true
+end
+
+function tuple_mt.__len(self)
+ return self.n
+end
+
+--- encode an arbitrary argument list as a tuple.
+-- This can be used to compare to other argument lists, which is
+-- very useful for testing functions which return a number of values.
+-- Unlike regular array-like tables ('sequences') they may contain nils.
+-- Tuples understand equality and know how to print themselves out.
+-- The # operator is defined to be the size, irrespecive of any nils,
+-- and there is an `unpack` method.
+-- @usage asserteq(tuple( ('ab'):find 'a'), tuple(1,1))
+function test.tuple(...)
+ return setmetatable(table.pack(...), tuple_mt)
+end
+
+--- Time a function. Call the function a given number of times, and report the number of seconds taken,
+-- together with a message. Any extra arguments will be passed to the function.
+-- @string msg a descriptive message
+-- @int n number of times to call the function
+-- @func fun the function
+-- @param ... optional arguments to fun
+function test.timer(msg,n,fun,...)
+ local start = clock()
+ for i = 1,n do fun(...) end
+ utils.printf("%s: took %7.2f sec\n",msg,clock()-start)
+end
+
+return test
diff --git a/lualibs/pl/text.lua b/lualibs/pl/text.lua
new file mode 100644
index 0000000000..d18fcc0f6f
--- /dev/null
+++ b/lualibs/pl/text.lua
@@ -0,0 +1,248 @@
+--- Text processing utilities.
+--
+-- This provides a Template class (modeled after the same from the Python
+-- libraries, see string.Template). It also provides similar functions to those
+-- found in the textwrap module.
+--
+-- See @{03-strings.md.String_Templates|the Guide}.
+--
+-- Calling `text.format_operator()` overloads the % operator for strings to give Python/Ruby style formated output.
+-- This is extended to also do template-like substitution for map-like data.
+--
+-- > require 'pl.text'.format_operator()
+-- > = '%s = %5.3f' % {'PI',math.pi}
+-- PI = 3.142
+-- > = '$name = $value' % {name='dog',value='Pluto'}
+-- dog = Pluto
+--
+-- Dependencies: `pl.utils`, `pl.types`
+-- @module pl.text
+
+local gsub = string.gsub
+local concat,append = table.concat,table.insert
+local utils = require 'pl.utils'
+local bind1,usplit,assert_arg = utils.bind1,utils.split,utils.assert_arg
+local is_callable = require 'pl.types'.is_callable
+local unpack = utils.unpack
+
+local function makelist(l)
+ return setmetatable(l, require('pl.List'))
+end
+
+local function lstrip(str) return (str:gsub('^%s+','')) end
+local function strip(str) return (lstrip(str):gsub('%s+$','')) end
+local function split(s,delim) return makelist(usplit(s,delim)) end
+
+local function imap(f,t,...)
+ local res = {}
+ for i = 1,#t do res[i] = f(t[i],...) end
+ return res
+end
+
+--[[
+module ('pl.text',utils._module)
+]]
+
+local text = {}
+
+local function _indent (s,sp)
+ local sl = split(s,'\n')
+ return concat(imap(bind1('..',sp),sl),'\n')..'\n'
+end
+
+--- indent a multiline string.
+-- @param s the string
+-- @param n the size of the indent
+-- @param ch the character to use when indenting (default ' ')
+-- @return indented string
+function text.indent (s,n,ch)
+ assert_arg(1,s,'string')
+ assert_arg(2,n,'number')
+ return _indent(s,string.rep(ch or ' ',n))
+end
+
+--- dedent a multiline string by removing any initial indent.
+-- useful when working with [[..]] strings.
+-- @param s the string
+-- @return a string with initial indent zero.
+function text.dedent (s)
+ assert_arg(1,s,'string')
+ local sl = split(s,'\n')
+ local i1,i2 = (#sl>0 and sl[1] or ''):find('^%s*')
+ sl = imap(string.sub,sl,i2+1)
+ return concat(sl,'\n')..'\n'
+end
+
+--- format a paragraph into lines so that they fit into a line width.
+-- It will not break long words, so lines can be over the length
+-- to that extent.
+-- @param s the string
+-- @param width the margin width, default 70
+-- @return a list of lines
+function text.wrap (s,width)
+ assert_arg(1,s,'string')
+ width = width or 70
+ s = s:gsub('\n',' ')
+ local i,nxt = 1
+ local lines,line = {}
+ while i < #s do
+ nxt = i+width
+ if s:find("[%w']",nxt) then -- inside a word
+ nxt = s:find('%W',nxt+1) -- so find word boundary
+ end
+ line = s:sub(i,nxt)
+ i = i + #line
+ append(lines,strip(line))
+ end
+ return makelist(lines)
+end
+
+--- format a paragraph so that it fits into a line width.
+-- @param s the string
+-- @param width the margin width, default 70
+-- @return a string
+-- @see wrap
+function text.fill (s,width)
+ return concat(text.wrap(s,width),'\n') .. '\n'
+end
+
+local Template = {}
+text.Template = Template
+Template.__index = Template
+setmetatable(Template, {
+ __call = function(obj,tmpl)
+ return Template.new(tmpl)
+ end})
+
+function Template.new(tmpl)
+ assert_arg(1,tmpl,'string')
+ local res = {}
+ res.tmpl = tmpl
+ setmetatable(res,Template)
+ return res
+end
+
+local function _substitute(s,tbl,safe)
+ local subst
+ if is_callable(tbl) then
+ subst = tbl
+ else
+ function subst(f)
+ local s = tbl[f]
+ if not s then
+ if safe then
+ return f
+ else
+ error("not present in table "..f)
+ end
+ else
+ return s
+ end
+ end
+ end
+ local res = gsub(s,'%${([%w_]+)}',subst)
+ return (gsub(res,'%$([%w_]+)',subst))
+end
+
+--- substitute values into a template, throwing an error.
+-- This will throw an error if no name is found.
+-- @param tbl a table of name-value pairs.
+function Template:substitute(tbl)
+ assert_arg(1,tbl,'table')
+ return _substitute(self.tmpl,tbl,false)
+end
+
+--- substitute values into a template.
+-- This version just passes unknown names through.
+-- @param tbl a table of name-value pairs.
+function Template:safe_substitute(tbl)
+ assert_arg(1,tbl,'table')
+ return _substitute(self.tmpl,tbl,true)
+end
+
+--- substitute values into a template, preserving indentation.
+-- If the value is a multiline string _or_ a template, it will insert
+-- the lines at the correct indentation.
+-- Furthermore, if a template, then that template will be subsituted
+-- using the same table.
+-- @param tbl a table of name-value pairs.
+function Template:indent_substitute(tbl)
+ assert_arg(1,tbl,'table')
+ if not self.strings then
+ self.strings = split(self.tmpl,'\n')
+ end
+ -- the idea is to substitute line by line, grabbing any spaces as
+ -- well as the $var. If the value to be substituted contains newlines,
+ -- then we split that into lines and adjust the indent before inserting.
+ local function subst(line)
+ return line:gsub('(%s*)%$([%w_]+)',function(sp,f)
+ local subtmpl
+ local s = tbl[f]
+ if not s then error("not present in table "..f) end
+ if getmetatable(s) == Template then
+ subtmpl = s
+ s = s.tmpl
+ else
+ s = tostring(s)
+ end
+ if s:find '\n' then
+ s = _indent(s,sp)
+ end
+ if subtmpl then return _substitute(s,tbl)
+ else return s
+ end
+ end)
+ end
+ local lines = imap(subst,self.strings)
+ return concat(lines,'\n')..'\n'
+end
+
+------- Python-style formatting operator ------
+-- (see the lua-users wiki) --
+
+function text.format_operator()
+
+ local format = string.format
+
+ -- a more forgiving version of string.format, which applies
+ -- tostring() to any value with a %s format.
+ local function formatx (fmt,...)
+ local args = {...}
+ local i = 1
+ for p in fmt:gmatch('%%.') do
+ if p == '%s' and type(args[i]) ~= 'string' then
+ args[i] = tostring(args[i])
+ end
+ i = i + 1
+ end
+ return format(fmt,unpack(args))
+ end
+
+ local function basic_subst(s,t)
+ return (s:gsub('%$([%w_]+)',t))
+ end
+
+ -- Note this goes further than the original, and will allow these cases:
+ -- 1. a single value
+ -- 2. a list of values
+ -- 3. a map of var=value pairs
+ -- 4. a function, as in gsub
+ -- For the second two cases, it uses $-variable substituion.
+ getmetatable("").__mod = function(a, b)
+ if b == nil then
+ return a
+ elseif type(b) == "table" and getmetatable(b) == nil then
+ if #b == 0 then -- assume a map-like table
+ return _substitute(a,b,true)
+ else
+ return formatx(a,unpack(b))
+ end
+ elseif type(b) == 'function' then
+ return basic_subst(a,b)
+ else
+ return formatx(a,b)
+ end
+ end
+end
+
+return text
diff --git a/lualibs/pl/types.lua b/lualibs/pl/types.lua
new file mode 100644
index 0000000000..f4ab236513
--- /dev/null
+++ b/lualibs/pl/types.lua
@@ -0,0 +1,145 @@
+---- Dealing with Detailed Type Information
+
+-- Dependencies `pl.utils`
+-- @module pl.types
+
+local utils = require 'pl.utils'
+local types = {}
+
+--- is the object either a function or a callable object?.
+-- @param obj Object to check.
+function types.is_callable (obj)
+ return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call and true
+end
+
+--- is the object of the specified type?.
+-- If the type is a string, then use type, otherwise compare with metatable
+-- @param obj An object to check
+-- @param tp String of what type it should be
+-- @function is_type
+types.is_type = utils.is_type
+
+local fileMT = getmetatable(io.stdout)
+
+--- a string representation of a type.
+-- For tables with metatables, we assume that the metatable has a `_name`
+-- field. Knows about Lua file objects.
+-- @param obj an object
+-- @return a string like 'number', 'table' or 'List'
+function types.type (obj)
+ local t = type(obj)
+ if t == 'table' or t == 'userdata' then
+ local mt = getmetatable(obj)
+ if mt == fileMT then
+ return 'file'
+ elseif mt == nil then
+ return t
+ else
+ return mt._name or "unknown "..t
+ end
+ else
+ return t
+ end
+end
+
+--- is this number an integer?
+-- @param x a number
+-- @raise error if x is not a number
+function types.is_integer (x)
+ return math.ceil(x)==x
+end
+
+--- Check if the object is "empty".
+-- An object is considered empty if it is nil, a table with out any items (key,
+-- value pairs or indexes), or a string with no content ("").
+-- @param o The object to check if it is empty.
+-- @param ignore_spaces If the object is a string and this is true the string is
+-- considered empty is it only contains spaces.
+-- @return true if the object is empty, otherwise false.
+function types.is_empty(o, ignore_spaces)
+ if o == nil or (type(o) == "table" and not next(o)) or (type(o) == "string" and (o == "" or (ignore_spaces and o:match("^%s+$")))) then
+ return true
+ end
+ return false
+end
+
+local function check_meta (val)
+ if type(val) == 'table' then return true end
+ return getmetatable(val)
+end
+
+--- is an object 'array-like'?
+-- @param val any value.
+function types.is_indexable (val)
+ local mt = check_meta(val)
+ if mt == true then return true end
+ return mt and mt.__len and mt.__index and true
+end
+
+--- can an object be iterated over with `pairs`?
+-- @param val any value.
+function types.is_iterable (val)
+ local mt = check_meta(val)
+ if mt == true then return true end
+ return mt and mt.__pairs and true
+end
+
+--- can an object accept new key/pair values?
+-- @param val any value.
+function types.is_writeable (val)
+ local mt = check_meta(val)
+ if mt == true then return true end
+ return mt and mt.__newindex and true
+end
+
+-- Strings that should evaluate to true.
+local trues = { yes=true, y=true, ["true"]=true, t=true, ["1"]=true }
+-- Conditions types should evaluate to true.
+local true_types = {
+ boolean=function(o, true_strs, check_objs) return o end,
+ string=function(o, true_strs, check_objs)
+ if trues[o:lower()] then
+ return true
+ end
+ -- Check alternative user provided strings.
+ for _,v in ipairs(true_strs or {}) do
+ if type(v) == "string" and o == v:lower() then
+ return true
+ end
+ end
+ return false
+ end,
+ number=function(o, true_strs, check_objs) return o ~= 0 end,
+ table=function(o, true_strs, check_objs) if check_objs and next(o) ~= nil then return true end return false end
+}
+--- Convert to a boolean value.
+-- True values are:
+--
+-- * boolean: true.
+-- * string: 'yes', 'y', 'true', 't', '1' or additional strings specified by `true_strs`.
+-- * number: Any non-zero value.
+-- * table: Is not empty and `check_objs` is true.
+-- * object: Is not `nil` and `check_objs` is true.
+--
+-- @param o The object to evaluate.
+-- @param[opt] true_strs optional Additional strings that when matched should evaluate to true. Comparison is case insensitive.
+-- This should be a List of strings. E.g. "ja" to support German.
+-- @param[opt] check_objs True if objects should be evaluated. Default is to evaluate objects as true if not nil
+-- or if it is a table and it is not empty.
+-- @return true if the input evaluates to true, otherwise false.
+function types.to_bool(o, true_strs, check_objs)
+ local true_func
+ if true_strs then
+ utils.assert_arg(2, true_strs, "table")
+ end
+ true_func = true_types[type(o)]
+ if true_func then
+ return true_func(o, true_strs, check_objs)
+ elseif check_objs and o ~= nil then
+ return true
+ end
+ return false
+end
+
+
+return types
diff --git a/lualibs/pl/url.lua b/lualibs/pl/url.lua
new file mode 100644
index 0000000000..962887151e
--- /dev/null
+++ b/lualibs/pl/url.lua
@@ -0,0 +1,49 @@
+--- Python-style URL quoting library.
+--
+-- @module pl.url
+
+local url = {}
+
+local function quote_char(c)
+ return string.format("%%%02X", string.byte(c))
+end
+
+--- Quote the url, replacing special characters using the '%xx' escape.
+-- @string s the string
+-- @bool quote_plus Also escape slashes and replace spaces by plus signs.
+function url.quote(s, quote_plus)
+ if type(s) ~= "string" then
+ return s
+ end
+
+ s = s:gsub("\n", "\r\n")
+ s = s:gsub("([^A-Za-z0-9 %-_%./])", quote_char)
+ if quote_plus then
+ s = s:gsub(" ", "+")
+ s = s:gsub("/", quote_char)
+ else
+ s = s:gsub(" ", "%%20")
+ end
+
+ return s
+end
+
+local function unquote_char(h)
+ return string.char(tonumber(h, 16))
+end
+
+--- Unquote the url, replacing '%xx' escapes and plus signs.
+-- @string s the string
+function url.unquote(s)
+ if type(s) ~= "string" then
+ return s
+ end
+
+ s = s:gsub("+", " ")
+ s = s:gsub("%%(%x%x)", unquote_char)
+ s = s:gsub("\r\n", "\n")
+
+ return s
+end
+
+return url
diff --git a/lualibs/pl/utils.lua b/lualibs/pl/utils.lua
new file mode 100644
index 0000000000..5fd11a2912
--- /dev/null
+++ b/lualibs/pl/utils.lua
@@ -0,0 +1,517 @@
+--- Generally useful routines.
+-- See @{01-introduction.md.Generally_useful_functions|the Guide}.
+--
+-- Dependencies: `pl.compat`
+--
+-- @module pl.utils
+local format = string.format
+local compat = require 'pl.compat'
+local stdout = io.stdout
+local append = table.insert
+local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
+
+local utils = {
+ _VERSION = "1.5.2",
+ lua51 = compat.lua51,
+ setfenv = compat.setfenv,
+ getfenv = compat.getfenv,
+ load = compat.load,
+ execute = compat.execute,
+ dir_separator = compat.dir_separator,
+ is_windows = compat.is_windows,
+ unpack = unpack
+}
+
+--- end this program gracefully.
+-- @param code The exit code or a message to be printed
+-- @param ... extra arguments for message's format'
+-- @see utils.fprintf
+function utils.quit(code,...)
+ if type(code) == 'string' then
+ utils.fprintf(io.stderr,code,...)
+ code = -1
+ else
+ utils.fprintf(io.stderr,...)
+ end
+ io.stderr:write('\n')
+ os.exit(code)
+end
+
+--- print an arbitrary number of arguments using a format.
+-- @param fmt The format (see string.format)
+-- @param ... Extra arguments for format
+function utils.printf(fmt,...)
+ utils.assert_string(1,fmt)
+ utils.fprintf(stdout,fmt,...)
+end
+
+--- write an arbitrary number of arguments to a file using a format.
+-- @param f File handle to write to.
+-- @param fmt The format (see string.format).
+-- @param ... Extra arguments for format
+function utils.fprintf(f,fmt,...)
+ utils.assert_string(2,fmt)
+ f:write(format(fmt,...))
+end
+
+local function import_symbol(T,k,v,libname)
+ local key = rawget(T,k)
+ -- warn about collisions!
+ if key and k ~= '_M' and k ~= '_NAME' and k ~= '_PACKAGE' and k ~= '_VERSION' then
+ utils.fprintf(io.stderr,"warning: '%s.%s' will not override existing symbol\n",libname,k)
+ return
+ end
+ rawset(T,k,v)
+end
+
+local function lookup_lib(T,t)
+ for k,v in pairs(T) do
+ if v == t then return k end
+ end
+ return '?'
+end
+
+local already_imported = {}
+
+--- take a table and 'inject' it into the local namespace.
+-- @param t The Table
+-- @param T An optional destination table (defaults to callers environment)
+function utils.import(t,T)
+ T = T or _G
+ t = t or utils
+ if type(t) == 'string' then
+ t = require (t)
+ end
+ local libname = lookup_lib(T,t)
+ if already_imported[t] then return end
+ already_imported[t] = libname
+ for k,v in pairs(t) do
+ import_symbol(T,k,v,libname)
+ end
+end
+
+utils.patterns = {
+ FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
+ INTEGER = '[+%-%d]%d*',
+ IDEN = '[%a_][%w_]*',
+ FILE = '[%a%.\\][:%][%w%._%-\\]*'
+}
+
+--- escape any 'magic' characters in a string
+-- @param s The input string
+function utils.escape(s)
+ utils.assert_string(1,s)
+ return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'))
+end
+
+--- return either of two values, depending on a condition.
+-- @param cond A condition
+-- @param value1 Value returned if cond is true
+-- @param value2 Value returned if cond is false (can be optional)
+function utils.choose(cond,value1,value2)
+ if cond then return value1
+ else return value2
+ end
+end
+
+local raise
+
+--- return the contents of a file as a string
+-- @param filename The file path
+-- @param is_bin open in binary mode
+-- @return file contents
+function utils.readfile(filename,is_bin)
+ local mode = is_bin and 'b' or ''
+ utils.assert_string(1,filename)
+ local f,open_err = io.open(filename,'r'..mode)
+ if not f then return utils.raise (open_err) end
+ local res,read_err = f:read('*a')
+ f:close()
+ if not res then
+ -- Errors in io.open have "filename: " prefix,
+ -- error in file:read don't, add it.
+ return raise (filename..": "..read_err)
+ end
+ return res
+end
+
+--- write a string to a file
+-- @param filename The file path
+-- @param str The string
+-- @param is_bin open in binary mode
+-- @return true or nil
+-- @return error message
+-- @raise error if filename or str aren't strings
+function utils.writefile(filename,str,is_bin)
+ local mode = is_bin and 'b' or ''
+ utils.assert_string(1,filename)
+ utils.assert_string(2,str)
+ local f,err = io.open(filename,'w'..mode)
+ if not f then return raise(err) end
+ f:write(str)
+ f:close()
+ return true
+end
+
+--- return the contents of a file as a list of lines
+-- @param filename The file path
+-- @return file contents as a table
+-- @raise errror if filename is not a string
+function utils.readlines(filename)
+ utils.assert_string(1,filename)
+ local f,err = io.open(filename,'r')
+ if not f then return raise(err) end
+ local res = {}
+ for line in f:lines() do
+ append(res,line)
+ end
+ f:close()
+ return res
+end
+
+--- split a string into a list of strings separated by a delimiter.
+-- @param s The input string
+-- @param re A Lua string pattern; defaults to '%s+'
+-- @param plain don't use Lua patterns
+-- @param n optional maximum number of splits
+-- @return a list-like table
+-- @raise error if s is not a string
+function utils.split(s,re,plain,n)
+ utils.assert_string(1,s)
+ local find,sub,append = string.find, string.sub, table.insert
+ local i1,ls = 1,{}
+ if not re then re = '%s+' end
+ if re == '' then return {s} end
+ while true do
+ local i2,i3 = find(s,re,i1,plain)
+ if not i2 then
+ local last = sub(s,i1)
+ if last ~= '' then append(ls,last) end
+ if #ls == 1 and ls[1] == '' then
+ return {}
+ else
+ return ls
+ end
+ end
+ append(ls,sub(s,i1,i2-1))
+ if n and #ls == n then
+ ls[#ls] = sub(s,i1)
+ return ls
+ end
+ i1 = i3+1
+ end
+end
+
+--- split a string into a number of values.
+-- @param s the string
+-- @param re the delimiter, default space
+-- @return n values
+-- @usage first,next = splitv('jane:doe',':')
+-- @see split
+function utils.splitv (s,re)
+ return unpack(utils.split(s,re))
+end
+
+--- convert an array of values to strings.
+-- @param t a list-like table
+-- @param temp buffer to use, otherwise allocate
+-- @param tostr custom tostring function, called with (value,index).
+-- Otherwise use `tostring`
+-- @return the converted buffer
+function utils.array_tostring (t,temp,tostr)
+ temp, tostr = temp or {}, tostr or tostring
+ for i = 1,#t do
+ temp[i] = tostr(t[i],i)
+ end
+ return temp
+end
+
+local is_windows = utils.is_windows
+
+--- Quote an argument of a command.
+-- Quotes a single argument of a command to be passed
+-- to `os.execute`, `pl.utils.execute` or `pl.utils.executeex`.
+-- @string argument the argument.
+-- @return quoted argument.
+function utils.quote_arg(argument)
+ if is_windows then
+ if argument == "" or argument:find('[ \f\t\v]') then
+ -- Need to quote the argument.
+ -- Quotes need to be escaped with backslashes;
+ -- additionally, backslashes before a quote, escaped or not,
+ -- need to be doubled.
+ -- See documentation for CommandLineToArgvW Windows function.
+ argument = '"' .. argument:gsub([[(\*)"]], [[%1%1\"]]):gsub([[\+$]], "%0%0") .. '"'
+ end
+
+ -- os.execute() uses system() C function, which on Windows passes command
+ -- to cmd.exe. Escape its special characters.
+ return (argument:gsub('["^<>!|&%%]', "^%0"))
+ else
+ if argument == "" or argument:find('[^a-zA-Z0-9_@%+=:,./-]') then
+ -- To quote arguments on posix-like systems use single quotes.
+ -- To represent an embedded single quote close quoted string ('),
+ -- add escaped quote (\'), open quoted string again (').
+ argument = "'" .. argument:gsub("'", [['\'']]) .. "'"
+ end
+
+ return argument
+ end
+end
+
+--- execute a shell command and return the output.
+-- This function redirects the output to tempfiles and returns the content of those files.
+-- @param cmd a shell command
+-- @param bin boolean, if true, read output as binary file
+-- @return true if successful
+-- @return actual return code
+-- @return stdout output (string)
+-- @return errout output (string)
+function utils.executeex(cmd, bin)
+ local mode
+ local outfile = os.tmpname()
+ local errfile = os.tmpname()
+
+ if is_windows and not outfile:find(':') then
+ outfile = os.getenv('TEMP')..outfile
+ errfile = os.getenv('TEMP')..errfile
+ end
+ cmd = cmd .. " > " .. utils.quote_arg(outfile) .. " 2> " .. utils.quote_arg(errfile)
+
+ local success, retcode = utils.execute(cmd)
+ local outcontent = utils.readfile(outfile, bin)
+ local errcontent = utils.readfile(errfile, bin)
+ os.remove(outfile)
+ os.remove(errfile)
+ return success, retcode, (outcontent or ""), (errcontent or "")
+end
+
+--- 'memoize' a function (cache returned value for next call).
+-- This is useful if you have a function which is relatively expensive,
+-- but you don't know in advance what values will be required, so
+-- building a table upfront is wasteful/impossible.
+-- @param func a function of at least one argument
+-- @return a function with at least one argument, which is used as the key.
+function utils.memoize(func)
+ local cache = {}
+ return function(k)
+ local res = cache[k]
+ if res == nil then
+ res = func(k)
+ cache[k] = res
+ end
+ return res
+ end
+end
+
+
+utils.stdmt = {
+ List = {_name='List'}, Map = {_name='Map'},
+ Set = {_name='Set'}, MultiMap = {_name='MultiMap'}
+}
+
+local _function_factories = {}
+
+--- associate a function factory with a type.
+-- A function factory takes an object of the given type and
+-- returns a function for evaluating it
+-- @tab mt metatable
+-- @func fun a callable that returns a function
+function utils.add_function_factory (mt,fun)
+ _function_factories[mt] = fun
+end
+
+local function _string_lambda(f)
+ local raise = utils.raise
+ if f:find '^|' or f:find '_' then
+ local args,body = f:match '|([^|]*)|(.+)'
+ if f:find '_' then
+ args = '_'
+ body = f
+ else
+ if not args then return raise 'bad string lambda' end
+ end
+ local fstr = 'return function('..args..') return '..body..' end'
+ local fn,err = utils.load(fstr)
+ if not fn then return raise(err) end
+ fn = fn()
+ return fn
+ else return raise 'not a string lambda'
+ end
+end
+
+--- an anonymous function as a string. This string is either of the form
+-- '|args| expression' or is a function of one argument, '_'
+-- @param lf function as a string
+-- @return a function
+-- @usage string_lambda '|x|x+1' (2) == 3
+-- @usage string_lambda '_+1' (2) == 3
+-- @function utils.string_lambda
+utils.string_lambda = utils.memoize(_string_lambda)
+
+local ops
+
+--- process a function argument.
+-- This is used throughout Penlight and defines what is meant by a function:
+-- Something that is callable, or an operator string as defined by pl.operator
,
+-- such as '>' or '#'. If a function factory has been registered for the type, it will
+-- be called to get the function.
+-- @param idx argument index
+-- @param f a function, operator string, or callable object
+-- @param msg optional error message
+-- @return a callable
+-- @raise if idx is not a number or if f is not callable
+function utils.function_arg (idx,f,msg)
+ utils.assert_arg(1,idx,'number')
+ local tp = type(f)
+ if tp == 'function' then return f end -- no worries!
+ -- ok, a string can correspond to an operator (like '==')
+ if tp == 'string' then
+ if not ops then ops = require 'pl.operator'.optable end
+ local fn = ops[f]
+ if fn then return fn end
+ local fn, err = utils.string_lambda(f)
+ if not fn then error(err..': '..f) end
+ return fn
+ elseif tp == 'table' or tp == 'userdata' then
+ local mt = getmetatable(f)
+ if not mt then error('not a callable object',2) end
+ local ff = _function_factories[mt]
+ if not ff then
+ if not mt.__call then error('not a callable object',2) end
+ return f
+ else
+ return ff(f) -- we have a function factory for this type!
+ end
+ end
+ if not msg then msg = " must be callable" end
+ if idx > 0 then
+ error("argument "..idx..": "..msg,2)
+ else
+ error(msg,2)
+ end
+end
+
+--- bind the first argument of the function to a value.
+-- @param fn a function of at least two values (may be an operator string)
+-- @param p a value
+-- @return a function such that f(x) is fn(p,x)
+-- @raise same as @{function_arg}
+-- @see func.bind1
+function utils.bind1 (fn,p)
+ fn = utils.function_arg(1,fn)
+ return function(...) return fn(p,...) end
+end
+
+--- bind the second argument of the function to a value.
+-- @param fn a function of at least two values (may be an operator string)
+-- @param p a value
+-- @return a function such that f(x) is fn(x,p)
+-- @raise same as @{function_arg}
+function utils.bind2 (fn,p)
+ fn = utils.function_arg(1,fn)
+ return function(x,...) return fn(x,p,...) end
+end
+
+
+--- assert that the given argument is in fact of the correct type.
+-- @param n argument index
+-- @param val the value
+-- @param tp the type
+-- @param verify an optional verification function
+-- @param msg an optional custom message
+-- @param lev optional stack position for trace, default 2
+-- @raise if the argument n is not the correct type
+-- @usage assert_arg(1,t,'table')
+-- @usage assert_arg(n,val,'string',path.isdir,'not a directory')
+function utils.assert_arg (n,val,tp,verify,msg,lev)
+ if type(val) ~= tp then
+ error(("argument %d expected a '%s', got a '%s'"):format(n,tp,type(val)),lev or 2)
+ end
+ if verify and not verify(val) then
+ error(("argument %d: '%s' %s"):format(n,val,msg),lev or 2)
+ end
+end
+
+--- assert the common case that the argument is a string.
+-- @param n argument index
+-- @param val a value that must be a string
+-- @raise val must be a string
+function utils.assert_string (n,val)
+ utils.assert_arg(n,val,'string',nil,nil,3)
+end
+
+local err_mode = 'default'
+
+--- control the error strategy used by Penlight.
+-- Controls how utils.raise
works; the default is for it
+-- to return nil and the error string, but if the mode is 'error' then
+-- it will throw an error. If mode is 'quit' it will immediately terminate
+-- the program.
+-- @param mode - either 'default', 'quit' or 'error'
+-- @see utils.raise
+function utils.on_error (mode)
+ if ({['default'] = 1, ['quit'] = 2, ['error'] = 3})[mode] then
+ err_mode = mode
+ else
+ -- fail loudly
+ if err_mode == 'default' then err_mode = 'error' end
+ utils.raise("Bad argument expected string; 'default', 'quit', or 'error'. Got '"..tostring(mode).."'")
+ end
+end
+
+--- used by Penlight functions to return errors. Its global behaviour is controlled
+-- by utils.on_error
+-- @param err the error string.
+-- @see utils.on_error
+function utils.raise (err)
+ if err_mode == 'default' then return nil,err
+ elseif err_mode == 'quit' then utils.quit(err)
+ else error(err,2)
+ end
+end
+
+--- is the object of the specified type?.
+-- If the type is a string, then use type, otherwise compare with metatable
+-- @param obj An object to check
+-- @param tp String of what type it should be
+function utils.is_type (obj,tp)
+ if type(tp) == 'string' then return type(obj) == tp end
+ local mt = getmetatable(obj)
+ return tp == mt
+end
+
+raise = utils.raise
+
+--- load a code string or bytecode chunk.
+-- @param code Lua code as a string or bytecode
+-- @param name for source errors
+-- @param mode kind of chunk, 't' for text, 'b' for bytecode, 'bt' for all (default)
+-- @param env the environment for the new chunk (default nil)
+-- @return compiled chunk
+-- @return error message (chunk is nil)
+-- @function utils.load
+
+---------------
+-- Get environment of a function.
+-- With Lua 5.2, may return nil for a function with no global references!
+-- Based on code by [Sergey Rozhenko](http://lua-users.org/lists/lua-l/2010-06/msg00313.html)
+-- @param f a function or a call stack reference
+-- @function utils.getfenv
+
+---------------
+-- Set environment of a function
+-- @param f a function or a call stack reference
+-- @param env a table that becomes the new environment of `f`
+-- @function utils.setfenv
+
+--- execute a shell command.
+-- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2
+-- @param cmd a shell command
+-- @return true if successful
+-- @return actual return code
+-- @function utils.execute
+
+return utils
+
+
diff --git a/lualibs/pl/xml.lua b/lualibs/pl/xml.lua
new file mode 100644
index 0000000000..ae26f5e0a9
--- /dev/null
+++ b/lualibs/pl/xml.lua
@@ -0,0 +1,778 @@
+--- XML LOM Utilities.
+--
+-- This implements some useful things on [LOM](http://matthewwild.co.uk/projects/luaexpat/lom.html) documents, such as returned by `lxp.lom.parse`.
+-- In particular, it can convert LOM back into XML text, with optional pretty-printing control.
+-- It is s based on stanza.lua from [Prosody](http://hg.prosody.im/trunk/file/4621c92d2368/util/stanza.lua)
+--
+-- > d = xml.parse "alice"
+-- > = d
+-- alice
+-- > = xml.tostring(d,'',' ')
+--
+-- alice
+--
+--
+-- Can be used as a lightweight one-stop-shop for simple XML processing; a simple XML parser is included
+-- but the default is to use `lxp.lom` if it can be found.
+--
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain--
+-- classic Lua XML parser by Roberto Ierusalimschy.
+-- modified to output LOM format.
+-- http://lua-users.org/wiki/LuaXml
+--
+-- See @{06-data.md.XML|the Guide}
+--
+-- Dependencies: `pl.utils`
+--
+-- Soft Dependencies: `lxp.lom` (fallback is to use basic Lua parser)
+-- @module pl.xml
+
+local utils = require 'pl.utils'
+local split = utils.split;
+local t_insert = table.insert;
+local t_concat = table.concat;
+local t_remove = table.remove;
+local s_match = string.match;
+local tostring = tostring;
+local setmetatable = setmetatable;
+local getmetatable = getmetatable;
+local pairs = pairs;
+local ipairs = ipairs;
+local type = type;
+local next = next;
+local print = print;
+local unpack = utils.unpack;
+local s_gsub = string.gsub;
+local s_find = string.find;
+local pcall,require,io = pcall,require,io
+
+local _M = {}
+local Doc = { __type = "doc" };
+Doc.__index = Doc;
+
+--- create a new document node.
+-- @param tag the tag name
+-- @param attr optional attributes (table of name-value pairs)
+function _M.new(tag, attr)
+ local doc = { tag = tag, attr = attr or {}, last_add = {}};
+ return setmetatable(doc, Doc);
+end
+
+--- parse an XML document. By default, this uses lxp.lom.parse, but
+-- falls back to basic_parse, or if use_basic is true
+-- @param text_or_file file or string representation
+-- @param is_file whether text_or_file is a file name or not
+-- @param use_basic do a basic parse
+-- @return a parsed LOM document with the document metatatables set
+-- @return nil, error the error can either be a file error or a parse error
+function _M.parse(text_or_file, is_file, use_basic)
+ local parser,status,lom
+ if use_basic then parser = _M.basic_parse
+ else
+ status,lom = pcall(require,'lxp.lom')
+ if not status then parser = _M.basic_parse else parser = lom.parse end
+ end
+ if is_file then
+ local f,err = io.open(text_or_file)
+ if not f then return nil,err end
+ text_or_file = f:read '*a'
+ f:close()
+ end
+ local doc,err = parser(text_or_file)
+ if not doc then return nil,err end
+ if lom then
+ _M.walk(doc,false,function(_,d)
+ setmetatable(d,Doc)
+ end)
+ end
+ return doc
+end
+
+---- convenient function to add a document node, This updates the last inserted position.
+-- @param tag a tag name
+-- @param attrs optional set of attributes (name-string pairs)
+function Doc:addtag(tag, attrs)
+ local s = _M.new(tag, attrs);
+ (self.last_add[#self.last_add] or self):add_direct_child(s);
+ t_insert(self.last_add, s);
+ return self;
+end
+
+--- convenient function to add a text node. This updates the last inserted position.
+-- @param text a string
+function Doc:text(text)
+ (self.last_add[#self.last_add] or self):add_direct_child(text);
+ return self;
+end
+
+---- go up one level in a document
+function Doc:up()
+ t_remove(self.last_add);
+ return self;
+end
+
+function Doc:reset()
+ local last_add = self.last_add;
+ for i = 1,#last_add do
+ last_add[i] = nil;
+ end
+ return self;
+end
+
+--- append a child to a document directly.
+-- @param child a child node (either text or a document)
+function Doc:add_direct_child(child)
+ t_insert(self, child);
+end
+
+--- append a child to a document at the last element added
+-- @param child a child node (either text or a document)
+function Doc:add_child(child)
+ (self.last_add[#self.last_add] or self):add_direct_child(child);
+ return self;
+end
+
+--accessing attributes: useful not to have to expose implementation (attr)
+--but also can allow attr to be nil in any future optimizations
+
+--- set attributes of a document node.
+-- @param t a table containing attribute/value pairs
+function Doc:set_attribs (t)
+ for k,v in pairs(t) do
+ self.attr[k] = v
+ end
+end
+
+--- set a single attribute of a document node.
+-- @param a attribute
+-- @param v its value
+function Doc:set_attrib(a,v)
+ self.attr[a] = v
+end
+
+--- access the attributes of a document node.
+function Doc:get_attribs()
+ return self.attr
+end
+
+local function is_text(s) return type(s) == 'string' end
+
+--- function to create an element with a given tag name and a set of children.
+-- @param tag a tag name
+-- @param items either text or a table where the hash part is the attributes and the list part is the children.
+function _M.elem(tag,items)
+ local s = _M.new(tag)
+ if is_text(items) then items = {items} end
+ if _M.is_tag(items) then
+ t_insert(s,items)
+ elseif type(items) == 'table' then
+ for k,v in pairs(items) do
+ if is_text(k) then
+ s.attr[k] = v
+ t_insert(s.attr,k)
+ else
+ s[k] = v
+ end
+ end
+ end
+ return s
+end
+
+--- given a list of names, return a number of element constructors.
+-- @param list a list of names, or a comma-separated string.
+-- @usage local parent,children = doc.tags 'parent,children'
+-- doc = parent {child 'one', child 'two'}
+function _M.tags(list)
+ local ctors = {}
+ local elem = _M.elem
+ if is_text(list) then list = split(list,'%s*,%s*') end
+ for _,tag in ipairs(list) do
+ local ctor = function(items) return _M.elem(tag,items) end
+ t_insert(ctors,ctor)
+ end
+ return unpack(ctors)
+end
+
+local templ_cache = {}
+
+local function template_cache (templ)
+ if is_text(templ) then
+ if templ_cache[templ] then
+ templ = templ_cache[templ]
+ else
+ local str,err = templ
+ templ,err = _M.parse(str,false,true)
+ if not templ then return nil,err end
+ templ_cache[str] = templ
+ end
+ elseif not _M.is_tag(templ) then
+ return nil, "template is not a document"
+ end
+ return templ
+end
+
+local function is_data(data)
+ return #data == 0 or type(data[1]) ~= 'table'
+end
+
+local function prepare_data(data)
+ -- a hack for ensuring that $1 maps to first element of data, etc.
+ -- Either this or could change the gsub call just below.
+ for i,v in ipairs(data) do
+ data[tostring(i)] = v
+ end
+end
+
+--- create a substituted copy of a document,
+-- @param templ may be a document or a string representation which will be parsed and cached
+-- @param data a table of name-value pairs or a list of such tables
+-- @return an XML document
+function Doc.subst(templ, data)
+ local err
+ if type(data) ~= 'table' or not next(data) then return nil, "data must be a non-empty table" end
+ if is_data(data) then
+ prepare_data(data)
+ end
+ templ,err = template_cache(templ)
+ if err then return nil, err end
+ local function _subst(item)
+ return _M.clone(templ,function(s)
+ return s:gsub('%$(%w+)',item)
+ end)
+ end
+ if is_data(data) then return _subst(data) end
+ local list = {}
+ for _,item in ipairs(data) do
+ prepare_data(item)
+ t_insert(list,_subst(item))
+ end
+ if data.tag then
+ list = _M.elem(data.tag,list)
+ end
+ return list
+end
+
+
+--- get the first child with a given tag name.
+-- @param tag the tag name
+function Doc:child_with_name(tag)
+ for _, child in ipairs(self) do
+ if child.tag == tag then return child; end
+ end
+end
+
+local _children_with_name
+function _children_with_name(self,tag,list,recurse)
+ for _, child in ipairs(self) do if type(child) == 'table' then
+ if child.tag == tag then t_insert(list,child) end
+ if recurse then _children_with_name(child,tag,list,recurse) end
+ end end
+end
+
+--- get all elements in a document that have a given tag.
+-- @param tag a tag name
+-- @param dont_recurse optionally only return the immediate children with this tag name
+-- @return a list of elements
+function Doc:get_elements_with_name(tag,dont_recurse)
+ local res = {}
+ _children_with_name(self,tag,res,not dont_recurse)
+ return res
+end
+
+-- iterate over all children of a document node, including text nodes.
+function Doc:children()
+ local i = 0;
+ return function (a)
+ i = i + 1
+ return a[i];
+ end, self, i;
+end
+
+-- return the first child element of a node, if it exists.
+function Doc:first_childtag()
+ if #self == 0 then return end
+ for _,t in ipairs(self) do
+ if type(t) == 'table' then return t end
+ end
+end
+
+function Doc:matching_tags(tag, xmlns)
+ xmlns = xmlns or self.attr.xmlns;
+ local tags = self;
+ local start_i, max_i, v = 1, #tags;
+ return function ()
+ for i=start_i,max_i do
+ v = tags[i];
+ if (not tag or v.tag == tag)
+ and (not xmlns or xmlns == v.attr.xmlns) then
+ start_i = i+1;
+ return v;
+ end
+ end
+ end, tags, start_i;
+end
+
+--- iterate over all child elements of a document node.
+function Doc:childtags()
+ local i = 0;
+ return function (a)
+ local v
+ repeat
+ i = i + 1
+ v = self[i]
+ if v and type(v) == 'table' then return v; end
+ until not v
+ end, self[1], i;
+end
+
+--- visit child element of a node and call a function, possibility modifying the document.
+-- @param callback a function passed the node (text or element). If it returns nil, that node will be removed.
+-- If it returns a value, that will replace the current node.
+function Doc:maptags(callback)
+ local is_tag = _M.is_tag
+ local i = 1;
+ while i <= #self do
+ if is_tag(self[i]) then
+ local ret = callback(self[i]);
+ if ret == nil then
+ t_remove(self, i);
+ else
+ self[i] = ret;
+ i = i + 1;
+ end
+ end
+ end
+ return self;
+end
+
+local xml_escape
+do
+ local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" };
+ function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end
+ _M.xml_escape = xml_escape;
+end
+
+-- pretty printing
+-- if indent, then put each new tag on its own line
+-- if attr_indent, put each new attribute on its own line
+local function _dostring(t, buf, self, xml_escape, parentns, idn, indent, attr_indent)
+ local nsid = 0;
+ local tag = t.tag
+ local lf,alf = ""," "
+ if indent then lf = '\n'..idn end
+ if attr_indent then alf = '\n'..idn..attr_indent end
+ t_insert(buf, lf.."<"..tag);
+ local function write_attr(k,v)
+ if s_find(k, "\1", 1, true) then
+ local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$");
+ nsid = nsid + 1;
+ t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'");
+ elseif not(k == "xmlns" and v == parentns) then
+ t_insert(buf, alf..k.."='"..xml_escape(v).."'");
+ end
+ end
+ -- it's useful for testing to have predictable attribute ordering, if available
+ if #t.attr > 0 then
+ for _,k in ipairs(t.attr) do
+ write_attr(k,t.attr[k])
+ end
+ else
+ for k, v in pairs(t.attr) do
+ write_attr(k,v)
+ end
+ end
+ local len,has_children = #t;
+ if len == 0 then
+ local out = "/>"
+ if attr_indent then out = '\n'..idn..out end
+ t_insert(buf, out);
+ else
+ t_insert(buf, ">");
+ for n=1,len do
+ local child = t[n];
+ if child.tag then
+ self(child, buf, self, xml_escape, t.attr.xmlns,idn and idn..indent, indent, attr_indent );
+ has_children = true
+ else -- text element
+ t_insert(buf, xml_escape(child));
+ end
+ end
+ t_insert(buf, (has_children and lf or '')..""..tag..">");
+ end
+end
+
+---- pretty-print an XML document
+--- @param t an XML document
+--- @param idn an initial indent (indents are all strings)
+--- @param indent an indent for each level
+--- @param attr_indent if given, indent each attribute pair and put on a separate line
+--- @param xml force prefacing with default or custom
+--- @return a string representation
+function _M.tostring(t,idn,indent, attr_indent, xml)
+ local buf = {};
+ if xml then
+ if type(xml) == "string" then
+ buf[1] = xml
+ else
+ buf[1] = ""
+ end
+ end
+ _dostring(t, buf, _dostring, xml_escape, nil,idn,indent, attr_indent);
+ return t_concat(buf);
+end
+
+Doc.__tostring = _M.tostring
+
+--- get the full text value of an element
+function Doc:get_text()
+ local res = {}
+ for i,el in ipairs(self) do
+ if is_text(el) then t_insert(res,el) end
+ end
+ return t_concat(res);
+end
+
+--- make a copy of a document
+-- @param doc the original document
+-- @param strsubst an optional function for handling string copying which could do substitution, etc.
+function _M.clone(doc, strsubst)
+ local lookup_table = {};
+ local function _copy(object,kind,parent)
+ if type(object) ~= "table" then
+ if strsubst and is_text(object) then return strsubst(object,kind,parent)
+ else return object
+ end
+ elseif lookup_table[object] then
+ return lookup_table[object]
+ end
+ local new_table = {};
+ lookup_table[object] = new_table
+ local tag = object.tag
+ new_table.tag = _copy(tag,'*TAG',parent)
+ if object.attr then
+ local res = {}
+ for attr,value in pairs(object.attr) do
+ res[attr] = _copy(value,attr,object)
+ end
+ new_table.attr = res
+ end
+ for index = 1,#object do
+ local v = _copy(object[index],'*TEXT',object)
+ t_insert(new_table,v)
+ end
+ return setmetatable(new_table, getmetatable(object))
+ end
+
+ return _copy(doc)
+end
+
+Doc.filter = _M.clone -- also available as method
+
+--- compare two documents.
+-- @param t1 any value
+-- @param t2 any value
+function _M.compare(t1,t2)
+ local ty1 = type(t1)
+ local ty2 = type(t2)
+ if ty1 ~= ty2 then return false, 'type mismatch' end
+ if ty1 == 'string' then
+ return t1 == t2 and true or 'text '..t1..' ~= text '..t2
+ end
+ if ty1 ~= 'table' or ty2 ~= 'table' then return false, 'not a document' end
+ if t1.tag ~= t2.tag then return false, 'tag '..t1.tag..' ~= tag '..t2.tag end
+ if #t1 ~= #t2 then return false, 'size '..#t1..' ~= size '..#t2..' for tag '..t1.tag end
+ -- compare attributes
+ for k,v in pairs(t1.attr) do
+ if t2.attr[k] ~= v then return false, 'mismatch attrib' end
+ end
+ for k,v in pairs(t2.attr) do
+ if t1.attr[k] ~= v then return false, 'mismatch attrib' end
+ end
+ -- compare children
+ for i = 1,#t1 do
+ local yes,err = _M.compare(t1[i],t2[i])
+ if not yes then return err end
+ end
+ return true
+end
+
+--- is this value a document element?
+-- @param d any value
+function _M.is_tag(d)
+ return type(d) == 'table' and is_text(d.tag)
+end
+
+--- call the desired function recursively over the document.
+-- @param doc the document
+-- @param depth_first visit child notes first, then the current node
+-- @param operation a function which will receive the current tag name and current node.
+function _M.walk (doc, depth_first, operation)
+ if not depth_first then operation(doc.tag,doc) end
+ for _,d in ipairs(doc) do
+ if _M.is_tag(d) then
+ _M.walk(d,depth_first,operation)
+ end
+ end
+ if depth_first then operation(doc.tag,doc) end
+end
+
+local html_empty_elements = { --lists all HTML empty (void) elements
+ br = true,
+ img = true,
+ meta = true,
+ frame = true,
+ area = true,
+ hr = true,
+ base = true,
+ col = true,
+ link = true,
+ input = true,
+ option = true,
+ param = true,
+ isindex = true,
+ embed = true,
+}
+
+local escapes = { quot = "\"", apos = "'", lt = "<", gt = ">", amp = "&" }
+local function unescape(str) return (str:gsub( "&(%a+);", escapes)); end
+
+--- Parse a well-formed HTML file as a string.
+-- Tags are case-insenstive, DOCTYPE is ignored, and empty elements can be .. empty.
+-- @param s the HTML
+function _M.parsehtml (s)
+ return _M.basic_parse(s,false,true)
+end
+
+--- Parse a simple XML document using a pure Lua parser based on Robero Ierusalimschy's original version.
+-- @param s the XML document to be parsed.
+-- @param all_text if true, preserves all whitespace. Otherwise only text containing non-whitespace is included.
+-- @param html if true, uses relaxed HTML rules for parsing
+function _M.basic_parse(s,all_text,html)
+ local t_insert,t_remove = table.insert,table.remove
+ local s_find,s_sub = string.find,string.sub
+ local stack = {}
+ local top = {}
+
+ local function parseargs(s)
+ local arg = {}
+ s:gsub("([%w:%-_]+)%s*=%s*([\"'])(.-)%2", function (w, _, a)
+ if html then w = w:lower() end
+ arg[w] = unescape(a)
+ end)
+ if html then
+ s:gsub("([%w:%-_]+)%s*=%s*([^\"']+)%s*", function (w, a)
+ w = w:lower()
+ arg[w] = unescape(a)
+ end)
+ end
+ return arg
+ end
+
+ t_insert(stack, top)
+ local ni,c,label,xarg, empty, _, istart
+ local i, j = 1, 1
+ -- we're not interested in
+ _,istart = s_find(s,'^%s*<%?[^%?]+%?>%s*')
+ if not istart then -- or
+ _,istart = s_find(s,'^%s*%s*')
+ end
+ if istart then i = istart+1 end
+ while true do
+ ni,j,c,label,xarg, empty = s_find(s, "<([%/!]?)([%w:%-_]+)(.-)(%/?)>", i)
+ if not ni then break end
+ if c == "!" then -- comment
+ -- case where there's no space inside comment
+ if not (label:match '%-%-$' and xarg == '') then
+ if xarg:match '%-%-$' then -- we've grabbed it all
+ j = j - 2
+ end
+ -- match end of comment
+ _,j = s_find(s, "-->", j, true)
+ end
+ else
+ local text = s_sub(s, i, ni-1)
+ if html then
+ label = label:lower()
+ if html_empty_elements[label] then empty = "/" end
+ if label == 'script' then
+ end
+ end
+ if all_text or not s_find(text, "^%s*$") then
+ t_insert(top, unescape(text))
+ end
+ if empty == "/" then -- empty element tag
+ t_insert(top, setmetatable({tag=label, attr=parseargs(xarg), empty=1},Doc))
+ elseif c == "" then -- start tag
+ top = setmetatable({tag=label, attr=parseargs(xarg)},Doc)
+ t_insert(stack, top) -- new level
+ else -- end tag
+ local toclose = t_remove(stack) -- remove top
+ top = stack[#stack]
+ if #stack < 1 then
+ error("nothing to close with "..label..':'..text)
+ end
+ if toclose.tag ~= label then
+ error("trying to close "..toclose.tag.." with "..label.." "..text)
+ end
+ t_insert(top, toclose)
+ end
+ end
+ i = j+1
+ end
+ local text = s_sub(s, i)
+ if all_text or not s_find(text, "^%s*$") then
+ t_insert(stack[#stack], unescape(text))
+ end
+ if #stack > 1 then
+ error("unclosed "..stack[#stack].tag)
+ end
+ local res = stack[1]
+ return is_text(res[1]) and res[2] or res[1]
+end
+
+local function empty(attr) return not attr or not next(attr) end
+local function is_element(d) return type(d) == 'table' and d.tag ~= nil end
+
+-- returns the key,value pair from a table if it has exactly one entry
+local function has_one_element(t)
+ local key,value = next(t)
+ if next(t,key) ~= nil then return false end
+ return key,value
+end
+
+local function append_capture(res,tbl)
+ if not empty(tbl) then -- no point in capturing empty tables...
+ local key
+ if tbl._ then -- if $_ was set then it is meant as the top-level key for the captured table
+ key = tbl._
+ tbl._ = nil
+ if empty(tbl) then return end
+ end
+ -- a table with only one pair {[0]=value} shall be reduced to that value
+ local numkey,val = has_one_element(tbl)
+ if numkey == 0 then tbl = val end
+ if key then
+ res[key] = tbl
+ else -- otherwise, we append the captured table
+ t_insert(res,tbl)
+ end
+ end
+end
+
+local function make_number(pat)
+ if pat:find '^%d+$' then -- $1 etc means use this as an array location
+ pat = tonumber(pat)
+ end
+ return pat
+end
+
+local function capture_attrib(res,pat,value)
+ pat = make_number(pat:sub(2))
+ res[pat] = value
+ return true
+end
+
+local match
+function match(d,pat,res,keep_going)
+ local ret = true
+ if d == nil then d = '' end --return false end
+ -- attribute string matching is straight equality, except if the pattern is a $ capture,
+ -- which always succeeds.
+ if is_text(d) then
+ if not is_text(pat) then return false end
+ if _M.debug then print(d,pat) end
+ if pat:find '^%$' then
+ return capture_attrib(res,pat,d)
+ else
+ return d == pat
+ end
+ else
+ if _M.debug then print(d.tag,pat.tag) end
+ -- this is an element node. For a match to succeed, the attributes must
+ -- match as well.
+ -- a tagname in the pattern ending with '-' is a wildcard and matches like an attribute
+ local tagpat = pat.tag:match '^(.-)%-$'
+ if tagpat then
+ tagpat = make_number(tagpat)
+ res[tagpat] = d.tag
+ end
+ if d.tag == pat.tag or tagpat then
+
+ if not empty(pat.attr) then
+ if empty(d.attr) then ret = false
+ else
+ for prop,pval in pairs(pat.attr) do
+ local dval = d.attr[prop]
+ if not match(dval,pval,res) then ret = false; break end
+ end
+ end
+ end
+ -- the pattern may have child nodes. We match partially, so that {P1,P2} shall match {X,P1,X,X,P2,..}
+ if ret and #pat > 0 then
+ local i,j = 1,1
+ local function next_elem()
+ j = j + 1 -- next child element of data
+ if is_text(d[j]) then j = j + 1 end
+ return j <= #d
+ end
+ repeat
+ local p = pat[i]
+ -- repeated {{<...>}} patterns shall match one or more elements
+ -- so e.g. {P+} will match {X,X,P,P,X,P,X,X,X}
+ if is_element(p) and p.repeated then
+ local found
+ repeat
+ local tbl = {}
+ ret = match(d[j],p,tbl,false)
+ if ret then
+ found = false --true
+ append_capture(res,tbl)
+ end
+ until not next_elem() or (found and not ret)
+ i = i + 1
+ else
+ ret = match(d[j],p,res,false)
+ if ret then i = i + 1 end
+ end
+ until not next_elem() or i > #pat -- run out of elements or patterns to match
+ -- if every element in our pattern matched ok, then it's been a successful match
+ if i > #pat then return true end
+ end
+ if ret then return true end
+ else
+ ret = false
+ end
+ -- keep going anyway - look at the children!
+ if keep_going then
+ for child in d:childtags() do
+ ret = match(child,pat,res,keep_going)
+ if ret then break end
+ end
+ end
+ end
+ return ret
+end
+
+function Doc:match(pat)
+ local err
+ pat,err = template_cache(pat)
+ if not pat then return nil, err end
+ _M.walk(pat,false,function(_,d)
+ if is_text(d[1]) and is_element(d[2]) and is_text(d[3]) and
+ d[1]:find '%s*{{' and d[3]:find '}}%s*' then
+ t_remove(d,1)
+ t_remove(d,2)
+ d[1].repeated = true
+ end
+ end)
+
+ local res = {}
+ local ret = match(self,pat,res,true)
+ return res,ret
+end
+
+
+return _M
+
diff --git a/src/LuaManager.cpp b/src/LuaManager.cpp
index 28e0cea58b..7ec006f006 100644
--- a/src/LuaManager.cpp
+++ b/src/LuaManager.cpp
@@ -3,6 +3,7 @@
#include "LuaManager.h"
#include "LuaReference.h"
#include "MessageManager.h"
+#include "RageFileManager.h"
#include "RageFile.h"
#include "RageLog.h"
#include "RageThreads.h"
@@ -638,6 +639,16 @@ LuaManager::LuaManager()
luaL_openlibs(L);
+ lua_getglobal(L, "package");
+ lua_getfield(L, -1, "path"); // get field "path" from table at top of stack (-1)
+ std::string cur_path = lua_tostring(L, -1); // grab path string from top of stack
+ RString dir = RageFileManager::DirOfExecutable();
+ cur_path.append(";" +dir+"\\..\\lualibs\\?.lua;"+dir+"\\..\\lualibs\\?\\init.lua"); // add the lualibs path
+ lua_pop(L, 1); // get rid of the string on the stack we just pushed on line 5
+ lua_pushstring(L, cur_path.c_str()); // push the new one
+ lua_setfield(L, -2, "path"); // set the field "path" in table at -2 with value at top of stack
+ lua_pop(L, 1); // get rid of package table from top of stack
+
// Store the thread pool in a table on the stack, in the main thread.
#define THREAD_POOL 1
lua_newtable( L );
diff --git a/src/RageFileManager.cpp b/src/RageFileManager.cpp
index 408e9c952a..1f4270e630 100644
--- a/src/RageFileManager.cpp
+++ b/src/RageFileManager.cpp
@@ -210,7 +210,6 @@ static RString ReadlinkRecursive( RString sPath )
return sPath;
}
-
static RString GetDirOfExecutable( RString argv0 )
{
// argv[0] can be wrong in most OS's; try to avoid using it.
@@ -274,6 +273,9 @@ static RString GetDirOfExecutable( RString argv0 )
}
return sPath;
}
+RString RageFileManager::DirOfExecutable() {
+ return GetDirOfExecutable(RString(""));
+}
static void ChangeToDirOfExecutable( const RString &argv0 )
{
diff --git a/src/RageFileManager.h b/src/RageFileManager.h
index db22a29cf2..5df5f32f1e 100644
--- a/src/RageFileManager.h
+++ b/src/RageFileManager.h
@@ -24,6 +24,7 @@ class RageFileManager
void MountInitialFilesystems();
void MountUserFilesystems();
+ static RString DirOfExecutable();
void GetDirListing( const RString &sPath, vector &AddTo, bool bOnlyDirs, bool bReturnPathToo );
void GetDirListingWithMultipleExtensions(const RString &sPath,
vector const& ExtensionList, vector &AddTo,