Jianw
2025-05-13 3b39fe3810c3ee2ec9ec97236c1769c5c85e062c
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
local fs = {}
 
local lfs = require "lfs"
local utils = require "luacheck.utils"
 
local function ensure_dir_sep(path)
   if path:sub(-1) ~= utils.dir_sep then
      return path .. utils.dir_sep
   end
 
   return path
end
 
function fs.split_base(path)
   if utils.is_windows then
      if path:match("^%a:\\") then
         return path:sub(1, 3), path:sub(4)
      else
         -- Disregard UNC paths and relative paths with drive letter.
         return "", path
      end
   else
      if path:match("^/") then
         if path:match("^//") then
            return "//", path:sub(3)
         else
            return "/", path:sub(2)
         end
      else
         return "", path
      end
   end
end
 
function fs.is_absolute(path)
   return fs.split_base(path) ~= ""
end
 
function fs.normalize(path)
   if utils.is_windows then
      path = path:lower()
   end
   local base, rest = fs.split_base(path)
   rest = rest:gsub("[/\\]", utils.dir_sep)
 
   local parts = {}
 
   for part in rest:gmatch("[^"..utils.dir_sep.."]+") do
      if part ~= "." then
         if part == ".." and #parts > 0 and parts[#parts] ~= ".." then
            parts[#parts] = nil
         else
            parts[#parts + 1] = part
         end
      end
   end
 
   if base == "" and #parts == 0 then
      return "."
   else
      return base..table.concat(parts, utils.dir_sep)
   end
end
 
local function join_two_paths(base, path)
   if base == "" or fs.is_absolute(path) then
      return path
   else
      return ensure_dir_sep(base) .. path
   end
end
 
function fs.join(base, ...)
   local res = base
 
   for i = 1, select("#", ...) do
      res = join_two_paths(res, select(i, ...))
   end
 
   return res
end
 
function fs.is_subpath(path, subpath)
   local base1, rest1 = fs.split_base(path)
   local base2, rest2 = fs.split_base(subpath)
 
   if base1 ~= base2 then
      return false
   end
 
   if rest2:sub(1, #rest1) ~= rest1 then
      return false
   end
 
   return rest1 == rest2 or rest2:sub(#rest1 + 1, #rest1 + 1) == utils.dir_sep
end
 
function fs.is_dir(path)
   return lfs.attributes(path, "mode") == "directory"
end
 
function fs.is_file(path)
   return lfs.attributes(path, "mode") == "file"
end
 
-- Searches for file starting from path, going up until the file
-- is found or root directory is reached.
-- Path must be absolute.
-- Returns absolute and relative paths to directory containing file or nil.
function fs.find_file(path, file)
   if fs.is_absolute(file) then
      return fs.is_file(file) and path, ""
   end
 
   path = fs.normalize(path)
   local base, rest = fs.split_base(path)
   local rel_path = ""
 
   while true do
      if fs.is_file(fs.join(base..rest, file)) then
         return base..rest, rel_path
      elseif rest == "" then
         return
      end
 
      rest = rest:match("^(.*)"..utils.dir_sep..".*$") or ""
      rel_path = rel_path..".."..utils.dir_sep
   end
end
 
-- Returns iterator over directory items or nil, error message.
function fs.dir_iter(dir_path)
   local ok, iter, state, var = pcall(lfs.dir, dir_path)
 
   if not ok then
      local err = utils.unprefix(iter, "cannot open " .. dir_path .. ": ")
      return nil, "couldn't list directory: " .. err
   end
 
   return iter, state, var
end
 
-- Returns list of all files in directory matching pattern.
-- Additionally returns a mapping from directory paths that couldn't be expanded
-- to error messages.
function fs.extract_files(dir_path, pattern)
   local res = {}
   local err_map = {}
 
   local function scan(dir)
      local iter, state, var = fs.dir_iter(dir)
 
      if not iter then
         err_map[dir] = state
         table.insert(res, dir)
         return
      end
 
      for path in iter, state, var do
         if path ~= "." and path ~= ".." then
            local full_path = fs.join(dir, path)
 
            if fs.is_dir(full_path) then
               scan(full_path)
            elseif path:match(pattern) and fs.is_file(full_path) then
               table.insert(res, full_path)
            end
         end
      end
   end
 
   scan(dir_path)
   table.sort(res)
   return res, err_map
end
 
-- Returns modification time for a file.
function fs.get_mtime(path)
   return lfs.attributes(path, "modification")
end
 
-- Returns absolute path to current working directory, with trailing directory separator.
function fs.get_current_dir()
   return ensure_dir_sep(assert(lfs.currentdir()))
end
 
return fs