/** 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. */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Written 2020, 2022 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, rename_file/2, file_copy/2, delete_directory/1, make_directory/1, make_directory_path/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)). :- use_module(library(dcgs)). %% directory_files(+Directory, -Files). % % Returns the list of files *and* directories available at a specific % directory in the current system. directory_files(Directory, Files) :- must_be(chars, Directory), can_be(list, Files), '$directory_files'(Directory, Files). %% file_size(+File, -Size). % % Returns the size (in bytes) of a file. The file must exist. file_size(File, Size) :- file_must_exist(File, file_size/2), can_be(integer, Size), '$file_size'(File, Size). %% file_exists(+File). % % Succeeds if File is a file that exists in the current system. file_exists(File) :- must_be(chars, File), '$file_exists'(File). %% directory_exists(+Directory). % % Succeeds if Directory is a directory that exists in the current system. directory_exists(Directory) :- must_be(chars, Directory), '$directory_exists'(Directory). %% make_directory(+Directory). % % Succeeds if it creates a new directory named Directory in the current system. % If you want to create a nested directory, use `make_directory_path/1`. make_directory(Directory) :- must_be(chars, Directory), '$make_directory'(Directory). %% make_directory_path(+Directory). % % Similar to `make_directory/1` but recursively creates directories if they're missing. % Equivalent to mkdir -p in Unix. make_directory_path(Directory) :- must_be(chars, Directory), '$make_directory_path'(Directory). %% delete_file(+File). % % Succeeds if deletes File from the current system. delete_file(File) :- file_must_exist(File, delete_file/1), '$delete_file'(File). %% rename_file(+File, +Renamed). % % Succeeds if File is renamed to Renamed rename_file(File, Renamed) :- file_must_exist(File, rename_file/2), must_be(chars, Renamed), '$rename_file'(File, Renamed). %% file_copy(+File, +Copied). % % Succeeds if File is copied to Copied file_copy(File, Copied) :- file_must_exist(File, file_copy/2), must_be(chars, Copied), '$file_copy'(File, Copied). %% delete_directory(+Directory). % % Succeeds if Directory is deleted from the current system. % Directory must be empty. delete_directory(Directory) :- directory_must_exist(Directory, delete_directory/1), must_be(chars, Directory), '$delete_directory'(Directory). file_must_exist(File, Context) :- ( file_exists(File) -> true ; throw(error(existence_error(file, File), Context)) ). directory_must_exist(Directory, Context) :- ( directory_exists(Directory) -> true ; throw(error(existence_error(directory, Directory), Context)) ). %% working_directory(Dir0, Dir). % % Dir0 is the current working directory, and the working directory % is changed to Dir. % % Use `working_directory/2` 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). %% path_canonical(Ps, Cs). % % 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(chars, 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). % % For a file File that must exist, it returns a time stamp T with the modification time % % T is a time stamp compatible with `library(time)`. file_modification_time(File, T) :- file_time_(File, modification, T). %% file_access_time(+File, -T). % % For a file File that must exist, it returns a time stamp T with the access time % % T is a time stamp compatible with `library(time)`. file_access_time(File, T) :- file_time_(File, access, T). %% file_creation_time(+File, -T). % % For a file File that must exist, it returns a time stamp T with the creation time % % T is a time stamp compatible with `library(time)`. 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_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"]. % ?- path_segments(Path, ["hello","there"]). % Path = "hello/there". % ``` % % To obtain the platform-specific directory separator, you can use: % % ``` % ?- path_segments(Separator, ["",""]). % Separator = "/". % ``` path_segments(Path, Segments) :- '$directory_separator'(Sep), ( var(Path) -> must_be(list, Segments), maplist(must_be(chars), Segments), phrase(append_with_separator(Segments, Sep), Path) ; must_be(chars, Path), path_to_segments(Path, Sep, Segments) ). append_with_separator([], _) --> []. append_with_separator([Segment|Segments], Sep) --> append_with_separator_(Segments, Segment, Sep). append_with_separator_([], Segment, _) --> seq(Segment). append_with_separator_([Segment|Segments], Prev, Sep) --> seq(Prev), [Sep], append_with_separator_(Segments, Segment, Sep). path_to_segments(Path, Sep, Segments) :- ( append(Front, [Sep|Ps], Path) -> Segments = [Front|Rest], path_to_segments(Ps, Sep, Rest) ; Segments = [Path] ).