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

lunarmodules / Penlight / 520

16 Apr 2024 09:46AM UTC coverage: 89.701% (+0.8%) from 88.86%
520

Pull #473

appveyor

Tieske
add tests
Pull Request #473: feat(utils/app): allow for global env var defaults

15 of 17 new or added lines in 3 files covered. (88.24%)

81 existing lines in 13 files now uncovered.

5513 of 6146 relevant lines covered (89.7%)

352.38 hits per line

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

96.31
/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
352✔
9
local compat = require 'pl.compat'
352✔
10
local stdout = io.stdout
352✔
11
local append = table.insert
352✔
12
local concat = table.concat
352✔
13
local _unpack = table.unpack  -- always injected by 'compat'
352✔
14
local find = string.find
352✔
15
local sub = string.sub
352✔
16
local next = next
352✔
17
local floor = math.floor
352✔
18

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

25

26
local utils = { _VERSION = "1.14.0" }
352✔
27
for k, v in pairs(compat) do utils[k] = v  end
3,344✔
28

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

38

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

52

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

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

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

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

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

112
    local function lookup_lib(T,t)
113
        for k,v in pairs(T) do
926✔
114
            if v == t then return k end
910✔
115
        end
116
        return '?'
16✔
117
    end
118

119
    local already_imported = {}
352✔
120

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

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

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

164

165

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

182

183

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

219
  else
220
    return function()
221
      i = i + step
512✔
222
      if i > i_end then
512✔
223
        return nil
136✔
224
      end
225
      return i, t[i]
376✔
226
    end
227
  end
228
end
229

230

231

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

271

272

273
--- Error handling
274
-- @section Error-handling
275

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

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

364
  if type(first) ~= "table" then
144✔
365
    -- vararg with strings
366
    lst = utils.pack(...)
100✔
367
    for i, value in utils.npairs(lst) do
424✔
368
      utils.assert_arg(i, value, "string")
192✔
369
      enum[value] = value
176✔
370
    end
371

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

397
  if not lst[1] then
104✔
398
    error("expected at least 1 entry", 2)
24✔
399
  end
400

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

420
  return enum
80✔
421
end
422

423

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

465

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

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

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

522

523

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

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

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

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

585
--- OS functions
586
-- @section OS-functions
587

588
do
589
  local env_defaults = {} -- table to track defaults for env variables
352✔
590
  local original_getenv = os.getenv -- in case the global function gets patched with this implementation
352✔
591

592

593
  --- Gets an environment variable, whilst falling back to defaults. The defaults can
594
  -- be set using `utils.setenv`. All Penlight modules will use this method to retrieve
595
  -- environment variables.
596
  -- The name is case-sensitive, except on Windows.
597
  -- @tparam string name the environment variable to lookup.
598
  -- @treturn[1] string the value retrieved
599
  -- @treturn[2] nil if the variables wasn't found, and didn't have a default set
600
  -- @see utils.setenv_default
601
  -- @see app.setenv_default
602
  function utils.getenv(name)
352✔
603
    utils.assert_string(1, name)
88✔
604
    name = is_windows and name:lower() or name
108✔
605
    return original_getenv(name) or env_defaults[name]
72✔
606
  end
607

608

609

610
  --- Sets/clears an environment variable default, to use with `utils.getenv`.
611
  -- The name is case-sensitive, except on Windows.
612
  -- @tparam string name the environment variable name to set a default for.
613
  -- @tparam[opt] string value the value to assign as a default, if `nil` the default will be cleared.
614
  -- @return nothing
615
  -- @see utils.getenv
616
  -- @see app.setenv_default
617
  function utils.setenv_default(name, value)
352✔
618
    utils.assert_string(1, name)
176✔
619
    name = is_windows and name:lower() or name
228✔
620
    if value == nil then
152✔
621
      env_defaults[name] = nil
104✔
622
    else
623
      utils.assert_string(1, value)
48✔
624
      env_defaults[name] = value
40✔
625
    end
626
  end
627
end
628

629

630

631
--- execute a shell command and return the output.
632
-- This function redirects the output to tempfiles and returns the content of those files.
633
-- @param cmd a shell command
634
-- @param bin boolean, if true, read output as binary file
635
-- @return true if successful
636
-- @return actual return code
637
-- @return stdout output (string)
638
-- @return errout output (string)
639
function utils.executeex(cmd, bin)
352✔
640
    local outfile = os.tmpname()
304✔
641
    local errfile = os.tmpname()
304✔
642

643
    if is_windows and not outfile:find(':') then
304✔
NEW
644
        outfile = utils.getenv('TEMP')..outfile
×
NEW
645
        errfile = utils.getenv('TEMP')..errfile
×
646
    end
647
    cmd = cmd .. " > " .. utils.quote_arg(outfile) .. " 2> " .. utils.quote_arg(errfile)
608✔
648

649
    local success, retcode = utils.execute(cmd)
304✔
650
    local outcontent = utils.readfile(outfile, bin)
304✔
651
    local errcontent = utils.readfile(errfile, bin)
304✔
652
    os.remove(outfile)
304✔
653
    os.remove(errfile)
304✔
654
    return success, retcode, (outcontent or ""), (errcontent or "")
304✔
655
end
656

657
--- Quote and escape an argument of a command.
658
-- Quotes a single (or list of) argument(s) of a command to be passed
659
-- to `os.execute`, `pl.utils.execute` or `pl.utils.executeex`.
660
-- @param argument (string or table/list) the argument to quote. If a list then
661
-- all arguments in the list will be returned as a single string quoted.
662
-- @return quoted and escaped argument.
663
-- @usage
664
-- local options = utils.quote_arg {
665
--     "-lluacov",
666
--     "-e",
667
--     "utils = print(require('pl.utils')._VERSION",
668
-- }
669
-- -- returns: -lluacov -e 'utils = print(require('\''pl.utils'\'')._VERSION'
670
function utils.quote_arg(argument)
352✔
671
    if type(argument) == "table" then
892✔
672
        -- encode an entire table
673
        local r = {}
40✔
674
        for i, arg in ipairs(argument) do
168✔
675
            r[i] = utils.quote_arg(arg)
192✔
676
        end
677

678
        return concat(r, " ")
40✔
679
    end
680
    -- only a single argument
681
    if is_windows then
852✔
682
        if argument == "" or argument:find('[ \f\t\v]') then
852✔
683
            -- Need to quote the argument.
684
            -- Quotes need to be escaped with backslashes;
685
            -- additionally, backslashes before a quote, escaped or not,
686
            -- need to be doubled.
687
            -- See documentation for CommandLineToArgvW Windows function.
688
            argument = '"' .. argument:gsub([[(\*)"]], [[%1%1\"]]):gsub([[\+$]], "%0%0") .. '"'
48✔
689
        end
690

691
        -- os.execute() uses system() C function, which on Windows passes command
692
        -- to cmd.exe. Escape its special characters.
693
        return (argument:gsub('["^<>!|&%%]', "^%0"))
852✔
694
    else
UNCOV
695
        if argument == "" or argument:find('[^a-zA-Z0-9_@%+=:,./-]') then
×
696
            -- To quote arguments on posix-like systems use single quotes.
697
            -- To represent an embedded single quote close quoted string ('),
698
            -- add escaped quote (\'), open quoted string again (').
UNCOV
699
            argument = "'" .. argument:gsub("'", [['\'']]) .. "'"
×
700
        end
701

UNCOV
702
        return argument
×
703
    end
704
end
705

706
--- error out of this program gracefully.
707
-- @param[opt] code The exit code, defaults to -`1` if omitted
708
-- @param msg The exit message will be sent to `stderr` (will be formatted with the extra parameters)
709
-- @param ... extra arguments for message's format'
710
-- @see utils.fprintf
711
-- @usage utils.quit(-1, "Error '%s' happened", "42")
712
-- -- is equivalent to
713
-- utils.quit("Error '%s' happened", "42")  --> Error '42' happened
714
function utils.quit(code, msg, ...)
352✔
715
    if type(code) == 'string' then
48✔
716
        utils.fprintf(io.stderr, code, msg, ...)
24✔
717
        io.stderr:write('\n')
24✔
718
        code = -1 -- TODO: this is odd, see the test. Which returns 255 as exit code
24✔
719
    elseif msg then
24✔
720
        utils.fprintf(io.stderr, msg, ...)
16✔
721
        io.stderr:write('\n')
16✔
722
    end
723
    os.exit(code, true)
48✔
724
end
725

726

727
--- String functions
728
-- @section string-functions
729

730
--- escape any Lua 'magic' characters in a string
731
-- @param s The input string
732
function utils.escape(s)
352✔
733
    utils.assert_string(1,s)
1,352✔
734
    return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'))
1,352✔
735
end
736

737
--- split a string into a list of strings separated by a delimiter.
738
-- @param s The input string
739
-- @param re optional A Lua string pattern; defaults to '%s+'
740
-- @param plain optional If truthy don't use Lua patterns
741
-- @param n optional maximum number of elements (if there are more, the last will remain un-split)
742
-- @return a list-like table
743
-- @raise error if s is not a string
744
-- @see splitv
745
function utils.split(s,re,plain,n)
352✔
746
    utils.assert_string(1,s)
1,395✔
747
    local i1,ls = 1,{}
1,395✔
748
    if not re then re = '%s+' end
1,395✔
749
    if re == '' then return {s} end
1,395✔
750
    while true do
751
        local i2,i3 = find(s,re,i1,plain)
4,865✔
752
        if not i2 then
4,865✔
753
            local last = sub(s,i1)
1,235✔
754
            if last ~= '' then append(ls,last) end
1,235✔
755
            if #ls == 1 and ls[1] == '' then
1,235✔
756
                return {}
16✔
757
            else
758
                return ls
1,219✔
759
            end
760
        end
761
        append(ls,sub(s,i1,i2-1))
5,442✔
762
        if n and #ls == n then
3,630✔
763
            ls[#ls] = sub(s,i1)
216✔
764
            return ls
144✔
765
        end
766
        i1 = i3+1
3,486✔
767
    end
768
end
769

770
--- split a string into a number of return values.
771
-- Identical to `split` but returns multiple sub-strings instead of
772
-- a single list of sub-strings.
773
-- @param s the string
774
-- @param re A Lua string pattern; defaults to '%s+'
775
-- @param plain don't use Lua patterns
776
-- @param n optional maximum number of splits
777
-- @return n values
778
-- @usage first,next = splitv('user=jane=doe','=', false, 2)
779
-- assert(first == "user")
780
-- assert(next == "jane=doe")
781
-- @see split
782
function utils.splitv (s,re, plain, n)
352✔
783
    return _unpack(utils.split(s,re, plain, n))
444✔
784
end
785

786

787
--- Functional
788
-- @section functional
789

790

791
--- 'memoize' a function (cache returned value for next call).
792
-- This is useful if you have a function which is relatively expensive,
793
-- but you don't know in advance what values will be required, so
794
-- building a table upfront is wasteful/impossible.
795
-- @param func a function that takes exactly one argument (which later serves as the cache key) and returns a single value
796
-- @return a function taking one argument and returning a single value either from the cache or by running the original input function
797
function utils.memoize(func)
352✔
798
    local cache = {}
360✔
799
    return function(k)
800
        local res = cache[k]
152✔
801
        if res == nil then
152✔
802
            res = func(k)
216✔
803
            cache[k] = res
144✔
804
        end
805
        return res
152✔
806
    end
807
end
808

809

810
--- associate a function factory with a type.
811
-- A function factory takes an object of the given type and
812
-- returns a function for evaluating it
813
-- @tab mt metatable
814
-- @func fun a callable that returns a function
815
function utils.add_function_factory (mt,fun)
352✔
816
    _function_factories[mt] = fun
24✔
817
end
818

819
local function _string_lambda(f)
820
    if f:find '^|' or f:find '_' then
128✔
821
        local args,body = f:match '|([^|]*)|(.+)'
128✔
822
        if f:find '_' then
128✔
823
            args = '_'
72✔
824
            body = f
72✔
825
        else
826
            if not args then return raise 'bad string lambda' end
56✔
827
        end
828
        local fstr = 'return function('..args..') return '..body..' end'
128✔
829
        local fn,err = utils.load(fstr)
128✔
830
        if not fn then return raise(err) end
128✔
831
        fn = fn()
192✔
832
        return fn
128✔
833
    else
834
        return raise 'not a string lambda'
×
835
    end
836
end
837

838

839
--- an anonymous function as a string. This string is either of the form
840
-- '|args| expression' or is a function of one argument, '_'
841
-- @param lf function as a string
842
-- @return a function
843
-- @function utils.string_lambda
844
-- @usage
845
-- string_lambda '|x|x+1' (2) == 3
846
-- string_lambda '_+1' (2) == 3
847
utils.string_lambda = utils.memoize(_string_lambda)
528✔
848

849

850
--- bind the first argument of the function to a value.
851
-- @param fn a function of at least two values (may be an operator string)
852
-- @param p a value
853
-- @return a function such that f(x) is fn(p,x)
854
-- @raise same as @{function_arg}
855
-- @see func.bind1
856
-- @usage local function f(msg, name)
857
--   print(msg .. " " .. name)
858
-- end
859
--
860
-- local hello = utils.bind1(f, "Hello")
861
--
862
-- print(hello("world"))     --> "Hello world"
863
-- print(hello("sunshine"))  --> "Hello sunshine"
864
function utils.bind1 (fn,p)
352✔
865
    fn = utils.function_arg(1,fn)
120✔
866
    return function(...) return fn(p,...) end
264✔
867
end
868

869

870
--- bind the second argument of the function to a value.
871
-- @param fn a function of at least two values (may be an operator string)
872
-- @param p a value
873
-- @return a function such that f(x) is fn(x,p)
874
-- @raise same as @{function_arg}
875
-- @usage local function f(a, b, c)
876
--   print(a .. " " .. b .. " " .. c)
877
-- end
878
--
879
-- local hello = utils.bind1(f, "world")
880
--
881
-- print(hello("Hello", "!"))  --> "Hello world !"
882
-- print(hello("Bye", "?"))    --> "Bye world ?"
883
function utils.bind2 (fn,p)
352✔
884
    fn = utils.function_arg(1,fn)
12✔
885
    return function(x,...) return fn(x,p,...) end
16✔
886
end
887

888

889

890

891
--- Deprecation
892
-- @section deprecation
893

894
do
895
  -- the default implementation
896
  local deprecation_func = function(msg, trace)
897
    if trace then
64✔
898
      warn(msg, "\n", trace)  -- luacheck: ignore
60✔
899
    else
900
      warn(msg)  -- luacheck: ignore
24✔
901
    end
902
  end
903

904
  --- Sets a deprecation warning function.
905
  -- An application can override this function to support proper output of
906
  -- deprecation warnings. The warnings can be generated from libraries or
907
  -- functions by calling `utils.raise_deprecation`. The default function
908
  -- will write to the 'warn' system (introduced in Lua 5.4, or the compatibility
909
  -- function from the `compat` module for earlier versions).
910
  --
911
  -- Note: only applications should set/change this function, libraries should not.
912
  -- @param func a callback with signature: `function(msg, trace)` both arguments are strings, the latter being optional.
913
  -- @see utils.raise_deprecation
914
  -- @usage
915
  -- -- write to the Nginx logs with OpenResty
916
  -- utils.set_deprecation_func(function(msg, trace)
917
  --   ngx.log(ngx.WARN, msg, (trace and (" " .. trace) or nil))
918
  -- end)
919
  --
920
  -- -- disable deprecation warnings
921
  -- utils.set_deprecation_func()
922
  function utils.set_deprecation_func(func)
352✔
923
    if func == nil then
112✔
924
      deprecation_func = function() end
8✔
925
    else
926
      utils.assert_arg(1, func, "function")
104✔
927
      deprecation_func = func
96✔
928
    end
929
  end
930

931
  --- raises a deprecation warning.
932
  -- For options see the usage example below.
933
  --
934
  -- Note: the `opts.deprecated_after` field is the last version in which
935
  -- a feature or option was NOT YET deprecated! Because when writing the code it
936
  -- is quite often not known in what version the code will land. But the last
937
  -- released version is usually known.
938
  -- @param opts options table
939
  -- @see utils.set_deprecation_func
940
  -- @usage
941
  -- warn("@on")   -- enable Lua warnings, they are usually off by default
942
  --
943
  -- function stringx.islower(str)
944
  --   raise_deprecation {
945
  --     source = "Penlight " .. utils._VERSION,                   -- optional
946
  --     message = "function 'islower' was renamed to 'is_lower'", -- required
947
  --     version_removed = "2.0.0",                                -- optional
948
  --     deprecated_after = "1.2.3",                               -- optional
949
  --     no_trace = true,                                          -- optional
950
  --   }
951
  --   return stringx.is_lower(str)
952
  -- end
953
  -- -- output: "[Penlight 1.9.2] function 'islower' was renamed to 'is_lower' (deprecated after 1.2.3, scheduled for removal in 2.0.0)"
954
  function utils.raise_deprecation(opts)
352✔
955
    utils.assert_arg(1, opts, "table")
136✔
956
    if type(opts.message) ~= "string" then
128✔
957
      error("field 'message' of the options table must be a string", 2)
8✔
958
    end
959
    local trace
960
    if not opts.no_trace then
120✔
961
      trace = debug.traceback("", 2):match("[\n%s]*(.-)$")
88✔
962
    end
963
    local msg
964
    if opts.deprecated_after and opts.version_removed then
120✔
965
      msg = (" (deprecated after %s, scheduled for removal in %s)"):format(
144✔
966
        tostring(opts.deprecated_after), tostring(opts.version_removed))
144✔
967
    elseif opts.deprecated_after then
48✔
968
      msg = (" (deprecated after %s)"):format(tostring(opts.deprecated_after))
8✔
969
    elseif opts.version_removed then
40✔
970
      msg = (" (scheduled for removal in %s)"):format(tostring(opts.version_removed))
32✔
971
    else
972
      msg = ""
8✔
973
    end
974

975
    msg = opts.message .. msg
120✔
976

977
    if opts.source then
120✔
978
      msg = "[" .. opts.source .."] " .. msg
88✔
979
    else
980
      if msg:sub(1,1) == "@" then
48✔
981
        -- in Lua 5.4 "@" prefixed messages are control messages to the warn system
982
        error("message cannot start with '@'", 2)
×
983
      end
984
    end
985

986
    deprecation_func(msg, trace)
120✔
987
  end
988

989
end
990

991

992
return utils
352✔
993

994

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

© 2026 Coveralls, Inc