core :: Path :: defaultinit
# 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
# Path to this file
redef fun to_s do return path
# Short name of the file at `to_s`
#
# ~~~
# var path = "/tmp/somefile".to_path
# assert path.filename == "somefile"
# ~~~
#
# 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
# assert "/fail/does not/exist".to_path.stat == null
#
# ~~~
# var p = "/tmp/".to_path
# var stat = p.stat
# 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
last_error = new IOError("Cannot open `{path}`: {sys.errno.strerror}")
return null
end
last_error = null
return new FileStat(stat)
end
# Information on the file or link at `self`
#
# Do not follow symbolic links.
fun link_stat: nullable FileStat
do
var stat = path.to_cstring.file_lstat
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.
#
# `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`.
#
# `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
var buffer = new CString(4096)
while not input.eof do
var read = input.read_bytes_to_cstring(buffer, 4096)
output.write_bytes_from_cstring(buffer, read)
end
input.close
output.close
last_error = input.last_error or else output.last_error
end
# 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`.
#
# ENSURE `last_error == result.last_error`
fun open_ro: FileReader
do
var res = new FileReader.open(path)
last_error = res.last_error
return res
end
# Open this file for writing
#
# ~~~
# 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
var res = new FileWriter.open(path)
last_error = res.last_error
return res
end
# 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
# Read all the lines of the file
#
# ~~~
# var lines = "/etc/passwd".to_path.read_lines
#
# print "{lines.length} users"
#
# for l in lines do
# var fields = l.split(":")
# print "name={fields[0]} uid={fields[2]}"
# 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
# Return an iterator on each line of the file
#
# ~~~
# for l in "/etc/passwd".to_path.each_line do
# var fields = l.split(":")
# print "name={fields[0]} uid={fields[2]}"
# end
# ~~~
#
# 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
# Correctly join `self` with `subpath` using the directory separator.
#
# Using a standard "{self}/{path}" does not work in the following cases:
#
# * `self` is empty.
# * `path` starts with `'/'`.
#
# This method ensures that the join is valid.
#
# var hello = "hello".to_path
# assert (hello/"world").to_s == "hello/world"
# assert ("hel/lo".to_path / "wor/ld").to_s == "hel/lo/wor/ld"
# assert ("".to_path / "world").to_s == "world"
# assert (hello / "/world").to_s == "/world"
# assert ("hello/".to_path / "world").to_s == "hello/world"
fun /(subpath: String): Path do return new Path(path / subpath)
# 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
if name == "." or name == ".." then continue
res.add self / name
end
d.closedir
return res
end
# Is `self` the path to an existing directory ?
#
# ~~~nit
# assert ".".to_path.is_dir
# assert not "/etc/issue".to_path.is_dir
# assert not "/should/not/exist".to_path.is_dir
# ~~~
fun is_dir: Bool do
var st = stat
if st == null then return false
return st.is_dir
end
# Recursively 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 with the first encountered error, or null on success.
# The method does not stop on the first error and tries to remove the most files and directories.
#
# ~~~
# var path = "/does/not/exists/".to_path
# path.rmdir
# assert path.last_error != null
#
# path = "/tmp/path/to/create".to_path
# path.to_s.mkdir
# assert path.exists
# path.rmdir
# assert path.last_error == null
# ~~~
fun rmdir
do
var first_error = null
for file in self.files do
var stat = file.link_stat
if stat == null then
if first_error == null then first_error = file.last_error
continue
end
if stat.is_dir then
# Recursively rmdir
file.rmdir
else
file.delete
end
if first_error == null then first_error = file.last_error
end
# Delete the directory itself if things are fine
if first_error == null then
if not path.to_cstring.rmdir then
first_error = new IOError("Cannot remove `{self}`: {sys.errno.strerror}")
end
end
self.last_error = first_error
end
redef fun ==(other) do return other isa Path and simplified.path == other.simplified.path
redef fun hash do return simplified.path.hash
end
lib/core/file.nit:375,1--758,3