/Src/Modules/penlight/lua/pl/utils.lua
Lua | 475 lines | 272 code | 46 blank | 157 comment | 63 complexity | ef5e764786b1dc09e4feb279bc0579f0 MD5 | raw file
- --- 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.2.1",
- 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' 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
- --- 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.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 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