local W=string.format local h=require("\112\108\046\099\111\109\112\097\116")local K=io.stdout local Q=table.insert local N=table.concat local y=table.unpack local X=string.find local A=string.sub local k=next local Y=math.floor local j=h.is_windows local F="\100\101\102\097\117\108\116"local G local n local I={}local r={_VERSION="\049\046\049\051\046\049"}for W,h in pairs(h)do r[W]=h end r.patterns={FLOAT="\091\037\043\037-\037\100\093\037\100\042\037\046\063\037\100\042\091\101\069\093\063\091\037\043\037-\093\063\037\100\042";INTEGER="\091\043\037-\037\100\093\037\100\042";IDEN="\091\037\097\095\093\091\037\119\095\093\042";FILE="\091\037\097\037\046\092\093\091\058\037\093\091\037\119\037\046\095\037-\092\093\042"}r.stdmt={List={_name="\076\105\115\116"};Map={_name="\077\097\112"};Set={_name="\083\101\116"},MultiMap={_name="\077\117\108\116\105\077\097\112"}}r.pack=table.pack function r.unpack(W,h,K)return y(W,h or 1,K or W.n or#W)end function r.printf(W,...)r.assert_string(1,W)r.fprintf(K,W,...)end function r.fprintf(h,K,...)r.assert_string(2,K)h:write(W(K,...))end do local function W(W,h,K,Q)local N=rawget(W,h)if N and(h~="\095\077"and(h~="\095\078\065\077\069"and(h~="\095\080\065\067\075\065\071\069"and h~="\095\086\069\082\083\073\079\078")))then r.fprintf(io.stderr,"\119\097\114\110\105\110\103\058 \'\037\115\046\037\115\' \119\105\108\108 \110\111\116 \111\118\101\114\114\105\100\101 \101\120\105\115\116\105\110\103 \115\121\109\098\111\108\n",Q,h)return end rawset(W,h,K)end local function h(W,h)for W,K in pairs(W)do if K==h then return W end end return"\063"end local K={}function r.import(Q,N)N=N or _G Q=Q or r if type(Q)=="\115\116\114\105\110\103"then Q=require(Q)end local y=h(N,Q)if K[Q]then return end K[Q]=y for h,K in pairs(Q)do W(N,h,K,y)end end end function r.choose(W,h,K)return W and h or K end function r.array_tostring(W,h,K)h,K=h or{},K or tostring for Q=1,#W,1 do h[Q]=K(W[Q],Q)end return h end function r.is_type(W,h)if type(h)=="\115\116\114\105\110\103"then return type(W)==h end local K=getmetatable(W)return h==K end function r.npairs(W,h,K,Q)Q=Q or 1 if Q==0 then error("\105\116\101\114\097\116\111\114 \115\116\101\112-\115\105\122\101 \099\097\110\110\111\116 \098\101 \048",2)end local N=(h or 1)-Q K=K or W.n or#W if Q<0 then return function()N=N+Q if N<K then return nil end return N,W[N]end else return function()N=N+Q if N>K then return nil end return N,W[N]end end end function r.kpairs(W)local h return function()local K while true do h,K=k(W,h)if type(h)~="\110\117\109\098\101\114"or Y(h)~=h then break end end return h,K end end function r.assert_arg(W,h,K,Q,N,y)if type(h)~=K then error(("\097\114\103\117\109\101\110\116 \037\100 \101\120\112\101\099\116\101\100 \097 \'\037\115\'\044 \103\111\116 \097 \'\037\115\'"):format(W,K,type(h)),y or 2)end if Q and not Q(h)then error(("\097\114\103\117\109\101\110\116 \037\100\058 \'\037\115\' \037\115"):format(W,h,N),y or 2)end return h end function r.enum(...)local W=select(1,...)local h={}local K if type(W)~="\116\097\098\108\101"then K=r.pack(...)for W,K in r.npairs(K)do r.assert_arg(W,K,"\115\116\114\105\110\103")h[K]=K end else r.assert_arg(1,W,"\116\097\098\108\101")K={}for W,Q in ipairs(W)do if type(Q)~="\115\116\114\105\110\103"then error(("\101\120\112\101\099\116\101\100 \'\115\116\114\105\110\103\' \098\117\116 \103\111\116 \'\037\115\' \097\116 \105\110\100\101\120 \037\100"):format(type(Q),W),2)end K[W]=Q h[Q]=Q end for W,Q in r.kpairs(W)do if type(W)~="\115\116\114\105\110\103"then error(("\101\120\112\101\099\116\101\100 \107\101\121 \116\111 \098\101 \'\115\116\114\105\110\103\' \098\117\116 \103\111\116 \'\037\115\'"):format(type(W)),2)end if h[W]then error(("\100\117\112\108\105\099\097\116\101 \101\110\116\114\121 \105\110 \097\114\114\097\121 \097\110\100 \104\097\115\104 \112\097\114\116\058 \'\037\115\'"):format(W),2)end h[W]=Q K[#K+1]=W end end if not K[1]then error("\101\120\112\101\099\116\101\100 \097\116 \108\101\097\115\116 \049 \101\110\116\114\121",2)end local Q="\040\101\120\112\101\099\116\101\100 \111\110\101 \111\102\058 \'"..(N(K,"\'\044 \'").."\'\041")setmetatable(h,{__index=function(W,h)error(("\'\037\115\' \105\115 \110\111\116 \097 \118\097\108\105\100 \118\097\108\117\101 \037\115"):format(tostring(h),Q),2)end;__newindex=function(W,h,K)error("\116\104\101 \069\110\117\109 \111\098\106\101\099\116 \105\115 \114\101\097\100-\111\110\108\121",2)end;__call=function(W,h)if type(h)=="\115\116\114\105\110\103"then local K=rawget(W,h)if K~=nil then return K end end return nil,("\'\037\115\' \105\115 \110\111\116 \097 \118\097\108\105\100 \118\097\108\117\101 \037\115"):format(tostring(h),Q)end})return h end function r.function_arg(W,h,K)r.assert_arg(1,W,"\110\117\109\098\101\114")local Q=type(h)if Q=="\102\117\110\099\116\105\111\110"then return h end if Q=="\115\116\114\105\110\103"then if not n then n=(require("\112\108\046\111\112\101\114\097\116\111\114")).optable end local W=n[h]if W then return W end local K,Q=r.string_lambda(h)if not K then error(Q..("\058 "..h))end return K elseif Q=="\116\097\098\108\101"or Q=="\117\115\101\114\100\097\116\097"then local W=getmetatable(h)if not W then error("\110\111\116 \097 \099\097\108\108\097\098\108\101 \111\098\106\101\099\116",2)end local K=I[W]if not K then if not W.__call then error("\110\111\116 \097 \099\097\108\108\097\098\108\101 \111\098\106\101\099\116",2)end return h else return K(h)end end if not K then K=" \109\117\115\116 \098\101 \099\097\108\108\097\098\108\101"end if W>0 then error("\097\114\103\117\109\101\110\116 "..(W..("\058 "..K)),2)else error(K,2)end end function r.assert_string(W,h)return r.assert_arg(W,h,"\115\116\114\105\110\103",nil,nil,3)end function r.on_error(W)W=tostring(W)if({default=1,quit=2;error=3})[W]then F=W else local h="\066\097\100 \097\114\103\117\109\101\110\116 \101\120\112\101\099\116\101\100 \115\116\114\105\110\103\059 \'\100\101\102\097\117\108\116\'\044 \'\113\117\105\116\'\044 \111\114 \'\101\114\114\111\114\'\046 \071\111\116 \'"..(tostring(W).."\'")if F=="\100\101\102\097\117\108\116"then error(h,2)end G(h)end end function r.raise(W)if F=="\100\101\102\097\117\108\116"then return nil,W elseif F=="\113\117\105\116"then return r.quit(W)else error(W,2)end end G=r.raise function r.readfile(W,h)local K=h and"\098"or""r.assert_string(1,W)local Q,N=io.open(W,"\114"..K)if not Q then return G(N)end local y,X=Q:read("\042\097")Q:close()if not y then return G(W..("\058 "..X))end return y end function r.writefile(W,h,K)local Q=K and"\098"or""r.assert_string(1,W)r.assert_string(2,h)local N,y=io.open(W,"\119"..Q)if not N then return G(y)end local X,A=N:write(h)N:close()if not X then return G(W..("\058 "..A))end return true end function r.readlines(W)r.assert_string(1,W)local h,K=io.open(W,"\114")if not h then return G(K)end local N={}for W in h:lines()do Q(N,W)end h:close()return N end function r.executeex(W,h)local K=os.tmpname()local Q=os.tmpname()if j and not K:find("\058")then K=os.getenv("\084\069\077\080")..K Q=os.getenv("\084\069\077\080")..Q end W=W..(" \062 "..(r.quote_arg(K)..(" \050\062 "..r.quote_arg(Q))))local N,y=r.execute(W)local X=r.readfile(K,h)local A=r.readfile(Q,h)os.remove(K)os.remove(Q)return N,y,X or"",A or""end function r.quote_arg(W)if type(W)=="\116\097\098\108\101"then local h={}for W,K in ipairs(W)do h[W]=r.quote_arg(K)end return N(h," ")end if j then if W==""or W:find("\091 \012\t\v\093")then W="\""..((W:gsub("\040\092\042\041\"","\037\049\037\049\092\"")):gsub("\092\043\036","\037\048\037\048").."\"")end return W:gsub("\091\"\094\060\062\033\124\038\037\037\093","\094\037\048")else if W==""or W:find("\091\094\097-\122\065-\090\048-\057\095\064\037\043\061\058\044\046\047-\093")then W="\'"..(W:gsub("\'","\'\092\'\'").."\'")end return W end end function r.quit(W,h,...)if type(W)=="\115\116\114\105\110\103"then r.fprintf(io.stderr,W,h,...)io.stderr:write("\n")W=-1 elseif h then r.fprintf(io.stderr,h,...)io.stderr:write("\n")end os.exit(W,true)end function r.escape(W)r.assert_string(1,W)return W:gsub("\091\037-\037\046\037\043\037\091\037\093\037\040\037\041\037\036\037\094\037\037\037\063\037\042\093","\037\037\037\049")end function r.split(W,h,K,N)r.assert_string(1,W)local y,k=1,{}if not h then h="\037\115\043"end if h==""then return{W}end while true do local Y,j=X(W,h,y,K)if not Y then local h=A(W,y)if h~=""then Q(k,h)end if#k==1 and k[1]==""then return{}else return k end end Q(k,A(W,y,Y-1))if N and#k==N then k[#k]=A(W,y)return k end y=j+1 end end function r.splitv(W,h,K,Q)return y(r.split(W,h,K,Q))end function r.memoize(W)local h={}return function(K)local Q=h[K]if Q==nil then Q=W(K)h[K]=Q end return Q end end function r.add_function_factory(W,h)I[W]=h end local function s(W)if W:find("\094\124")or W:find("\095")then local h,K=W:match("\124\040\091\094\124\093\042\041\124\040\046\043\041")if W:find("\095")then h="\095"K=W else if not h then return G("\098\097\100 \115\116\114\105\110\103 \108\097\109\098\100\097")end end local Q="\114\101\116\117\114\110 \102\117\110\099\116\105\111\110\040"..(h..("\041 \114\101\116\117\114\110 "..(K.." \101\110\100")))local N,y=r.load(Q)if not N then return G(y)end N=N()return N else return G("\110\111\116 \097 \115\116\114\105\110\103 \108\097\109\098\100\097")end end r.string_lambda=r.memoize(s)function r.bind1(W,h)W=r.function_arg(1,W)return function(...)return W(h,...)end end function r.bind2(W,h)W=r.function_arg(1,W)return function(K,...)return W(K,h,...)end end do local W=function(W,h)if h then warn(W,"\n",h)else warn(W)end end function r.set_deprecation_func(h)if h==nil then W=function() end else r.assert_arg(1,h,"\102\117\110\099\116\105\111\110")W=h end end function r.raise_deprecation(h)r.assert_arg(1,h,"\116\097\098\108\101")if type(h.message)~="\115\116\114\105\110\103"then error("\102\105\101\108\100 \'\109\101\115\115\097\103\101\' \111\102 \116\104\101 \111\112\116\105\111\110\115 \116\097\098\108\101 \109\117\115\116 \098\101 \097 \115\116\114\105\110\103",2)end local K if not h.no_trace then K=(debug.traceback("",2)):match("\091\n\037\115\093\042\040\046-\041\036")end local Q if h.deprecated_after and h.version_removed then Q=(" \040\100\101\112\114\101\099\097\116\101\100 \097\102\116\101\114 \037\115\044 \115\099\104\101\100\117\108\101\100 \102\111\114 \114\101\109\111\118\097\108 \105\110 \037\115\041"):format(tostring(h.deprecated_after),tostring(h.version_removed))elseif h.deprecated_after then Q=(" \040\100\101\112\114\101\099\097\116\101\100 \097\102\116\101\114 \037\115\041"):format(tostring(h.deprecated_after))elseif h.version_removed then Q=(" \040\115\099\104\101\100\117\108\101\100 \102\111\114 \114\101\109\111\118\097\108 \105\110 \037\115\041"):format(tostring(h.version_removed))else Q=""end Q=h.message..Q if h.source then Q="\091"..(h.source..("\093 "..Q))else if Q:sub(1,1)=="\064"then error("\109\101\115\115\097\103\101 \099\097\110\110\111\116 \115\116\097\114\116 \119\105\116\104 \'\064\'",2)end end W(Q,K)end end return r
--- 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 concat = table.concat
local _unpack = table.unpack  -- always injected by 'compat'
local find = string.find
local sub = string.sub
local next = next
local floor = math.floor

local is_windows = compat.is_windows
local err_mode = 'default'
local raise
local operators
local _function_factories = {}


local utils = { _VERSION = "1.13.1" }
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] j index of the last element to unpack, defaults to `t.n` or else `#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



--- an iterator over all non-integer keys (inverse of `ipairs`).
-- It will skip any key that is an integer number, so negative indices or an
-- array with holes will not return those either (so it returns slightly less than
-- 'the inverse of `ipairs`').
--
-- This uses `pairs` under the hood, so any value that is iterable using `pairs`
-- will work with this function.
-- @tparam table t the table to iterate over
-- @treturn key
-- @treturn value
-- @usage
-- local t = {
--   "hello",
--   "world",
--   hello = "hallo",
--   world = "Welt",
-- }
--
-- for k, v in utils.kpairs(t) do
--   print("German: ", v)
-- end
--
-- -- output;
-- -- German: hallo
-- -- German: Welt
function utils.kpairs(t)
  local index
  return function()
    local value
    while true do
      index, value = next(t, index)
      if type(index) ~= "number" or floor(index) ~= index then
        break
      end
    end
    return index, value
  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

--- creates an Enum or constants lookup table for improved error handling.
-- This helps prevent magic strings in code by throwing errors for accessing
-- non-existing values, and/or converting strings/identifiers to other values.
--
-- Calling on the object does the same, but returns a soft error; `nil + err`, if
-- the call is succesful (the key exists), it will return the value.
--
-- When calling with varargs or an array the values will be equal to the keys.
-- The enum object is read-only.
-- @tparam table|vararg ... the input for the Enum. If varargs or an array then the
-- values in the Enum will be equal to the names (must be strings), if a hash-table
-- then values remain (any type), and the keys must be strings.
-- @return Enum object (read-only table/object)
-- @usage -- Enum access at runtime
-- local obj = {}
-- obj.MOVEMENT = utils.enum("FORWARD", "REVERSE", "LEFT", "RIGHT")
--
-- if current_movement == obj.MOVEMENT.FORWARD then
--   -- do something
--
-- elseif current_movement == obj.MOVEMENT.REVERES then
--   -- throws error due to typo 'REVERES', so a silent mistake becomes a hard error
--   -- "'REVERES' is not a valid value (expected one of: 'FORWARD', 'REVERSE', 'LEFT', 'RIGHT')"
--
-- end
-- @usage -- standardized error codes
-- local obj = {
--   ERR = utils.enum {
--     NOT_FOUND = "the item was not found",
--     OUT_OF_BOUNDS = "the index is outside the allowed range"
--   },
--
--   some_method = function(self)
--     return self.ERR.OUT_OF_BOUNDS
--   end,
-- }
--
-- local result, err = obj:some_method()
-- if not result then
--   if err == obj.ERR.NOT_FOUND then
--     -- check on error code, not magic strings
--
--   else
--     -- return the error description, contained in the constant
--     return nil, "error: "..err  -- "error: the index is outside the allowed range"
--   end
-- end
-- @usage -- validating/converting user-input
-- local color = "purple"
-- local ansi_colors = utils.enum {
--   black     = 30,
--   red       = 31,
--   green     = 32,
-- }
-- local color_code, err = ansi_colors(color) -- calling on the object, returns the value from the enum
-- if not color_code then
--   print("bad 'color', " .. err)
--   -- "bad 'color', 'purple' is not a valid value (expected one of: 'black', 'red', 'green')"
--   os.exit(1)
-- end
function utils.enum(...)
  local first = select(1, ...)
  local enum = {}
  local lst

  if type(first) ~= "table" then
    -- vararg with strings
    lst = utils.pack(...)
    for i, value in utils.npairs(lst) do
      utils.assert_arg(i, value, "string")
      enum[value] = value
    end

  else
    -- table/array with values
    utils.assert_arg(1, first, "table")
    lst = {}
    -- first add array part
    for i, value in ipairs(first) do
      if type(value) ~= "string" then
        error(("expected 'string' but got '%s' at index %d"):format(type(value), i), 2)
      end
      lst[i] = value
      enum[value] = value
    end
    -- add key-ed part
    for key, value in utils.kpairs(first) do
      if type(key) ~= "string" then
        error(("expected key to be 'string' but got '%s'"):format(type(key)), 2)
      end
      if enum[key] then
        error(("duplicate entry in array and hash part: '%s'"):format(key), 2)
      end
      enum[key] = value
      lst[#lst+1] = key
    end
  end

  if not lst[1] then
    error("expected at least 1 entry", 2)
  end

  local valid = "(expected one of: '" .. concat(lst, "', '") .. "')"
  setmetatable(enum, {
    __index = function(self, key)
      error(("'%s' is not a valid value %s"):format(tostring(key), valid), 2)
    end,
    __newindex = function(self, key, value)
      error("the Enum object is read-only", 2)
    end,
    __call = function(self, key)
      if type(key) == "string" then
        local v = rawget(self, key)
        if v ~= nil then
          return v
        end
      end
      return nil, ("'%s' is not a valid value %s"):format(tostring(key), valid)
    end
  })

  return enum
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 <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


--- 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 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 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


