Differences in file_exists/1 semantics [GNU versus SICStus]

For a library(report) I use this bootstrapping:

% ensure_directory(+Atom)
ensure_directory(F) :-
   file_exists(F),
   file_property(F, type(directory)), !.
ensure_directory(F) :-
   make_directory(F).

If I don’t do it like this and directly call make_directory/1 then
make_directory/1 barks, which is consistent on most Prolog systems.
Also consistent with the underlying operating system calls.

Works fine on GNU Prolog. But file_property/2 and file_exists/1 are
different. file_exists/1 in SWI-Prolog expects a regular file, in GNU
Prolog it doesn’t need to be a regular file, just a file which can

be regular, directory, etc… file_property/2 in SWI-Prolog doesn’t
return a type/1 property. Its also more key-value oriented. I think
the stuff is in SICStus library(file_systems).

Quite a dilemma. What now? So I could write my own file_systems
library in SWI-Prolog for GNU Prolog portability? Did somebody
do this already? Or whats the idea here?

file_exists/1 and file_property/2 are from the SICStus emulation library, so they are not in our hands. SWI-Prolog’s native is exists_file/1 and exists_directory/1. The latter is what you want and something you can implement in GNU Prolog to reach the same common ground.

A bit more standard to the file system access would be nice :slight_smile:

Don’t take me wrong, the library(report) is only an example.
I am looking for a boostrapping of file_exists/1 in SWI-Prolog.
How should this work? I would possibly do, two primitive

calls instead of a single primitive call?

/* ?Jan W. suggestion? */
file_exists(F) :-  
    exists_file(F), !. /* existence and regular type */
file_exists(F) :-
    exists_directory(F).

Its not the same semantics. Does SWI-Prolog have no
native file_exists/1 ? Maybe some file_access/2 ? How
do you boostrap propertly in SWI-Prolog file_exists/1 and

file_property/2. Need to think. Also have to double check
whether GNU Prolog really works for non-regular and non-directory.
At least it has some other types than regular and directory here:

type(Type): Type is the type of PathName.
Possible values are: regular, directory, fifo, socket,
character_device, block_device or unknown.
http://www.gprolog.org/manual/html_node/gprolog051.html#file-property%2F2

Edit 19.11.2023
Maybe there should be also a type link? GNU Prolog
might be also not perfect or the last word. Lets say the requirement
is that links are followed, both links, links in the directory segments

of the given path, and links in the final name of the given path.
file_exists/1 should not stumble and bark when a file name exists,
is a link and the target of the link doesn’t exist, it should simply fail?

To guard against the timing issue of some other process creating the directory in the time between our check-for-existence and creation-attempt, it should do the equivalent of mkdir -p in shell scripts, i.e. run make_directory_path/1

The difference:

?- make_directory('testbad').
true.

?- make_directory('testbad').
ERROR: directory `testbad' does not exist (File exists)
ERROR: In:
ERROR:   [10] make_directory(testbad)
ERROR:    [9] toplevel_call(user:user: ...) at /usr/lib/swipl/boot/toplevel.pl:1173

vs:

?- make_directory_path('testgood').
true.  % Was created

?- make_directory_path('testgood').
true.  % Already exists - success

Does too much for the use case at hand. It creates all directory
segments. I only want to create one directory. You see the
difference between the two also in the JavaScript API:

fs.mkdirSync(path[, options])
recursive Default: false
https://nodejs.org/api/fs.html

In JavaScript its just one API with a recursive option. But
they retain the usual operating system semantics when recursive=false,
that it will also bark when the directory already exists.

A predicate that creates only one directory segment on demand
does not exist, because make_directory_path/1 does too much,
and make_directory/1 does too less. So you have to bootstrap it.

ensure_directory/1 is more defensive than make_directory_path/1.
Its safer for scripting. If you have an errorneaous path in make_directory_path/1
it will arbitarily write into your harddisk, since make_directory_path/1

might create the path against your intention. For ensure_directory/1
you have the requirement that the parent directory already existing.
But its not meant to be used in a parallel multi-user environment for shared

directories, its not a concurrent use case that I have.

Edit 19.11.2023
Or with user interaction, again recursive=false and nothing parallel:

/* assumption, parent already exists */
ensure_directory_interactive(F) :-
    \+ file_exists(F), !,
    ask('Directory does not yet exist, do you want to create it?', y),
    make_directory(F).
ensure_directory_interactive(F) :-
    \+ file_property(F, type(directory)), !,
    prompt('Cannot create directory, already other file at location.'), fail.
ensure_directory_interactive(_).

If file_exists/1 has wrong semantics, the above script doesn’t work
correctly. I also tend now that it should not follow links, and fail if a link
is broken. But for user interaction, and when its an important path in

script, I would indeed rather use make_directory_path/1. ensure_directory/1 is
more conceived for automation, when the important paths already exist and
you want to overwrite and/or create some content. Since its more defensive

it prevents errorneous overwrite. Also an interactive script might want to also
check access rights and give the end-user some hints, what do to or guide it
through more stuff. Thats also not covered by file_exists/1.

Then it would be clearer to have 2 arguments to the predicate, e.g.:

ensure_single_directory('/home/myname/myplaydir', 'dir-under-myplaydir').

… so that failure can be quick, if ‘/home/myname/myplaydir’ is not already a directory.

You seem to mean the GNU-Prolog version. This SICStus version behaves as exists_file/1. I’m not claiming one of them is better than the other. ISO says AFAIK nothing about accessing the file system. I forgot where SWI-Prolog’s exists_file/1 comes from. Most likely the name and semantics are copied from some system as that is what I typically do if there is no good reason otherwise. Quintus Prolog is typically a good guess for predicates that existed already in old versions :slight_smile:

Anyway, to test for existence of a name in the file system, use access_file(Path, exists). That does come from Quintus AFAIK.

As @brebs states, a correct implementation is hard. The safe way is to deal with the error of make_directory/1, but the error is system dependent. If the entry exists in the file system, SWI-Prolog raises error(existence_error(directory, Dir), _), which is misleading. SICStus says error(system_error,system_error('create_directory error: SPIO_E_FILE_EXISTS')) and GNU-Prolog error(system_error('File exists'),make_directory/1). Seems GNU-Prolog uses the strerror() string, which makes it OS and locale dependent and thus hard to act upon.

make_directory_path/1 does this correctly (I hope).

If you want portability, you either create your own abstraction or you take an existing one and emulate that for the others. SWI-Prolog comes with the dialect emulation mechanism that was developed together with YAP. There is no GNU-Prolog implementation, but no reason not to create one. It is a nice challenge to emulate the integer wrap-around semantics :slight_smile:

1 Like

(Nothing of importance)

Its not documented. Its not listed here:

access_file(+File, +Mode)
SWI-Prolog -- access_file/2

Could be exit though. This is listed. Need to test it a little bit
before I can say more. Test it on Windows and WSL for example.
Could be indeed a way forward for some bootstrapping.

Edit 19.11.2023
Yeah, works also for exotic file types,
a character special file:

$ ls -la /dev/tty8
crw------- 1 root root 4, 8 Nov 19 23:35 /dev/tty8

I get with an old version of SWI-Prolog on WSL2:

/* SWI-Prolog 8.4.2 */
?- access_file('/dev/tty8', exist).
true.

Here is a code list, maybe incomplete,
what file_property/2 type/1 could give:

c      if the entry is a character special file;
b      if the entry is a block special file;
d      if the entry is a directory;
a      if the entry is an append-only file;
D      if the entry is a Unix device;
L      if the entry is a symbolic link;
P      if the entry is a named pipe;
S      if the entry is a socket;
-      if the entry is a plain file.