Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/stevedonovan/Penlight.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2018-11-24 04:09:30 +0300
committerThijs Schreijer <thijs@thijsschreijer.nl>2018-11-27 00:48:33 +0300
commitb94b9b19a7d35affe4691cc01a221bbea4a602b4 (patch)
treeb7339073f6a6dc158585c2b82cc9c38308df5f60
parent3f4c179b7b70392100f274a0e68d65de9814401f (diff)
increased test coverage and updated docs: compat + utils
includes a feww small backwards compatible changes
-rw-r--r--CHANGELOG.md18
-rw-r--r--lua/pl/compat.lua56
-rw-r--r--lua/pl/utils.lua666
-rw-r--r--run.lua2
-rw-r--r--tests/test-utils.lua175
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
diff --git a/run.lua b/run.lua
index ec450ea..f3e8a8b 100644
--- a/run.lua
+++ b/run.lua
@@ -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