local table = require("@neyuki/table")
local string = require("@neyuki/string")
local fs = {}
function checkString(s: string, name: string)
if (s == nil) then error(name .. " must be provided") end
if (type(s) != "string") then error(name .. " must be a string") end
end
function checkOptionalBoolean(b: boolean?, name: string)
if (b != nil and type(b) != "boolean") then error(name .. " must be a boolean") end
end
function splitLines(content: string): {string}
local lines = string.split(content, "\n")
local count = #lines
if (count > 0 and lines[count] == "") then
table.remove(lines, count)
count--
end
for index = 1, count do
local line = lines[index]
if (string.sub(line, -1) == "\r") then
lines[index] = string.sub(line, 1, -2)
end
end
return lines
end
function fs.open(path: string, mode: string?): File
checkString(path, "path")
mode = mode ?? "r"
checkString(mode, "mode")
__fs_open(path, mode)
local closed = false
local written_once = false
local file = {}
file.path = path
file.mode = mode
function checkOpen()
if (closed) then error("file `" .. path .. "` is closed") end
end
function checkWritable()
checkOpen()
if (mode == "r") then error("file `" .. path .. "` was opened read-only") end
end
function file.read(self: File): string
checkOpen()
return __fs_read(path)
end
function file.write(self: File, text: string, append: boolean?): ()
checkWritable()
checkString(text, "text")
checkOptionalBoolean(append, "append")
local should_append = false
if (mode == "a" or (mode == "w" and written_once)) then
should_append = true
elseif (append != nil) then
should_append = append
end
__fs_write(path, text, should_append)
written_once = true
end
function file.lines(self: File): () -> string?
checkOpen()
local lines = splitLines(__fs_read(path))
local index = 0
return function(): string?
index++
return lines[index]
end
end
function file.close(self: File): ()
closed = true
end
return table.freeze(file)
end
function fs.exists(path: string): boolean
checkString(path, "path")
return __fs_exists(path)
end
-- Removes a file or an empty directory; with `recursive`, a directory and
-- everything in it.
function fs.remove(path: string, recursive: boolean?): ()
checkString(path, "path")
checkOptionalBoolean(recursive, "recursive")
__fs_remove(path, recursive ?? false)
end
function fs.rename(from: string, to: string): ()
checkString(from, "from")
checkString(to, "to")
__fs_rename(from, to)
end
function fs.mkdir(path: string, recursive: boolean?): ()
checkString(path, "path")
checkOptionalBoolean(recursive, "recursive")
__fs_mkdir(path, recursive ?? false)
end
function fs.list(path: string): {string}
checkString(path, "path")
return __fs_list(path)
end
function fs.isDir(path: string): boolean
checkString(path, "path")
return __fs_isdir(path)
end
-- WHOLE FILES --------------------------------------------------------------
-- The contents of a text file.
function fs.read(path: string): string
checkString(path, "path")
return __fs_read(path)
end
-- Replaces the contents of a file, creating it if needed.
function fs.write(path: string, text: string): ()
checkString(path, "path")
checkString(text, "text")
__fs_write(path, text, false)
end
-- Adds text to the end of a file, creating it if needed.
function fs.append(path: string, text: string): ()
checkString(path, "path")
checkString(text, "text")
__fs_write(path, text, true)
end
-- An iterator over the lines of a file, without their line endings.
function fs.lines(path: string): () -> string?
checkString(path, "path")
local lines = splitLines(__fs_read(path))
local index = 0
return function(): string?
index++
return lines[index]
end
end
-- Copies a file, replacing `to`, and returns the number of bytes copied.
function fs.copy(from: string, to: string): number
checkString(from, "from")
checkString(to, "to")
return __fs_copy(from, to)
end
-- `{ size, isDir, isFile, readonly, modified, created }` for a path, or nil
-- when nothing is there. Times are seconds since the Unix epoch.
function fs.info(path: string): FileInfo?
checkString(path, "path")
return __fs_info(path)
end
function fs.isFile(path: string): boolean
checkString(path, "path")
local info = __fs_info(path)
return info != nil and info.isFile
end
-- Every file under a directory, as paths relative to it joined with "/",
-- in sorted order. Directories themselves are not listed.
function fs.walk(path: string): {string}
checkString(path, "path")
if (not __fs_isdir(path)) then error("`" .. path .. "` is not a directory") end
local found = {}
local function visit(dir: string, prefix: string)
for _, name in __fs_list(dir) do
local full = fs.join(dir, name)
local relative = prefix .. name
if (__fs_isdir(full)) then
visit(full, relative .. "/")
else
found[#found + 1] = relative
end
end
end
visit(path, "")
return found
end
-- PATHS --------------------------------------------------------------------
function isSeparator(char: string): boolean
return char == "/" or char == "\\"
end
-- The index of the last separator in `path`, or 0.
function lastSeparator(path: string): number
for index = #path, 1, -1 do
if (isSeparator(string.sub(path, index, index))) then return index end
end
return 0
end
-- Joins path parts with "/", which every platform accepts, without doubling
-- separators. A part that is absolute starts over.
function fs.join(...: string): string
local result = ""
for index, part in {...} do
if (type(part) != "string") then
error("part " .. tostring(index) .. " must be a string")
end
if (part == "") then continue end
local first = string.sub(part, 1, 1)
if (result == "" or isSeparator(first) or string.match(part, "^%a:") != nil) then
result = part
elseif (isSeparator(string.sub(result, -1))) then
result = result .. part
else
result = result .. "/" .. part
end
end
return result
end
-- The last part of a path: `basename("docs/guide.md")` is "guide.md".
function fs.basename(path: string): string
checkString(path, "path")
while (#path > 1 and isSeparator(string.sub(path, -1))) do path = string.sub(path, 1, -2) end
return string.sub(path, lastSeparator(path) + 1)
end
-- Everything before the last part: `dirname("docs/guide.md")` is "docs",
-- and a bare name gives ".".
function fs.dirname(path: string): string
checkString(path, "path")
while (#path > 1 and isSeparator(string.sub(path, -1))) do path = string.sub(path, 1, -2) end
local at = lastSeparator(path)
if (at == 0) then return "." end
if (at == 1) then return string.sub(path, 1, 1) end
return string.sub(path, 1, at - 1)
end
-- The extension of the last part, dot included: ".md" for "guide.md" and ""
-- for "Makefile" or ".gitignore".
function fs.extension(path: string): string
local name = fs.basename(path)
local dot = nil
for index = #name, 2, -1 do
if (string.sub(name, index, index) == ".") then
dot = index
break
end
end
if (dot == nil) then return "" end
return string.sub(name, dot)
end
-- `path` made absolute against the working directory.
function fs.absolute(path: string): string
checkString(path, "path")
return __fs_absolute(path)
end
-- The working directory.
function fs.cwd(): string
return __fs_cwd()
end
return table.freeze(fs)