• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / Penlight / 589

29 Dec 2025 04:27PM UTC coverage: 89.278% (+0.4%) from 88.871%
589

Pull #503

appveyor

web-flow
chore(ci): Update Lua and LuaRocks versions for Lua 5.5
Pull Request #503: chore(ci): Update Lua and LuaRocks versions for Lua 5.5

5479 of 6137 relevant lines covered (89.28%)

165.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.15
/lua/pl/utils.lua
1
--- Generally useful routines.
2
-- See  @{01-introduction.md.Generally_useful_functions|the Guide}.
3
--
4
-- Dependencies: `pl.compat`, all exported fields and functions from
5
-- `pl.compat` are also available in this module.
6
--
7
-- @module pl.utils
8
local format = string.format
176✔
9
local compat = require 'pl.compat'
176✔
10
local stdout = io.stdout
176✔
11
local append = table.insert
176✔
12
local concat = table.concat
176✔
13
local _unpack = table.unpack  -- always injected by 'compat'
176✔
14
local find = string.find
176✔
15
local sub = string.sub
176✔
16
local next = next
176✔
17
local floor = math.floor
176✔
18

19
local is_windows = compat.is_windows
176✔
20
local err_mode = 'default'
176✔
21
local raise
22
local operators
23
local _function_factories = {}
176✔
24

25

26
local utils = { _VERSION = "1.14.0" }
176✔
27

28
for k, v in pairs(compat) do utils[k] = v  end
1,584✔
29

30
--- Some standard patterns
31
-- @table patterns
32
utils.patterns = {
176✔
33
    FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*', -- floating point number
176✔
34
    INTEGER = '[+%-%d]%d*',                     -- integer number
176✔
35
    IDEN = '[%a_][%w_]*',                       -- identifier
176✔
36
    FILE = '[%a%.\\][:%][%w%._%-\\]*',          -- file
176✔
37
}
176✔
38

39

40
--- Standard meta-tables as used by other Penlight modules
41
-- @table stdmt
42
-- @field List the List metatable
43
-- @field Map the Map metatable
44
-- @field Set the Set metatable
45
-- @field MultiMap the MultiMap metatable
46
utils.stdmt = {
176✔
47
    List = {_name='List'},
176✔
48
    Map = {_name='Map'},
176✔
49
    Set = {_name='Set'},
176✔
50
    MultiMap = {_name='MultiMap'},
176✔
51
}
176✔
52

53

54
--- pack an argument list into a table.
55
-- @param ... any arguments
56
-- @return a table with field `n` set to the length
57
-- @function utils.pack
58
-- @see compat.pack
59
-- @see utils.npairs
60
-- @see utils.unpack
61
utils.pack = table.pack  -- added here to be symmetrical with unpack
176✔
62

63
--- unpack a table and return its contents.
64
--
65
-- NOTE: this implementation differs from the Lua implementation in the way
66
-- that this one DOES honor the `n` field in the table `t`, such that it is 'nil-safe'.
67
-- @param t table to unpack
68
-- @param[opt] i index from which to start unpacking, defaults to 1
69
-- @param[opt] j index of the last element to unpack, defaults to `t.n` or else `#t`
70
-- @return multiple return values from the table
71
-- @function utils.unpack
72
-- @see compat.unpack
73
-- @see utils.pack
74
-- @see utils.npairs
75
-- @usage
76
-- local t = table.pack(nil, nil, nil, 4)
77
-- local a, b, c, d = table.unpack(t)   -- this `unpack` is NOT nil-safe, so d == nil
78
--
79
-- local a, b, c, d = utils.unpack(t)   -- this is nil-safe, so d == 4
80
function utils.unpack(t, i, j)
176✔
81
    return _unpack(t, i or 1, j or t.n or #t)
404✔
82
end
83

84
--- print an arbitrary number of arguments using a format.
85
-- Output will be sent to `stdout`.
86
-- @param fmt The format (see `string.format`)
87
-- @param ... Extra arguments for format
88
function utils.printf(fmt, ...)
176✔
89
    utils.assert_string(1, fmt)
12✔
90
    utils.fprintf(stdout, fmt, ...)
8✔
91
end
92

93
--- write an arbitrary number of arguments to a file using a format.
94
-- @param f File handle to write to.
95
-- @param fmt The format (see `string.format`).
96
-- @param ... Extra arguments for format
97
function utils.fprintf(f,fmt,...)
176✔
98
    utils.assert_string(2,fmt)
94✔
99
    f:write(format(fmt,...))
94✔
100
end
101

102
do
103
    local function import_symbol(T,k,v,libname)
104
        local key = rawget(T,k)
451✔
105
        -- warn about collisions!
106
        if key and k ~= '_M' and k ~= '_NAME' and k ~= '_PACKAGE' and k ~= '_VERSION' then
451✔
107
            utils.fprintf(io.stderr,"warning: '%s.%s' will not override existing symbol\n",libname,k)
2✔
108
            return
2✔
109
        end
110
        rawset(T,k,v)
449✔
111
    end
112

113
    local function lookup_lib(T,t)
114
        for k,v in pairs(T) do
438✔
115
            if v == t then return k end
430✔
116
        end
117
        return '?'
8✔
118
    end
119

120
    local already_imported = {}
176✔
121

122
    --- take a table and 'inject' it into the local namespace.
123
    -- @param t The table (table), or module name (string), defaults to this `utils` module table
124
    -- @param T An optional destination table (defaults to callers environment)
125
    function utils.import(t,T)
176✔
126
        T = T or _G
12✔
127
        t = t or utils
12✔
128
        if type(t) == 'string' then
12✔
129
            t = require (t)
4✔
130
        end
131
        local libname = lookup_lib(T,t)
12✔
132
        if already_imported[t] then return end
12✔
133
        already_imported[t] = libname
12✔
134
        for k,v in pairs(t) do
463✔
135
            import_symbol(T,k,v,libname)
451✔
136
        end
137
    end
138
end
139

140
--- return either of two values, depending on a condition.
141
-- @param cond A condition
142
-- @param value1 Value returned if cond is truthy
143
-- @param value2 Value returned if cond is falsy
144
function utils.choose(cond, value1, value2)
176✔
145
    if cond then
48✔
146
        return value1
28✔
147
    else
148
        return value2
20✔
149
    end
150
end
151

152
--- convert an array of values to strings.
153
-- @param t a list-like table
154
-- @param[opt] temp (table) buffer to use, otherwise allocate
155
-- @param[opt] tostr custom tostring function, called with (value,index). Defaults to `tostring`.
156
-- @return the converted buffer
157
function utils.array_tostring (t,temp,tostr)
176✔
158
    temp, tostr = temp or {}, tostr or tostring
96✔
159
    for i = 1,#t do
652✔
160
        temp[i] = tostr(t[i],i)
556✔
161
    end
162
    return temp
96✔
163
end
164

165

166

167
--- is the object of the specified type?
168
-- If the type is a string, then use type, otherwise compare with metatable
169
-- @param obj An object to check
170
-- @param tp String of what type it should be
171
-- @return boolean
172
-- @usage utils.is_type("hello world", "string")   --> true
173
-- -- or check metatable
174
-- local my_mt = {}
175
-- local my_obj = setmetatable(my_obj, my_mt)
176
-- utils.is_type(my_obj, my_mt)  --> true
177
function utils.is_type (obj,tp)
176✔
178
    if type(tp) == 'string' then return type(obj) == tp end
12✔
179
    local mt = getmetatable(obj)
4✔
180
    return tp == mt
4✔
181
end
182

183

184

185
--- an iterator with indices, similar to `ipairs`, but with a range.
186
-- This is a nil-safe index based iterator that will return `nil` when there
187
-- is a hole in a list. To be safe ensure that table `t.n` contains the length.
188
-- @tparam table t the table to iterate over
189
-- @tparam[opt=1] integer i_start start index
190
-- @tparam[opt=t.n or #t] integer i_end end index
191
-- @tparam[opt=1] integer step step size
192
-- @treturn integer index
193
-- @treturn any value at index (which can be `nil`!)
194
-- @see utils.pack
195
-- @see utils.unpack
196
-- @usage
197
-- local t = utils.pack(nil, 123, nil)  -- adds an `n` field when packing
198
--
199
-- for i, v in utils.npairs(t, 2) do  -- start at index 2
200
--   t[i] = tostring(t[i])
201
-- end
202
--
203
-- -- t = { n = 3, [2] = "123", [3] = "nil" }
204
function utils.npairs(t, i_start, i_end, step)
176✔
205
  step = step or 1
88✔
206
  if step == 0 then
88✔
207
    error("iterator step-size cannot be 0", 2)
4✔
208
  end
209
  local i = (i_start or 1) - step
84✔
210
  i_end = i_end or t.n or #t
84✔
211
  if step < 0 then
84✔
212
    return function()
213
      i = i + step
32✔
214
      if i < i_end then
32✔
215
        return nil
8✔
216
      end
217
      return i, t[i]
24✔
218
    end
219

220
  else
221
    return function()
222
      i = i + step
256✔
223
      if i > i_end then
256✔
224
        return nil
68✔
225
      end
226
      return i, t[i]
188✔
227
    end
228
  end
229
end
230

231

232

233
--- an iterator over all non-integer keys (inverse of `ipairs`).
234
-- It will skip any key that is an integer number, so negative indices or an
235
-- array with holes will not return those either (so it returns slightly less than
236
-- 'the inverse of `ipairs`').
237
--
238
-- This uses `pairs` under the hood, so any value that is iterable using `pairs`
239
-- will work with this function.
240
-- @tparam table t the table to iterate over
241
-- @treturn key
242
-- @treturn value
243
-- @usage
244
-- local t = {
245
--   "hello",
246
--   "world",
247
--   hello = "hallo",
248
--   world = "Welt",
249
-- }
250
--
251
-- for k, v in utils.kpairs(t) do
252
--   print("German: ", v)
253
-- end
254
--
255
-- -- output;
256
-- -- German: hallo
257
-- -- German: Welt
258
function utils.kpairs(t)
176✔
259
  local index
260
  return function()
261
    local value
262
    while true do
263
      index, value = next(t, index)
96✔
264
      if type(index) ~= "number" or floor(index) ~= index then
96✔
265
        break
1✔
266
      end
267
    end
268
    return index, value
64✔
269
  end
270
end
271

272

273

274
--- Error handling
275
-- @section Error-handling
276

277
--- assert that the given argument is in fact of the correct type.
278
-- @param n argument index
279
-- @param val the value
280
-- @param tp the type
281
-- @param verify an optional verification function
282
-- @param msg an optional custom message
283
-- @param lev optional stack position for trace, default 2
284
-- @return the validated value
285
-- @raise if `val` is not the correct type
286
-- @usage
287
-- local param1 = assert_arg(1,"hello",'table')  --> error: argument 1 expected a 'table', got a 'string'
288
-- local param4 = assert_arg(4,'!@#$%^&*','string',path.isdir,'not a directory')
289
--      --> error: argument 4: '!@#$%^&*' not a directory
290
function utils.assert_arg (n,val,tp,verify,msg,lev)
176✔
291
    if type(val) ~= tp then
16,822✔
292
        error(("argument %d expected a '%s', got a '%s'"):format(n,tp,type(val)),lev or 2)
32✔
293
    end
294
    if verify and not verify(val) then
16,790✔
295
        error(("argument %d: '%s' %s"):format(n,val,msg),lev or 2)
12✔
296
    end
297
    return val
16,778✔
298
end
299

300
--- creates an Enum or constants lookup table for improved error handling.
301
-- This helps prevent magic strings in code by throwing errors for accessing
302
-- non-existing values, and/or converting strings/identifiers to other values.
303
--
304
-- Calling on the object does the same, but returns a soft error; `nil + err`, if
305
-- the call is successful (the key exists), it will return the value.
306
--
307
-- When calling with varargs or an array the values will be equal to the keys.
308
-- The enum object is read-only.
309
-- @tparam table|vararg ... the input for the Enum. If varargs or an array then the
310
-- values in the Enum will be equal to the names (must be strings), if a hash-table
311
-- then values remain (any type), and the keys must be strings.
312
-- @return Enum object (read-only table/object)
313
-- @usage -- Enum access at runtime
314
-- local obj = {}
315
-- obj.MOVEMENT = utils.enum("FORWARD", "REVERSE", "LEFT", "RIGHT")
316
--
317
-- if current_movement == obj.MOVEMENT.FORWARD then
318
--   -- do something
319
--
320
-- elseif current_movement == obj.MOVEMENT.REVERES then
321
--   -- throws error due to typo 'REVERES', so a silent mistake becomes a hard error
322
--   -- "'REVERES' is not a valid value (expected one of: 'FORWARD', 'REVERSE', 'LEFT', 'RIGHT')"
323
--
324
-- end
325
-- @usage -- standardized error codes
326
-- local obj = {
327
--   ERR = utils.enum {
328
--     NOT_FOUND = "the item was not found",
329
--     OUT_OF_BOUNDS = "the index is outside the allowed range"
330
--   },
331
--
332
--   some_method = function(self)
333
--     return nil, self.ERR.OUT_OF_BOUNDS
334
--   end,
335
-- }
336
--
337
-- local result, err = obj:some_method()
338
-- if not result then
339
--   if err == obj.ERR.NOT_FOUND then
340
--     -- check on error code, not magic strings
341
--
342
--   else
343
--     -- return the error description, contained in the constant
344
--     return nil, "error: "..err  -- "error: the index is outside the allowed range"
345
--   end
346
-- end
347
-- @usage -- validating/converting user-input
348
-- local color = "purple"
349
-- local ansi_colors = utils.enum {
350
--   black     = 30,
351
--   red       = 31,
352
--   green     = 32,
353
-- }
354
-- local color_code, err = ansi_colors(color) -- calling on the object, returns the value from the enum
355
-- if not color_code then
356
--   print("bad 'color', " .. err)
357
--   -- "bad 'color', 'purple' is not a valid value (expected one of: 'black', 'red', 'green')"
358
--   os.exit(1)
359
-- end
360
function utils.enum(...)
176✔
361
  local first = select(1, ...)
72✔
362
  local enum = {}
72✔
363
  local lst
364

365
  if type(first) ~= "table" then
72✔
366
    -- vararg with strings
367
    lst = utils.pack(...)
40✔
368
    for i, value in utils.npairs(lst) do
128✔
369
      utils.assert_arg(i, value, "string")
96✔
370
      enum[value] = value
88✔
371
    end
372

373
  else
374
    -- table/array with values
375
    utils.assert_arg(1, first, "table")
32✔
376
    lst = {}
32✔
377
    -- first add array part
378
    for i, value in ipairs(first) do
56✔
379
      if type(value) ~= "string" then
28✔
380
        error(("expected 'string' but got '%s' at index %d"):format(type(value), i), 2)
4✔
381
      end
382
      lst[i] = value
24✔
383
      enum[value] = value
24✔
384
    end
385
    -- add key-ed part
386
    for key, value in utils.kpairs(first) do
44✔
387
      if type(key) ~= "string" then
24✔
388
        error(("expected key to be 'string' but got '%s'"):format(type(key)), 2)
4✔
389
      end
390
      if enum[key] then
20✔
391
        error(("duplicate entry in array and hash part: '%s'"):format(key), 2)
4✔
392
      end
393
      enum[key] = value
16✔
394
      lst[#lst+1] = key
16✔
395
    end
396
  end
397

398
  if not lst[1] then
52✔
399
    error("expected at least 1 entry", 2)
12✔
400
  end
401

402
  local valid = "(expected one of: '" .. concat(lst, "', '") .. "')"
40✔
403
  setmetatable(enum, {
80✔
404
    __index = function(self, key)
405
      error(("'%s' is not a valid value %s"):format(tostring(key), valid), 2)
8✔
406
    end,
407
    __newindex = function(self, key, value)
408
      error("the Enum object is read-only", 2)
4✔
409
    end,
410
    __call = function(self, key)
411
      if type(key) == "string" then
8✔
412
        local v = rawget(self, key)
8✔
413
        if v ~= nil then
8✔
414
          return v
4✔
415
        end
416
      end
417
      return nil, ("'%s' is not a valid value %s"):format(tostring(key), valid)
4✔
418
    end
419
  })
420

421
  return enum
40✔
422
end
423

424

425
--- process a function argument.
426
-- This is used throughout Penlight and defines what is meant by a function:
427
-- Something that is callable, or an operator string as defined by <code>pl.operator</code>,
428
-- such as '>' or '#'. If a function factory has been registered for the type, it will
429
-- be called to get the function.
430
-- @param idx argument index
431
-- @param f a function, operator string, or callable object
432
-- @param msg optional error message
433
-- @return a callable
434
-- @raise if idx is not a number or if f is not callable
435
function utils.function_arg (idx,f,msg)
176✔
436
    utils.assert_arg(1,idx,'number')
1,328✔
437
    local tp = type(f)
1,328✔
438
    if tp == 'function' then return f end  -- no worries!
1,328✔
439
    -- ok, a string can correspond to an operator (like '==')
440
    if tp == 'string' then
268✔
441
        if not operators then operators = require 'pl.operator'.optable end
228✔
442
        local fn = operators[f]
228✔
443
        if fn then return fn end
228✔
444
        local fn, err = utils.string_lambda(f)
28✔
445
        if not fn then error(err..': '..f) end
28✔
446
        return fn
28✔
447
    elseif tp == 'table' or tp == 'userdata' then
40✔
448
        local mt = getmetatable(f)
36✔
449
        if not mt then error('not a callable object',2) end
36✔
450
        local ff = _function_factories[mt]
32✔
451
        if not ff then
32✔
452
            if not mt.__call then error('not a callable object',2) end
×
453
            return f
×
454
        else
455
            return ff(f) -- we have a function factory for this type!
32✔
456
        end
457
    end
458
    if not msg then msg = " must be callable" end
4✔
459
    if idx > 0 then
4✔
460
        error("argument "..idx..": "..msg,2)
4✔
461
    else
462
        error(msg,2)
×
463
    end
464
end
465

466

467
--- assert the common case that the argument is a string.
468
-- @param n argument index
469
-- @param val a value that must be a string
470
-- @return the validated value
471
-- @raise val must be a string
472
-- @usage
473
-- local val = 42
474
-- local param2 = utils.assert_string(2, val) --> error: argument 2 expected a 'string', got a 'number'
475
function utils.assert_string (n, val)
176✔
476
    return utils.assert_arg(n,val,'string',nil,nil,3)
11,985✔
477
end
478

479
--- control the error strategy used by Penlight.
480
-- This is a global setting that controls how `utils.raise` behaves:
481
--
482
-- - 'default': return `nil + error` (this is the default)
483
-- - 'error': throw a Lua error
484
-- - 'quit': exit the program
485
--
486
-- @param mode either 'default', 'quit'  or 'error'
487
-- @see utils.raise
488
function utils.on_error (mode)
176✔
489
    mode = tostring(mode)
40✔
490
    if ({['default'] = 1, ['quit'] = 2, ['error'] = 3})[mode] then
40✔
491
      err_mode = mode
28✔
492
    else
493
      -- fail loudly
494
      local err = "Bad argument expected string; 'default', 'quit', or 'error'. Got '"..tostring(mode).."'"
12✔
495
      if err_mode == 'default' then
12✔
496
        error(err, 2)  -- even in 'default' mode fail loud in this case
4✔
497
      end
498
      raise(err)
8✔
499
    end
500
end
501

502
--- used by Penlight functions to return errors. Its global behaviour is controlled
503
-- by `utils.on_error`.
504
-- To use this function you MUST use it in conjunction with `return`, since it might
505
-- return `nil + error`.
506
-- @param err the error string.
507
-- @see utils.on_error
508
-- @usage
509
-- if some_condition then
510
--   return utils.raise("some condition was not met")  -- MUST use 'return'!
511
-- end
512
function utils.raise (err)
176✔
513
    if err_mode == 'default' then
36✔
514
        return nil, err
20✔
515
    elseif err_mode == 'quit' then
16✔
516
        return utils.quit(err)
8✔
517
    else
518
        error(err, 2)
8✔
519
    end
520
end
521
raise = utils.raise
176✔
522

523

524

525
--- File handling
526
-- @section files
527

528
--- return the contents of a file as a string
529
-- @param filename The file path
530
-- @param is_bin open in binary mode
531
-- @return file contents
532
function utils.readfile(filename,is_bin)
176✔
533
    local mode = is_bin and 'b' or ''
320✔
534
    utils.assert_string(1,filename)
320✔
535
    local f,open_err = io.open(filename,'r'..mode)
320✔
536
    if not f then return raise (open_err) end
320✔
537
    local res,read_err = f:read('*a')
320✔
538
    f:close()
320✔
539
    if not res then
320✔
540
        -- Errors in io.open have "filename: " prefix,
541
        -- error in file:read don't, add it.
542
        return raise (filename..": "..read_err)
×
543
    end
544
    return res
320✔
545
end
546

547
--- write a string to a file
548
-- @param filename The file path
549
-- @param str The string
550
-- @param is_bin open in binary mode
551
-- @return true or nil
552
-- @return error message
553
-- @raise error if filename or str aren't strings
554
function utils.writefile(filename,str,is_bin)
176✔
555
    local mode = is_bin and 'b' or ''
68✔
556
    utils.assert_string(1,filename)
68✔
557
    utils.assert_string(2,str)
68✔
558
    local f,err = io.open(filename,'w'..mode)
68✔
559
    if not f then return raise(err) end
68✔
560
    local ok, write_err = f:write(str)
68✔
561
    f:close()
68✔
562
    if not ok then
68✔
563
        -- Errors in io.open have "filename: " prefix,
564
        -- error in file:write don't, add it.
565
        return raise (filename..": "..write_err)
×
566
    end
567
    return true
68✔
568
end
569

570
--- return the contents of a file as a list of lines
571
-- @param filename The file path
572
-- @return file contents as a table
573
-- @raise error if filename is not a string
574
function utils.readlines(filename)
176✔
575
    utils.assert_string(1,filename)
4✔
576
    local f,err = io.open(filename,'r')
4✔
577
    if not f then return raise(err) end
4✔
578
    local res = {}
4✔
579
    for line in f:lines() do
1,284✔
580
        append(res,line)
1,280✔
581
    end
582
    f:close()
4✔
583
    return res
4✔
584
end
585

586
--- OS functions
587
-- @section OS-functions
588

589
--- Execute a shell command.
590
-- This function is a copy of `compat.execute`.
591
-- @class function
592
-- @name utils.execute
593

594
--- execute a shell command and return the output.
595
-- This function redirects the output to tempfiles and returns the content of those files.
596
-- @param cmd a shell command
597
-- @param bin boolean, if true, read output as binary file
598
-- @return true if successful
599
-- @return actual return code
600
-- @return stdout output (string)
601
-- @return errout output (string)
602
function utils.executeex(cmd, bin)
176✔
603
    local outfile = os.tmpname()
152✔
604
    local errfile = os.tmpname()
152✔
605

606
    if is_windows and not outfile:find(':') then
152✔
607
        outfile = os.getenv('TEMP')..outfile
×
608
        errfile = os.getenv('TEMP')..errfile
×
609
    end
610
    cmd = cmd .. " > " .. utils.quote_arg(outfile) .. " 2> " .. utils.quote_arg(errfile)
152✔
611

612
    local success, retcode = utils.execute(cmd)
152✔
613
    local outcontent = utils.readfile(outfile, bin)
152✔
614
    local errcontent = utils.readfile(errfile, bin)
152✔
615
    os.remove(outfile)
152✔
616
    os.remove(errfile)
152✔
617
    return success, retcode, (outcontent or ""), (errcontent or "")
152✔
618
end
619

620
--- Quote and escape an argument of a command.
621
-- Quotes a single (or list of) argument(s) of a command to be passed
622
-- to `os.execute`, `pl.utils.execute` or `pl.utils.executeex`.
623
-- @param argument (string or table/list) the argument to quote. If a list then
624
-- all arguments in the list will be returned as a single string quoted.
625
-- @return quoted and escaped argument.
626
-- @usage
627
-- local options = utils.quote_arg {
628
--     "-lluacov",
629
--     "-e",
630
--     "utils = print(require('pl.utils')._VERSION",
631
-- }
632
-- -- returns: -lluacov -e 'utils = print(require('\''pl.utils'\'')._VERSION'
633
function utils.quote_arg(argument)
176✔
634
    if type(argument) == "table" then
480✔
635
        -- encode an entire table
636
        local r = {}
20✔
637
        for i, arg in ipairs(argument) do
84✔
638
            r[i] = utils.quote_arg(arg)
64✔
639
        end
640

641
        return concat(r, " ")
20✔
642
    end
643
    -- only a single argument
644
    if is_windows then
460✔
645
        if argument == "" or argument:find('[ \f\t\v]') then
460✔
646
            -- Need to quote the argument.
647
            -- Quotes need to be escaped with backslashes;
648
            -- additionally, backslashes before a quote, escaped or not,
649
            -- need to be doubled.
650
            -- See documentation for CommandLineToArgvW Windows function.
651
            argument = '"' .. argument:gsub([[(\*)"]], [[%1%1\"]]):gsub([[\+$]], "%0%0") .. '"'
24✔
652
        end
653

654
        -- os.execute() uses system() C function, which on Windows passes command
655
        -- to cmd.exe. Escape its special characters.
656
        return (argument:gsub('["^<>!|&%%]', "^%0"))
460✔
657
    else
658
        if argument == "" or argument:find('[^a-zA-Z0-9_@%+=:,./-]') then
×
659
            -- To quote arguments on posix-like systems use single quotes.
660
            -- To represent an embedded single quote close quoted string ('),
661
            -- add escaped quote (\'), open quoted string again (').
662
            argument = "'" .. argument:gsub("'", [['\'']]) .. "'"
×
663
        end
664

665
        return argument
×
666
    end
667
end
668

669
--- error out of this program gracefully.
670
-- @param[opt] code The exit code, defaults to -`1` if omitted
671
-- @param msg The exit message will be sent to `stderr` (will be formatted with the extra parameters)
672
-- @param ... extra arguments for message's format'
673
-- @see utils.fprintf
674
-- @usage utils.quit(-1, "Error '%s' happened", "42")
675
-- -- is equivalent to
676
-- utils.quit("Error '%s' happened", "42")  --> Error '42' happened
677
function utils.quit(code, msg, ...)
176✔
678
    if type(code) == 'string' then
24✔
679
        utils.fprintf(io.stderr, code, msg, ...)
12✔
680
        io.stderr:write('\n')
12✔
681
        code = -1 -- TODO: this is odd, see the test. Which returns 255 as exit code
12✔
682
    elseif msg then
12✔
683
        utils.fprintf(io.stderr, msg, ...)
8✔
684
        io.stderr:write('\n')
8✔
685
    end
686
    os.exit(code, true)
24✔
687
end
688

689

690
--- String functions
691
-- @section string-functions
692

693
--- escape any Lua 'magic' characters in a string
694
-- @param s The input string
695
function utils.escape(s)
176✔
696
    utils.assert_string(1,s)
672✔
697
    return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'))
672✔
698
end
699

700
--- split a string into a list of strings separated by a delimiter.
701
-- @param s The input string
702
-- @param re optional A Lua string pattern; defaults to '%s+'
703
-- @param plain optional If truthy don't use Lua patterns
704
-- @param n optional maximum number of elements (if there are more, the last will remain un-split)
705
-- @return a list-like table
706
-- @raise error if s is not a string
707
-- @see splitv
708
function utils.split(s,re,plain,n)
176✔
709
    utils.assert_string(1,s)
699✔
710
    local i1,ls = 1,{}
699✔
711
    if not re then re = '%s+' end
699✔
712
    if re == '' then return {s} end
699✔
713
    while true do
714
        local i2,i3 = find(s,re,i1,plain)
2,437✔
715
        if not i2 then
2,437✔
716
            local last = sub(s,i1)
619✔
717
            if last ~= '' then append(ls,last) end
619✔
718
            if #ls == 1 and ls[1] == '' then
619✔
719
                return {}
8✔
720
            else
721
                return ls
611✔
722
            end
723
        end
724
        append(ls,sub(s,i1,i2-1))
1,818✔
725
        if n and #ls == n then
1,818✔
726
            ls[#ls] = sub(s,i1)
72✔
727
            return ls
72✔
728
        end
729
        i1 = i3+1
1,746✔
730
    end
731
end
732

733
--- split a string into a number of return values.
734
-- Identical to `split` but returns multiple sub-strings instead of
735
-- a single list of sub-strings.
736
-- @param s the string
737
-- @param re A Lua string pattern; defaults to '%s+'
738
-- @param plain don't use Lua patterns
739
-- @param n optional maximum number of splits
740
-- @return n values
741
-- @usage first,next = splitv('user=jane=doe','=', false, 2)
742
-- assert(first == "user")
743
-- assert(next == "jane=doe")
744
-- @see split
745
function utils.splitv (s,re, plain, n)
176✔
746
    return _unpack(utils.split(s,re, plain, n))
148✔
747
end
748

749

750
--- Functional
751
-- @section functional
752

753

754
--- 'memoize' a function (cache returned value for next call).
755
-- This is useful if you have a function which is relatively expensive,
756
-- but you don't know in advance what values will be required, so
757
-- building a table upfront is wasteful/impossible.
758
-- @param func a function that takes exactly one argument (which later serves as the cache key) and returns a single value
759
-- @return a function taking one argument and returning a single value either from the cache or by running the original input function
760
function utils.memoize(func)
176✔
761
    local cache = {}
180✔
762
    return function(k)
763
        local res = cache[k]
76✔
764
        if res == nil then
76✔
765
            res = func(k)
72✔
766
            cache[k] = res
72✔
767
        end
768
        return res
76✔
769
    end
770
end
771

772

773
--- associate a function factory with a type.
774
-- A function factory takes an object of the given type and
775
-- returns a function for evaluating it
776
-- @tab mt metatable
777
-- @func fun a callable that returns a function
778
function utils.add_function_factory (mt,fun)
176✔
779
    _function_factories[mt] = fun
12✔
780
end
781

782
local function _string_lambda(f)
783
    if f:find '^|' or f:find '_' then
64✔
784
        local args,body = f:match '|([^|]*)|(.+)'
64✔
785
        if f:find '_' then
64✔
786
            args = '_'
36✔
787
            body = f
36✔
788
        else
789
            if not args then return raise 'bad string lambda' end
28✔
790
        end
791
        local fstr = 'return function('..args..') return '..body..' end'
64✔
792
        local fn,err = utils.load(fstr)
64✔
793
        if not fn then return raise(err) end
64✔
794
        fn = fn()
64✔
795
        return fn
64✔
796
    else
797
        return raise 'not a string lambda'
×
798
    end
799
end
800

801

802
--- an anonymous function as a string. This string is either of the form
803
-- '|args| expression' or is a function of one argument, '_'
804
-- @param lf function as a string
805
-- @return a function
806
-- @function utils.string_lambda
807
-- @usage
808
-- string_lambda '|x|x+1' (2) == 3
809
-- string_lambda '_+1' (2) == 3
810
utils.string_lambda = utils.memoize(_string_lambda)
176✔
811

812

813
--- bind the first argument of the function to a value.
814
-- @param fn a function of at least two values (may be an operator string)
815
-- @param p a value
816
-- @return a function such that f(x) is fn(p,x)
817
-- @raise same as @{function_arg}
818
-- @see func.bind1
819
-- @usage local function f(msg, name)
820
--   print(msg .. " " .. name)
821
-- end
822
--
823
-- local hello = utils.bind1(f, "Hello")
824
--
825
-- print(hello("world"))     --> "Hello world"
826
-- print(hello("sunshine"))  --> "Hello sunshine"
827
function utils.bind1 (fn,p)
176✔
828
    fn = utils.function_arg(1,fn)
40✔
829
    return function(...) return fn(p,...) end
132✔
830
end
831

832

833
--- bind the second argument of the function to a value.
834
-- @param fn a function of at least two values (may be an operator string)
835
-- @param p a value
836
-- @return a function such that f(x) is fn(x,p)
837
-- @raise same as @{function_arg}
838
-- @usage local function f(a, b, c)
839
--   print(a .. " " .. b .. " " .. c)
840
-- end
841
--
842
-- local hello = utils.bind1(f, "world")
843
--
844
-- print(hello("Hello", "!"))  --> "Hello world !"
845
-- print(hello("Bye", "?"))    --> "Bye world ?"
846
function utils.bind2 (fn,p)
176✔
847
    fn = utils.function_arg(1,fn)
4✔
848
    return function(x,...) return fn(x,p,...) end
8✔
849
end
850

851

852

853

854
--- Deprecation
855
-- @section deprecation
856

857
do
858
  -- the default implementation
859
  local deprecation_func = function(msg, trace)
860
    if trace then
32✔
861
      warn(msg, "\n", trace)  -- luacheck: ignore
20✔
862
    else
863
      warn(msg)  -- luacheck: ignore
12✔
864
    end
865
  end
866

867
  --- Sets a deprecation warning function.
868
  -- An application can override this function to support proper output of
869
  -- deprecation warnings. The warnings can be generated from libraries or
870
  -- functions by calling `utils.raise_deprecation`. The default function
871
  -- will write to the 'warn' system (introduced in Lua 5.4, or the compatibility
872
  -- function from the `compat` module for earlier versions).
873
  --
874
  -- Note: only applications should set/change this function, libraries should not.
875
  -- @param func a callback with signature: `function(msg, trace)` both arguments are strings, the latter being optional.
876
  -- @see utils.raise_deprecation
877
  -- @usage
878
  -- -- write to the Nginx logs with OpenResty
879
  -- utils.set_deprecation_func(function(msg, trace)
880
  --   ngx.log(ngx.WARN, msg, (trace and (" " .. trace) or nil))
881
  -- end)
882
  --
883
  -- -- disable deprecation warnings
884
  -- utils.set_deprecation_func()
885
  function utils.set_deprecation_func(func)
176✔
886
    if func == nil then
56✔
887
      deprecation_func = function() end
4✔
888
    else
889
      utils.assert_arg(1, func, "function")
52✔
890
      deprecation_func = func
48✔
891
    end
892
  end
893

894
  --- raises a deprecation warning.
895
  -- For options see the usage example below.
896
  --
897
  -- Note: the `opts.deprecated_after` field is the last version in which
898
  -- a feature or option was NOT YET deprecated! Because when writing the code it
899
  -- is quite often not known in what version the code will land. But the last
900
  -- released version is usually known.
901
  -- @param opts options table
902
  -- @see utils.set_deprecation_func
903
  -- @usage
904
  -- warn("@on")   -- enable Lua warnings, they are usually off by default
905
  --
906
  -- function stringx.islower(str)
907
  --   raise_deprecation {
908
  --     source = "Penlight " .. utils._VERSION,                   -- optional
909
  --     message = "function 'islower' was renamed to 'is_lower'", -- required
910
  --     version_removed = "2.0.0",                                -- optional
911
  --     deprecated_after = "1.2.3",                               -- optional
912
  --     no_trace = true,                                          -- optional
913
  --   }
914
  --   return stringx.is_lower(str)
915
  -- end
916
  -- -- output: "[Penlight 1.9.2] function 'islower' was renamed to 'is_lower' (deprecated after 1.2.3, scheduled for removal in 2.0.0)"
917
  function utils.raise_deprecation(opts)
176✔
918
    utils.assert_arg(1, opts, "table")
68✔
919
    if type(opts.message) ~= "string" then
64✔
920
      error("field 'message' of the options table must be a string", 2)
4✔
921
    end
922
    local trace
923
    if not opts.no_trace then
60✔
924
      trace = debug.traceback("", 2):match("[\n%s]*(.-)$")
44✔
925
    end
926
    local msg
927
    if opts.deprecated_after and opts.version_removed then
60✔
928
      msg = (" (deprecated after %s, scheduled for removal in %s)"):format(
72✔
929
        tostring(opts.deprecated_after), tostring(opts.version_removed))
72✔
930
    elseif opts.deprecated_after then
24✔
931
      msg = (" (deprecated after %s)"):format(tostring(opts.deprecated_after))
4✔
932
    elseif opts.version_removed then
20✔
933
      msg = (" (scheduled for removal in %s)"):format(tostring(opts.version_removed))
16✔
934
    else
935
      msg = ""
4✔
936
    end
937

938
    msg = opts.message .. msg
60✔
939

940
    if opts.source then
60✔
941
      msg = "[" .. opts.source .."] " .. msg
44✔
942
    else
943
      if msg:sub(1,1) == "@" then
16✔
944
        -- in Lua 5.4 "@" prefixed messages are control messages to the warn system
945
        error("message cannot start with '@'", 2)
×
946
      end
947
    end
948

949
    deprecation_func(msg, trace)
60✔
950
  end
951

952
end
953

954

955
return utils
176✔
956

957

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc