-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathpath.lua
684 lines (627 loc) · 23.3 KB
/
path.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
--[=[
Path manipulation for Windows and UNIX paths.
Written by Cosmin Apreutesei. Public Domain.
Parses all Windows path formats including long paths (`\\?\`),
device paths (`\\.\`) and UNC paths.
The only paths that are portable between Windows and UNIX (Linux, OSX)
without translation are relative paths using forward slashes only which
are no longer than 259 bytes and which don't contain any control characters
(code 0-31) or the symbols `<>:"|%?*\`.
path_platform -> 'win'|'unix' get the current platform
path_platform_sep -> '\'|'/' get the path separator for this plaform
path_default_sep([pl]) -> '\'|'/' get the default separator for a platform
path_dev_alias(s) -> s|nil check if a path is a Windows device alias
path_type(s, [pl]) -> type get the path type
path_parse(s, [pl]) -> type, path[, drv|srv] break down a path to its basic parts
path_format(type, path, [drv|srv], pl) -> s put together a path from parsed parts
path_isabs(s, [pl]) -> is_abs, is_empty, is_valid check if path is absolute, empty and valid
path_endsep(s, [pl], [sep], [dsep]) -> s, ok get/add/remove the ending separator
path_sep(s, [pl], [sep], ...) -> s detect/set the path separator
path_long(s, [pl], [long]) -> s|nil get/set a Windows long absolute path
path_file(s, [pl]) -> s get the last component of a path
path_nameext(s, [pl]) -> name, ext split `path_file(s)` into name and extension
path_ext(s, [pl]) -> s return only the extension from `nameext(s)`
path_dir(s, [pl]) -> s|nil get the path without the last component
path_split(s, [pl], [full]) ->iter() -> s, sep iterate over path's components
path_normalize(s, [pl], [opt]) -> s normalize a path in various ways
path_commonpath(s1, s2, [pl]) -> s|nil get the common prefix of two paths
path_depth(s, [pl]) -> n get the number of non-empty path components
path_combine(s1, s2, [pl], [sep], [dsep]) -> s|nil combine two paths if possible
indir(dir, ...) -> path combine path with one or more components
relpath(s, pwd, [pl], [sep], [dsep]) -> s|nil convert absolute path to relative
path_filename(s, [pl], [repl]) -> s|nil,err,code validate/make-valid filename
`pl` stands for platform and defaults to path_platform.
]=]
if not ... then require'path_test'; return end
local insert = table.insert
local remove = table.remove
local concat = table.concat
path_platform_sep = package.config:sub(1, 1)
path_platform = path_platform_sep == '\\' and 'win' or 'unix'
local function win(pl) --check if pl (or current platform) is Windows
if pl == nil then pl = path_platform end
assert(pl == 'unix' or pl == 'win', 'invalid platform')
return pl == 'win'
end
function path_default_sep(pl)
return win(pl) and '\\' or '/'
end
--device aliases are file names that are found _in any directory_.
local dev_aliases = {
CON=1, PRN=1, AUX=1, NUL=1,
COM1=1, COM2=1, COM3=1, COM4=1, COM5=1, COM6=1, COM7=1, COM8=1, COM9=1,
LPT1=1, LPT2=1, LPT3=1, LPT4=1, LPT5=1, LPT6=1, LPT7=1, LPT8=1, LPT9=1,
}
--check if a path refers to a device alias and return that alias.
function path_dev_alias(s)
s = s:match'[^\\/]+$' --basename (dev aliases are present in all dirs)
s = s and s:match'^[^%.]+' --strip extension (they can have any extension)
s = s and s:upper() --they're case-insensitive
return s and dev_aliases[s] and s
end
--get the path type which can be:
-- * 'abs' `C:\path` or `/path` Windows or UNIX
-- * 'rel' `a/b` Windows and UNIX
-- * 'abs_long' `\\?\C:\path` Windows
-- * 'abs_nodrive' `\path` Windows
-- * 'rel_drive' `C:a\b` Windows
-- * 'unc' `\\server\share\path` Windows
-- * 'unc_long' `\\?\UNC\server\share\path` Windows
-- * 'global' `\\?\path` Windows
-- * 'dev' `\\.\path` Windows
-- * 'dev_alias' `CON`, `c:\path\nul.txt`, etc. Windows
--NOTE: The empty path ('') comes off as type 'rel'.
function path_type(s, pl)
if win(pl) then
if s:find'^\\\\' then
if s:find'^\\\\%?\\' then
if s:find'^\\\\%?\\%a:\\' then
return 'abs_long'
elseif s:find'^\\\\%?\\[uU][nN][cC]\\' then
return 'unc_long'
else
return 'global'
end
elseif s:find'^\\\\%.\\' then
return 'dev'
else
return 'unc'
end
elseif path_dev_alias(s) then
return 'dev_alias'
elseif s:find'^%a:' then
return s:find'^..[\\/]' and 'abs' or 'rel_drive'
else
return s:find'^[\\/]' and 'abs_nodrive' or 'rel'
end
else
return s:byte(1) == ('/'):byte(1) and 'abs' or 'rel'
end
end
--Split a path into its local path component (i.e. the part containing only
--directories and files, eg. `\path` for `C:\path` or for `\\server\path`)
--and, depending on the path type, the drive letter or server name.
--UNC paths are not validated and can have an empty server or share path.
function path_parse(s, pl)
local type = path_type(s, pl)
if win(pl) then
if type == 'rel' or type == 'abs_nodrive' then
return type, s -- nothing to split
elseif type == 'abs' or type == 'rel_drive' then
return type, s:sub(3), s:sub(1,1) -- \path, drive
elseif type == 'abs_long' then
return type, s:sub(3+4), s:sub(1+4,1 +4) -- \path, drive
elseif type == 'unc' then
local server, path = s:match'^..([^\\]*)(.*)$'
return type, path, server
elseif type == 'unc_long' then
local server, path = s:match'^........([^\\]*)(.*)$'
return type, path, server
elseif type == 'dev' then
return type, s:sub(4) -- \path
elseif type == 'dev_alias' then
return type, s -- CON, NUL, ...
elseif type == 'global' then
return type, s:sub(4) -- \path
end
else
return type, s --unix path: nothing to split
end
end
--put together a path from its broken-down components (no validation).
function path_format(type, path, drive, pl)
if win(pl) and type == 'abs' or type == 'rel_drive' then
return drive .. ':' .. path
elseif type == 'abs_long' then
return '\\\\?\\' .. drive .. ':' .. path
elseif type == 'unc' then
return '\\\\' .. drive .. path
elseif type == 'unc_long' then
return '\\\\?\\UNC\\' .. drive .. path
elseif type == 'dev' then
return '\\\\.' .. path
elseif type == 'global' then
return '\\\\?' .. path
else --abs/unix, rel, abs_nodrive, dev_alias
return path
end
end
--Check if a path is an absolute path or not, if it's empty (i.e. root) or not,
--and if it's valid or not. Absolute paths for which their local path
--is '' are actually invalid (currently only UNC paths can be invalid and
--still parse); for those paths the third return value will be false.
local function isabs(type, p, win)
if type == 'rel' or type == 'rel_drive' or type == 'dev_alias' then
return false, p == '', true
elseif p == '' then
return true, true, false --invalid absolute path
else
local isroot = p:find(win and '^[\\/]+$' or '^/+$') and true or false
return true, isroot, true
end
end
function path_isabs(s, pl)
local type, p = path_parse(s, pl)
return isabs(type, p, win(pl))
end
--determine a path's separator if possible.
local function detect_sep(p, win)
if win then
local fws = p:find'[^/]*/'
local bks = p:find'[^\\/]*\\'
if not fws == not bks then
return nil --can't determine
end
return fws and '/' or '\\'
else
return '/'
end
end
--[[
Get/add/remove an ending separator of a path. `sep` can be:
* nil or missing : the ending separator is returned (nil is returned
if the path has no ending separator).
* true, '\\', '/' : the path is returned with an ending separator added
(true means use path's own separator if it has one and
failing that, use dsep or the default platform separator).
* false or '' : the path without its ending separator is returned.
`success` is `false` if trying to add or remove the ending separator from
an empty path (note that even when that happens, the path can still be
concatenated directly to a relative path and result in a valid path).
Multiple consecutive separators are treated as one in that they are returned
together and are replaced together.
]]
local function set_endsep(type, p, win, sep, default_sep)
local _, isempty = isabs(type, p, win)
if isempty then --refuse to change empty paths
return
elseif sep == false or sep == '' then --remove it
return p:gsub(win and '[\\/]+$' or '/+$', '')
elseif p:find(win and '[\\/]$' or '/$') then --add it/already set
return p
else
if sep == true then
sep = detect_sep(p, win) or default_sep or (win and '\\' or '/')
end
assert(sep == '\\' or sep == '/', 'invalid separator')
return p .. sep
end
end
function path_endsep(s, pl, sep, default_sep)
local win = win(pl)
local type, p, drive = path_parse(s, pl)
if sep == nil then
return p:match(win and '[\\/]+$' or '/+$')
else
local p = set_endsep(type, p, win, sep, default_sep)
return p and path_format(type, p, drive, pl) or s, p and true or false
end
end
--[[
Detect or set a path's separator (for Windows paths only).
`sep` can be:
* nil : detect.
* true : set to default_sep.
* false : set to default_sep if both `\` and `/` are found in the path.
* '\\' or '/' : set specifically.
* nil when empty_names is explicitly false: collapse duplicate separators only.
`default_sep` defaults to the platform separator. Unless `empty_names` is `true`,
consecutive separators are collapsed into the first one.
Setting the separator as `\` on a UNIX path may result in an invalid path
because `\` is a valid character in UNIX filenames.
]]
local function set_sep(p, win, sep, default_sep, empty_names)
local dsep = default_sep or (win and '\\' or '/')
if sep == true then --set to default
sep = dsep
elseif sep == false then --set to default only if mixed
sep = detect_sep(p, win) or dsep
elseif sep == nil then --collapse only
sep = '%1'
else
assert(sep == '\\' or sep == '/', 'invalid separator')
end
if empty_names then
return p:gsub(win and '[\\/]' or '/', sep)
else
return p:gsub(win and '([\\/])[\\/]*' or '(/)/*', sep)
end
end
function path_sep(s, pl, sep, default_sep, empty_names)
local win = win(pl)
local type, p, drive = path_parse(s, pl)
if sep == nil and empty_names == nil then
return detect_sep(p, win)
else
p = set_sep(p, win, sep, default_sep, empty_names)
return path_format(type, p, drive, pl)
end
end
--[[
Get/set a Windows long absolute path (one starting with `\\?\C:\`). If
`long` is `nil`, returns whether the path is a long or short Windows absolute
path (returns `nil` for all other kinds of paths). Otherwise it converts the
path, in which case `long` can be `true` (convert to long path), `false`
(convert to short path) or `'auto'` (convert to long style if too long,
or to short style if short enough).
]]
function path_long(s, pl, long)
local type, p, drive = path_parse(s, pl)
local is_long = type == 'abs_long' or type == 'unc_long'
local is_short = (win(pl) and type == 'abs') or type == 'unc'
if long == nil then
if is_long then
return true
elseif is_short then
return false
else
return nil --does not apply
end
end
if
is_short and (long == true or (long == 'auto' and #s > 259))
then
p = p:gsub('/+', '\\') --NOTE: this might create a smaller path
local long_type = type == 'abs' and 'abs_long' or 'unc_long'
s = path_format(long_type, p, drive, pl)
elseif
is_long and (long == false or (long == 'auto' and #s <= 259 + 4))
then
local short_type = type == 'abs_long' and 'abs' or 'unc'
s = path_format(short_type, p, drive, pl)
end
return s
end
--Get the last path component of a path.
--Returns `''` if the path is empty or ends with a separator.
function path_file(s, pl)
local _, p = path_parse(s, pl)
return p:match(win(pl) and '[^\\/]*$' or '[^/]*$')
end
--[[
Split a path's last component into the name and extension parts like so:
* a.txt' -> 'a', 'txt'
* '.bashrc' -> '.bashrc', nil
* 'a' -> 'a', nil
* 'a.' -> 'a', ''
]]
function path_nameext(s, pl)
local patt = win(pl) and '^(.-)%.([^%.\\/]*)$' or '^(.-)%.([^%./]*)$'
local file = path_file(s, pl)
local name, ext = file:match(patt)
if not name or name == '' then -- 'dir' or '.bashrc'
name, ext = file, nil
end
return name, ext
end
function path_ext(s, pl)
return (select(2, path_nameext(s, pl)))
end
--Get a path without the last component and separator. If the path ends with
--a separator then it returns the whole path without the separator. Multiple
--consecutive separators are treated as one. Returns `nil` for `''`, `'.'`,
--`'C:'`, `'/'`, `'C:\\'` and `\\server\`. Returns `'.'` for simple filenames.
function path_dir(s, pl)
local type, p, drive = path_parse(s, pl)
if p == '' or p == '.' then --current dir has no dir
return nil
end
local i1, i2, i3 = p:match(win(pl)
and '()[\\/]*()[^\\/]*()$' or '()/*()[^/]*()$')
if i1 == 1 and i3 == i2 then --root dir has no dir
return nil
end
local i = i1 == 1 and i2 or i1
local s = path_format(type, p:sub(1, i-1), drive, pl)
return s == '' and '.' or s --fix '' as '.'
end
--[[
Iterate over a path's components (that is excluding prefixes like `\\server`
or `C:`) and separators. Pass `true` to the `full` arg to iterate over the
whole unparsed path. For absolute paths, the first iteration is
`'', <root_separator>`. Empty names are not iterated. Instead, consecutive
separators are returned together. Concatenating all the iterated path
components and separators always results in the exact original path.
]]
function path_split(s, pl, full)
local win = win(pl)
local p = full and s or select(2, path_parse(s, pl))
local root_sep = p:match(win and '^[\\/]+' or '^/+')
local next_pc = p:gmatch(win and '([^\\/]+)([\\/]*)' or '([^/]+)(/*)')
local started = not root_sep
return function()
if not started then
started = true
return '', root_sep
elseif started then
return next_pc()
end
end
end
local function repl(x, v, r)
if x == v then return r else return x end
end
--[[
Normalize a path. `opt` can contain:
* dot_dirs : keep `.` dirs (false).
* dot_dot_dirs : keep unnecessary `..` dirs (true).
WARNING: removing `..` breaks the path if there are symlinks involved!
* sep (false), default_sep, empty_names : args for path_sep(). use 'leave' to skip.
* endsep (false) : sep arg for path_endsep(). use 'leave' to skip.
* long ('auto') : long arg for path_long(). use 'leave' to skip.
If normalization results in the empty relative path '', then '.' is returned.
]]
function path_normalize(s, pl, opt)
opt = opt or {}
local win = win(pl)
local type, p, drive = path_parse(s, pl)
local t = {} --{dir1, sep1, ...}
local lastsep --last separator that was not added to the list
for s, sep in path_split(p, pl, true) do
if s == '.' and not opt.dot_dirs then
--skip adding the `.` dir and the separator following it
lastsep = sep
elseif s == '..' and opt.dot_dot_dirs ~= false and #t > 0 then
--find the last dir past any `.` dirs, in case opt.dot_dirs = true.
local i = #t-1
while t[i] == '.' do
i = i - 2
end
--remove the last dir (and the separator following it)
--that's not `..` and it's not the root element.
if i > 0 and ((i > 1 or t[i] ~= '') and t[i] ~= '..') then
remove(t, i)
remove(t, i)
lastsep = sep
elseif #t == 2 and t[1] == '' then
--skip any `..` after the root slash
lastsep = sep
else
insert(t, s)
insert(t, sep)
end
else
insert(t, s)
insert(t, sep)
lastsep = nil
end
end
if type == 'rel' and #t == 0 then
--rel path '' is invalid. fix that.
insert(t, '.')
insert(t, lastsep)
elseif lastsep == '' and (#t > 2 or t[1] ~= '') then
--if there was no end separator originally before removing path
--components, remove the left over end separator now.
remove(t)
end
p = concat(t)
if opt.sep ~= 'leave' then
p = set_sep(p, win, repl(opt.sep, nil, false),
opt.default_sep, opt.empty_names)
end
if opt.endsep ~= 'leave' then
p = set_endsep(type, p, win, repl(opt.endsep, nil, false),
opt.default_sep) or p
end
s = path_format(type, p, drive, pl)
if win and opt.long ~= 'leave' then
s = path_long(s, pl, repl(opt.long, nil, 'auto'))
end
return s
end
--[[
Get the common path prefix of two paths, including the end separator if both
paths share it, or `nil` if the paths don't have anything in common.
Note that `path_commonpath('C:', 'C:\\', 'win') == nil` because the paths are
of different type even if they look like they share a common prefix.
BUG: The case-insensitive comparison for Windows doesn't work with
paths with non-ASCII characters because it's made with `string.lower()`.
Proper lowercase your paths before using this function, or patch
`string.lower()` to support utf8 lowercasing. This is not an issue if both
paths are in the original letter case (eg. they come from the same API).
]]
function path_commonpath(s1, s2, pl)
local win = win(pl)
local t1, p1, d1 = path_parse(s1, pl)
local t2, p2, d2 = path_parse(s2, pl)
local t, p, d
if #p1 <= #p2 then --pick the smaller/first path when formatting
t, p, d = t1, p1, d1
else
t, p, d = t2, p2, d2
end
if win then --make the search case-insensitive and normalize separators
d1 = d1 and d1:lower()
d2 = d2 and d2:lower()
p1 = p1:lower():gsub('/', '\\')
p2 = p2:lower():gsub('/', '\\')
end
if t1 ~= t2 or d1 ~= d2 then
return nil
elseif p1 == '' or p2 == '' then
return path_format(t, p, d, pl)
end
local sep = (win and '\\' or '/'):byte(1)
local si = 0 --index where the last common separator was found
for i = 1, #p + 1 do
local c1 = p1:byte(i)
local c2 = p2:byte(i)
local sep1 = c1 == nil or c1 == sep
local sep2 = c2 == nil or c2 == sep
if sep1 and sep2 then
si = i
elseif c1 ~= c2 then
break
end
end
p = p:sub(1, si)
return path_format(t, p, d, pl)
end
--Get the number of non-empty path components, excluding prefixes
--like `C:\`, `\\server\`, etc.
local function depth(p, win)
local n = 0
for _ in p:gmatch(win and '()[^\\/]+' or '()[^/]+') do
n = n + 1
end
return n
end
function path_depth(s, pl)
local _, p = path_parse(s, pl)
return depth(p, win(pl))
end
--[[
Combine two paths if possible (return `nil, err` if not). Supported
combinations are between anything except between `dev_alias` and `rel` paths,
between `abs_nodrive` and `rel_drive`, and between `rel_drive` and `abs`
or `abs_long`. When the paths can only be combined in one way, paths can be
given in any order. The separator with which paths are combined is either
`sep` or if `sep` is nil it's detected and if that fails `default_sep` or the
default separator is used.
]]
local function combinable(type1, type2)
if type2 == 'rel' then -- any + c/d -> any/c/d
return type1 ~= 'dev_alias'
elseif type2 == 'abs_nodrive' then -- C:a/b + /c/d -> C:/c/d/a/b
return type1 == 'rel_drive'
elseif type2 == 'rel_drive' then -- C:/a/b + C:c/d -> C:/a/b/c/d
return type1 == 'abs' or type1 == 'abs_long'
end
end
function path_combine(s1, s2, pl, sep, default_sep)
local type1, p1, drive1 = path_parse(tostring(s1), pl)
local type2, p2, drive2 = path_parse(tostring(s2), pl)
if not combinable(type1, type2) then
if combinable(type2, type1) then
type1, type2, s1, s2, p1, p2, drive1, drive2 =
type2, type1, s2, s1, p2, p1, drive2, drive1
else
return nil, ('cannot combine %s and %s paths'):format(type1, type2)
end
end
if s2 == '' then -- any + '' -> any
return s1
elseif type2 == 'rel' or type2 == 'abs_nodrive' then
local win = win(pl)
local sep = sep or detect_sep(p1, win) or detect_sep(p2, win)
or default_sep or (win and '\\' or '/')
if type2 == 'rel' then -- any + c/d -> any/c/d
p1 = set_endsep(type1, p1, win, sep) or p1
return path_format(type1, p1 .. s2, drive1, pl)
elseif type2 == 'abs_nodrive' then -- C:a/b + /d/e -> C:/d/e/a/b
p2 = set_endsep(type2, p2, win, sep) or p2
return path_format(type1, p2 .. p1, drive1, pl)
end
elseif type2 == 'rel_drive' then -- C:/a/b + C:d/e -> C:/a/b/d/e
if drive1 ~= drive2 then
return nil, 'path drives are different'
end
return path_combine(s1, p2, pl)
end
end
--make a path by combining dir with one or more path components.
local type, select = type, select
function indir(dir, ...)
if select('#', ...) == 0 then return dir end
local s = ...
local ts = type(s)
assert(ts == 'number' or ts == 'string', 'indir: invalid arg type')
local p = assert(path_combine(dir, s))
return indir(p, select(2, ...))
end
--Convert a path (of any type) into a rel path that is relative to `pwd`.
--Returns `nil` if the paths are of different types or don't have a base path
--in common. The ending (back)slash is preserved if present.
function relpath(s, pwd, pl, sep, default_sep)
local prefix = path_commonpath(s, pwd, pl)
if not prefix then return nil end
local type, p, drive = path_parse(s, pl)
local win = win(pl)
local sep = sep or detect_sep(pwd, win) or detect_sep(p, win)
or default_sep or (win and '\\' or '/')
local endsep = p:match(win and '[\\/]*$' or '/*$')
local pwd_suffix = pwd:sub(#prefix + 1)
local n = depth(pwd_suffix, win)
local p1 = ('..' .. sep):rep(n - 1) .. (n > 0 and '..' or '')
local p2 = s:sub(#prefix + 1)
local p2 = p2:gsub(win and '^[\\/]+' or '^/+', '')
local p2 = p2:gsub(win and '[\\/]+$' or '/+$', '')
local p2 = p1 == '' and p2 == '' and '.' or p2
local p3 = p1 .. (p1 ~= '' and p2 ~= '' and sep or '') .. p2 .. endsep
return path_format('rel', p3, nil, pl)
end
--[[
Validate a filename or apply a replacement function on it in order to make it
valid. `repl` can be a `function(match, err) -> repl_str` that receives the
problematic match and an error code indicating the problem which can be one
of '', '.', '..', 'dev_alias', 'char', 'length', 'evil' and it should return
a replacement string or `false/nil` if it cannot do the replacement
(`'evil'` errors should generally be replaced with `''`).
--NOTE: If repl isn't itself escaped then duplicate filenames can result.
]]
function path_filename(s, pl, repl, break_on_err)
local win = win(pl)
local function check(err, msg)
if not repl or err == break_on_err then
return nil, msg, err
end
local s = repl(s, err)
if not s then
return nil, msg, err
end
return path_filename(s, pl, repl, err) --tail call
end
function subcheck(patt, err, msg)
if repl then
local user_repl = repl
function repl(s, err)
local s, repl_count = s:gsub(patt, function(c)
return user_repl(c, err) --returning nil/false means no repl.
end)
return repl_count > 0 and s
end
end
return check(err, msg)
end
local invalid_chars = win and '[%z\1-\31<>:"|%?%*\\/]' or '[%z/]'
local empty
if s == '' then
return check(s, 'empty filename')
elseif s == '.' or s == '..' then
return check(s, 'filename is `.` or `..`')
elseif win and path_dev_alias(s) then
return check('dev_alias', 'filename is a Windows device alias')
elseif win and s:find(invalid_chars) then
return subcheck(invalid_chars, 'char', 'invalid characters in filename')
elseif #s > 255 then --same maximum for Windows and Linux
return check('length', 'filename too long')
elseif s:find' +$' then
return subcheck(' +$', 'evil', 'filename ends with spaces')
elseif s:find'^ +' then
return subcheck('^ +', 'evil', 'filename begins with spaces')
elseif s:find'%.+$' then
return subcheck('%.+$', 'evil', 'filename ends with a dot')
end
return s
end