/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Written June 2020 by Markus Triska (triska@metalevel.at) Part of Scryer Prolog. Predicates for reasoning about files and directories. In this library, directories and files are represented as *lists of characters*. This is an ideal representation: -) Lists of characters can be conveniently reasoned about with DCGs and built-in Prolog predicates from library(lists). This alone is already a very compelling argument to use them. -) Other Scryer libraries such as library(http/http_open) also already use lists of characters to represent paths. -) File names are mostly ephemeral, so it is good for efficiency that they can quickly allocated transiently on the heap, leaving the atom table mostly unaffected. Indexing is almost never needed for file names. If needed, it should be added to the engine. -) The previous point is also good for security, since the system leaves little trace of which files were even accessed. -) Scryer Prolog represents lists of characters extremely compactly. Some Prolog programmers will likely find this representation quite unusual, because files are represented as *atoms* in many systems. However, the ISO standard only demands that sources and sinks be *ground* terms, so lists of characters are completely admissible: A source/sink is specified as an implementation defined ground term in a call of open/4 (8.11.5). All subsequent references to the source/sink are made by referring to a stream-term (7.10.2) or alias (7.10.2.2). I believe that with the advent of Scryer Prolog and its efficient representation of strings as lists of characters, we should take this opportunity for improvement, and in fact extend the use of lists of characters also to other predicates like open/3. Please note that we *cannot* simply accept *both* representations, because that would invalidate the type errors raised by this library, and make future extensions for type checking impossible. So I ask you: Please try out this representation for a few months. It simplifies working with files considerably. Once we have collected more experience with this representations, please let us consider how to proceed in such a way that we can call it an improvement. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ :- module(files, [directory_files/2, file_size/2, file_exists/1, directory_exists/1, delete_file/1, make_directory/1, working_directory/2, path_canonical/2, path_segments/2, file_modification_time/2, file_creation_time/2, file_access_time/2]). :- use_module(library(error)). :- use_module(library(lists)). :- use_module(library(charsio)). list_of_chars(Cs) :- must_be(list, Cs), maplist(must_be(character), Cs). directory_files(Directory, Files) :- list_of_chars(Directory), can_be(list, Files), '$directory_files'(Directory, Files). file_size(File, Size) :- file_must_exist(File, file_size/2), list_of_chars(File), can_be(integer, Size), '$file_size'(File, Size). file_exists(File) :- list_of_chars(File), '$file_exists'(File). directory_exists(Directory) :- list_of_chars(Directory), '$directory_exists'(Directory). make_directory(Directory) :- list_of_chars(Directory), '$make_directory'(Directory). delete_file(File) :- file_must_exist(File, delete_file/1), list_of_chars(File), '$delete_file'(File). file_must_exist(File, Context) :- ( file_exists(File) -> true ; throw(error(existence_error(file, File), Context)) ). /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Dir0 is the current working directory, and the working directory is changed to Dir. Use working_directory(Ds, Ds) to determine the current working directory, and leave it as is. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ working_directory(Dir0, Dir) :- can_be(list, Dir0), can_be(list, Dir), '$working_directory'(Dir0, Dir). /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True iff Cs is the canonical, absolute path of Ps. All intermediate components are normalized, and all symbolic links are resolved. The predicate fails in the following situations, though not necessarily *only* in these cases: 1. Ps is a path that does not exist. 2. A non-final component in Ps is not a directory. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ path_canonical(Ps, Cs) :- must_be(list, Ps), maplist(must_be(character), Ps), can_be(list, Cs), '$path_canonical'(Ps, Cs). /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - T is, respectively, the modification, access or creation time of File. T is a time stamp, suitable for use in format_time//2 in library(time). For two time stamps A and B, if A precedes B, then A @< B holds. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ file_modification_time(File, T) :- file_time_(File, modification, T). file_access_time(File, T) :- file_time_(File, access, T). file_creation_time(File, T) :- file_time_(File, creation, T). file_time_(File, Which, T) :- file_must_exist(File, file_time_/3), '$file_time'(File, Which, T0), read_term_from_chars(T0, T). /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - path_segments(Ps, Segments): True iff Segments are the segments of Ps. Segments is the list of components of the path Ps that are separated by the platform-specific directory separator. Each segment is a list of characters. At least one of the arguments must be instantiated. Examples: ?- path_segments("/hello/there", Segments). Segments = [[],"hello","there"] ; false. ?- path_segments(Path, ["hello","there"]). Path = "hello/there" ; false. To obtain the platform-specific directory separator, you can use: ?- path_segments(Separator, ["",""]). Separator = "/" ; false. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ path_segments(Path, Segments) :- '$directory_separator'(Sep), ( var(Path) -> must_be(list, Segments), maplist(list_of_chars, Segments), append_with_separator(Segments, Sep, Path) ; list_of_chars(Path), path_to_segments(Path, Sep, Segments) ). append_with_separator([], _, []). append_with_separator([Segment|Segments], Sep, Path) :- append_with_separator_(Segments, Segment, Sep, Path). append_with_separator_([], Segment, _, Segment). append_with_separator_([Segment|Segments], Prev, Sep, Path) :- append(Prev, [Sep|Rest], Path), append_with_separator_(Segments, Segment, Sep, Rest). path_to_segments(Path, Sep, Segments) :- ( append(Front, [Sep|Ps], Path) -> Segments = [Front|Rest], path_to_segments(Ps, Sep, Rest) ; Segments = [Path] ).