lib/file: revamp `poll_in` to use in `FileReader`
[nit.git] / lib / standard / file.nit
index efdf46e..e5fc85b 100644 (file)
@@ -77,9 +77,10 @@ abstract class FileStream
        # If the buf_size is <= 0, its value will be 512 by default
        #
        # The mode is any of the buffer_mode enumeration in `Sys`:
-       #       - buffer_mode_full
-       #       - buffer_mode_line
-       #       - buffer_mode_none
+       #
+       # * `buffer_mode_full`
+       # * `buffer_mode_line`
+       # * `buffer_mode_none`
        fun set_buffering_mode(buf_size, mode: Int) do
                if buf_size <= 0 then buf_size = 512
                if _file.set_buffering_type(buf_size, mode) != 0 then
@@ -108,7 +109,7 @@ class FileReader
                last_error = null
                _file = new NativeFile.io_open_read(path.to_cstring)
                if _file.address_is_null then
-                       last_error = new IOError("Error: Opening file at '{path.as(not null)}' failed with '{sys.errno.strerror}'")
+                       last_error = new IOError("Cannot open `{path.as(not null)}`: {sys.errno.strerror}")
                        end_reached = true
                        return
                end
@@ -126,6 +127,10 @@ class FileReader
        redef fun fill_buffer
        do
                var nb = _file.io_read(_buffer, _buffer_capacity)
+               if last_error == null and _file.ferror then
+                       last_error = new IOError("Cannot read `{path.as(not null)}`: {sys.errno.strerror}")
+                       end_reached = true
+               end
                if nb <= 0 then
                        end_reached = true
                        nb = 0
@@ -154,7 +159,7 @@ class FileReader
                prepare_buffer(10)
                _file = new NativeFile.io_open_read(path.to_cstring)
                if _file.address_is_null then
-                       last_error = new IOError("Error: Opening file at '{path}' failed with '{sys.errno.strerror}'")
+                       last_error = new IOError("Cannot open `{path}`: {sys.errno.strerror}")
                        end_reached = true
                end
        end
@@ -171,6 +176,20 @@ class FileReader
                        end_reached = true
                end
        end
+
+       redef fun poll_in
+       do
+               var res = native_poll_in(fd)
+               if res == -1 then
+                       last_error = new IOError(errno.to_s)
+                       return false
+               else return res > 0
+       end
+
+       private fun native_poll_in(fd: Int): Int `{
+               struct pollfd fds = {fd, POLLIN, 0};
+               return poll(&fds, 1, 0);
+       `}
 end
 
 # `Stream` that can write to a File
@@ -184,7 +203,7 @@ class FileWriter
                        last_error = new IOError("cannot write to non-writable stream")
                        return
                end
-               write_native(s.items, s.length)
+               write_native(s.items, 0, s.length)
        end
 
        redef fun write(s)
@@ -194,7 +213,7 @@ class FileWriter
                        last_error = new IOError("cannot write to non-writable stream")
                        return
                end
-               for i in s.substrings do write_native(i.to_cstring, i.length)
+               s.write_native_to(self)
        end
 
        redef fun write_byte(value)
@@ -225,7 +244,7 @@ class FileWriter
        redef var is_writable = false
 
        # Write `len` bytes from `native`.
-       private fun write_native(native: NativeString, len: Int)
+       private fun write_native(native: NativeString, from, len: Int)
        do
                if last_error != null then return
                if not _is_writable then
@@ -237,7 +256,7 @@ class FileWriter
                        _is_writable = false
                        return
                end
-               var err = _file.io_write(native, len)
+               var err = _file.io_write(native, from, len)
                if err != len then
                        # Big problem
                        last_error = new IOError("Problem in writing : {err} {len} \n")
@@ -251,7 +270,7 @@ class FileWriter
                self.path = path
                _is_writable = true
                if _file.address_is_null then
-                       last_error = new IOError("Error: Opening file at '{path}' failed with '{sys.errno.strerror}'")
+                       last_error = new IOError("Cannot open `{path}`: {sys.errno.strerror}")
                        is_writable = false
                end
        end
@@ -300,16 +319,6 @@ class Stdin
                path = "/dev/stdin"
                prepare_buffer(1)
        end
-
-       redef fun poll_in `{
-               struct pollfd fd = {0, POLLIN, 0};
-               int res = poll(&fd, 1, 0);
-               if (res == -1) {
-                       perror("Error poll stdin");
-                       exit(EXIT_FAILURE);
-               }
-               return res > 0;
-       `}
 end
 
 # Standard output stream.
@@ -349,9 +358,13 @@ redef class Writable
        end
 end
 
-# Utility class to access file system services
+# Utility class to access file system services.
 #
 # Usually created with `Text::to_path`.
+#
+# `Path` objects does not necessarily represent existing files in a file system.
+# They are sate-less objects that efficiently represent path information.
+# They also provide an easy to use API on file-system services and are used to store their error status (see `last_error`)
 class Path
 
        private var path: String
@@ -359,20 +372,69 @@ class Path
        # Path to this file
        redef fun to_s do return path
 
-       # Name of the file name at `to_s`
+       # Short name of the file at `to_s`
        #
        # ~~~
        # var path = "/tmp/somefile".to_path
        # assert path.filename == "somefile"
        # ~~~
-       var filename: String = path.basename("") is lazy
+       #
+       # The result does not depend of the file system, thus is cached for efficiency.
+       var filename: String = path.basename is lazy
+
+       # The path simplified by removing useless `.`, removing `//`, and resolving `..`
+       #
+       # ~~~
+       # var path = "somedir/./tmp/../somefile".to_path
+       # assert path.simplified.to_s == "somedir/somefile"
+       # ~~~
+       #
+       # See `String:simplify_path` for details.
+       #
+       # The result does not depend of the file system, thus is cached for efficiency.
+       var simplified: Path is lazy do
+               var res = path.simplify_path.to_path
+               res.simplified = res
+               return res
+       end
+
+       # Return the directory part of the path.
+       #
+       # ~~~
+       # var path = "/foo/bar/baz".to_path
+       # assert path.dir.to_s == "/foo/bar"
+       # assert path.dir.dir.to_s == "/foo"
+       # assert path.dir.dir.dir.to_s == "/"
+       # ~~~
+       #
+       # See `String:dirname` for details.
+       #
+       # The result does not depend of the file system, thus is cached for efficiency.
+       var dir: Path is lazy do
+               return path.dirname.to_path
+       end
+
+       # Last error produced by I/O operations.
+       #
+       # ~~~
+       # var path = "/does/not/exists".to_path
+       # assert path.last_error == null
+       # path.read_all
+       # assert path.last_error != null
+       # ~~~
+       #
+       # Since `Path` objects are stateless, `last_error` is reset on most operations and reflect its status.
+       var last_error: nullable IOError = null is writable
 
        # Does the file at `path` exists?
+       #
+       # If the file does not exists, `last_error` is set to the information.
        fun exists: Bool do return stat != null
 
        # Information on the file at `self` following symbolic links
        #
        # Returns `null` if there is no file at `self`.
+       # `last_error` is updated to contains the error information on error, and null on success.
        #
        #     assert "/etc/".to_path.stat.is_dir
        #     assert "/etc/issue".to_path.stat.is_file
@@ -384,12 +446,18 @@ class Path
        # if stat != null then # Does `p` exist?
        #     print "It's size is {stat.size}"
        #     if stat.is_dir then print "It's a directory"
+       # else
+       #     print p.last_error.to_s
        # end
        # ~~~
        fun stat: nullable FileStat
        do
                var stat = path.to_cstring.file_stat
-               if stat.address_is_null then return null
+               if stat.address_is_null then
+                       last_error = new IOError("Cannot open `{path}`: {sys.errno.strerror}")
+                       return null
+               end
+               last_error = null
                return new FileStat(stat)
        end
 
@@ -399,18 +467,33 @@ class Path
        fun link_stat: nullable FileStat
        do
                var stat = path.to_cstring.file_lstat
-               if stat.address_is_null then return null
+               if stat.address_is_null then
+                       last_error = new IOError("Cannot open `{path}`: {sys.errno.strerror}")
+                       return null
+               end
+               last_error = null
                return new FileStat(stat)
        end
 
-       # Delete a file from the file system, return `true` on success
-       fun delete: Bool do return path.to_cstring.file_delete
+       # Delete a file from the file system.
+       #
+       # `last_error` is updated to contains the error information on error, and null on success.
+       fun delete
+       do
+               var res = path.to_cstring.file_delete
+               if not res then
+                       last_error = new IOError("Cannot delete `{path}`: {sys.errno.strerror}")
+               else
+                       last_error = null
+               end
+       end
 
-       # Copy content of file at `path` to `dest`
+       # Copy content of file at `path` to `dest`.
        #
-       # Require: `exists`
+       # `last_error` is updated to contains the error information on error, and null on success.
        fun copy(dest: Path)
        do
+               last_error = null
                var input = open_ro
                var output = dest.open_wo
 
@@ -421,41 +504,75 @@ class Path
 
                input.close
                output.close
+               last_error = input.last_error or else output.last_error
        end
 
-       # Open this file for reading
+       # Open this file for reading.
+       #
+       # ~~~
+       # var file = "/etc/issue".to_path.open_ro
+       # print file.read_line
+       # file.close
+       # ~~~
+       #
+       # Note that it is the user's responsibility to close the stream.
+       # Therefore, for simple use case, look at `read_all` or `each_line`.
        #
-       # Require: `exists and not link_stat.is_dir`
+       # ENSURE `last_error == result.last_error`
        fun open_ro: FileReader
        do
-               # TODO manage streams error when they are merged
-               return new FileReader.open(path)
+               var res = new FileReader.open(path)
+               last_error = res.last_error
+               return res
        end
 
        # Open this file for writing
        #
-       # Require: `not exists or not stat.is_dir`
+       # ~~~
+       # var file = "bla.log".to_path.open_wo
+       # file.write "Blabla\n"
+       # file.close
+       # ~~~
+       #
+       # Note that it is the user's responsibility to close the stream.
+       # Therefore, for simple use case, look at `Writable::write_to_file`.
+       #
+       # ENSURE `last_error == result.last_error`
        fun open_wo: FileWriter
        do
-               # TODO manage streams error when they are merged
-               return new FileWriter.open(path)
+               var res = new FileWriter.open(path)
+               last_error = res.last_error
+               return res
        end
 
-       # Read all the content of the file
+       # Read all the content of the file as a string.
        #
        # ~~~
        # var content = "/etc/issue".to_path.read_all
        # print content
        # ~~~
        #
+       # `last_error` is updated to contains the error information on error, and null on success.
+       # In case of error, the result might be empty or truncated.
+       #
        # See `Reader::read_all` for details.
        fun read_all: String do return read_all_bytes.to_s
 
+       # Read all the content on the file as a raw sequence of bytes.
+       #
+       # ~~~
+       # var content = "/etc/issue".to_path.read_all_bytes
+       # print content.to_s
+       # ~~~
+       #
+       # `last_error` is updated to contains the error information on error, and null on success.
+       # In case of error, the result might be empty or truncated.
        fun read_all_bytes: Bytes
        do
                var s = open_ro
                var res = s.read_all_bytes
                s.close
+               last_error = s.last_error
                return res
        end
 
@@ -472,12 +589,16 @@ class Path
        # end
        # ~~~
        #
+       # `last_error` is updated to contains the error information on error, and null on success.
+       # In case of error, the result might be empty or truncated.
+       #
        # See `Reader::read_lines` for details.
        fun read_lines: Array[String]
        do
                var s = open_ro
                var res = s.read_lines
                s.close
+               last_error = s.last_error
                return res
        end
 
@@ -492,52 +613,97 @@ class Path
        #
        # Note: the stream is automatically closed at the end of the file (see `LineIterator::close_on_finish`)
        #
+       # `last_error` is updated to contains the error information on error, and null on success.
+       #
        # See `Reader::each_line` for details.
        fun each_line: LineIterator
        do
                var s = open_ro
                var res = s.each_line
                res.close_on_finish = true
+               last_error = s.last_error
                return res
        end
 
 
-       # Lists the name of the files contained within the directory at `path`
+       # Lists the files contained within the directory at `path`.
+       #
+       #     var files = "/etc".to_path.files
+       #     assert files.has("/etc/issue".to_path)
        #
-       # Require: `exists and is_dir`
+       # `last_error` is updated to contains the error information on error, and null on success.
+       # In case of error, the result might be empty or truncated.
+       #
+       #     var path = "/etc/issue".to_path
+       #     files = path.files
+       #     assert files.is_empty
+       #     assert path.last_error != null
        fun files: Array[Path]
        do
-               var files = new Array[Path]
-               for filename in path.files do
-                       files.add new Path(path / filename)
+               last_error = null
+               var res = new Array[Path]
+               var d = new NativeDir.opendir(path.to_cstring)
+               if d.address_is_null then
+                       last_error = new IOError("Cannot list directory `{path}`: {sys.errno.strerror}")
+                       return res
                end
-               return files
+
+               loop
+                       var de = d.readdir
+                       if de.address_is_null then
+                               # readdir cannot fail, so null means end of list
+                               break
+                       end
+                       var name = de.to_s_with_copy
+                       if name == "." or name == ".." then continue
+                       res.add new Path(path / name)
+               end
+               d.closedir
+
+               return res
        end
 
-       # Delete a directory and all of its content, return `true` on success
+       # Delete a directory and all of its content
        #
        # Does not go through symbolic links and may get stuck in a cycle if there
        # is a cycle in the file system.
-       fun rmdir: Bool
+       #
+       # `last_error` is updated to contains the error information on error, and null on success.
+       # The method does not stop on the first error and try to remove most file and directories.
+       #
+       # ~~~
+       # var path = "/does/not/exists/".to_path
+       # path.rmdir
+       # assert path.last_error != null
+       # ~~~
+       fun rmdir
        do
-               var ok = true
+               last_error = null
                for file in self.files do
                        var stat = file.link_stat
+                       if stat == null then
+                               last_error = file.last_error
+                               continue
+                       end
                        if stat.is_dir then
-                               ok = file.rmdir and ok
+                               # Recursively rmdir
+                               file.rmdir
                        else
-                               ok = file.delete and ok
+                               file.delete
                        end
+                       if last_error == null then last_error = file.last_error
                end
 
-               # Delete the directory itself
-               if ok then ok = path.to_cstring.rmdir and ok
-
-               return ok
+               # Delete the directory itself if things are fine
+               if last_error == null then
+                       if path.to_cstring.rmdir then
+                               last_error = new IOError("Cannot remove `{self}`: {sys.errno.strerror}")
+                       end
+               end
        end
 
-       redef fun ==(other) do return other isa Path and path.simplify_path == other.path.simplify_path
-       redef fun hash do return path.simplify_path.hash
+       redef fun ==(other) do return other isa Path and simplified.path == other.simplified.path
+       redef fun hash do return simplified.path.hash
 end
 
 # Information on a file
@@ -675,6 +841,11 @@ end
 redef class Text
        # Access file system related services on the path at `self`
        fun to_path: Path do return new Path(to_s)
+
+       private fun write_native_to(s: FileWriter)
+       do
+               for i in substrings do s.write_native(i.to_cstring, 0, i.bytelen)
+       end
 end
 
 redef class String
@@ -703,36 +874,51 @@ redef class String
        # Copy content of file at `self` to `dest`
        fun file_copy_to(dest: String) do to_path.copy(dest.to_path)
 
-       # Remove the trailing extension `ext`.
+       # Remove the trailing `extension`.
        #
-       # `ext` usually starts with a dot but could be anything.
+       # `extension` usually starts with a dot but could be anything.
        #
-       #     assert "file.txt".strip_extension(".txt")  == "file"
-       #     assert "file.txt".strip_extension("le.txt")  == "fi"
-       #     assert "file.txt".strip_extension("xt")  == "file.t"
+       #     assert "file.txt".strip_extension(".txt")   == "file"
+       #     assert "file.txt".strip_extension("le.txt") == "fi"
+       #     assert "file.txt".strip_extension("xt")     == "file.t"
        #
-       # if `ext` is not present, `self` is returned unmodified.
+       # If `extension == null`, the rightmost extension is stripped, including the last dot.
+       #
+       #     assert "file.txt".strip_extension           == "file"
+       #
+       # If `extension` is not present, `self` is returned unmodified.
        #
        #     assert "file.txt".strip_extension(".tar.gz")  == "file.txt"
-       fun strip_extension(ext: String): String
+       fun strip_extension(extension: nullable String): String
        do
-               if has_suffix(ext) then
-                       return substring(0, length - ext.length)
+               if extension == null then
+                       extension = file_extension
+                       if extension == null then
+                               return self
+                       else extension = ".{extension}"
+               end
+
+               if has_suffix(extension) then
+                       return substring(0, length - extension.length)
                end
                return self
        end
 
-       # Extract the basename of a path and remove the extension
+       # Extract the basename of a path and strip the `extension`
        #
-       #     assert "/path/to/a_file.ext".basename(".ext")         == "a_file"
-       #     assert "path/to/a_file.ext".basename(".ext")          == "a_file"
-       #     assert "path/to".basename(".ext")                     == "to"
-       #     assert "path/to/".basename(".ext")                    == "to"
+       # The extension is stripped only if `extension != null`.
+       #
+       #     assert "/path/to/a_file.ext".basename(".ext")     == "a_file"
+       #     assert "path/to/a_file.ext".basename(".ext")      == "a_file"
+       #     assert "path/to/a_file.ext".basename              == "a_file.ext"
+       #     assert "path/to".basename(".ext")                 == "to"
+       #     assert "path/to/".basename(".ext")                == "to"
+       #     assert "path/to".basename                         == "to"
        #     assert "path".basename("")                        == "path"
        #     assert "/path".basename("")                       == "path"
        #     assert "/".basename("")                           == "/"
        #     assert "".basename("")                            == ""
-       fun basename(ext: String): String
+       fun basename(extension: nullable String): String
        do
                var l = length - 1 # Index of the last char
                while l > 0 and self.chars[l] == '/' do l -= 1 # remove all trailing `/`
@@ -742,7 +928,10 @@ redef class String
                if pos >= 0 then
                        n = substring(pos+1, l-pos)
                end
-               return n.strip_extension(ext)
+
+               if extension != null then
+                       return n.strip_extension(extension)
+               else return n
        end
 
        # Extract the dirname of a path
@@ -901,7 +1090,7 @@ redef class String
        # In other cases, parts of the current directory may be exhibited:
        #
        #     var p = "../foo/bar".relpath("baz")
-       #     var c = getcwd.basename("")
+       #     var c = getcwd.basename
        #     assert p == "../../{c}/baz"
        #
        # For path resolution independent of the current directory (eg. for paths in URL),
@@ -988,10 +1177,9 @@ redef class String
        #     assert "/fail/does not/exist".rmdir != null
        fun rmdir: nullable Error
        do
-               var res = to_path.rmdir
-               if res then return null
-               var error = new IOError("Cannot change remove `{self}`: {sys.errno.strerror}")
-               return error
+               var p = to_path
+               p.rmdir
+               return p.last_error
        end
 
        # Change the current working directory
@@ -1072,6 +1260,13 @@ redef class String
        end
 end
 
+redef class FlatString
+       redef fun write_native_to(s)
+       do
+               s.write_native(items, first_byte, bytelen)
+       end
+end
+
 redef class NativeString
        private fun file_exists: Bool `{
                FILE *hdl = fopen(self,"r");
@@ -1159,11 +1354,11 @@ private extern class NativeFile `{ FILE* `}
                return fread(buf, 1, len, self);
        `}
 
-       fun io_write(buf: NativeString, len: Int): Int `{
-               return fwrite(buf, 1, len, self);
+       fun io_write(buf: NativeString, from, len: Int): Int `{
+               return fwrite(buf+from, 1, len, self);
        `}
 
-       fun write_byte(value: Int): Int `{
+       fun write_byte(value: Byte): Int `{
                unsigned char b = (unsigned char)value;
                return fwrite(&b, 1, 1, self);
        `}
@@ -1180,6 +1375,8 @@ private extern class NativeFile `{ FILE* `}
                return 0;
        `}
 
+       fun ferror: Bool `{ return ferror(self); `}
+
        fun fileno: Int `{ return fileno(self); `}
 
        # Flushes the buffer, forcing the write operation