lib/std: improve doc of the class `Path`
[nit.git] / lib / standard / file.nit
index 00575f2..03a671e 100644 (file)
@@ -185,7 +185,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)
@@ -195,7 +195,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)
@@ -226,7 +226,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
@@ -238,7 +238,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")
@@ -350,9 +350,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
@@ -360,13 +364,25 @@ 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
+       var filename: String = path.basename is lazy
+
+       # 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?
        fun exists: Bool do return stat != null
@@ -404,14 +420,25 @@ class Path
                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
 
@@ -422,41 +449,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
+       # ~~~
        #
-       # Require: `exists and not link_stat.is_dir`
+       # Note that it is the user's responsibility to close the stream.
+       # Therefore, for simple use case, look at `read_all` or `each_line`.
+       #
+       # 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
 
@@ -473,12 +534,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
 
@@ -493,48 +558,93 @@ 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
+
+               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
-               return files
+               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
@@ -676,6 +786,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.length)
+       end
 end
 
 redef class String
@@ -704,36 +819,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 `/`
@@ -743,7 +873,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
@@ -902,7 +1035,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),
@@ -989,10 +1122,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
@@ -1073,6 +1205,13 @@ redef class String
        end
 end
 
+redef class FlatString
+       redef fun write_native_to(s)
+       do
+               s.write_native(items, index_from, length)
+       end
+end
+
 redef class NativeString
        private fun file_exists: Bool `{
                FILE *hdl = fopen(self,"r");
@@ -1160,11 +1299,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);
        `}