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