diff options
author | Thijs Schreijer <thijs@thijsschreijer.nl> | 2018-11-24 04:09:30 +0300 |
---|---|---|
committer | Thijs Schreijer <thijs@thijsschreijer.nl> | 2018-11-27 00:48:33 +0300 |
commit | b94b9b19a7d35affe4691cc01a221bbea4a602b4 (patch) | |
tree | b7339073f6a6dc158585c2b82cc9c38308df5f60 | |
parent | 3f4c179b7b70392100f274a0e68d65de9814401f (diff) |
increased test coverage and updated docs: compat + utils
includes a feww small backwards compatible changes
-rw-r--r-- | CHANGELOG.md | 18 | ||||
-rw-r--r-- | lua/pl/compat.lua | 56 | ||||
-rw-r--r-- | lua/pl/utils.lua | 666 | ||||
-rw-r--r-- | run.lua | 2 | ||||
-rw-r--r-- | tests/test-utils.lua | 175 |
5 files changed, 583 insertions, 334 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 161a7a4..3f1bc44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 1.6.1 (UNRELEASED) + +### New features + + +### Changes + + - Documentation updates + - `utils.quit`: exit message is no longer required, and closes the Lua state (on 5.2+). + - `utils.assert_arg` and `utils.assert_string`: now return the validated value + - `pl.compat`: now exports the `jit` and `jit52` flags + + +### Fixes + + - `utils.raise` changed the global `on_error`-level when passing in bad arguments + - `utils.writefile` now checks and returns errors when writing + ## 1.6.0 (2018-11-23) ### New features diff --git a/lua/pl/compat.lua b/lua/pl/compat.lua index 775e2cc..aa5b435 100644 --- a/lua/pl/compat.lua +++ b/lua/pl/compat.lua @@ -1,27 +1,40 @@ ---------------- --- Lua 5.1/5.2/5.3 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. +-- Injects `table.pack`, `table.unpack`, and `package.searchpath` in the global +-- environment, to make sure they are available for Lua 5.1 and LuaJIT. +-- +-- All other functions are exported as usual in the returned module table. +-- +-- NOTE: everything in this module is also available in `pl.utils`. -- @module pl.compat - local compat = {} +--- boolean flag this is Lua 5.1 (or LuaJIT). +-- @field lua51 compat.lua51 = _VERSION == 'Lua 5.1' -local isJit = (tostring(assert):match('builtin') ~= nil) -if isJit then +--- boolean flag this is LuaJIT. +-- @field jit +compat.jit = (tostring(assert):match('builtin') ~= nil) + +--- boolean flag this is LuaJIT with 5.2 compatibility compiled in. +-- @field jit52 +if compat.jit then -- 'goto' is a keyword when 52 compatibility is enabled in LuaJit compat.jit52 = not loadstring("local goto = 1") end +--- the directory separator character for the current platform. +-- @field dir_separator compat.dir_separator = _G.package.config:sub(1,1) + +--- boolean flag this is a Windows platform. +-- @field is_windows compat.is_windows = compat.dir_separator == '\\' ---- execute a shell command. --- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2 +--- execute a shell command, in a compatible and platform independent way. +-- This is a compatibility function that returns the same for Lua 5.1 and +-- Lua 5.2, on Unixes as well as Windows. -- @param cmd a shell command -- @return true if successful -- @return actual return code @@ -46,7 +59,7 @@ function compat.execute (cmd) end ---------------- --- Load Lua code as a text or binary chunk. +-- Load Lua code as a text or binary chunk (in a Lua 5.2 compatible way). -- @param ld code string or loader -- @param[opt] source name of chunk for errors -- @param[opt] mode 'b', 't' or 'bt' @@ -54,20 +67,21 @@ end -- @function compat.load --------------- --- Get environment of a function. --- With Lua 5.2, may return nil for a function with no global references! +-- Get environment of a function (in a Lua 5.1 compatible way). +-- Not 100% compatible, so with Lua 5.2 it 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.getfenv --------------- --- Set environment of a function +-- Set environment of a function (in a Lua 5.1 compatible way). -- @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 isJit then -- but LuaJIT's load _is_ compatible + if not compat.jit then -- but LuaJIT's load _is_ compatible local lua51_load = load function compat.load(str,src,mode,env) local chunk,err @@ -122,7 +136,7 @@ else end end ---- Lua 5.2 Functions Available for 5.1 +--- Global exported functions (for Lua 5.1 & LuaJIT) -- @section lua52 --- pack an argument list into a table. @@ -136,16 +150,20 @@ if not table.pack then end --- unpack a table and return the elements. +-- +-- NOTE: this version does NOT honor the n field, and hence it is not nil-safe. +-- See `utils.unpack` for a version that 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 --- @return multiple returns values from the table +-- @return multiple return values from the table +-- @function table.unpack +-- @see utils.unpack if not table.unpack then table.unpack = unpack -- luacheck: ignore end ------- --- return the full path where a Lua module name would be matched. +--- 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 diff --git a/lua/pl/utils.lua b/lua/pl/utils.lua index 9869926..1dfa009 100644 --- a/lua/pl/utils.lua +++ b/lua/pl/utils.lua @@ -1,7 +1,8 @@ --- Generally useful routines. -- See @{01-introduction.md.Generally_useful_functions|the Guide}. -- --- Dependencies: `pl.compat` +-- Dependencies: `pl.compat`, all exported fields and functions from +-- `pl.compat` are also available in this module. -- -- @module pl.utils local format = string.format @@ -10,129 +11,289 @@ local stdout = io.stdout local append = table.insert local _unpack = table.unpack -- always injected by 'compat' -local utils = { - _VERSION = "1.6.0", - 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, +local is_windows = compat.is_windows +local err_mode = 'default' +local raise +local operators +local _function_factories = {} + + +local utils = { _VERSION = "1.6.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 +-- @return a table with field `n` set to the length -- @function utils.pack +-- @see compat.pack 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'. +-- 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 returns values from the table +-- @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 +-- @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 ---- 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) +-- 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,...) +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 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 +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 - 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 +--- 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 '?' + 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 -local already_imported = {} +--- Error handling +-- @section Error-handling ---- 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) +--- 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 - 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) + if verify and not verify(val) then + error(("argument %d: '%s' %s"):format(n,val,msg),lev or 2) end + return val end -utils.patterns = { - FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*', - INTEGER = '[+%-%d]%d*', - IDEN = '[%a_][%w_]*', - FILE = '[%a%.\\][:%][%w%._%-\\]*' -} +--- 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 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 ---- 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')) + +--- 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 ---- 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 +--- 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 -local raise +--- 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 @@ -142,7 +303,7 @@ 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 + if not f then return raise (open_err) end local res,read_err = f:read('*a') f:close() if not res then @@ -166,15 +327,20 @@ function utils.writefile(filename,str,is_bin) utils.assert_string(2,str) local f,err = io.open(filename,'w'..mode) if not f then return raise(err) end - f:write(str) + 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 errror if filename is not a string +-- @raise error if filename is not a string function utils.readlines(filename) utils.assert_string(1,filename) local f,err = io.open(filename,'r') @@ -187,6 +353,97 @@ function utils.readlines(filename) 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 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 + +--- 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 '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 A Lua string pattern; defaults to '%s+' @@ -220,7 +477,7 @@ function utils.split(s,re,plain,n) end end ---- split a string into a number of values. +--- split a string into a number of return values. -- @param s the string -- @param re the delimiter, default space -- @return n values @@ -230,78 +487,10 @@ 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 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) +--- Functional +-- @section functional - 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, @@ -322,13 +511,6 @@ function utils.memoize(func) 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 @@ -339,7 +521,6 @@ function utils.add_function_factory (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 @@ -353,7 +534,8 @@ local function _string_lambda(f) if not fn then return raise(err) end fn = fn() return fn - else return raise 'not a string lambda' + else + return raise 'not a string lambda' end end @@ -361,53 +543,12 @@ end -- '|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 +-- @usage +-- string_lambda '|x|x+1' (2) == 3 +-- string_lambda '_+1' (2) == 3 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) @@ -415,6 +556,14 @@ end -- @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 @@ -425,110 +574,19 @@ end -- @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 - ---- 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 <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 @@ -32,7 +32,7 @@ end local dir_sep = package.config:sub(1, 1) local quote = dir_sep == "/" and "'" or '"' -local pl_src = "lua" .. dir_sep .. "?.lua;lua" .. dir_sep .. "?" .. dir_sep .. "/init.lua" +local pl_src = "lua" .. dir_sep .. "?.lua;lua" .. dir_sep .. "?" .. dir_sep .. "init.lua" lua = lua .. " -e " .. quote .. "package.path=[[" .. pl_src .. ";]]..package.path" .. quote local function run_directory(dir) diff --git a/tests/test-utils.lua b/tests/test-utils.lua index 9ddc39d..ae8f201 100644 --- a/tests/test-utils.lua +++ b/tests/test-utils.lua @@ -3,10 +3,58 @@ local path = require 'pl.path' local test = require 'pl.test' local asserteq, T = test.asserteq, test.tuple ---- escaping magic chars -local escape = utils.escape -asserteq(escape '[a]','%[a%]') -asserteq(escape '$(bonzo)','%$%(bonzo%)') +-- construct command to run external lua, we need to to be able to run some +-- tests on the same lua engine, but also need to pass on the LuaCov flag +-- if it was used, to make sure we report the proper coverage. +local cmd = "-e " +do + local i = 0 + while arg[i-1] do + local a = arg[i-1] + if a:find("package%.path") then + a = "'"..a.."'" + end + cmd = a .. " " .. cmd + i = i - 1 + end +end + + +--- quitting +do + local luacode = [['require("pl.utils").quit("hello world")']] + local success, code, stdout, stderr = utils.executeex(cmd..luacode) + asserteq(success, false) + asserteq(code, 255) -- TODO: odd, should have been -1 ? + asserteq(stdout, "") + asserteq(stderr, "hello world\n") + + local luacode = [['require("pl.utils").quit(2, "hello world")']] + local success, code, stdout, stderr = utils.executeex(cmd..luacode) + asserteq(success, false) + asserteq(code, 2) + asserteq(stdout, "") + asserteq(stderr, "hello world\n") + + local luacode = [['require("pl.utils").quit(2, "hello %s", 42)']] + local success, code, stdout, stderr = utils.executeex(cmd..luacode) + asserteq(success, false) + asserteq(code, 2) + asserteq(stdout, "") + asserteq(stderr, "hello 42\n") + + local luacode = [['require("pl.utils").quit(2)']] + local success, code, stdout, stderr = utils.executeex(cmd..luacode) + asserteq(success, false) + asserteq(code, 2) + asserteq(stdout, "") + asserteq(stderr, "") +end + +----- importing module tables wholesale --- +utils.import(math) +asserteq(type(sin),"function") +asserteq(type(abs),"function") --- useful patterns local P = utils.patterns @@ -14,6 +62,15 @@ asserteq(("+0.1e10"):match(P.FLOAT) ~= nil, true) asserteq(("-23430"):match(P.INTEGER) ~= nil, true) asserteq(("my_little_pony99"):match(P.IDEN) ~= nil, true) +--- escaping magic chars +local escape = utils.escape +asserteq(escape '[a]','%[a%]') +asserteq(escape '$(bonzo)','%$%(bonzo%)') + +--- choose +asserteq(utils.choose(true, 1, 2), 1) +asserteq(utils.choose(false, 1, 2), 2) + --- splitting strings --- local split = utils.split asserteq(split("hello dolly"),{"hello","dolly"}) @@ -117,12 +174,6 @@ else asserteq(utils.quote_arg([['a\'b]]), [[''\''a\'\''b']]) end ------ importing module tables wholesale --- -utils.import(math) -asserteq(type(sin),"function") -asserteq(type(abs),"function") - - -- packing and unpacking arguments in a nil-safe way local t = utils.pack(nil, nil, "hello", nil) asserteq(t.n, 4) -- the last nil does count as an argument @@ -134,5 +185,109 @@ asserteq("hello", arg3) assert(arg4 == nil) +-- Assert arguments assert_arg +local ok, err = pcall(function() + utils.assert_arg(4,'!@#$%^&*','string',require("pl.path").isdir,'not a directory') +end) +asserteq(ok, false) +asserteq(err:match("(argument .+)$"), "argument 4: '!@#$%^&*' not a directory") + +local ok, err = pcall(function() + utils.assert_arg(1, "hello", "table") +end) +asserteq(ok, false) +asserteq(err:match("(argument .+)$"), "argument 1 expected a 'table', got a 'string'") + +local ok, err = pcall(function() + return utils.assert_arg(1, "hello", "string") +end) +asserteq(ok, true) +asserteq(err, "hello") + +-- assert_string +local success, err = pcall(utils.assert_string, 2, 5) +asserteq(success, false) +asserteq(err:match("(argument .+)$"), "argument 2 expected a 'string', got a 'number'") + +local x = utils.assert_string(2, "5") +asserteq(x, "5") + + +do + -- printf -- without template + local luacode = [['require("pl.utils").printf("hello world")']] + local success, code, stdout, stderr = utils.executeex(cmd..luacode) + asserteq(success, true) + asserteq(code, 0) + asserteq(stdout, "hello world") + asserteq(stderr, "") + + -- printf -- with template + local luacode = [['require("pl.utils").printf("hello %s", "world")']] + local success, code, stdout, stderr = utils.executeex(cmd..luacode) + asserteq(success, true) + asserteq(code, 0) + asserteq(stdout, "hello world") + asserteq(stderr, "") + + -- printf -- with bad template + local luacode = [['require("pl.utils").printf(42)']] + local success, code, stdout, stderr = utils.executeex(cmd..luacode) + asserteq(success, false) + asserteq(code, 1) + asserteq(stdout, "") + assert(stderr:find("argument 1 expected a 'string', got a 'number'")) +end +do + -- on_error, raise -- default + utils.on_error("default") + local ok, err = utils.raise("some error") + asserteq(ok, nil) + asserteq(err, "some error") + local ok, err = pcall(utils.on_error, "bad one") + asserteq(ok, false) + asserteq(err, "Bad argument expected string; 'default', 'quit', or 'error'. Got 'bad one'") + + -- on_error, raise -- error + utils.on_error("error") + local ok, err = pcall(utils.raise, "some error") + asserteq(ok, false) + asserteq(err, "some error") + local ok, err = pcall(utils.on_error, "bad one") + asserteq(ok, false) + assert(err:find("Bad argument expected string; 'default', 'quit', or 'error'. Got 'bad one'")) + + -- on_error, raise -- quit + utils.on_error("quit") + local luacode = [['local u=require("pl.utils") u.on_error("quit") u.raise("some error")']] + local success, code, stdout, stderr = utils.executeex(cmd..luacode) + asserteq(success, false) + asserteq(code, 255) + asserteq(stdout, "") + asserteq(stderr, "some error\n") + + local luacode = [['local u=require("pl.utils") u.on_error("quit") u.on_error("bad one")']] + local success, code, stdout, stderr = utils.executeex(cmd..luacode) + asserteq(success, false) + asserteq(code, 255) + asserteq(stdout, "") + asserteq(stderr, "Bad argument expected string; 'default', 'quit', or 'error'. Got 'bad one'\n") + + utils.on_error("default") -- cleanup by restoring behaviour after on_error + raise tests +end +do + -- readlines + local f = utils.readlines("tests/test-utils.lua") + asserteq(type(f), "table") + local v = "some extraordinary string this is only in this file for test purposes so we can go and find it" + local found = false + for i, line in ipairs(f) do + if line:find(v) then + found = true + break + end + end + asserteq(found, true) +end
\ No newline at end of file |