--- Generally useful routines. -- See @{01-introduction.md.Generally_useful_functions|the Guide}. -- @module pl.utils local format,gsub,byte = string.format,string.gsub,string.byte local clock = os.clock local stdout = io.stdout local append = table.insert local collisions = {} local utils = {} utils._VERSION = "1.0.3" local lua51 = rawget(_G,'setfenv') utils.lua51 = lua51 if not lua51 then -- Lua 5.2 compatibility unpack = table.unpack loadstring = load end utils.dir_separator = _G.package.config:sub(1,1) --- 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.printf("warning: '%s.%s' overrides existing symbol\n",libname,k) 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,err = io.open(filename,'r'..mode) if not f then return utils.raise (err) end local res,err = f:read('*a') f:close() if not res then return raise (err) end return res end --- write a string to a file -- @param filename The file path -- @param str The string -- @return true or nil -- @return error message -- @raise error if filename or str aren't strings function utils.writefile(filename,str) utils.assert_string(1,filename) utils.assert_string(2,str) local f,err = io.open(filename,'w') 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 local lua51_load = load if utils.lua51 then -- define Lua 5.2 style load() function utils.load(str,src,mode,env) local chunk,err if type(str) == 'string' then chunk,err = loadstring(str,src) else chunk,err = lua51_load(str,src) end if chunk and env then setfenv(chunk,env) end return chunk,err end else utils.load = load -- setfenv/getfenv replacements for Lua 5.2 -- by Sergey Rozhenko -- http://lua-users.org/lists/lua-l/2010-06/msg00313.html -- Roberto Ierusalimschy notes that it is possible for getfenv to return nil -- in the case of a function with no globals: -- http://lua-users.org/lists/lua-l/2010-06/msg00315.html function setfenv(f, t) f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) local name local up = 0 repeat up = up + 1 name = debug.getupvalue(f, up) until name == '_ENV' or name == nil if name then debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue debug.setupvalue(f, up, t) end if f ~= 0 then return f end end function getfenv(f) local f = f or 0 f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) local name, val local up = 0 repeat up = up + 1 name, val = debug.getupvalue(f, up) until name == '_ENV' or name == nil return val end end --- 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 (cmd) local res1,res2,res2 = os.execute(cmd) if lua51 then return res1==0,res1 else return res1,res2 end end if lua51 then function table.pack (...) local n = select('#',...) return {n=n; ...} end local sep = package.config:sub(1,1) function package.searchpath (mod,path) mod = mod:gsub('%.',sep) for m in path:gmatch('[^;]+') do local nm = m:gsub('?',mod) local f = io.open(nm,'r') if f then f:close(); return nm end end end end if not table.pack then table.pack = _G.pack end if not rawget(_G,"pack") then _G.pack = table.pack end --- take an arbitrary set of arguments and make into a table. -- This returns the table and the size; works fine for nil arguments -- @param ... arguments -- @return table -- @return table size -- @usage local t,n = utils.args(...) --- '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) return setmetatable({}, { __index = function(self, k, ...) local v = func(k,...) self[k] = v return v end, __call = function(self, k) return self[k] end }) end --- is the object either a function or a callable object?. -- @param obj Object to check. function utils.is_callable (obj) return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call 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 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 utils.type (obj) local t = type(obj) if t == 'table' or t == 'userdata' then local mt = getmetatable(obj) if mt == fileMT then return 'file' 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 utils.is_integer (x) return math.ceil(x)==x 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 -- @param mt metatable -- @param 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 = loadstring(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 -- @see utils.is_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 pl.func.curry 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 verfication 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) err_mode = mode 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 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 --- Lua 5.2 Compatible Functions -- @section lua52 --- pack an argument list into a table. -- @param ... any arguments -- @return a table with field n set to the length -- @return the length -- @function table.pack ------ -- return the full path where a Lua module name would be matched. -- @param mod module name, possibly dotted -- @param path a path in the same form as package.path or package.cpath -- @see path.package_path -- @function package.searchpath return utils