Using absolute_file_name/3 and user:file_search_path/2

Note:
If you have Trust level: Basic you can edit this Wiki topic by clicking on the edit icon in the lower right. Capture

Note:
Do not reply to this topic; questions, concerns, comments, etc. are to be handled in
Wiki Discussion: Using absolute_file_name/3 and user:file_search_path/2



References:
file_search_path/2
absolute_file_name/3

Often software applications need a path to a file while having the freedom to be independent of a hard coded path.

The SWI-Prolog solution to this problem, as well as some other Prolog systems, is via a system of composing paths. The fixed and relative paths are called alias. Aliases via composition can generate list of paths, be warned that not all generated paths exist, more on this later. The association between alias and path is via the hook predicate user:file_search_path/2 which can be fact(s) and/or have a body. (ref) The hook is used by absolute_file_name/2,3 which generates paths based on combinations of aliases.

Now all compositions can not be relative as there has to be some fixed starting path that can be used with relative paths to compose a desired final path.

Here are some examples of how fixed starting paths are located:

  • OS fixed starting path are typically found in the environment variables, see: getenv/2
    • The environment variable path which SWI-Prolog sets up upon startup as the alias path, e.g. query user:file_search_path(path,Path).
    • A specific environment variable. SWI-Prolog installation path is setup to the alias swi, e.g. query user:file_search_path(swi,Path).
  • The application (AKA myapp) fixed starting path can be determined using prolog_load_context/2 as a Prolog directive (ref) with key directory, (ref)
  • When SWI-Prolog packages are installed they are added to the alias library, e.g. query user:file_search_path(library,Path).

Here are some examples of how relative paths are created:

  • Current directory, .. This is often used to translate an alias into an absolute directory name, e.g. query absolute_file_name(swi(.),Path).
  • Parent directory, ... A common use for this is to get from a source directory returned using prolog_load_context/2 with key directory to the home directory of an application. Then the home directory is used as the base directory for creating more alias.
  • Child directory , /. A common use for this is with projects with multiple subdirectories such as web sites, (ref).
  • Simple composed aliases, library(yall). This is the most commonly found usage often seen as :- use_module(library(yall)). When used with use_module/2, the use of absolute_file_name is found here, e.g. query absolute_file_name(library(yall),Path,[file_type(prolog),access(read)]).
  • Complex composed aliases, library(dcg/basics). e.g query absolute_file_name(library(dcg/basics),Path,[file_type(prolog),access(read)]).

The beauty of this system of compositions is that you don’t always have to know the exact path for a file if given some combination of aliases that can produce the correct path. For example git is often installed but the install location can very. Often when git is installed it adds its home path to the environment variable path. If git was installed allowing the environment variable path to be updated then the composed alias path(git) should work.

On my Windows system

?- absolute_file_name(path(git),Path,[solutions(all),extensions([exe]),access(exist)]).
Path = 'c:/program files/git/cmd/git.exe' ;
false.

Using Windows where to verify

C:\Users\Groot>where git
C:\Program Files\Git\cmd\git.exe

As I noted earlier not all paths returned by absolute_file_name/2,3 exist. If the example used for path(git) leaves out access(exist) then a parade of paths are generated with only one being valid.


What if an existing combination of aliases does not work?

In that case you can add user defined alias.

Here is an example where there are multiple versions of GhostScript installed on a system and a specific version is needed.

Since GhostScript is typically installed relative to the Windows environment variable ProgramFiles a fixed alias for program_files is created.

user:file_search_path(program_files,Dir) :-
    current_prolog_flag(windows,true),
    getenv('ProgramFiles',Dir).

Note that hooks need to be associated with a module otherwise they will become a part of the module they are defined and not be accessible for calling as needed. file_search_path/2 needs to be with module user for use by absolute_file_name/2,3.

GhostScript installation is into a parent directory of gs with a child directory for different versions then the executable is in the bin directory. So a relative alias for gs_path is created with the alias program_files as the starting alias.

user:file_search_path(gs_path,program_files('gs/gs9.50/bin')).

This still does not get us to the executable file, for that the combination of the alias gs_path with the name of the executable gswin64c is needed. However that will not return the file name with the file type of exe and for that the extensions option of absolute_file_name/2,3 is needed. Thus to get the full absolute file name

?- absolute_file_name(gs_path(gswin64c),Path,[extensions([exe])]).
Path = 'c:/program files/gs/gs9.50/bin/gswin64c.exe'.

If you look for many examples of absolute_file_name/2,3 using executables you will notice that they don’t use the extensions option with exe this is because they most likely are used with a predicate like process_create/3 that adds the option for the executable, e.g.

Call to process_create/3

process_create(gs_path(gswin64c),...)

process_create/3 implementation (ref)

process_create(Exe, Args, Options) :-
   exe_options(ExeOptions),
   absolute_file_name(Exe, PlProg, ExeOptions),
   ...

Note:
If you are creating hooks such as user:file_search_path with the top level (REPL) then you need to assert the hook for it to work, e.g.

Welcome to SWI-Prolog (threaded, 64 bits, version 8.5.10)
...

?- user:file_search_path(program_files,Path).
false.

% This will not work
?- user:file_search_path(program_files,Dir) :- current_prolog_flag(windows,true),getenv('ProgramFiles',Dir).
ERROR: Unknown procedure: (:-)/2

% assert/1 needs to be used
?- assert((user:file_search_path(program_files,Dir) :- current_prolog_flag(windows,true),getenv('ProgramFiles',Dir))).
true.

?- user:file_search_path(program_files,Path).
Path = 'C:\\Program Files'.

However in source code assert/1 is typically not needed, just enter the predicate as a fact or with a body and don’t forget to qualify the module as user. A case where assert/1 is needed for user:file_search_path/2 even in code is with volatile clauses, e.g. prolog_load_context/2 (ref).


Notes:

On Linux ~ is often used for \home\<user>. When ~ is used as part of an alias with use_module/1 it will cause the predicate to fail.

As noted in absolute_file_name/3 for the option expand(Boolean)

This is a SWI-Prolog extension intended to minimise porting effort after SWI-Prolog stopped expanding environment variables and the ~ by default. This option should be considered deprecated. In particular the use of wildcard patterns such as * should be avoided.


Also see:
Why is prolog_load_context with user:file_search_path done this way?



Useful queries

On Windows

?- setof(Name,[A]^(user:file_search_path(Name,A)),Names);true.
Names = [app_config, app_data, app_preferences, autoload, common_app_config, common_app_data, foreign, library, pack, path, pce, swi, user_app_config, user_app_data, user_profile] ;
true.
?- absolute_file_name(pack(.),Path).
Path = 'c:/users/groot/appdata/local/swi-prolog/pack'.

On WSL 2

?- setof(Name,[A]^(user:file_search_path(Name,A)),Names);true.
Names = [app_config, app_data, app_preferences, autoload, foreign, library, pack, path, pce, swi, user_app_config, user_app_data, user_profile] ;
true.
?- absolute_file_name(pack(.),Path).
Path = '/home/groot/.local/share/swi-prolog/pack'.