add preliminary half-tested half-working implementation of the `io` global
parent
9813dee130
commit
a1e6b75b22
@ -0,0 +1,307 @@
|
|||||||
|
local openNull, openPath
|
||||||
|
do
|
||||||
|
local filedata = setmetatable({}, { __mode = 'k' }) --weak-keyed mapping of file object to data/inner (fh of wrapFile)
|
||||||
|
local filemt = { __metatable = '<file>' }
|
||||||
|
filemt.__index = filemt
|
||||||
|
|
||||||
|
function isFile(obj)
|
||||||
|
if filedata[obj] then
|
||||||
|
-- if this is true, it's an object we control, so it's safe to trust attribute access
|
||||||
|
return true, not obj._closed
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function wrapFile(mode, fh)
|
||||||
|
local rwa, bin, plus, b2 = match(mode or 'r', '^([rwa])(b?)(%+?)(b?)$') --match all of x, xb, x+, xb+, x+b
|
||||||
|
if not rwa or (bin ~= '' and b2 ~= '') then return nil, 'bad mode' end
|
||||||
|
bin = bin .. b2 --when (bin == '') xor (b2 == ''), this is logical or
|
||||||
|
|
||||||
|
local read = rwa == 'r' or plus
|
||||||
|
local write = rwa ~= 'r' or plus
|
||||||
|
local append = rwa == 'a'
|
||||||
|
local update = plus ~= ''
|
||||||
|
|
||||||
|
local file = setmetatable({}, filemt)
|
||||||
|
filedata[file] = fh
|
||||||
|
if read then
|
||||||
|
fh._file_rbuf = ''
|
||||||
|
end
|
||||||
|
if write then
|
||||||
|
fh._file_wbuf, fh._file_wmode, fh._file_wcap = '', 'full', nil --nil capacity means default, which may vary with e.g. system memory usage
|
||||||
|
end
|
||||||
|
|
||||||
|
return file
|
||||||
|
end
|
||||||
|
|
||||||
|
local function null_close() return true end
|
||||||
|
local function null_read() return nil end --always eof
|
||||||
|
local function null_write() return true end --eats data
|
||||||
|
--null devices can't be seeked
|
||||||
|
|
||||||
|
function openNull(mode)
|
||||||
|
return assert(wrapFile(mode, { close = null_close, read = null_read, write = null_write }))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function path_close(fh) return invoke(fh.address, 'close', fh.handle) end
|
||||||
|
local function path_read(fh, n) return invoke(fh.address, 'read', fh.handle, n) end
|
||||||
|
local function path_write(fh, data) return invoke(fh.address, 'write', fh.handle, data) end
|
||||||
|
local function path_seek(fh, ...) return invoke(fh.address, 'seek', fh.handle, ...) end
|
||||||
|
|
||||||
|
function openPath(mode, addr, path)
|
||||||
|
local fd, err = invoke(addr, 'open', path, mode) --should we validate `mode` first?
|
||||||
|
if not fd then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
return assert(wrapFile(mode, { address = addr, handle = fd, close = path_close, read = path_read, write = path_write, seek = path_seek }))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--file method helpers
|
||||||
|
local function assertOpen(file, how)
|
||||||
|
local fh = filedata[file]
|
||||||
|
if fh._file_closed then error('attempt to use a closed file', 3) end
|
||||||
|
if how == 'read' then
|
||||||
|
if not fh._file_rbuf then return nil, 'file not readable' end
|
||||||
|
elseif how == 'write' then
|
||||||
|
if not fh._file_wbuf then return nil, 'file not writable' end
|
||||||
|
elseif how == 'seek' then
|
||||||
|
if not fh.seek then return nil, 'file not seekable' end
|
||||||
|
elseif how then
|
||||||
|
error('bad how: should be read, write, seek, or nil', 2)
|
||||||
|
end
|
||||||
|
return fh
|
||||||
|
end
|
||||||
|
|
||||||
|
local function flush(fh)
|
||||||
|
local ok, err = fh:write(fh._file_wbuf)
|
||||||
|
if ok then
|
||||||
|
fh._file_wbuf = ''
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read1(fh, n)
|
||||||
|
if type(n) == 'number' then
|
||||||
|
while #fh._file_rbuf < n do
|
||||||
|
local blocksz = n - #fh._file_rbuf
|
||||||
|
if blocksz < 512 then blocksz = 512 end --TODO tune default capacity to e.g. free memory
|
||||||
|
local data, err = fh:read(blocksz)
|
||||||
|
if err then
|
||||||
|
return nil, err
|
||||||
|
elseif not data then --eof
|
||||||
|
break
|
||||||
|
end
|
||||||
|
fh._file_rbuf = fh._file_rbuf .. data
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = sub(fh._file_rbuf, 1, n)
|
||||||
|
fh._file_rbuf = sub(fh._file_rbuf, n + 1)
|
||||||
|
return data
|
||||||
|
elseif n == 'a' then
|
||||||
|
local parts = { fh._file_rbuf }
|
||||||
|
local chunk, err
|
||||||
|
repeat
|
||||||
|
chunk, err = fh:read(512)
|
||||||
|
parts[#parts+1] = chunk
|
||||||
|
until not chunk
|
||||||
|
|
||||||
|
local cat = concat(parts)
|
||||||
|
|
||||||
|
if err then
|
||||||
|
fh._file_rbuf = cat
|
||||||
|
return nil, err
|
||||||
|
else
|
||||||
|
fh._file_rbuf = ''
|
||||||
|
return cat
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error('TODO read1 patterns')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function write1(fh, data)
|
||||||
|
local wbuf = fh._file_wbuf .. data
|
||||||
|
fh._file_wbuf = wbuf
|
||||||
|
|
||||||
|
local mode = fh._file_wmode
|
||||||
|
if mode == 'no' then
|
||||||
|
if wbuf ~= '' then return flush(fh) end
|
||||||
|
elseif mode == 'full' then
|
||||||
|
local cap = fh._file_wcap or 512 --TODO tune default capacity to e.g. free memory
|
||||||
|
if #fh._file_wbuf >= cap then
|
||||||
|
return flush(fh)
|
||||||
|
end
|
||||||
|
else --for line buffering, only flush complete lines
|
||||||
|
local i1, i2 = find(fh._file_wbuf, '.+[\r\n]') --`i2` is the last cr or lf in the buffer
|
||||||
|
if i1 then --if there are any newlines
|
||||||
|
local rest = sub(fh._file_wbuf, i2 + 1)
|
||||||
|
fh._file_wbuf = sub(fh._file_wbuf, 1, i2)
|
||||||
|
local ok, err = flush(fh)
|
||||||
|
if ok then fh._file_wbuf = rest end
|
||||||
|
return ok, err
|
||||||
|
else --no newlines, nothing to flush
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--file methods
|
||||||
|
function filemt:close()
|
||||||
|
local fh = assertOpen(self)
|
||||||
|
if fh._file_wbuf then
|
||||||
|
local ok, err = flush(fh)
|
||||||
|
if not ok then return nil, err end --XXX should close ever fail? what do we expect the caller to do?
|
||||||
|
end
|
||||||
|
fh:close()
|
||||||
|
fh._file_closed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
filemt.__close = filemt.close --for future lua 5.4 compat
|
||||||
|
|
||||||
|
function filemt:flush()
|
||||||
|
local fh, err = assertOpen(self, 'write')
|
||||||
|
if not fh then return nil, err end
|
||||||
|
|
||||||
|
local ok, err = flush(fh)
|
||||||
|
if not ok then return nil, err end
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function filemt:lines(...)
|
||||||
|
local pats = pack(...)
|
||||||
|
for i = 1, pats.n do
|
||||||
|
local pat = pats[i]
|
||||||
|
local t = checkArgEx(i, pat, 'string', 'integer')
|
||||||
|
if t == 'string' and pat ~= 'n' and pat ~= 'a' and pat ~= 'l' and pat ~= 'L' then
|
||||||
|
error(format('bad argument #%d to \'lines\' (invalid format)', i), 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if pats.n == 0 then
|
||||||
|
pats[1], pats.n = 'l', 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, err = assertOpen(self, 'read')
|
||||||
|
if not ok then error(err, 2) end --more informative than the error from `for line in nil, err do end`
|
||||||
|
|
||||||
|
return function()
|
||||||
|
local ret = {}
|
||||||
|
for i = 1, pats.n do
|
||||||
|
local ok, err = read1(fh, pats[i])
|
||||||
|
if not ok then
|
||||||
|
if err then error(err, 2) end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
ret[i] = ok
|
||||||
|
end
|
||||||
|
return unpack(ret, 1, pats.n)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function filemt:read(...)
|
||||||
|
local args = pack(...)
|
||||||
|
for i = 1, args.n do
|
||||||
|
local pat = args[i]
|
||||||
|
local t = checkArgEx(i, pat, 'string', 'integer')
|
||||||
|
if t == 'string' and pat ~= 'n' and pat ~= 'a' and pat ~= 'l' and pat ~= 'L' then
|
||||||
|
error(format('bad argument #%d to \'read\' (invalid format)', i), 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if args.n == 0 then
|
||||||
|
args[1], args.n = 'l', 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local fh, err = assertOpen(self, 'read')
|
||||||
|
if not fh then return nil, err end
|
||||||
|
|
||||||
|
local args = pack(...)
|
||||||
|
local iend = args.n
|
||||||
|
for i = 1, args.n do
|
||||||
|
local ok, err = read1(fh, args[i])
|
||||||
|
if ok then
|
||||||
|
args[i] = ok
|
||||||
|
else
|
||||||
|
iend = i + 1
|
||||||
|
args[i] = nil
|
||||||
|
args[i + 1] = err
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return unpack(args, 1, iend)
|
||||||
|
end
|
||||||
|
|
||||||
|
function filemt:seek(whence, offset)
|
||||||
|
if type(whence) == 'number' and rawequal(offset, nil) then
|
||||||
|
checkArgEx(1, whence, 'integer') --produce a better error message citing arg 1
|
||||||
|
whence, offset = 'cur', whence
|
||||||
|
end
|
||||||
|
checkArgEx(1, whence, 'string', 'nil')
|
||||||
|
checkArgEx(2, offset, 'integer', 'nil')
|
||||||
|
|
||||||
|
local fh, err = assertOpen(self, 'seek')
|
||||||
|
if not fh then return nil, err end
|
||||||
|
|
||||||
|
--before seeking, flush any buffered writes
|
||||||
|
if fh._file_wbuf then
|
||||||
|
local ok, err = flush(fh)
|
||||||
|
if not ok then return nil, err end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, err = fh:seek(whence or 'cur', offset or 0)
|
||||||
|
if not ok then return nil, err end
|
||||||
|
|
||||||
|
|
||||||
|
local tell = (whence == 'cur' or not whence) and (offset == 0 or not offset) --true if we didn't move ie ftell()
|
||||||
|
--after seeking, if we moved, invalidate the read buffer
|
||||||
|
if (whence and whence ~= cur) or (offset and offset ~= 0) then
|
||||||
|
fh._file_rbuf = fh._file_rbuf and ''
|
||||||
|
end
|
||||||
|
|
||||||
|
return ok
|
||||||
|
end
|
||||||
|
|
||||||
|
function filemt:setvbuf(mode, size)
|
||||||
|
checkArg(1, mode, 'string')
|
||||||
|
if mode ~= 'no' and mode ~= 'line' and mode ~= 'full' then
|
||||||
|
error(format('bad argument #1 to \'setvbuf\' (invalid option %q)', mode), 2)
|
||||||
|
end
|
||||||
|
checkArgEx(2, size, 'integer', 'nil')
|
||||||
|
|
||||||
|
local fh, err = assertOpen(self)
|
||||||
|
if not fh then return nil, err end
|
||||||
|
|
||||||
|
if not fh._file_wbuf then
|
||||||
|
return true --on readonly files, setvbuf is a noop
|
||||||
|
end
|
||||||
|
|
||||||
|
fh._file_wmode, fh._file_wcap = mode, size
|
||||||
|
--don't flush now; we don't have a way to propagate errors. the next write (or close) will take care of it.
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function filemt:write(...)
|
||||||
|
local args = pack(...)
|
||||||
|
for i = 1, args.n do
|
||||||
|
local arg = args[i]
|
||||||
|
local t = checkArgEx(i, arg, 'string', 'number')
|
||||||
|
if t ~= 'string' then args[i] = tostring(arg) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local fh, err = assertOpen(self, 'write')
|
||||||
|
if not fh then return nil, err end
|
||||||
|
|
||||||
|
for i = 1, args.n do
|
||||||
|
local ok, err = write1(fh, args[i])
|
||||||
|
if not ok then return nil, err end
|
||||||
|
end
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vi: set ts=2:
|
Loading…
Reference in New Issue