l-dir.lua /size: 18 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['l-dir'] = {
2    version   = 1.001,
3    comment   = "companion to luat-lib.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- todo: dir.expandname will be sped up and merged with cleanpath and collapsepath
10-- todo: keep track of currentdir (chdir, pushdir, popdir)
11
12local type, select = type, select
13local find, gmatch, match, gsub, sub = string.find, string.gmatch, string.match, string.gsub, string.sub
14local concat, insert, remove, unpack = table.concat, table.insert, table.remove, table.unpack
15local lpegmatch = lpeg.match
16
17local P, S, R, C, Cc, Cs, Ct, Cv, V = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Cc, lpeg.Cs, lpeg.Ct, lpeg.Cv, lpeg.V
18
19dir = dir or { }
20local dir = dir
21local lfs = lfs
22
23local attributes = lfs.attributes
24----- walkdir    = lfs.dir
25local scandir    = lfs.dir
26local isdir      = lfs.isdir  -- not robust, will be overloaded anyway
27local isfile     = lfs.isfile -- not robust, will be overloaded anyway
28local currentdir = lfs.currentdir
29local chdir      = lfs.chdir
30local mkdir      = lfs.mkdir
31
32local onwindows  = os.type == "windows" or find(os.getenv("PATH"),";",1,true)
33
34-- in case we load outside luatex
35
36if onwindows then
37
38    -- lfs.isdir does not like trailing /
39    -- lfs.dir accepts trailing /
40
41    local tricky = S("/\\") * P(-1)
42
43    isdir = function(name)
44        if lpegmatch(tricky,name) then
45            return attributes(name,"mode") == "directory"
46        else
47            return attributes(name.."/.","mode") == "directory"
48        end
49    end
50
51    isfile = function(name)
52        return attributes(name,"mode") == "file"
53    end
54
55    lfs.isdir  = isdir
56    lfs.isfile = isfile
57
58else
59
60    isdir = function(name)
61        return attributes(name,"mode") == "directory"
62    end
63
64    isfile = function(name)
65        return attributes(name,"mode") == "file"
66    end
67
68    lfs.isdir  = isdir
69    lfs.isfile = isfile
70
71end
72
73-- safeguard
74
75local isreadable = file.isreadable
76
77local walkdir = function(p,...)
78    if isreadable(p.."/.") then
79        return scandir(p,...)
80    else
81        return function() end
82    end
83end
84
85lfs.walkdir = walkdir
86
87-- handy
88
89function dir.current()
90    return (gsub(currentdir(),"\\","/"))
91end
92
93-- The next one is somewhat optimized but still slow but it's a pitty that the iterator
94-- doesn't return a mode too.
95
96local function glob_pattern_function(path,patt,recurse,action)
97    if isdir(path) then
98        local usedpath
99        if path == "/" then
100            usedpath = "/."
101        elseif not find(path,"/$") then
102            usedpath = path .. "/."
103            path = path .. "/"
104        else
105            usedpath = path
106        end
107        local dirs
108        local nofdirs  = 0
109        for name, mode, size, time in walkdir(usedpath) do
110            if name ~= "." and name ~= ".." then
111                local full = path .. name
112                if mode == nil then
113                    mode = attributes(full,'mode')
114                end
115                if mode == 'file' then
116                    if not patt or find(full,patt) then
117                        action(full,size,time)
118                    end
119                elseif recurse and mode == "directory" then
120                    if dirs then
121                        nofdirs = nofdirs + 1
122                        dirs[nofdirs] = full
123                    else
124                        nofdirs = 1
125                        dirs    = { full }
126                    end
127                end
128            end
129        end
130        if dirs then
131            for i=1,nofdirs do
132                glob_pattern_function(dirs[i],patt,recurse,action)
133            end
134        end
135    end
136end
137
138local function glob_pattern_table(path,patt,recurse,result)
139    if not result then
140        result = { }
141    end
142    local usedpath
143    if path == "/" then
144        usedpath = "/."
145    elseif not find(path,"/$") then
146        usedpath = path .. "/."
147        path = path .. "/"
148    else
149        usedpath = path
150    end
151    local dirs
152    local nofdirs  = 0
153    local noffiles = #result
154    for name, mode in walkdir(usedpath) do
155        if name ~= "." and name ~= ".." then
156            local full = path .. name
157            if mode == nil then
158                mode = attributes(full,'mode')
159            end
160            if mode == 'file' then
161                if not patt or find(full,patt) then
162                    noffiles = noffiles + 1
163                    result[noffiles] = full
164                end
165            elseif recurse and mode == "directory" then
166                if dirs then
167                    nofdirs = nofdirs + 1
168                    dirs[nofdirs] = full
169                else
170                    nofdirs = 1
171                    dirs    = { full }
172                end
173            end
174        end
175    end
176    if dirs then
177        for i=1,nofdirs do
178            glob_pattern_table(dirs[i],patt,recurse,result)
179        end
180    end
181    return result
182end
183
184local function globpattern(path,patt,recurse,method)
185    local kind = type(method)
186    if patt and sub(patt,1,-3) == path then
187        patt = false
188    end
189    local okay = isdir(path)
190    if kind == "function" then
191        return okay and glob_pattern_function(path,patt,recurse,method) or { }
192    elseif kind == "table" then
193        return okay and glob_pattern_table(path,patt,recurse,method) or method
194    else
195        return okay and glob_pattern_table(path,patt,recurse,{ }) or { }
196    end
197end
198
199dir.globpattern = globpattern
200
201-- never or seldom used so far:
202
203local function collectpattern(path,patt,recurse,result)
204    local ok, scanner
205    result = result or { }
206    if path == "/" then
207        ok, scanner, first = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
208    else
209        ok, scanner, first = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
210    end
211    if ok and type(scanner) == "function" then
212        if not find(path,"/$") then
213            path = path .. '/'
214        end
215        for name in scanner, first do -- can be optimized
216            if name == "." then
217                -- skip
218            elseif name == ".." then
219                -- skip
220            else
221                local full = path .. name
222                local attr = attributes(full)
223                local mode = attr.mode
224                if mode == 'file' then
225                    if find(full,patt) then
226                        result[name] = attr
227                    end
228                elseif recurse and mode == "directory" then
229                    attr.list = collectpattern(full,patt,recurse)
230                    result[name] = attr
231                end
232            end
233        end
234    end
235    return result
236end
237
238dir.collectpattern = collectpattern
239
240local separator, pattern
241
242if onwindows then -- we could sanitize here
243
244    local slash = S("/\\") / "/"
245
246--     pattern = Ct {
247    pattern = {
248        (Cs(P(".") + slash^1) + Cs(R("az","AZ") * P(":") * slash^0) + Cc("./")) * V(2) * V(3),
249        Cs(((1-S("*?/\\"))^0 * slash)^0),
250        Cs(P(1)^0)
251    }
252
253else -- assume unix
254
255--     pattern = Ct {
256    pattern = {
257        (C(P(".") + P("/")^1) + Cc("./")) * V(2) * V(3),
258        C(((1-S("*?/"))^0 * P("/"))^0),
259        C(P(1)^0)
260    }
261
262end
263
264local filter = Cs ( (
265    P("**") / ".*" +
266    P("*")  / "[^/]*" +
267    P("?")  / "[^/]" +
268    P(".")  / "%%." +
269    P("+")  / "%%+" +
270    P("-")  / "%%-" +
271    P(1)
272)^0 )
273
274local function glob(str,t)
275    if type(t) == "function" then
276        if type(str) == "table" then
277            for s=1,#str do
278                glob(str[s],t)
279            end
280        elseif isfile(str) then
281            t(str)
282        else
283            local root, path, base = lpegmatch(pattern,str) -- we could use the file splitter
284            if root and path and base then
285                local recurse = find(base,"**",1,true) -- find(base,"%*%*")
286                local start   = root .. path
287                local result  = lpegmatch(filter,start .. base)
288                globpattern(start,result,recurse,t)
289            end
290        end
291    else
292        if type(str) == "table" then
293            local t = t or { }
294            for s=1,#str do
295                glob(str[s],t)
296            end
297            return t
298        elseif isfile(str) then
299            if t then
300                t[#t+1] = str
301                return t
302            else
303                return { str }
304            end
305        else
306            local root, path, base = lpegmatch(pattern,str) -- we could use the file splitter
307            if root and path and base then
308                local recurse = find(base,"**",1,true) -- find(base,"%*%*")
309                local start   = root .. path
310                local result  = lpegmatch(filter,start .. base)
311                return globpattern(start,result,recurse,t)
312            else
313                return { }
314            end
315        end
316    end
317end
318
319dir.glob = glob
320
321-- local c = os.clock()
322-- local t = dir.glob("e:/**")
323-- local t = dir.glob("t:/sources/**")
324-- local t = dir.glob("t:/**")
325-- print(os.clock()-c,#t)
326
327-- for i=1,3000 do print(t[i]) end
328-- for i=1,10 do print(t[i]) end
329
330-- list = dir.glob("**/*.tif")
331-- list = dir.glob("/**/*.tif")
332-- list = dir.glob("./**/*.tif")
333-- list = dir.glob("oeps/**/*.tif")
334-- list = dir.glob("/oeps/**/*.tif")
335
336local function globfiles(path,recurse,func,files) -- func == pattern or function
337    if type(func) == "string" then
338        local s = func
339        func = function(name) return find(name,s) end
340    end
341    files = files or { }
342    local noffiles = #files
343    for name, mode in walkdir(path) do
344        if find(name,"^%.") then
345            --- skip
346        else
347            if mode == nil then
348                mode = attributes(name,'mode')
349            end
350            if mode == "directory" then
351                if recurse then
352                    globfiles(path .. "/" .. name,recurse,func,files)
353                end
354            elseif mode == "file" then
355                if not func or func(name) then
356                    noffiles = noffiles + 1
357                    files[noffiles] = path .. "/" .. name
358                end
359            end
360        end
361    end
362    return files
363end
364
365dir.globfiles = globfiles
366
367local function globdirs(path,recurse,func,files) -- func == pattern or function
368    if type(func) == "string" then
369        local s = func
370        func = function(name) return find(name,s) end
371    end
372    files = files or { }
373    local noffiles = #files
374    for name, mode in walkdir(path) do
375        if find(name,"^%.") then
376            --- skip
377        else
378            if mode == nil then
379                mode = attributes(name,'mode')
380            end
381            if mode == "directory" then
382                if not func or func(name) then
383                    noffiles = noffiles + 1
384                    files[noffiles] = path .. "/" .. name
385                    if recurse then
386                        globdirs(path .. "/" .. name,recurse,func,files)
387                    end
388                end
389            end
390        end
391    end
392    return files
393end
394
395dir.globdirs = globdirs
396
397-- inspect(globdirs("e:/tmp"))
398
399-- t = dir.glob("c:/data/develop/context/sources/**/????-*.tex")
400-- t = dir.glob("c:/data/develop/tex/texmf/**/*.tex")
401-- t = dir.glob("c:/data/develop/context/texmf/**/*.tex")
402-- t = dir.glob("f:/minimal/tex/**/*")
403-- print(dir.ls("f:/minimal/tex/**/*"))
404-- print(dir.ls("*.tex"))
405
406function dir.ls(pattern)
407    return concat(glob(pattern),"\n")
408end
409
410-- mkdirs("temp")
411-- mkdirs("a/b/c")
412-- mkdirs(".","/a/b/c")
413-- mkdirs("a","b","c")
414
415local make_indeed = true -- false
416
417if onwindows then
418
419    function dir.mkdirs(...)
420        local n = select("#",...)
421        local str
422        if n == 1 then
423            str = select(1,...)
424            if isdir(str) then
425                return str, true
426            end
427        else
428            str = ""
429            for i=1,n do
430                local s = select(i,...)
431                if s == "" then
432                    -- skip
433                elseif str == "" then
434                    str = s
435                else
436                    str = str .. "/" .. s
437                end
438            end
439        end
440        local pth = ""
441        local drive = false
442        local first, middle, last = match(str,"^(//)(//*)(.*)$")
443        if first then
444            -- empty network path == local path
445        else
446            first, last = match(str,"^(//)/*(.-)$")
447            if first then
448                middle, last = match(str,"([^/]+)/+(.-)$")
449                if middle then
450                    pth = "//" .. middle
451                else
452                    pth = "//" .. last
453                    last = ""
454                end
455            else
456                first, middle, last = match(str,"^([a-zA-Z]:)(/*)(.-)$")
457                if first then
458                    pth, drive = first .. middle, true
459                else
460                    middle, last = match(str,"^(/*)(.-)$")
461                    if not middle then
462                        last = str
463                    end
464                end
465            end
466        end
467        for s in gmatch(last,"[^/]+") do
468            if pth == "" then
469                pth = s
470            elseif drive then
471                pth, drive = pth .. s, false
472            else
473                pth = pth .. "/" .. s
474            end
475            if make_indeed and not isdir(pth) then
476                mkdir(pth)
477            end
478        end
479        return pth, (isdir(pth) == true)
480    end
481
482    -- print(dir.mkdirs("","","a","c"))
483    -- print(dir.mkdirs("a"))
484    -- print(dir.mkdirs("a:"))
485    -- print(dir.mkdirs("a:/b/c"))
486    -- print(dir.mkdirs("a:b/c"))
487    -- print(dir.mkdirs("a:/bbb/c"))
488    -- print(dir.mkdirs("/a/b/c"))
489    -- print(dir.mkdirs("/aaa/b/c"))
490    -- print(dir.mkdirs("//a/b/c"))
491    -- print(dir.mkdirs("///a/b/c"))
492    -- print(dir.mkdirs("a/bbb//ccc/"))
493
494else
495
496    function dir.mkdirs(...)
497        local n = select("#",...)
498        local str, pth
499        if n == 1 then
500            str = select(1,...)
501            if isdir(str) then
502                return str, true
503            end
504        else
505            str = ""
506            for i=1,n do
507                local s = select(i,...)
508                if s and s ~= "" then -- we catch nil and false
509                    if str ~= "" then
510                        str = str .. "/" .. s
511                    else
512                        str = s
513                    end
514                end
515            end
516        end
517        str = gsub(str,"/+","/")
518        if find(str,"^/") then
519            pth = "/"
520            for s in gmatch(str,"[^/]+") do
521                local first = (pth == "/")
522                if first then
523                    pth = pth .. s
524                else
525                    pth = pth .. "/" .. s
526                end
527                if make_indeed and not first and not isdir(pth) then
528                    mkdir(pth)
529                end
530            end
531        else
532            pth = "."
533            for s in gmatch(str,"[^/]+") do
534                pth = pth .. "/" .. s
535                if make_indeed and not isdir(pth) then
536                    mkdir(pth)
537                end
538            end
539        end
540        return pth, (isdir(pth) == true)
541    end
542
543    -- print(dir.mkdirs("","","a","c"))
544    -- print(dir.mkdirs("a"))
545    -- print(dir.mkdirs("/a/b/c"))
546    -- print(dir.mkdirs("/aaa/b/c"))
547    -- print(dir.mkdirs("//a/b/c"))
548    -- print(dir.mkdirs("///a/b/c"))
549    -- print(dir.mkdirs("a/bbb//ccc/"))
550
551end
552
553dir.makedirs = dir.mkdirs
554
555
556do
557
558    -- we can only define it here as it uses dir.chdir and we also need to
559    -- make sure we use the non sandboxed variant because otherwise we get
560    -- into a recursive loop due to usage of expandname in the file resolver
561
562    local chdir = sandbox and sandbox.original(chdir) or chdir
563
564    if onwindows then
565
566        local xcurrentdir = dir.current
567
568        function dir.expandname(str) -- will be merged with cleanpath and collapsepath\
569            local first, nothing, last = match(str,"^(//)(//*)(.*)$")
570            if first then
571                first = xcurrentdir() .. "/" -- xcurrentdir sanitizes
572            end
573            if not first then
574                first, last = match(str,"^(//)/*(.*)$")
575            end
576            if not first then
577                first, last = match(str,"^([a-zA-Z]:)(.*)$")
578                if first and not find(last,"^/") then
579                    local d = currentdir() -- push / pop
580                    if chdir(first) then
581                        first = xcurrentdir() -- xcurrentdir sanitizes
582                    end
583                    chdir(d)
584                end
585            end
586            if not first then
587                first, last = xcurrentdir(), str
588            end
589            last = gsub(last,"//","/")
590            last = gsub(last,"/%./","/")
591            last = gsub(last,"^/*","")
592            first = gsub(first,"/*$","")
593            if last == "" or last == "." then
594                return first
595            else
596                return first .. "/" .. last
597            end
598        end
599
600    else
601
602        function dir.expandname(str) -- will be merged with cleanpath and collapsepath
603            if not find(str,"^/") then
604                str = currentdir() .. "/" .. str
605            end
606            str = gsub(str,"//","/")
607            str = gsub(str,"/%./","/")
608            str = gsub(str,"(.)/%.$","%1")
609            return str
610        end
611
612    end
613
614    -- This go there anc check works okay in tricky situation as we encounter
615    -- on osx, where tex installations use rather complex chains of links.
616
617    function dir.expandlink(dir,report)
618        local curdir = currentdir()
619        local trace  = type(report) == "function"
620        if chdir(dir) then
621            local newdir = currentdir()
622            if newdir ~= dir and trace then
623                report("following symlink %a to %a",dir,newdir)
624            end
625            chdir(curdir)
626            return newdir
627        else
628            if trace then
629                report("unable to check path %a",dir)
630            end
631            return dir
632        end
633    end
634
635end
636
637file.expandname = dir.expandname -- for convenience
638
639local stack = { }
640
641function dir.push(newdir)
642    local curdir = currentdir()
643    insert(stack,curdir)
644    if newdir and newdir ~= "" and chdir(newdir) then
645        return newdir
646    else
647        return curdir
648    end
649end
650
651function dir.pop()
652    local d = remove(stack)
653    if d then
654        chdir(d)
655    end
656    return d
657end
658
659local function found(...) -- can have nil entries
660    for i=1,select("#",...) do
661        local path = select(i,...)
662        local kind = type(path)
663        if kind == "string" then
664            if isdir(path) then
665                return path
666            end
667        elseif kind == "table" then
668            -- here we asume no holes, i.e. an indexed table
669            local path = found(unpack(path))
670            if path then
671                return path
672            end
673        end
674    end
675 -- return nil -- if we want print("crappath") to show something
676end
677
678dir.found = found
679