X-Git-Url: http://nitlanguage.org diff --git a/lib/standard/stream.nit b/lib/standard/stream.nit index 9046044..7df1bd1 100644 --- a/lib/standard/stream.nit +++ b/lib/standard/stream.nit @@ -1,36 +1,55 @@ # This file is part of NIT ( http://www.nitlanguage.org ). # -# Copyright 2004-2008 Jean Privat -# -# This file is free software, which comes along with NIT. This software is +# This file is free software, which comes along with NIT. This software is # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. You can modify it is you want, provided this header +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. You can modify it is you want, provided this header # is kept unaltered, and a notification of the changes is added. -# You are allowed to redistribute it and sell it, alone or is a part of +# You are allowed to redistribute it and sell it, alone or is a part of # another product. -# This module handle abstract input and output streams -package stream +# Input and output streams of characters +module stream + +intrude import ropes +import error + +in "C" `{ + #include + #include + #include +`} -import string +# Any kind of error that could be produced by an operation on Streams +class IOError + super Error +end + +# Any kind of stream to read/write/both to or from a source +abstract class Stream + # Error produced by the file stream + # + # var ifs = new FileReader.open("donotmakethisfile.binx") + # ifs.read_all + # ifs.close + # assert ifs.last_error != null + var last_error: nullable IOError = null -# Abstract stream class -class IOS # close the stream - meth close is abstract + fun close is abstract end -# Abstract input streams -class IStream -special IOS +# A `Stream` that can be read from +abstract class Reader + super Stream # Read a character. Return its ASCII value, -1 on EOF or timeout - meth read_char: Int is abstract + fun read_char: Int is abstract # Read at most i bytes - meth read(i: Int): String + fun read(i: Int): String do - var s = new String.with_capacity(i) + if last_error != null then return "" + var s = new FlatBuffer.with_capacity(i) while i > 0 and not eof do var c = read_char if c >= 0 then @@ -38,100 +57,364 @@ special IOS i -= 1 end end - return s + return s.to_s end # Read a string until the end of the line. - meth read_line: String + # + # The line terminator '\n' and '\r\n', if any, is removed in each line. + # + # ~~~ + # var txt = "Hello\n\nWorld\n" + # var i = new StringReader(txt) + # assert i.read_line == "Hello" + # assert i.read_line == "" + # assert i.read_line == "World" + # assert i.eof + # ~~~ + # + # Only LINE FEED (`\n`), CARRIAGE RETURN & LINE FEED (`\r\n`), and + # the end or file (EOF) is considered to delimit the end of lines. + # CARRIAGE RETURN (`\r`) alone is not used for the end of line. + # + # ~~~ + # var txt2 = "Hello\r\n\n\rWorld" + # var i2 = new StringReader(txt2) + # assert i2.read_line == "Hello" + # assert i2.read_line == "" + # assert i2.read_line == "\rWorld" + # assert i2.eof + # ~~~ + # + # NOTE: Use `append_line_to` if the line terminator needs to be preserved. + fun read_line: String do - assert not eof - var s = new String + if last_error != null then return "" + if eof then return "" + var s = new FlatBuffer append_line_to(s) - return s + return s.to_s.chomp end + # Read all the lines until the eof. + # + # The line terminator '\n' and `\r\n` is removed in each line, + # + # ~~~ + # var txt = "Hello\n\nWorld\n" + # var i = new StringReader(txt) + # assert i.read_lines == ["Hello", "", "World"] + # ~~~ + # + # This method is more efficient that splitting + # the result of `read_all`. + # + # NOTE: SEE `read_line` for details. + fun read_lines: Array[String] + do + var res = new Array[String] + while not eof do + res.add read_line + end + return res + end + + # Return an iterator that read each line. + # + # The line terminator '\n' and `\r\n` is removed in each line, + # The line are read with `read_line`. See this method for details. + # + # ~~~ + # var txt = "Hello\n\nWorld\n" + # var i = new StringReader(txt) + # assert i.each_line.to_a == ["Hello", "", "World"] + # ~~~ + # + # Unlike `read_lines` that read all lines at the call, `each_line` is lazy. + # Therefore, the stream should no be closed until the end of the stream. + # + # ~~~ + # i = new StringReader(txt) + # var el = i.each_line + # + # assert el.item == "Hello" + # el.next + # assert el.item == "" + # el.next + # + # i.close + # + # assert not el.is_ok + # # closed before "world" is read + # ~~~ + fun each_line: LineIterator do return new LineIterator(self) + # Read all the stream until the eof. - meth read_all: String + # + # The content of the file is returned verbatim. + # + # ~~~ + # var txt = "Hello\n\nWorld\n" + # var i = new StringReader(txt) + # assert i.read_all == txt + # ~~~ + fun read_all: String do - var s = "" + if last_error != null then return "" + var s = new FlatBuffer while not eof do var c = read_char if c >= 0 then s.add(c.ascii) end - return s + return s.to_s end - # Read a string until the end of the line and append it to `s'. - meth append_line_to(s: String) + # Read a string until the end of the line and append it to `s`. + # + # Unlike `read_line` and other related methods, + # the line terminator '\n', if any, is preserved in each line. + # Use the method `Text::chomp` to safely remove it. + # + # ~~~ + # var txt = "Hello\n\nWorld\n" + # var i = new StringReader(txt) + # var b = new FlatBuffer + # i.append_line_to(b) + # assert b == "Hello\n" + # i.append_line_to(b) + # assert b == "Hello\n\n" + # i.append_line_to(b) + # assert b == txt + # assert i.eof + # ~~~ + # + # If `\n` is not present at the end of the result, it means that + # a non-eol terminated last line was returned. + # + # ~~~ + # var i2 = new StringReader("hello") + # assert not i2.eof + # var b2 = new FlatBuffer + # i2.append_line_to(b2) + # assert b2 == "hello" + # assert i2.eof + # ~~~ + # + # NOTE: The single character LINE FEED (`\n`) delimits the end of lines. + # Therefore CARRIAGE RETURN & LINE FEED (`\r\n`) is also recognized. + fun append_line_to(s: Buffer) do - while true do + if last_error != null then return + loop var x = read_char if x == -1 then if eof then return else var c = x.ascii - s.push(c) + s.chars.push(c) if c == '\n' then return end end end # Is there something to read. - meth eof: Bool is abstract + # This function returns 'false' if there is something to read. + fun eof: Bool is abstract + + # Read the next sequence of non whitespace characters. + # + # Leading whitespace characters are skipped. + # The first whitespace character that follows the result is consumed. + # + # An empty string is returned if the end of the file or an error is encounter. + # + # ~~~ + # var w = new StringReader(" Hello, \n\t World!") + # assert w.read_word == "Hello," + # assert w.read_char == '\n'.ascii + # assert w.read_word == "World!" + # assert w.read_word == "" + # ~~~ + # + # `Char::is_whitespace` determines what is a whitespace. + fun read_word: String + do + var buf = new FlatBuffer + var c = read_nonwhitespace + if c > 0 then + buf.add(c.ascii) + while not eof do + c = read_char + if c < 0 then break + var a = c.ascii + if a.is_whitespace then break + buf.add(a) + end + end + var res = buf.to_s + return res + end + + # Skip whitespace characters (if any) then return the following non-whitespace character. + # + # Returns the code point of the character. + # Return -1 on end of file or error. + # + # In fact, this method works like `read_char` except it skips whitespace. + # + # ~~~ + # var w = new StringReader(" \nab\tc") + # assert w.read_nonwhitespace == 'a'.ascii + # assert w.read_nonwhitespace == 'b'.ascii + # assert w.read_nonwhitespace == 'c'.ascii + # assert w.read_nonwhitespace == -1 + # ~~~ + # + # `Char::is_whitespace` determines what is a whitespace. + fun read_nonwhitespace: Int + do + var c = -1 + while not eof do + c = read_char + if c < 0 or not c.ascii.is_whitespace then break + end + return c + end +end + +# Iterator returned by `Reader::each_line`. +# See the aforementioned method for details. +class LineIterator + super Iterator[String] + + # The original stream + var stream: Reader + + redef fun is_ok + do + var res = not stream.eof + if not res and close_on_finish then stream.close + return res + end + + redef fun item + do + var line = self.line + if line == null then + line = stream.read_line + end + self.line = line + return line + end + + # The last line read (cache) + private var line: nullable String = null + + redef fun next + do + # force the read + if line == null then item + # drop the line + line = null + end + + # Close the stream when the stream is at the EOF. + # + # Default is false. + var close_on_finish = false is writable + + redef fun finish + do + if close_on_finish then stream.close + end end -# Abstract output stream -class OStream -special IOS +# `Reader` capable of declaring if readable without blocking +abstract class PollableReader + super Reader + + # Is there something to read? (without blocking) + fun poll_in: Bool is abstract + +end + +# A `Stream` that can be written to +abstract class Writer + super Stream # write a string - meth write(s: String) is abstract + fun write(s: Text) is abstract # Can the stream be used to write - meth is_writable: Bool is abstract + fun is_writable: Bool is abstract +end + +# Things that can be efficienlty written to a `Writer` +# +# The point of this interface is to allow the instance to be efficiently +# written into a `Writer`. +# +# Ready-to-save documents usually provide this interface. +interface Writable + # Write itself to a `stream` + # The specific logic it let to the concrete subclasses + fun write_to(stream: Writer) is abstract + + # Like `write_to` but return a new String (may be quite large) + # + # This funtionnality is anectodical, since the point + # of streamable object to to be efficienlty written to a + # stream without having to allocate and concatenate strings + fun write_to_string: String + do + var stream = new StringWriter + write_to(stream) + return stream.to_s + end +end + +redef class Text + super Writable + redef fun write_to(stream) do stream.write(self) end -# Input streams with a buffer -class BufferedIStream -special IStream - redef meth read_char +# Input streams with a buffered input for efficiency purposes +abstract class BufferedReader + super Reader + redef fun read_char do - assert not eof - if _buffer_pos >= _buffer.length then - fill_buffer - end - if _buffer_pos >= _buffer.length then + if last_error != null then return -1 + if eof then + last_error = new IOError("Stream has reached eof") return -1 end - var c = _buffer[_buffer_pos] + var c = _buffer.chars[_buffer_pos] _buffer_pos += 1 return c.ascii end - redef meth read(i) + redef fun read(i) do - var s = new String.with_capacity(i) - var j = _buffer_pos - var k = _buffer.length - while i > 0 do - if j >= k then - fill_buffer - if eof then return s - j = _buffer_pos - k = _buffer.length - end - while j < k and i > 0 do - s.add(_buffer[j]) - j += 1 - i -= 1 + if last_error != null then return "" + if _buffer.length == _buffer_pos then + if not eof then + return read(i) end + return "" + end + if _buffer_pos + i >= _buffer.length then + var from = _buffer_pos + _buffer_pos = _buffer.length + if from == 0 then return _buffer.to_s + return _buffer.substring_from(from).to_s end - _buffer_pos = j - return s + _buffer_pos += i + return _buffer.substring(_buffer_pos - i, i).to_s end - redef meth read_all + redef fun read_all do - var s = "" + if last_error != null then return "" + var s = new FlatBuffer while not eof do var j = _buffer_pos var k = _buffer.length @@ -142,15 +425,24 @@ special IStream _buffer_pos = j fill_buffer end - return s - end + return s.to_s + end - redef meth append_line_to(s) + redef fun append_line_to(s) do - while true do + loop # First phase: look for a '\n' var i = _buffer_pos - while i < _buffer.length and _buffer[i] != '\n' do i += 1 + while i < _buffer.length and _buffer.chars[i] != '\n' do i += 1 + + var eol + if i < _buffer.length then + assert _buffer.chars[i] == '\n' + i += 1 + eol = true + else + eol = false + end # if there is something to append if i > _buffer_pos then @@ -160,110 +452,114 @@ special IStream # Copy from the buffer to the string var j = _buffer_pos while j < i do - s.add(_buffer[j]) + s.add(_buffer.chars[j]) j += 1 end + _buffer_pos = i + else + assert end_reached + return end - if i < _buffer.length then - # so \n is in _buffer[i] - _buffer_pos = i + 1 # skip \n + if eol then + # so \n is found return else # so \n is not found - _buffer_pos = i - if end_reached then - return - else - fill_buffer - end + if end_reached then return + fill_buffer end end end - redef meth eof do return _buffer_pos >= _buffer.length and end_reached + redef fun eof + do + if _buffer_pos < _buffer.length then return false + if end_reached then return true + fill_buffer + return _buffer_pos >= _buffer.length and end_reached + end # The buffer - attr _buffer: String = null + private var buffer: nullable FlatBuffer = null # The current position in the buffer - attr _buffer_pos: Int = 0 + private var buffer_pos: Int = 0 # Fill the buffer - protected meth fill_buffer is abstract + protected fun fill_buffer is abstract - # Is the last fill_buffer reach the end - protected meth end_reached: Bool is abstract + # Is the last fill_buffer reach the end + protected fun end_reached: Bool is abstract - # Allocate a `_buffer' for a given `capacity'. - protected meth prepare_buffer(capacity: Int) + # Allocate a `_buffer` for a given `capacity`. + protected fun prepare_buffer(capacity: Int) do - _buffer = new String.with_capacity(capacity) + _buffer = new FlatBuffer.with_capacity(capacity) _buffer_pos = 0 # need to read end end -class IOStream -special IStream -special OStream +# A `Stream` that can be written to and read from +abstract class Duplex + super Reader + super Writer end -##############################################################" - -class FDStream -special IOS - # File description - attr _fd: Int - - redef meth close do native_close(_fd) +# `Stream` that can be used to write to a `String` +# +# Mainly used for compatibility with Writer type and tests. +class StringWriter + super Writer + + private var content = new Array[String] + redef fun to_s do return content.to_s + redef fun is_writable do return not closed + redef fun write(str) + do + assert not closed + content.add(str.to_s) + end - private meth native_close(i: Int): Int is extern "stream_FDStream_FDStream_native_close_1" - private meth native_read_char(i: Int): Int is extern "stream_FDStream_FDStream_native_read_char_1" - private meth native_read(i: Int, buf: NativeString, len: Int): Int is extern "stream_FDStream_FDStream_native_read_3" - private meth native_write(i: Int, buf: NativeString, len: Int): Int is extern "stream_FDStream_FDStream_native_write_3" + # Is the stream closed? + protected var closed = false - init(fd: Int) do _fd = fd + redef fun close do closed = true end -class FDIStream -special FDStream -special IStream - redef readable attr _eof: Bool = false - - redef meth read_char - do - var nb = native_read_char(_fd) - if nb == -1 then _eof = true - return nb - end +# `Stream` used to read from a `String` +# +# Mainly used for compatibility with Reader type and tests. +class StringReader + super Reader - init(fd: Int) do end -end + # The string to read from. + var source: String -class FDOStream -special FDStream -special OStream - redef readable attr _is_writable: Bool + # The current position in the string. + private var cursor: Int = 0 - redef meth write(s) - do - var nb = native_write(_fd, s.to_cstring, s.length) - if nb < s.length then _is_writable = false + redef fun read_char do + if cursor < source.length then + var c = source[cursor].ascii + + cursor += 1 + return c + else + return -1 + end end - init(fd: Int) - do - _is_writable = true + redef fun close do + source = "" end -end -class FDIOStream -special FDIStream -special FDOStream -special IOStream - init(fd: Int) - do - _fd = fd - _is_writable = true + redef fun read_all do + var c = cursor + cursor = source.length + if c == 0 then return source + return source.substring_from(c) end + + redef fun eof do return cursor >= source.length end