local z=require("\112\108\046\117\116\105\108\115")local e=require("\112\108\046\112\097\116\104")local K=e.is_windows local D=e.dir local O=e.mkdir local n=e.rmdir local t=string.sub local b,P,A,c,f,a=os,pcall,ipairs,pairs,require,setmetatable local U=b.remove local T=table.insert local p,i,B=z.assert_arg,z.assert_string,z.raise local X,F=e.exists,e.isdir local Y=e.sep local V={}local function k(z)return a(z,f("\112\108\046\076\105\115\116"))end local function l(z,K)p(z,K,"\115\116\114\105\110\103",e.isdir,"\110\111\116 \097 \100\105\114\101\099\116\111\114\121",4)end local function y(K)K=z.escape(e.normcase(K))return"\094"..((K:gsub("\037\037\037\042","\046\042")):gsub("\037\037\037\063","\046").."\036")end function V.fnmatch(z,K)i(1,z)i(2,K)return(e.normcase(z)):find(y(K))~=nil end function V.filter(z,K)p(1,z,"\116\097\098\108\101")i(2,K)local D={}local O=y(K)for z,K in A(z)do if(e.normcase(K)):find(O)then T(D,K)end end return k(D)end local function N(K,O,n)local t={}local b=z.choose(O,e.isfile,e.isdir)if not K then K="\046"end for z in D(K)do if z~="\046"and z~="\046\046"then local D=e.join(K,z)if b(D)and(not n or n(z))then T(t,D)end end end return k(t)end function V.getfiles(z,K)z=z or"\046"l(1,z)if K then i(2,K)end local D if K then K=y(K)D=function(z)return(e.normcase(z)):find(K)end end return N(z,true,D)end function V.getdirectories(z)z=z or"\046"l(1,z)return N(z,false)end local R,Q,h,q,w,I,G,C local function r(K,D)if not C then C=e.tmpname()end local O=e.is_windows and" \062 "or" \050\062 "K=K..(" "..(D..(O..z.quote_arg(C))))local n=z.execute(K)if not n then local e=(z.readfile(C)):gsub("\n\040\046\042\041","")U(C)return false,e else U(C)return true end end local function H()if not h then h=true local z z,R=P(f,"\097\108\105\101\110")if not z then R=nil z,Q=P(f,"\102\102\105")end if not z then Q=nil return end else return end if R then local z=R.load("\107\101\114\110\101\108\051\050\046\100\108\108")q=z.CopyFileA q:types({"\115\116\114\105\110\103","\115\116\114\105\110\103";"\105\110\116";ret="\105\110\116";abi="\115\116\100\099\097\108\108"})w=z.MoveFileA w:types({"\115\116\114\105\110\103";"\115\116\114\105\110\103";ret="\105\110\116",abi="\115\116\100\099\097\108\108"})I=z.GetLastError I:types({ret="\105\110\116",abi="\115\116\100\099\097\108\108"})elseif Q then Q.cdef("            \105\110\116 \067\111\112\121\070\105\108\101\065\040\099\111\110\115\116 \099\104\097\114 \042\115\114\099\044 \099\111\110\115\116 \099\104\097\114 \042\100\101\115\116\044 \105\110\116 \105\111\118\114\041\059\n            \105\110\116 \077\111\118\101\070\105\108\101\065\040\099\111\110\115\116 \099\104\097\114 \042\115\114\099\044 \099\111\110\115\116 \099\104\097\114 \042\100\101\115\116\041\059\n            \105\110\116 \071\101\116\076\097\115\116\069\114\114\111\114\040\041\059\n        ")q=Q.C.CopyFileA w=Q.C.MoveFileA I=Q.C.GetLastError end G={ERROR_FILE_NOT_FOUND=2,ERROR_PATH_NOT_FOUND=3,ERROR_ACCESS_DENIED=5,ERROR_WRITE_PROTECT=19;ERROR_BAD_UNIT=20;ERROR_NOT_READY=21,ERROR_WRITE_FAULT=29,ERROR_READ_FAULT=30;ERROR_SHARING_VIOLATION=32;ERROR_LOCK_VIOLATION=33;ERROR_HANDLE_DISK_FULL=39,ERROR_BAD_NETPATH=53,ERROR_NETWORK_BUSY=54;ERROR_DEV_NOT_EXIST=55;ERROR_FILE_EXISTS=80;ERROR_OPEN_FAILED=110;ERROR_INVALID_NAME=123,ERROR_BAD_PATHNAME=161,ERROR_ALREADY_EXISTS=183}end local function J(e,K)return z.quote_arg(e)..(" "..z.quote_arg(K))end local function L(D,O,n,t)if t==1 and e.exists(n)then return false,"\099\097\110\110\111\116 \111\118\101\114\119\114\105\116\101 \100\101\115\116\105\110\097\116\105\111\110"end if K then H()if not q then if e.is_windows then O=O:gsub("\047","\092")n=n:gsub("\047","\092")end local K,t=r("\099\111\112\121",J(O,n))if not K then return false,t end if not D then return r("\100\101\108",z.quote_arg(O))end return true else if e.isdir(n)then n=e.join(n,e.basename(O))end local z if D then z=q(O,n,t)else z=w(O,n)end if z==0 then local z=I()for e,K in c(G)do if K==z then return false,e end end return false,"\069\114\114\111\114 \035"..z else return true end end else return r(D and"\099\112"or"\109\118",J(O,n))end end function V.copyfile(z,e,K)i(1,z)i(2,e)K=K==nil or K return L(true,z,e,K and 0 or 1)end function V.movefile(z,e)i(1,z)i(2,e)return L(false,z,e,0)end local function M(z,K)local O={}local n={}for D in D(z)do if D~="\046"and D~="\046\046"then local t=e.join(z,D)local b=K(t,"\109\111\100\101")if b=="\100\105\114\101\099\116\111\114\121"then T(O,D)else T(n,D)end end end return k(O),k(n)end function V.walk(K,D,O)l(1,K)local n if e.is_windows or not O then n=e.attrib else n=e.link_attrib end local t={K}local b={}local P=function()while#t>0 do local z=table.remove(t)local K,O=M(z,n)for K,D in A(K)do table.insert(t,z..(e.sep..D))end if not D then return z,K,O else table.insert(b,{z,K;O})end end if#b>0 then return z.unpack(table.remove(b))end end return P end function V.rmtree(z)l(1,z)if e.islink(z)then return false,"\119\105\108\108 \110\111\116 \102\111\108\108\111\119 \115\121\109\108\105\110\107"end for z,D,O in V.walk(z,true)do if e.islink(z)then if K then local e,K=n(z)if not e then return nil,K..("\058 "..z)end else local e,K=U(z)if not e then return nil,K..("\058 "..z)end end else for K,D in A(O)do local O,n=U(e.join(z,D))if not O then return nil,n..("\058 "..e.join(z,D))end end local K,D=n(z)if not K then return nil,D..("\058 "..z)end end end return true end do local z if e.is_windows then z="\040\046\043\041\092\091\094\092\093\043\036"else z="\040\046\043\041\047\091\094\047\093\043\036"end local K function K(D)if D:find("\094\037\097\058\091\092\093\042\036")then return true end if not e.isdir(D)then local e=D:match(z)if e then local z,D=K(e)if not z then return nil,D end end return O(D)else return true end end function V.makepath(z)i(1,z)if e.is_windows then z=z:gsub("\047","\092")end return K(e.abspath(z))end end function V.clonetree(z,K,D,O)i(1,z)i(2,K)if O==true then O=print end local n,t,b,P=e.abspath,e.normcase,e.isdir,e.join local c,f={},{}if not b(z)then return B("\115\111\117\114\099\101 \105\115 \110\111\116 \097 \118\097\108\105\100 \100\105\114\101\099\116\111\114\121")end z=n(t(z))K=n(t(K))if O then O("\110\111\114\109\097\108\105\122\101\100\058",z,K)end if z==K then return B("\112\097\116\104\115 \097\114\101 \116\104\101 \115\097\109\101")end local a,U=K:find(z,1,true)if U==#z and K:sub(U+1,U+1)==e.sep then return B("\100\101\115\116\105\110\097\116\105\111\110 \105\115 \097 \115\117\098\100\105\114\101\099\116\111\114\121 \111\102 \116\104\101 \115\111\117\114\099\101")end local p=e.common_prefix(z,K)local X=#p if X==0 then if z:sub(2,2)=="\058"then X=3 end end for z,e,n in V.walk(z)do local t=K..z:sub(X)if O then O("\112\097\116\104\115\058",t,z)end if not b(t)then local z=V.makepath(t)if not z then T(c,t)end if O then O("\099\114\101\097\116\105\110\103\058",t,z)end end if D then for e,K in A(n)do local n=P(z,K)local b=P(t,K)local A=D(n,b)if not A then T(f,b)end if O then O("\102\105\108\101\115\058",n,b,A)end end end end return true,c,f end local function S(z)local e=z[#z]if not e then return end local K=e[1]local O=e[2](e[3])if not O then table.remove(z)return S(z)end if O~="\046"and O~="\046\046"then O=K..(Y..O)if X(O)then local e=F(O)if e then table.insert(z,{O,D(O)})end return O,e end end return S(z)end function V.dirtree(z)assert(z and z~="","\100\105\114\101\099\116\111\114\121 \112\097\114\097\109\101\116\101\114 \105\115 \109\105\115\115\105\110\103 \111\114 \101\109\112\116\121")local e=t(z,-1)if e==Y or e=="\047"then z=t(z,1,-2)end local K={{z,D(z)}}return S,K end function V.getallfiles(z,K)z=z or"\046"l(1,z)K=K or"\042"local D={}local O=e.normcase for z,e in V.dirtree(z)do if not e then local e=y(K)if(O(z)):find(e)then D[#D+1]=z end end end return k(D)end return V
--- Listing files in directories and creating/removing directory paths.
--
-- Dependencies: `pl.utils`, `pl.path`
--
-- Soft Dependencies: `alien`, `ffi` (either are used on Windows for copying/moving files)
-- @module pl.dir

local utils = require 'pl.utils'
local path = require 'pl.path'
local is_windows = path.is_windows
local ldir = path.dir
local mkdir = path.mkdir
local rmdir = path.rmdir
local sub = string.sub
local os,pcall,ipairs,pairs,require,setmetatable = os,pcall,ipairs,pairs,require,setmetatable
local remove = os.remove
local append = table.insert
local assert_arg,assert_string,raise = utils.assert_arg,utils.assert_string,utils.raise

local exists, isdir = path.exists, path.isdir
local sep = path.sep

local dir = {}

local function makelist(l)
    return setmetatable(l, require('pl.List'))
end

local function assert_dir (n,val)
    assert_arg(n,val,'string',path.isdir,'not a directory',4)
end

local function filemask(mask)
    mask = utils.escape(path.normcase(mask))
    return '^'..mask:gsub('%%%*','.*'):gsub('%%%?','.')..'$'
end

--- Test whether a file name matches a shell pattern.
-- Both parameters are case-normalized if operating system is
-- case-insensitive.
-- @string filename A file name.
-- @string pattern A shell pattern. The only special characters are
-- `'*'` and `'?'`: `'*'` matches any sequence of characters and
-- `'?'` matches any single character.
-- @treturn bool
-- @raise dir and mask must be strings
function dir.fnmatch(filename,pattern)
    assert_string(1,filename)
    assert_string(2,pattern)
    return path.normcase(filename):find(filemask(pattern)) ~= nil
end

--- Return a list of all file names within an array which match a pattern.
-- @tab filenames An array containing file names.
-- @string pattern A shell pattern (see `fnmatch`).
-- @treturn List(string) List of matching file names.
-- @raise dir and mask must be strings
function dir.filter(filenames,pattern)
    assert_arg(1,filenames,'table')
    assert_string(2,pattern)
    local res = {}
    local mask = filemask(pattern)
    for i,f in ipairs(filenames) do
        if path.normcase(f):find(mask) then append(res,f) end
    end
    return makelist(res)
end

local function _listfiles(dirname,filemode,match)
    local res = {}
    local check = utils.choose(filemode,path.isfile,path.isdir)
    if not dirname then dirname = '.' end
    for f in ldir(dirname) do
        if f ~= '.' and f ~= '..' then
            local p = path.join(dirname,f)
            if check(p) and (not match or match(f)) then
                append(res,p)
            end
        end
    end
    return makelist(res)
end

--- return a list of all files in a directory which match a shell pattern.
-- @string[opt='.'] dirname A directory.
-- @string[opt] mask A shell pattern (see `fnmatch`). If not given, all files are returned.
-- @treturn {string} list of files
-- @raise dirname and mask must be strings
function dir.getfiles(dirname,mask)
    dirname = dirname or '.'
    assert_dir(1,dirname)
    if mask then assert_string(2,mask) end
    local match
    if mask then
        mask = filemask(mask)
        match = function(f)
            return path.normcase(f):find(mask)
        end
    end
    return _listfiles(dirname,true,match)
end

--- return a list of all subdirectories of the directory.
-- @string[opt='.'] dirname A directory.
-- @treturn {string} a list of directories
-- @raise dir must be a valid directory
function dir.getdirectories(dirname)
    dirname = dirname or '.'
    assert_dir(1,dirname)
    return _listfiles(dirname,false)
end

local alien,ffi,ffi_checked,CopyFile,MoveFile,GetLastError,win32_errors,cmd_tmpfile

local function execute_command(cmd,parms)
   if not cmd_tmpfile then cmd_tmpfile = path.tmpname () end
   local err = path.is_windows and ' > ' or ' 2> '
    cmd = cmd..' '..parms..err..utils.quote_arg(cmd_tmpfile)
    local ret = utils.execute(cmd)
    if not ret then
        local err = (utils.readfile(cmd_tmpfile):gsub('\n(.*)',''))
        remove(cmd_tmpfile)
        return false,err
    else
        remove(cmd_tmpfile)
        return true
    end
end

local function find_ffi_copyfile ()
    if not ffi_checked then
        ffi_checked = true
        local res
        res,alien = pcall(require,'alien')
        if not res then
            alien = nil
            res, ffi = pcall(require,'ffi')
        end
        if not res then
            ffi = nil
            return
        end
    else
        return
    end
    if alien then
        -- register the Win32 CopyFile and MoveFile functions
        local kernel = alien.load('kernel32.dll')
        CopyFile = kernel.CopyFileA
        CopyFile:types{'string','string','int',ret='int',abi='stdcall'}
        MoveFile = kernel.MoveFileA
        MoveFile:types{'string','string',ret='int',abi='stdcall'}
        GetLastError = kernel.GetLastError
        GetLastError:types{ret ='int', abi='stdcall'}
    elseif ffi then
        ffi.cdef [[
            int CopyFileA(const char *src, const char *dest, int iovr);
            int MoveFileA(const char *src, const char *dest);
            int GetLastError();
        ]]
        CopyFile = ffi.C.CopyFileA
        MoveFile = ffi.C.MoveFileA
        GetLastError = ffi.C.GetLastError
    end
    win32_errors = {
        ERROR_FILE_NOT_FOUND    =         2,
        ERROR_PATH_NOT_FOUND    =         3,
        ERROR_ACCESS_DENIED    =          5,
        ERROR_WRITE_PROTECT    =          19,
        ERROR_BAD_UNIT         =          20,
        ERROR_NOT_READY        =          21,
        ERROR_WRITE_FAULT      =          29,
        ERROR_READ_FAULT       =          30,
        ERROR_SHARING_VIOLATION =         32,
        ERROR_LOCK_VIOLATION    =         33,
        ERROR_HANDLE_DISK_FULL  =         39,
        ERROR_BAD_NETPATH       =         53,
        ERROR_NETWORK_BUSY      =         54,
        ERROR_DEV_NOT_EXIST     =         55,
        ERROR_FILE_EXISTS       =         80,
        ERROR_OPEN_FAILED       =         110,
        ERROR_INVALID_NAME      =         123,
        ERROR_BAD_PATHNAME      =         161,
        ERROR_ALREADY_EXISTS    =         183,
    }
end

local function two_arguments (f1,f2)
    return utils.quote_arg(f1)..' '..utils.quote_arg(f2)
end

local function file_op (is_copy,src,dest,flag)
    if flag == 1 and path.exists(dest) then
        return false,"cannot overwrite destination"
    end
    if is_windows then
        -- if we haven't tried to load Alien/LuaJIT FFI before, then do so
        find_ffi_copyfile()
        -- fallback if there's no Alien, just use DOS commands *shudder*
        -- 'rename' involves a copy and then deleting the source.
        if not CopyFile then
            if path.is_windows then
                src = src:gsub("/","\\")
                dest = dest:gsub("/","\\")
            end
            local res, err = execute_command('copy',two_arguments(src,dest))
            if not res then return false,err end
            if not is_copy then
                return execute_command('del',utils.quote_arg(src))
            end
            return true
        else
            if path.isdir(dest) then
                dest = path.join(dest,path.basename(src))
            end
            local ret
            if is_copy then ret = CopyFile(src,dest,flag)
            else ret = MoveFile(src,dest) end
            if ret == 0 then
                local err = GetLastError()
                for name,value in pairs(win32_errors) do
                    if value == err then return false,name end
                end
                return false,"Error #"..err
            else return true
            end
        end
    else -- for Unix, just use cp for now
        return execute_command(is_copy and 'cp' or 'mv',
            two_arguments(src,dest))
    end
end

--- copy a file.
-- @string src source file
-- @string dest destination file or directory
-- @bool flag true if you want to force the copy (default)
-- @treturn bool operation succeeded
-- @raise src and dest must be strings
function dir.copyfile (src,dest,flag)
    assert_string(1,src)
    assert_string(2,dest)
    flag = flag==nil or flag
    return file_op(true,src,dest,flag and 0 or 1)
end

--- move a file.
-- @string src source file
-- @string dest destination file or directory
-- @treturn bool operation succeeded
-- @raise src and dest must be strings
function dir.movefile (src,dest)
    assert_string(1,src)
    assert_string(2,dest)
    return file_op(false,src,dest,0)
end

local function _dirfiles(dirname,attrib)
    local dirs = {}
    local files = {}
    for f in ldir(dirname) do
        if f ~= '.' and f ~= '..' then
            local p = path.join(dirname,f)
            local mode = attrib(p,'mode')
            if mode=='directory' then
                append(dirs,f)
            else
                append(files,f)
            end
        end
    end
    return makelist(dirs), makelist(files)
end


--- return an iterator which walks through a directory tree starting at root.
-- The iterator returns (root,dirs,files)
-- Note that dirs and files are lists of names (i.e. you must say path.join(root,d)
-- to get the actual full path)
-- If bottom_up is false (or not present), then the entries at the current level are returned
-- before we go deeper. This means that you can modify the returned list of directories before
-- continuing.
-- This is a clone of os.walk from the Python libraries.
-- @string root A starting directory
-- @bool bottom_up False if we start listing entries immediately.
-- @bool follow_links follow symbolic links
-- @return an iterator returning root,dirs,files
-- @raise root must be a directory
function dir.walk(root,bottom_up,follow_links)
    assert_dir(1,root)
    local attrib
    if path.is_windows or not follow_links then
        attrib = path.attrib
    else
        attrib = path.link_attrib
    end

    local to_scan = { root }
    local to_return = {}
    local iter = function()
        while #to_scan > 0 do
            local current_root = table.remove(to_scan)
            local dirs,files = _dirfiles(current_root, attrib)
            for _, d in ipairs(dirs) do
                table.insert(to_scan, current_root..path.sep..d)
            end
            if not bottom_up then
                return current_root, dirs, files
            else
                table.insert(to_return, { current_root, dirs, files })
            end
        end
        if #to_return > 0 then
            return utils.unpack(table.remove(to_return))
        end
    end

    return iter
end

--- remove a whole directory tree.
-- Symlinks in the tree will be deleted without following them.
-- @string fullpath A directory path (must be an actual directory, not a symlink)
-- @return true or nil
-- @return error if failed
-- @raise fullpath must be a string
function dir.rmtree(fullpath)
    assert_dir(1,fullpath)
    if path.islink(fullpath) then return false,'will not follow symlink' end
    for root,dirs,files in dir.walk(fullpath,true) do
        if path.islink(root) then
            -- sub dir is a link, remove link, do not follow
            if is_windows then
                -- Windows requires using "rmdir". Deleting the link like a file
                -- will instead delete all files from the target directory!!
                local res, err = rmdir(root)
                if not res then return nil,err .. ": " .. root end
            else
                local res, err = remove(root)
                if not res then return nil,err .. ": " .. root end
            end
        else
            for i,f in ipairs(files) do
                local res, err = remove(path.join(root,f))
                if not res then return nil,err .. ": " .. path.join(root,f) end
            end
            local res, err = rmdir(root)
            if not res then return nil,err .. ": " .. root end
        end
    end
    return true
end


do
  local dirpat
  if path.is_windows then
      dirpat = '(.+)\\[^\\]+$'
  else
      dirpat = '(.+)/[^/]+$'
  end

  local _makepath
  function _makepath(p)
      -- windows root drive case
      if p:find '^%a:[\\]*$' then
          return true
      end
      if not path.isdir(p) then
          local subp = p:match(dirpat)
          if subp then
            local ok, err = _makepath(subp)
            if not ok then return nil, err end
          end
          return mkdir(p)
      else
          return true
      end
  end

  --- create a directory path.
  -- This will create subdirectories as necessary!
  -- @string p A directory path
  -- @return true on success, nil + errormsg on failure
  -- @raise failure to create
  function dir.makepath (p)
      assert_string(1,p)
      if path.is_windows then
          p = p:gsub("/", "\\")
      end
      return _makepath(path.abspath(p))
  end
end

--- clone a directory tree. Will always try to create a new directory structure
-- if necessary.
-- @string path1 the base path of the source tree
-- @string path2 the new base path for the destination
-- @func file_fun an optional function to apply on all files
-- @bool verbose an optional boolean to control the verbosity of the output.
--  It can also be a logging function that behaves like print()
-- @return true, or nil
-- @return error message, or list of failed directory creations
-- @return list of failed file operations
-- @raise path1 and path2 must be strings
-- @usage clonetree('.','../backup',copyfile)
function dir.clonetree (path1,path2,file_fun,verbose)
    assert_string(1,path1)
    assert_string(2,path2)
    if verbose == true then verbose = print end
    local abspath,normcase,isdir,join = path.abspath,path.normcase,path.isdir,path.join
    local faildirs,failfiles = {},{}
    if not isdir(path1) then return raise 'source is not a valid directory' end
    path1 = abspath(normcase(path1))
    path2 = abspath(normcase(path2))
    if verbose then verbose('normalized:',path1,path2) end
    -- particularly NB that the new path isn't fully contained in the old path
    if path1 == path2 then return raise "paths are the same" end
    local _,i2 = path2:find(path1,1,true)
    if i2 == #path1 and path2:sub(i2+1,i2+1) == path.sep then
        return raise 'destination is a subdirectory of the source'
    end
    local cp = path.common_prefix (path1,path2)
    local idx = #cp
    if idx == 0 then -- no common path, but watch out for Windows paths!
        if path1:sub(2,2) == ':' then idx = 3 end
    end
    for root,dirs,files in dir.walk(path1) do
        local opath = path2..root:sub(idx)
        if verbose then verbose('paths:',opath,root) end
        if not isdir(opath) then
            local ret = dir.makepath(opath)
            if not ret then append(faildirs,opath) end
            if verbose then verbose('creating:',opath,ret) end
        end
        if file_fun then
            for i,f in ipairs(files) do
                local p1 = join(root,f)
                local p2 = join(opath,f)
                local ret = file_fun(p1,p2)
                if not ret then append(failfiles,p2) end
                if verbose then
                    verbose('files:',p1,p2,ret)
                end
            end
        end
    end
    return true,faildirs,failfiles
end


-- each entry of the stack is an array with three items:
-- 1. the name of the directory
-- 2. the lfs iterator function
-- 3. the lfs iterator userdata
local function treeiter(iterstack)
    local diriter = iterstack[#iterstack]
    if not diriter then
      return -- done
    end

    local dirname = diriter[1]
    local entry = diriter[2](diriter[3])
    if not entry then
      table.remove(iterstack)
      return treeiter(iterstack) -- tail-call to try next
    end

    if entry ~= "." and entry ~= ".." then
        entry = dirname .. sep .. entry
        if exists(entry) then  -- Just in case a symlink is broken.
            local is_dir = isdir(entry)
            if is_dir then
                table.insert(iterstack, { entry, ldir(entry) })
            end
            return entry, is_dir
        end
    end

    return treeiter(iterstack) -- tail-call to try next
end


--- return an iterator over all entries in a directory tree
-- @string d a directory
-- @return an iterator giving pathname and mode (true for dir, false otherwise)
-- @raise d must be a non-empty string
function dir.dirtree( d )
    assert( d and d ~= "", "directory parameter is missing or empty" )

    local last = sub ( d, -1 )
    if last == sep or last == '/' then
        d = sub( d, 1, -2 )
    end

    local iterstack = { {d, ldir(d)} }

    return treeiter, iterstack
end


--- Recursively returns all the file starting at 'path'. It can optionally take a shell pattern and
-- only returns files that match 'shell_pattern'. If a pattern is given it will do a case insensitive search.
-- @string[opt='.'] start_path  A directory.
-- @string[opt='*'] shell_pattern A shell pattern (see `fnmatch`).
-- @treturn List(string) containing all the files found recursively starting at 'path' and filtered by 'shell_pattern'.
-- @raise start_path must be a directory
function dir.getallfiles( start_path, shell_pattern )
    start_path = start_path or '.'
    assert_dir(1,start_path)
    shell_pattern = shell_pattern or "*"

    local files = {}
    local normcase = path.normcase
    for filename, mode in dir.dirtree( start_path ) do
        if not mode then
            local mask = filemask( shell_pattern )
            if normcase(filename):find( mask ) then
                files[#files + 1] = filename
            end
        end
    end

    return makelist(files)
end

return dir
