diff options
Diffstat (limited to 'pl')
-rw-r--r-- | pl/compat.lua | 137 | ||||
-rw-r--r-- | pl/utils.lua | 476 |
2 files changed, 613 insertions, 0 deletions
diff --git a/pl/compat.lua b/pl/compat.lua new file mode 100644 index 0000000..7959ac3 --- /dev/null +++ b/pl/compat.lua @@ -0,0 +1,137 @@ +----------------
+--- Lua 5.1/5.2 compatibility
+-- Ensures that `table.pack` and `package.searchpath` are available
+-- for Lua 5.1 and LuaJIT.
+-- The exported function `load` is Lua 5.2 compatible.
+-- `compat.setfenv` and `compat.getfenv` are available for Lua 5.2, although
+-- they are not always guaranteed to work.
+-- @module pl.compat
+
+local compat = {}
+
+compat.lua51 = _VERSION == 'Lua 5.1'
+
+--- 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 compat.execute (cmd)
+ local res1,res2,res2 = os.execute(cmd)
+ if compat.lua51 then
+ return res1==0,res1
+ else
+ return not not res1,res2
+ end
+end
+
+----------------
+-- Load Lua code as a text or binary chunk.
+-- @param ld code string or loader
+-- @param[opt] source name of chunk for errors
+-- @param[opt] mode 'b', 't' or 'bt'
+-- @param[opt] env environment to load the chunk in
+-- @function compat.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 compat.setfenv
+
+---------------
+-- 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 compat.setfenv
+
+if compat.lua51 then -- define Lua 5.2 style load()
+ if not tostring(assert):match 'builtin' then -- but LuaJIT's load _is_ compatible
+ local lua51_load = load
+ function compat.load(str,src,mode,env)
+ local chunk,err
+ if type(str) == 'string' then
+ if str:byte(1) == 27 and not (mode or 'bt'):find 'b' then
+ return nil,"attempt to load a binary chunk"
+ end
+ 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
+ compat.load = load
+ end
+ compat.setfenv, compat.getfenv = setfenv, getfenv
+else
+ compat.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 compat.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 compat.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
+
+--- Lua 5.2 Functions Available for 5.1
+-- @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
+if not table.pack then
+ function table.pack (...)
+ return {n=select('#',...); ...}
+ end
+end
+
+------
+-- 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
+if not package.searchpath then
+ 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
+
+return compat
diff --git a/pl/utils.lua b/pl/utils.lua new file mode 100644 index 0000000..f933afb --- /dev/null +++ b/pl/utils.lua @@ -0,0 +1,476 @@ +--- 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 compat = require 'pl.compat' +local clock = os.clock +local stdout = io.stdout +local append = table.insert +local unpack = rawget(_G,'unpack') or rawget(table,'unpack') + +local collisions = {} + +local utils = { + _VERSION = "1.3.2", + lua51 = compat.lua51, + setfenv = compat.setfenv, + getfenv = compat.getfenv, + load = compat.load, + execute = compat.execute, + dir_separator = _G.package.config:sub(1,1), + 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.printf("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,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 + +--- 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 + +--- 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 utils.dir_separator == '\\' then + outfile = os.getenv('TEMP')..outfile + errfile = os.getenv('TEMP')..errfile + end + cmd = cmd .. [[ >"]]..outfile..[[" 2>"]]..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) + 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 + + +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 <code>pl.operator</code>, +-- 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 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 <code>utils.raise</code> 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 <code>utils.on_error</code> +-- @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 + + |