-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe -- -- SPDX-License-Identifier: LicenseRef-CCPL --- 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, --[[- 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 Once the end of the file has been reached, [`nil`] will be returned. The file is *not* automatically closed. @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 @since 1.3 @see io.lines @usage Iterate over every line in a file and print it out. ```lua local file = io.open("/rom/help/intro.txt") for line in file:lines() do print(line) end file:close() ``` ]] 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, --[[- Reads data from the file, using the specified formats. For each format provided, the function returns either the data read, or `nil` if no data could be read. The following formats are available: - `l`: Returns the next line (without a newline on the end). - `L`: Returns the next line (with a newline on the end). - `a`: Returns the entire rest of the file. - ~~`n`: Returns a number~~ (not implemented in CC). These formats can be preceded by a `*` to make it compatible with Lua 5.1. If no format is provided, `l` is assumed. @param ... The formats to use. @treturn (string|nil)... The data read from the file. ]] 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 .. " (string expected, 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, --[[- Seeks the file cursor to the specified position, and returns the new position. `whence` controls where the seek operation starts, and is a string that may be one of these three values: - `set`: base position is 0 (beginning of the file) - `cur`: base is current position - `end`: base is end of file The default value of `whence` is `cur`, and the default value of `offset` is 0. This means that `file:seek()` without arguments returns the current position without moving. @tparam[opt] string whence The place to set the cursor from. @tparam[opt] number offset The offset from the start to move to. @treturn number The new location of the file cursor. ]] 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. -- @changed 1.81.0 Multiple arguments are now allowed. 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 -- @since 1.55 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 -- @since 1.55 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. -- @since 1.55 function input(file) if type_of(file) == "string" then local res, err = open(file, "r") 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 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 [current input][`io.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 @since 1.55 @usage Iterate over every line in a file and print it out. ```lua for line in io.lines("/rom/help/intro.txt") do print(line) end ``` ]] function lines(filename, ...) expect(1, filename, "string", "nil") if filename then local ok, err = open(filename, "r") 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 "r" 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. -- @since 1.55 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 [the documentation][`Handle:read`] -- 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 [the documentation][`Handle:write`] -- there for full details. -- -- @tparam string ... The strings to write -- @changed 1.81.0 Multiple arguments are now allowed. function write(...) return currentOutput:write(...) end