--- Generally useful routines. -- See @{01-introduction.md.Generally_useful_functions|the Guide}. -- -- Dependencies: `pl.compat`, all exported fields and functions from -- `pl.compat` are also available in this module. -- -- @module pl.utils local format = string.format local compat = require 'pl.compat' local stdout = io.stdout local append = table.insert local _unpack = table.unpack -- always injected by 'compat' local is_windows = compat.is_windows local err_mode = 'default' local raise local operators local _function_factories = {} local utils = { _VERSION = "1.11.0" } for k, v in pairs(compat) do utils[k] = v end --- Some standard patterns -- @table patterns utils.patterns = { FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*', -- floating point number INTEGER = '[+%-%d]%d*', -- integer number IDEN = '[%a_][%w_]*', -- identifier FILE = '[%a%.\\][:%][%w%._%-\\]*', -- file } --- Standard meta-tables as used by other Penlight modules -- @table stdmt -- @field List the List metatable -- @field Map the Map metatable -- @field Set the Set metatable -- @field MultiMap the MultiMap metatable utils.stdmt = { List = {_name='List'}, Map = {_name='Map'}, Set = {_name='Set'}, MultiMap = {_name='MultiMap'}, } --- pack an argument list into a table. -- @param ... any arguments -- @return a table with field `n` set to the length -- @function utils.pack -- @see compat.pack -- @see utils.npairs -- @see utils.unpack utils.pack = table.pack -- added here to be symmetrical with unpack --- unpack a table and return its contents. -- -- NOTE: this implementation differs from the Lua implementation in the way -- that this one DOES honor the `n` field in the table `t`, such that it is 'nil-safe'. -- @param t table to unpack -- @param[opt] i index from which to start unpacking, defaults to 1 -- @param[opt] t index of the last element to unpack, defaults to `t.n` or `#t` -- @return multiple return values from the table -- @function utils.unpack -- @see compat.unpack -- @see utils.pack -- @see utils.npairs -- @usage -- local t = table.pack(nil, nil, nil, 4) -- local a, b, c, d = table.unpack(t) -- this `unpack` is NOT nil-safe, so d == nil -- -- local a, b, c, d = utils.unpack(t) -- this is nil-safe, so d == 4 function utils.unpack(t, i, j) return _unpack(t, i or 1, j or t.n or #t) end --- print an arbitrary number of arguments using a format. -- Output will be sent to `stdout`. -- @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 do 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 (table), or module name (string), defaults to this `utils` module 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 end --- return either of two values, depending on a condition. -- @param cond A condition -- @param value1 Value returned if cond is truthy -- @param value2 Value returned if cond is falsy function utils.choose(cond, value1, value2) return cond and value1 or value2 end --- convert an array of values to strings. -- @param t a list-like table -- @param[opt] temp (table) buffer to use, otherwise allocate -- @param[opt] tostr custom tostring function, called with (value,index). Defaults to `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 --- 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 -- @return boolean -- @usage utils.is_type("hello world", "string") --> true -- -- or check metatable -- local my_mt = {} -- local my_obj = setmetatable(my_obj, my_mt) -- utils.is_type(my_obj, my_mt) --> true function utils.is_type (obj,tp) if type(tp) == 'string' then return type(obj) == tp end local mt = getmetatable(obj) return tp == mt end --- an iterator with indices, similar to `ipairs`, but with a range. -- This is a nil-safe index based iterator that will return `nil` when there -- is a hole in a list. To be safe ensure that table `t.n` contains the length. -- @tparam table t the table to iterate over -- @tparam[opt=1] integer i_start start index -- @tparam[opt=t.n or #t] integer i_end end index -- @tparam[opt=1] integer step step size -- @treturn integer index -- @treturn any value at index (which can be `nil`!) -- @see utils.pack -- @see utils.unpack -- @usage -- local t = utils.pack(nil, 123, nil) -- adds an `n` field when packing -- -- for i, v in utils.npairs(t, 2) do -- start at index 2 -- t[i] = tostring(t[i]) -- end -- -- -- t = { n = 3, [2] = "123", [3] = "nil" } function utils.npairs(t, i_start, i_end, step) step = step or 1 if step == 0 then error("iterator step-size cannot be 0", 2) end local i = (i_start or 1) - step i_end = i_end or t.n or #t if step < 0 then return function() i = i + step if i < i_end then return nil end return i, t[i] end else return function() i = i + step if i > i_end then return nil end return i, t[i] end end end --- Error handling -- @section Error-handling --- 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 -- @return the validated value -- @raise if `val` is not the correct type -- @usage -- local param1 = assert_arg(1,"hello",'table') --> error: argument 1 expected a 'table', got a 'string' -- local param4 = assert_arg(4,'!@#$%^&*','string',path.isdir,'not a directory') -- --> error: argument 4: '!@#$%^&*' 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 return val end --- 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 operators then operators = require 'pl.operator'.optable end local fn = operators[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 --- assert the common case that the argument is a string. -- @param n argument index -- @param val a value that must be a string -- @return the validated value -- @raise val must be a string -- @usage -- local val = 42 -- local param2 = utils.assert_string(2, val) --> error: argument 2 expected a 'string', got a 'number' function utils.assert_string (n, val) return utils.assert_arg(n,val,'string',nil,nil,3) end --- control the error strategy used by Penlight. -- This is a global setting that controls how `utils.raise` behaves: -- -- - 'default': return `nil + error` (this is the default) -- - 'error': throw a Lua error -- - 'quit': exit the program -- -- @param mode either 'default', 'quit' or 'error' -- @see utils.raise function utils.on_error (mode) mode = tostring(mode) if ({['default'] = 1, ['quit'] = 2, ['error'] = 3})[mode] then err_mode = mode else -- fail loudly local err = "Bad argument expected string; 'default', 'quit', or 'error'. Got '"..tostring(mode).."'" if err_mode == 'default' then error(err, 2) -- even in 'default' mode fail loud in this case end raise(err) end end --- used by Penlight functions to return errors. Its global behaviour is controlled -- by `utils.on_error`. -- To use this function you MUST use it in conjunction with `return`, since it might -- return `nil + error`. -- @param err the error string. -- @see utils.on_error -- @usage -- if some_condition then -- return utils.raise("some condition was not met") -- MUST use 'return'! -- end function utils.raise (err) if err_mode == 'default' then return nil, err elseif err_mode == 'quit' then return utils.quit(err) else error(err, 2) end end raise = utils.raise --- File handling -- @section files --- 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 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 local ok, write_err = f:write(str) f:close() if not ok then -- Errors in io.open have "filename: " prefix, -- error in file:write don't, add it. return raise (filename..": "..write_err) end 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 error 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 --- OS functions -- @section OS-functions --- 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 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 --- Quote and escape an argument of a command. -- Quotes a single (or list of) argument(s) of a command to be passed -- to `os.execute`, `pl.utils.execute` or `pl.utils.executeex`. -- @param argument (string or table/list) the argument to quote. If a list then -- all arguments in the list will be returned as a single string quoted. -- @return quoted and escaped argument. -- @usage -- local options = utils.quote_arg { -- "-lluacov", -- "-e", -- "utils = print(require('pl.utils')._VERSION", -- } -- -- returns: -lluacov -e 'utils = print(require('\''pl.utils'\'')._VERSION' function utils.quote_arg(argument) if type(argument) == "table" then -- encode an entire table local r = {} for i, arg in ipairs(argument) do r[i] = utils.quote_arg(arg) end return table.concat(r, " ") end -- only a single 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 --- error out of this program gracefully. -- @param[opt] code The exit code, defaults to -`1` if omitted -- @param msg The exit message will be sent to `stderr` (will be formatted with the extra parameters) -- @param ... extra arguments for message's format' -- @see utils.fprintf -- @usage utils.quit(-1, "Error '%s' happened", "42") -- -- is equivalent to -- utils.quit("Error '%s' happened", "42") --> Error '42' happened function utils.quit(code, msg, ...) if type(code) == 'string' then utils.fprintf(io.stderr, code, msg, ...) io.stderr:write('\n') code = -1 -- TODO: this is odd, see the test. Which returns 255 as exit code elseif msg then utils.fprintf(io.stderr, msg, ...) io.stderr:write('\n') end os.exit(code, true) end --- String functions -- @section string-functions --- escape any Lua 'magic' characters in a string -- @param s The input string function utils.escape(s) utils.assert_string(1,s) return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1')) end --- split a string into a list of strings separated by a delimiter. -- @param s The input string -- @param re optional A Lua string pattern; defaults to '%s+' -- @param plain optional If truthy don't use Lua patterns -- @param n optional maximum number of elements (if there are more, the last will remian un-split) -- @return a list-like table -- @raise error if s is not a string -- @see splitv 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 return values. -- Identical to `split` but returns multiple sub-strings instead of -- a single list of sub-strings. -- @param s the 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 n values -- @usage first,next = splitv('user=jane=doe','=', false, 2) -- assert(first == "user") -- assert(next == "jane=doe") -- @see split function utils.splitv (s,re, plain, n) return _unpack(utils.split(s,re, plain, n)) end --- Functional -- @section functional --- '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 --- 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) 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 -- @function utils.string_lambda -- @usage -- string_lambda '|x|x+1' (2) == 3 -- string_lambda '_+1' (2) == 3 utils.string_lambda = utils.memoize(_string_lambda) --- 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 -- @usage local function f(msg, name) -- print(msg .. " " .. name) -- end -- -- local hello = utils.bind1(f, "Hello") -- -- print(hello("world")) --> "Hello world" -- print(hello("sunshine")) --> "Hello sunshine" 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} -- @usage local function f(a, b, c) -- print(a .. " " .. b .. " " .. c) -- end -- -- local hello = utils.bind1(f, "world") -- -- print(hello("Hello", "!")) --> "Hello world !" -- print(hello("Bye", "?")) --> "Bye world ?" function utils.bind2 (fn,p) fn = utils.function_arg(1,fn) return function(x,...) return fn(x,p,...) end end --- Deprecation -- @section deprecation do -- the default implementation local deprecation_func = function(msg, trace) if trace then warn(msg, "\n", trace) -- luacheck: ignore else warn(msg) -- luacheck: ignore end end --- Sets a deprecation warning function. -- An application can override this function to support proper output of -- deprecation warnings. The warnings can be generated from libraries or -- functions by calling `utils.raise_deprecation`. The default function -- will write to the 'warn' system (introduced in Lua 5.4, or the compatibility -- function from the `compat` module for earlier versions). -- -- Note: only applications should set/change this function, libraries should not. -- @param func a callback with signature: `function(msg, trace)` both arguments are strings, the latter being optional. -- @see utils.raise_deprecation -- @usage -- -- write to the Nginx logs with OpenResty -- utils.set_deprecation_func(function(msg, trace) -- ngx.log(ngx.WARN, msg, (trace and (" " .. trace) or nil)) -- end) -- -- -- disable deprecation warnings -- utils.set_deprecation_func() function utils.set_deprecation_func(func) if func == nil then deprecation_func = function() end else utils.assert_arg(1, func, "function") deprecation_func = func end end --- raises a deprecation warning. -- For options see the usage example below. -- -- Note: the `opts.deprecated_after` field is the last version in which -- a feature or option was NOT YET deprecated! Because when writing the code it -- is quite often not known in what version the code will land. But the last -- released version is usually known. -- @param opts options table -- @see utils.set_deprecation_func -- @usage -- warn("@on") -- enable Lua warnings, they are usually off by default -- -- function stringx.islower(str) -- raise_deprecation { -- source = "Penlight " .. utils._VERSION, -- optional -- message = "function 'islower' was renamed to 'is_lower'", -- required -- version_removed = "2.0.0", -- optional -- deprecated_after = "1.2.3", -- optional -- no_trace = true, -- optional -- } -- return stringx.is_lower(str) -- end -- -- output: "[Penlight 1.9.2] function 'islower' was renamed to 'is_lower' (deprecated after 1.2.3, scheduled for removal in 2.0.0)" function utils.raise_deprecation(opts) utils.assert_arg(1, opts, "table") if type(opts.message) ~= "string" then error("field 'message' of the options table must be a string", 2) end local trace if not opts.no_trace then trace = debug.traceback("", 2):match("[\n%s]*(.-)$") end local msg if opts.deprecated_after and opts.version_removed then msg = (" (deprecated after %s, scheduled for removal in %s)"):format( tostring(opts.deprecated_after), tostring(opts.version_removed)) elseif opts.deprecated_after then msg = (" (deprecated after %s)"):format(tostring(opts.deprecated_after)) elseif opts.version_removed then msg = (" (scheduled for removal in %s)"):format(tostring(opts.version_removed)) else msg = "" end msg = opts.message .. msg if opts.source then msg = "[" .. opts.source .."] " .. msg else if msg:sub(1,1) == "@" then -- in Lua 5.4 "@" prefixed messages are control messages to the warn system error("message cannot start with '@'", 2) end end deprecation_func(msg, trace) end end return utils