aboutsummaryrefslogtreecommitdiffhomepage
path: root/core/lfs_ext.lua
blob: 64da4d890e950325e828403d0290d1b5c28a4bfc (plain)
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
-- 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