Property definitions

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