--- Emulates Lua's standard [io library][io]. -- -- [io]: https://www.lua.org/manual/5.1/manual.html#5.7 -- -- @module io local expect, type_of = dofile("rom/modules/main/cc/expect.lua").expect, _G.type --- If we return nil then close the file, as we've reached the end. -- We use this weird wrapper function as we wish to preserve the varargs local function checkResult(handle, ...) if ... == nil and handle._autoclose and not handle._closed then handle:close() end return ... end --- A file handle which can be read or written to. -- -- @type Handle local handleMetatable handleMetatable = { __name = "FILE*", __tostring = function(self) if self._closed then return "file (closed)" else local hash = tostring(self._handle):match("table: (%x+)") return "file (" .. hash .. ")" end end, __index = { --- Close this file handle, freeing any resources it uses. -- -- @treturn[1] true If this handle was successfully closed. -- @treturn[2] nil If this file handle could not be closed. -- @treturn[2] string The reason it could not be closed. -- @throws If this handle was already closed. close = function(self) if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) end if self._closed then error("attempt to use a closed file", 2) end local handle = self._handle if handle.close then self._closed = true handle.close() return true else return nil, "attempt to close standard stream" end end, --- Flush any buffered output, forcing it to be written to the file -- -- @throws If the handle has been closed flush = function(self) if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) end if self._closed then error("attempt to use a closed file", 2) end local handle = self._handle if handle.flush then handle.flush() end return true end, lines = function(self, ...) if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) end if self._closed then error("attempt to use a closed file", 2) end local handle = self._handle if not handle.read then return nil, "file is not readable" end local args = table.pack(...) return function() if self._closed then error("file is already closed", 2) end return checkResult(self, self:read(table.unpack(args, 1, args.n))) end end, read = function(self, ...) if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) end if self._closed then error("attempt to use a closed file", 2) end local handle = self._handle if not handle.read and not handle.readLine then return nil, "Not opened for reading" end local n = select("#", ...) local output = {} for i = 1, n do local arg = select(i, ...) local res if type_of(arg) == "number" then if handle.read then res = handle.read(arg) end elseif type_of(arg) == "string" then local format = arg:gsub("^%*", ""):sub(1, 1) if format == "l" then if handle.readLine then res = handle.readLine() end elseif format == "L" and handle.readLine then if handle.readLine then res = handle.readLine(true) end elseif format == "a" then if handle.readAll then res = handle.readAll() or "" end elseif format == "n" then res = nil -- Skip this format as we can't really handle it else error("bad argument #" .. i .. " (invalid format)", 2) end else error("bad argument #" .. i .. " (expected string, got " .. type_of(arg) .. ")", 2) end output[i] = res if not res then break end end -- Default to "l" if possible if n == 0 and handle.readLine then return handle.readLine() end return table.unpack(output, 1, n) end, seek = function(self, whence, offset) if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) end if self._closed then error("attempt to use a closed file", 2) end local handle = self._handle if not handle.seek then return nil, "file is not seekable" end -- It's a tail call, so error positions are preserved return handle.seek(whence, offset) end, --[[- Sets the buffering mode for an output file. This has no effect under ComputerCraft, and exists with compatility with base Lua. @tparam string mode The buffering mode. @tparam[opt] number size The size of the buffer. @see file:setvbuf Lua's documentation for `setvbuf`. @deprecated This has no effect in CC. ]] setvbuf = function(self, mode, size) end, --- Write one or more values to the file -- -- @tparam string|number ... The values to write. -- @treturn[1] Handle The current file, allowing chained calls. -- @treturn[2] nil If the file could not be written to. -- @treturn[2] string The error message which occurred while writing. write = function(self, ...) if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) end if self._closed then error("attempt to use a closed file", 2) end local handle = self._handle if not handle.write then return nil, "file is not writable" end for i = 1, select("#", ...) do local arg = select(i, ...) expect(i, arg, "string", "number") handle.write(arg) end return self end, }, } local function make_file(handle) return setmetatable({ _handle = handle }, handleMetatable) end local defaultInput = make_file({ readLine = _G.read }) local defaultOutput = make_file({ write = _G.write }) local defaultError = make_file({ write = function(...) local oldColour if term.isColour() then oldColour = term.getTextColour() term.setTextColour(colors.red) end _G.write(...) if term.isColour() then term.setTextColour(oldColour) end end, }) local currentInput = defaultInput local currentOutput = defaultOutput --- A file handle representing the "standard input". Reading from this -- file will prompt the user for input. stdin = defaultInput --- A file handle representing the "standard output". Writing to this -- file will display the written text to the screen. stdout = defaultOutput --- A file handle representing the "standard error" stream. -- -- One may use this to display error messages, writing to it will display -- them on the terminal. stderr = defaultError --- Closes the provided file handle. -- -- @tparam[opt] Handle file The file handle to close, defaults to the -- current output file. -- -- @see Handle:close -- @see io.output function close(file) if file == nil then return currentOutput:close() end if type_of(file) ~= "table" or getmetatable(file) ~= handleMetatable then error("bad argument #1 (FILE expected, got " .. type_of(file) .. ")", 2) end return file:close() end --- Flushes the current output file. -- -- @see Handle:flush -- @see io.output function flush() return currentOutput:flush() end --- Get or set the current input file. -- -- @tparam[opt] Handle|string file The new input file, either as a file path or pre-existing handle. -- @treturn Handle The current input file. -- @throws If the provided filename cannot be opened for reading. function input(file) if type_of(file) == "string" then local res, err = open(file, "rb") if not res then error(err, 2) end currentInput = res elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then currentInput = file elseif file ~= nil then error("bad fileument #1 (FILE expected, got " .. type_of(file) .. ")", 2) end return currentInput end --- Opens the given file name in read mode and returns an iterator that, -- each time it is called, returns a new line from the file. -- -- This can be used in a for loop to iterate over all lines of a file: -- -- ```lua -- for line in io.lines(filename) do print(line) end -- ``` -- -- Once the end of the file has been reached, @{nil} will be -- returned. The file is automatically closed. -- -- If no file name is given, the @{io.input|current input} will be used -- instead. In this case, the handle is not used. -- -- @tparam[opt] string filename The name of the file to extract lines from -- @param ... The argument to pass to @{Handle:read} for each line. -- @treturn function():string|nil The line iterator. -- @throws If the file cannot be opened for reading -- -- @see Handle:lines -- @see io.input function lines(filename, ...) expect(1, filename, "string", "nil") if filename then local ok, err = open(filename, "rb") if not ok then error(err, 2) end -- We set this magic flag to mark this file as being opened by io.lines and so should be -- closed automatically ok._autoclose = true return ok:lines(...) else return currentInput:lines(...) end end --- Open a file with the given mode, either returning a new file handle -- or @{nil}, plus an error message. -- -- The `mode` string can be any of the following: -- - **"r"**: Read mode -- - **"w"**: Write mode -- - **"a"**: Append mode -- -- The mode may also have a `b` at the end, which opens the file in "binary -- mode". This allows you to read binary files, as well as seek within a file. -- -- @tparam string filename The name of the file to open. -- @tparam[opt] string mode The mode to open the file with. This defaults to `rb`. -- @treturn[1] Handle The opened file. -- @treturn[2] nil In case of an error. -- @treturn[2] string The reason the file could not be opened. function open(filename, mode) expect(1, filename, "string") expect(2, mode, "string", "nil") local sMode = mode and mode:gsub("%+", "") or "rb" local file, err = fs.open(filename, sMode) if not file then return nil, err end return make_file(file) end --- Get or set the current output file. -- -- @tparam[opt] Handle|string file The new output file, either as a file path or pre-existing handle. -- @treturn Handle The current output file. -- @throws If the provided filename cannot be opened for writing. function output(file) if type_of(file) == "string" then local res, err = open(file, "wb") if not res then error(err, 2) end currentOutput = res elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then currentOutput = file elseif file ~= nil then error("bad argument #1 (FILE expected, got " .. type_of(file) .. ")", 2) end return currentOutput end --- Read from the currently opened input file. -- -- This is equivalent to `io.input():read(...)`. See @{Handle:read|the -- documentation} there for full details. -- -- @tparam string ... The formats to read, defaulting to a whole line. -- @treturn (string|nil)... The data read, or @{nil} if nothing can be read. function read(...) return currentInput:read(...) end --- Checks whether `handle` is a given file handle, and determine if it is open -- or not. -- -- @param obj The value to check -- @treturn string|nil `"file"` if this is an open file, `"closed file"` if it -- is a closed file handle, or `nil` if not a file handle. function type(obj) if type_of(obj) == "table" and getmetatable(obj) == handleMetatable then if obj._closed then return "closed file" else return "file" end end return nil end --- Write to the currently opened output file. -- -- This is equivalent to `io.output():write(...)`. See @{Handle:write|the -- documentation} there for full details. -- -- @tparam string ... The strings to write function write(...) return currentOutput:write(...) end