+ var s = open_ro
+ var res = s.each_line
+ res.close_on_finish = true
+ last_error = s.last_error
+ return res
+ end
+
+
+ # Lists the files contained within the directory at `path`.
+ #
+ # var files = "/etc".to_path.files
+ # assert files.has("/etc/issue".to_path)
+ #
+ # `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
+ 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
+ d.closedir
+
+ return res
+ end
+
+ # 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.
+ #
+ # `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
+ 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
+ # Recursively rmdir
+ file.rmdir
+ else
+ file.delete
+ end
+ if last_error == null then last_error = file.last_error
+ end
+
+ # 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
+end
+
+# Information on a file
+#
+# Created by `Path::stat` and `Path::link_stat`.
+#
+# The information within this class is gathered when the instance is initialized
+# it will not be updated if the targeted file is modified.
+class FileStat
+ super Finalizable
+
+ # TODO private init
+
+ # The low-level status of a file
+ #
+ # See: POSIX stat(2)
+ private var stat: NativeFileStat
+
+ private var finalized = false
+
+ redef fun finalize
+ do
+ if not finalized then
+ stat.free
+ finalized = true
+ end
+ end
+
+ # Returns the last access time in seconds since Epoch
+ fun last_access_time: Int
+ do
+ assert not finalized
+ return stat.atime
+ end
+
+ # Returns the last access time
+ #
+ # alias for `last_access_time`
+ fun atime: Int do return last_access_time
+
+ # Returns the last modification time in seconds since Epoch
+ fun last_modification_time: Int
+ do
+ assert not finalized
+ return stat.mtime
+ end
+
+ # Returns the last modification time
+ #
+ # alias for `last_modification_time`
+ fun mtime: Int do return last_modification_time
+
+
+ # Size of the file at `path`
+ fun size: Int
+ do
+ assert not finalized
+ return stat.size
+ end
+
+ # Is self a regular file and not a device file, pipe, socket, etc.?
+ fun is_file: Bool
+ do
+ assert not finalized
+ return stat.is_reg
+ end
+
+ # Alias for `is_file`
+ fun is_reg: Bool do return is_file
+
+ # Is this a directory?
+ fun is_dir: Bool
+ do
+ assert not finalized
+ return stat.is_dir
+ end
+
+ # Is this a symbolic link?
+ fun is_link: Bool
+ do
+ assert not finalized
+ return stat.is_lnk
+ end
+
+ # FIXME Make the following POSIX only? or implement in some other way on Windows
+
+ # Returns the last status change time in seconds since Epoch
+ fun last_status_change_time: Int
+ do
+ assert not finalized
+ return stat.ctime
+ end
+
+ # Returns the last status change time
+ #
+ # alias for `last_status_change_time`
+ fun ctime: Int do return last_status_change_time
+
+ # Returns the permission bits of file
+ fun mode: Int
+ do
+ assert not finalized
+ return stat.mode
+ end
+
+ # Is this a character device?
+ fun is_chr: Bool
+ do
+ assert not finalized
+ return stat.is_chr
+ end
+
+ # Is this a block device?
+ fun is_blk: Bool
+ do
+ assert not finalized
+ return stat.is_blk
+ end
+
+ # Is this a FIFO pipe?
+ fun is_fifo: Bool
+ do
+ assert not finalized
+ return stat.is_fifo
+ end
+
+ # Is this a UNIX socket
+ fun is_sock: Bool
+ do
+ assert not finalized
+ return stat.is_sock
+ end
+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
+ # return true if a file with this names exists
+ fun file_exists: Bool do return to_cstring.file_exists
+
+ # The status of a file. see POSIX stat(2).
+ fun file_stat: nullable FileStat
+ do
+ var stat = to_cstring.file_stat
+ if stat.address_is_null then return null
+ return new FileStat(stat)
+ end
+
+ # The status of a file or of a symlink. see POSIX lstat(2).
+ fun file_lstat: nullable FileStat
+ do
+ var stat = to_cstring.file_lstat
+ if stat.address_is_null then return null
+ return new FileStat(stat)
+ end
+
+ # Remove a file, return true if success
+ fun file_delete: Bool do return to_cstring.file_delete
+
+ # 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`.
+ #
+ # `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"
+ #
+ # 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(extension: nullable String): String
+ do
+ 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)