-- Copyright 2007-2022 Mitchell. See LICENSE. --[[ This comment is for LuaDoc. --- -- Extends the `lfs` library to find files in directories and determine absolute file paths. module('lfs')]] -- LuaFormatter off --- -- The filter table containing common binary file extensions and version control directories -- to exclude when iterating over files and directories using `walk`. -- Extensions excluded: a, bmp, bz2, class, dll, exe, gif, gz, jar, jpeg, jpg, o, pdf, png, -- so, tar, tgz, tif, tiff, xz, and zip. -- Directories excluded: .bzr, .git, .hg, .svn, _FOSSIL_, and node_modules. -- @see walk -- @class table -- @name default_filter lfs.default_filter = {--[[Extensions]]'!.a','!.bmp','!.bz2','!.class','!.dll','!.exe','!.gif','!.gz','!.jar','!.jpeg','!.jpg','!.o','!.pdf','!.png','!.so','!.tar','!.tgz','!.tif','!.tiff','!.xz','!.zip',--[[Directories]]'!/%.bzr$','!/%.git$','!/%.hg$','!/%.svn$','!/_FOSSIL_$','!/node_modules$'} -- LuaFormatter on -- Documentation is in `lfs.walk()`. -- @param seen Utility table that holds directories seen. If there is a duplicate, stop walking -- down that path (it's probably a recursive symlink). -- @param level Utility value indicating the directory level this function is at. local function walk(dir, filter, n, include_dirs, seen, level) if not seen then seen = {} end local sep = not WIN32 and '/' or '\\' seen[not WIN32 and dir or dir:gsub('/', sep)] = true for basename in lfs.dir(dir) do if basename:find('^%.%.?$') then goto continue end -- ignore . and .. local filename = dir .. (dir ~= '/' and '/' or '') .. basename local mode = lfs.attributes(filename, 'mode') if mode ~= 'directory' and mode ~= 'file' then goto continue end local include if mode == 'file' then local ext = filename:match('[^.]+$') if ext and not filter.exts[ext] then goto continue end include = filter.consider_any or ext ~= nil elseif mode == 'directory' then include = filter.consider_any end for _, patt in ipairs(filter) do -- Treat exclusive patterns as logical AND. if patt:find('^!') and filename:find(patt:sub(2)) then goto continue end -- Treat inclusive patterns as logical OR. include = include or (not patt:find('^!') and filename:find(patt)) end if not include then goto continue end local os_filename = not WIN32 and filename or filename:gsub('/', sep) if mode == 'file' then coroutine.yield(os_filename) elseif mode == 'directory' then local link = lfs.symlinkattributes(filename, 'target') if link and seen[lfs.abspath(link .. sep, dir):gsub('[/\\]+$', '')] then goto continue end if include_dirs then coroutine.yield(os_filename .. sep) end if n and (level or 0) >= n then goto continue end walk(filename, filter, n, include_dirs, seen, (level or 0) + 1) end ::continue:: end end --- -- Returns an iterator that iterates over all files and sub-directories (up to *n* levels deep) -- in directory *dir* and yields each file found. -- String or list *filter* determines which files to yield, with the default filter being -- `lfs.default_filter`. A filter consists of Lua patterns that match file and directory paths -- to include or exclude. Exclusive patterns begin with a '!'. If no inclusive patterns are -- given, any path is initially considered. As a convenience, file extensions can be specified -- literally instead of as a Lua pattern (e.g. '.lua' vs. '%.lua$'), and '/' also matches the -- Windows directory separator ('[/\\]' is not needed). -- @param dir The directory path to iterate over. -- @param filter Optional filter for files and directories to include and exclude. The default -- value is `lfs.default_filter`. -- @param n Optional maximum number of directory levels to descend into. The default value is -- `nil`, which indicates no limit. -- @param include_dirs Optional flag indicating whether or not to yield directory names too. -- Directory names are passed with a trailing '/' or '\', depending on the current platform. -- The default value is `false`. -- @see filter -- @name walk function lfs.walk(dir, filter, n, include_dirs) dir = assert_type(dir, 'string', 1):match('^(..-)[/\\]?$') if not assert_type(filter, 'string/table/nil', 2) then filter = lfs.default_filter end assert_type(n, 'number/nil', 3) -- Process the given filter into something that can match files more easily and/or quickly. For -- example, convert '.ext' shorthand to '%.ext$', substitute '/' with '[/\\]', and enable -- hash lookup for file extensions to include or exclude. local processed_filter = { consider_any = true, exts = setmetatable({}, {__index = function() return true end}) } for _, patt in ipairs(type(filter) == 'table' and filter or {filter}) do patt = patt:gsub('^(!?)%%?%.([^.]+)$', '%1%%.%2$') -- '.lua' to '%.lua$' patt = patt:gsub('/([^\\])', '[/\\]%1') -- '/' to '[/\\]' local include = not patt:find('^!') local ext = patt:match('^!?%%.([^.]+)%$$') if ext then processed_filter.exts[ext] = include if include then setmetatable(processed_filter.exts, nil) end else if include then processed_filter.consider_any = false end processed_filter[#processed_filter + 1] = patt end end local co = coroutine.create(function() walk(dir, processed_filter, n, include_dirs) end) return function() return select(2, coroutine.resume(co)) end end --- -- Returns the absolute path to string *filename*. -- *prefix* or `lfs.currentdir()` is prepended to a relative filename. The returned path is -- not guaranteed to exist. -- @param filename The relative or absolute path to a file. -- @param prefix Optional prefix path prepended to a relative filename. -- @return string absolute path -- @name abspath function lfs.abspath(filename, prefix) assert_type(filename, 'string', 1) assert_type(prefix, 'string/nil', 2) if WIN32 then filename = filename:gsub('/', '\\'):gsub('^%l:[/\\]', string.upper) end if not filename:find(not WIN32 and '^/' or '^%a:[/\\]') and not (WIN32 and filename:find('^\\\\')) then if not prefix then prefix = lfs.currentdir() end filename = prefix .. (not WIN32 and '/' or '\\') .. filename end filename = filename:gsub('%f[^/\\]%.[/\\]', '') -- clean up './' while filename:find('[^/\\]+[/\\]%.%.[/\\]') do filename = filename:gsub('[^/\\]+[/\\]%.%.[/\\]', '', 1) -- clean up '../' end return filename end