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

lunarmodules / Penlight / 478

12 Feb 2024 09:01AM UTC coverage: 89.675% (+0.7%) from 88.938%
478

push

appveyor

Tieske
fix(docs): proper escape back-slash

5489 of 6121 relevant lines covered (89.67%)

357.21 hits per line

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

80.08
/lua/pl/path.lua
1
--- Path manipulation and file queries.
2
--
3
-- This is modelled after Python's os.path library (10.1); see @{04-paths.md|the Guide}.
4
--
5
-- NOTE: the functions assume the paths being dealt with to originate
6
-- from the OS the application is running on. Windows drive letters are not
7
-- to be used when running on a Unix system for example. The one exception
8
-- is Windows paths to allow both forward and backward slashes (since Lua
9
-- also accepts those)
10
--
11
-- Dependencies: `pl.utils`, `lfs`
12
-- @module pl.path
13

14
-- imports and locals
15
local _G = _G
280✔
16
local sub = string.sub
280✔
17
local getenv = os.getenv
280✔
18
local tmpnam = os.tmpname
280✔
19
local package = package
280✔
20
local append, concat, remove = table.insert, table.concat, table.remove
280✔
21
local utils = require 'pl.utils'
280✔
22
local assert_string,raise = utils.assert_string,utils.raise
280✔
23

24
local res,lfs = _G.pcall(_G.require,'lfs')
280✔
25
if not res then
280✔
26
    error("pl.path requires LuaFileSystem")
×
27
end
28

29
local attrib = lfs.attributes
280✔
30
local currentdir = lfs.currentdir
280✔
31
local link_attrib = lfs.symlinkattributes
280✔
32

33
local path = {}
280✔
34

35
local function err_func(name, param, err, code)
36
  local ret = ("%s failed"):format(tostring(name))
×
37
  if param ~= nil then
×
38
    ret = ret .. (" for '%s'"):format(tostring(param))
×
39
  end
40
  ret = ret .. (": %s"):format(tostring(err))
×
41
  if code ~= nil then
×
42
    ret = ret .. (" (code %s)"):format(tostring(code))
×
43
  end
44
  return ret
×
45
end
46

47
--- Lua iterator over the entries of a given directory.
48
-- Implicit link to [`luafilesystem.dir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
49
-- @function dir
50
path.dir = lfs.dir
280✔
51

52
--- Creates a directory.
53
-- Implicit link to [`luafilesystem.mkdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
54
-- @function mkdir
55
path.mkdir = function(d)
56
  local ok, err, code = lfs.mkdir(d)
64✔
57
  if not ok then
64✔
58
    return ok, err_func("mkdir", d, err, code), code
×
59
  end
60
  return ok, err, code
64✔
61
end
62

63
--- Removes a directory.
64
-- Implicit link to [`luafilesystem.rmdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
65
-- @function rmdir
66
path.rmdir = function(d)
67
  local ok, err, code = lfs.rmdir(d)
48✔
68
  if not ok then
48✔
69
    return ok, err_func("rmdir", d, err, code), code
×
70
  end
71
  return ok, err, code
48✔
72
end
73

74
--- Gets attributes.
75
-- Implicit link to [`luafilesystem.attributes`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
76
-- @function attrib
77
path.attrib = function(d, r)
78
  local ok, err, code = attrib(d, r)
64✔
79
  if not ok then
64✔
80
    return ok, err_func("attrib", d, err, code), code
×
81
  end
82
  return ok, err, code
64✔
83
end
84

85
--- Get the working directory.
86
-- Implicit link to [`luafilesystem.currentdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
87
-- @function currentdir
88
path.currentdir = function()
89
  local ok, err, code = currentdir()
64✔
90
  if not ok then
64✔
91
    return ok, err_func("currentdir", nil, err, code), code
×
92
  end
93
  return ok, err, code
64✔
94
end
95

96
--- Gets symlink attributes.
97
-- Implicit link to [`luafilesystem.symlinkattributes`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
98
-- @function link_attrib
99
path.link_attrib = function(d, r)
100
  local ok, err, code = link_attrib(d, r)
24✔
101
  if not ok then
24✔
102
    return ok, err_func("link_attrib", d, err, code), code
×
103
  end
104
  return ok, err, code
24✔
105
end
106

107
--- Changes the working directory.
108
-- On Windows, if a drive is specified, it also changes the current drive. If
109
-- only specifying the drive, it will only switch drive, but not modify the path.
110
-- Implicit link to [`luafilesystem.chdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
111
-- @function chdir
112
path.chdir = function(d)
113
  local ok, err, code = lfs.chdir(d)
16✔
114
  if not ok then
16✔
115
    return ok, err_func("chdir", d, err, code), code
×
116
  end
117
  return ok, err, code
16✔
118
end
119

120
--- is this a directory?
121
-- @string P A file path
122
function path.isdir(P)
280✔
123
    assert_string(1,P)
1,100✔
124
    return attrib(P,'mode') == 'directory'
1,100✔
125
end
126

127
--- is this a file?
128
-- @string P A file path
129
function path.isfile(P)
280✔
130
    assert_string(1,P)
176✔
131
    return attrib(P,'mode') == 'file'
176✔
132
end
133

134
-- is this a symbolic link?
135
-- @string P A file path
136
function path.islink(P)
280✔
137
    assert_string(1,P)
64✔
138
    if link_attrib then
64✔
139
        return link_attrib(P,'mode')=='link'
64✔
140
    else
141
        return false
×
142
    end
143
end
144

145
--- return size of a file.
146
-- @string P A file path
147
function path.getsize(P)
280✔
148
    assert_string(1,P)
×
149
    return attrib(P,'size')
×
150
end
151

152
--- does a path exist?
153
-- @string P A file path
154
-- @return the file path if it exists (either as file, directory, socket, etc), nil otherwise
155
function path.exists(P)
280✔
156
    assert_string(1,P)
800✔
157
    return attrib(P,'mode') ~= nil and P
800✔
158
end
159

160
--- Return the time of last access as the number of seconds since the epoch.
161
-- @string P A file path
162
function path.getatime(P)
280✔
163
    assert_string(1,P)
×
164
    return attrib(P,'access')
×
165
end
166

167
--- Return the time of last modification as the number of seconds since the epoch.
168
-- @string P A file path
169
function path.getmtime(P)
280✔
170
    assert_string(1,P)
×
171
    return attrib(P,'modification')
×
172
end
173

174
---Return the system's ctime as the number of seconds since the epoch.
175
-- @string P A file path
176
function path.getctime(P)
280✔
177
    assert_string(1,P)
×
178
    return path.attrib(P,'change')
×
179
end
180

181

182
local function at(s,i)
183
    return sub(s,i,i)
93,502✔
184
end
185

186
path.is_windows = utils.is_windows
280✔
187

188
local sep, other_sep, seps
189
-- constant sep is the directory separator for this platform.
190
-- constant dirsep is the separator in the PATH environment variable
191
if path.is_windows then
280✔
192
    path.sep = '\\'; other_sep = '/'
280✔
193
    path.dirsep = ';'
280✔
194
    seps = { ['/'] = true, ['\\'] = true }
280✔
195
else
196
    path.sep = '/'
×
197
    path.dirsep = ':'
×
198
    seps = { ['/'] = true }
×
199
end
200
sep = path.sep
280✔
201

202
--- are we running Windows?
203
-- @class field
204
-- @name path.is_windows
205

206
--- path separator for this platform.
207
-- @class field
208
-- @name path.sep
209

210
--- separator for PATH for this platform
211
-- @class field
212
-- @name path.dirsep
213

214
--- given a path, return the directory part and a file part.
215
-- if there's no directory part, the first value will be empty
216
-- @string P A file path
217
-- @return directory part
218
-- @return file part
219
-- @usage
220
-- local dir, file = path.splitpath("some/dir/myfile.txt")
221
-- assert(dir == "some/dir")
222
-- assert(file == "myfile.txt")
223
--
224
-- local dir, file = path.splitpath("some/dir/")
225
-- assert(dir == "some/dir")
226
-- assert(file == "")
227
--
228
-- local dir, file = path.splitpath("some_dir")
229
-- assert(dir == "")
230
-- assert(file == "some_dir")
231
function path.splitpath(P)
280✔
232
    assert_string(1,P)
6,736✔
233
    local i = #P
6,736✔
234
    local ch = at(P,i)
6,736✔
235
    while i > 0 and ch ~= sep and ch ~= other_sep do
87,598✔
236
        i = i - 1
80,862✔
237
        ch = at(P,i)
121,291✔
238
    end
239
    if i == 0 then
6,736✔
240
        return '',P
136✔
241
    else
242
        return sub(P,1,i-1), sub(P,i+1)
13,200✔
243
    end
244
end
245

246
--- return an absolute path.
247
-- @string P A file path
248
-- @string[opt] pwd optional start path to use (default is current dir)
249
function path.abspath(P,pwd)
280✔
250
    assert_string(1,P)
88✔
251
    if pwd then assert_string(2,pwd) end
88✔
252
    local use_pwd = pwd ~= nil
88✔
253
    if not use_pwd and not currentdir() then return P end
88✔
254
    P = P:gsub('[\\/]$','')
88✔
255
    pwd = pwd or currentdir()
88✔
256
    if not path.isabs(P) then
132✔
257
        P = path.join(pwd,P)
24✔
258
    elseif path.is_windows and not use_pwd and at(P,2) ~= ':' and at(P,2) ~= '\\' then
92✔
259
        P = pwd:sub(1,2)..P -- attach current drive to path like '\\fred.txt'
×
260
    end
261
    return path.normpath(P)
88✔
262
end
263

264
--- given a path, return the root part and the extension part.
265
-- if there's no extension part, the second value will be empty
266
-- @string P A file path
267
-- @treturn string root part (everything upto the "."", maybe empty)
268
-- @treturn string extension part (including the ".", maybe empty)
269
-- @usage
270
-- local file_path, ext = path.splitext("/bonzo/dog_stuff/cat.txt")
271
-- assert(file_path == "/bonzo/dog_stuff/cat")
272
-- assert(ext == ".txt")
273
--
274
-- local file_path, ext = path.splitext("")
275
-- assert(file_path == "")
276
-- assert(ext == "")
277
function path.splitext(P)
280✔
278
    assert_string(1,P)
208✔
279
    local i = #P
208✔
280
    local ch = at(P,i)
208✔
281
    while i > 0 and ch ~= '.' do
832✔
282
        if seps[ch] then
624✔
283
            return P,''
×
284
        end
285
        i = i - 1
624✔
286
        ch = at(P,i)
936✔
287
    end
288
    if i == 0 then
208✔
289
        return P,''
40✔
290
    else
291
        return sub(P,1,i-1),sub(P,i)
336✔
292
    end
293
end
294

295
--- return the directory part of a path
296
-- @string P A file path
297
-- @treturn string everything before the last dir-separator
298
-- @see splitpath
299
-- @usage
300
-- path.dirname("/some/path/file.txt")   -- "/some/path"
301
-- path.dirname("file.txt")              -- "" (empty string)
302
function path.dirname(P)
280✔
303
    assert_string(1,P)
5,720✔
304
    local p1 = path.splitpath(P)
5,720✔
305
    return p1
5,720✔
306
end
307

308
--- return the file part of a path
309
-- @string P A file path
310
-- @treturn string
311
-- @see splitpath
312
-- @usage
313
-- path.basename("/some/path/file.txt")  -- "file.txt"
314
-- path.basename("/some/path/file/")     -- "" (empty string)
315
function path.basename(P)
280✔
316
    assert_string(1,P)
952✔
317
    local _,p2 = path.splitpath(P)
952✔
318
    return p2
952✔
319
end
320

321
--- get the extension part of a path.
322
-- @string P A file path
323
-- @treturn string
324
-- @see splitext
325
-- @usage
326
-- path.extension("/some/path/file.txt") -- ".txt"
327
-- path.extension("/some/path/file_txt") -- "" (empty string)
328
function path.extension(P)
280✔
329
    assert_string(1,P)
128✔
330
    local _,p2 = path.splitext(P)
128✔
331
    return p2
128✔
332
end
333

334
--- is this an absolute path?
335
-- @string P A file path
336
-- @usage
337
-- path.isabs("hello/path")    -- false
338
-- path.isabs("/hello/path")   -- true
339
-- -- Windows;
340
-- path.isabs("hello\path")    -- false
341
-- path.isabs("\hello\path")   -- true
342
-- path.isabs("C:\hello\path") -- true
343
-- path.isabs("C:hello\path")  -- false
344
function path.isabs(P)
280✔
345
    assert_string(1,P)
824✔
346
    if path.is_windows and at(P,2) == ":" then
1,236✔
347
        return seps[at(P,3)] ~= nil
132✔
348
    end
349
    return seps[at(P,1)] ~= nil
1,104✔
350
end
351

352
--- return the path resulting from combining the individual paths.
353
-- if the second (or later) path is absolute, we return the last absolute path (joined with any non-absolute paths following).
354
-- empty elements (except the last) will be ignored.
355
-- @string p1 A file path
356
-- @string p2 A file path
357
-- @string ... more file paths
358
-- @treturn string the combined path
359
-- @usage
360
-- path.join("/first","second","third")   -- "/first/second/third"
361
-- path.join("first","second/third")      -- "first/second/third"
362
-- path.join("/first","/second","third")  -- "/second/third"
363
function path.join(p1,p2,...)
280✔
364
    assert_string(1,p1)
656✔
365
    assert_string(2,p2)
656✔
366
    if select('#',...) > 0 then
656✔
367
        local p = path.join(p1,p2)
56✔
368
        local args = {...}
56✔
369
        for i = 1,#args do
128✔
370
            assert_string(i,args[i])
72✔
371
            p = path.join(p,args[i])
108✔
372
        end
373
        return p
56✔
374
    end
375
    if path.isabs(p2) then return p2 end
900✔
376
    local endc = at(p1,#p1)
552✔
377
    if endc ~= path.sep and endc ~= other_sep and endc ~= "" then
552✔
378
        p1 = p1..path.sep
504✔
379
    end
380
    return p1..p2
552✔
381
end
382

383
--- normalize the case of a pathname. On Unix, this returns the path unchanged,
384
-- for Windows it converts;
385
--
386
-- * the path to lowercase
387
-- * forward slashes to backward slashes
388
-- @string P A file path
389
-- @usage path.normcase("/Some/Path/File.txt")
390
-- -- Windows: "\some\path\file.txt"
391
-- -- Others : "/Some/Path/File.txt"
392
function path.normcase(P)
280✔
393
    assert_string(1,P)
1,832✔
394
    if path.is_windows then
1,832✔
395
        return P:gsub('/','\\'):lower()
1,832✔
396
    else
397
        return P
×
398
    end
399
end
400

401
--- normalize a path name.
402
-- `A//B`, `A/./B`, and `A/foo/../B` all become `A/B`.
403
--
404
-- An empty path results in '.'.
405
-- @string P a file path
406
function path.normpath(P)
280✔
407
    assert_string(1,P)
512✔
408
    -- Split path into anchor and relative path.
409
    local anchor = ''
512✔
410
    if path.is_windows then
512✔
411
        if P:match '^\\\\' then -- UNC
512✔
412
            anchor = '\\\\'
16✔
413
            P = P:sub(3)
24✔
414
        elseif seps[at(P, 1)] then
744✔
415
            anchor = '\\'
88✔
416
            P = P:sub(2)
132✔
417
        elseif at(P, 2) == ':' then
612✔
418
            anchor = P:sub(1, 2)
204✔
419
            P = P:sub(3)
204✔
420
            if seps[at(P, 1)] then
204✔
421
                anchor = anchor..'\\'
136✔
422
                P = P:sub(2)
204✔
423
            end
424
        end
425
        P = P:gsub('/','\\')
512✔
426
    else
427
        -- According to POSIX, in path start '//' and '/' are distinct,
428
        -- but '///+' is equivalent to '/'.
429
        if P:match '^//' and at(P, 3) ~= '/' then
×
430
            anchor = '//'
×
431
            P = P:sub(3)
×
432
        elseif at(P, 1) == '/' then
×
433
            anchor = '/'
×
434
            P = P:match '^/*(.*)$'
×
435
        end
436
    end
437
    local parts = {}
512✔
438
    for part in P:gmatch('[^'..sep..']+') do
2,592✔
439
        if part == '..' then
2,080✔
440
            if #parts ~= 0 and parts[#parts] ~= '..' then
224✔
441
                remove(parts)
190✔
442
            else
443
                append(parts, part)
72✔
444
            end
445
        elseif part ~= '.' then
1,856✔
446
            append(parts, part)
1,792✔
447
        end
448
    end
449
    P = anchor..concat(parts, sep)
512✔
450
    if P == '' then P = '.' end
512✔
451
    return P
512✔
452
end
453

454
--- relative path from current directory or optional start point
455
-- @string P a path
456
-- @string[opt] start optional start point (default current directory)
457
function path.relpath (P,start)
280✔
458
    assert_string(1,P)
48✔
459
    if start then assert_string(2,start) end
48✔
460
    local split,min,append = utils.split, math.min, table.insert
48✔
461
    P = path.abspath(P,start)
72✔
462
    start = start or currentdir()
48✔
463
    local compare
464
    if path.is_windows then
48✔
465
        P = P:gsub("/","\\")
48✔
466
        start = start:gsub("/","\\")
48✔
467
        compare = function(v) return v:lower() end
416✔
468
    else
469
        compare = function(v) return v end
×
470
    end
471
    local startl, Pl = split(start,sep), split(P,sep)
72✔
472
    local n = min(#startl,#Pl)
48✔
473
    if path.is_windows and n > 0 and at(Pl[1],2) == ':' and Pl[1] ~= startl[1] then
72✔
474
        return P
×
475
    end
476
    local k = n+1 -- default value if this loop doesn't bail out!
48✔
477
    for i = 1,n do
208✔
478
        if compare(startl[i]) ~= compare(Pl[i]) then
368✔
479
            k = i
24✔
480
            break
18✔
481
        end
482
    end
483
    local rell = {}
48✔
484
    for i = 1, #startl-k+1 do rell[i] = '..' end
65✔
485
    if k <= #Pl then
48✔
486
        for i = k,#Pl do append(rell,Pl[i]) end
74✔
487
    end
488
    return table.concat(rell,sep)
48✔
489
end
490

491

492
--- Replace a starting '~' with the user's home directory.
493
-- In windows, if HOME isn't set, then USERPROFILE is used in preference to
494
-- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows.
495
-- @string P A file path
496
function path.expanduser(P)
280✔
497
    assert_string(1,P)
8✔
498
    if at(P,1) == '~' then
12✔
499
        local home = getenv('HOME')
8✔
500
        if not home then -- has to be Windows
8✔
501
            home = getenv 'USERPROFILE' or (getenv 'HOMEDRIVE' .. getenv 'HOMEPATH')
8✔
502
        end
503
        return home..sub(P,2)
12✔
504
    else
505
        return P
×
506
    end
507
end
508

509

510
---Return a suitable full path to a new temporary file name.
511
-- unlike os.tmpname(), it always gives you a writeable path (uses TEMP environment variable on Windows)
512
function path.tmpname ()
280✔
513
    local res = tmpnam()
172✔
514
    -- On Windows if Lua is compiled using MSVC14 os.tmpname
515
    -- already returns an absolute path within TEMP env variable directory,
516
    -- no need to prepend it.
517
    if path.is_windows and not res:find(':') then
172✔
518
        res = getenv('TEMP')..res
×
519
    end
520
    return res
172✔
521
end
522

523
--- return the largest common prefix path of two paths.
524
-- @string path1 a file path
525
-- @string path2 a file path
526
-- @return the common prefix (Windows: separators will be normalized, casing will be original)
527
function path.common_prefix (path1,path2)
280✔
528
    assert_string(1,path1)
72✔
529
    assert_string(2,path2)
72✔
530
    -- get them in order!
531
    if #path1 > #path2 then path2,path1 = path1,path2 end
72✔
532
    local compare
533
    if path.is_windows then
72✔
534
        path1 = path1:gsub("/", "\\")
72✔
535
        path2 = path2:gsub("/", "\\")
72✔
536
        compare = function(v) return v:lower() end
1,736✔
537
    else
538
        compare = function(v) return v end
×
539
    end
540
    for i = 1,#path1 do
904✔
541
        if compare(at(path1,i)) ~= compare(at(path2,i)) then
2,496✔
542
            local cp = path1:sub(1,i-1)
×
543
            if at(path1,i-1) ~= sep then
×
544
                cp = path.dirname(cp)
×
545
            end
546
            return cp
×
547
        end
548
    end
549
    if at(path2,#path1+1) ~= sep then path1 = path.dirname(path1) end
124✔
550
    return path1
72✔
551
    --return ''
552
end
553

554
--- return the full path where a particular Lua module would be found.
555
-- Both package.path and package.cpath is searched, so the result may
556
-- either be a Lua file or a shared library.
557
-- @string mod name of the module
558
-- @return on success: path of module, lua or binary
559
-- @return on error: nil, error string listing paths tried
560
function path.package_path(mod)
280✔
561
    assert_string(1,mod)
×
562
    local res, err1, err2
563
    res, err1 = package.searchpath(mod,package.path)
×
564
    if res then return res,true end
×
565
    res, err2 = package.searchpath(mod,package.cpath)
×
566
    if res then return res,false end
×
567
    return raise ('cannot find module on path\n' .. err1 .. "\n" .. err2)
×
568
end
569

570

571
---- finis -----
572
return path
280✔
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