Path manipulation library

I recently updated my Fortran “pathlib” library to have an object-oriented interface.
It works equally well across operating systems (Linux, macOS, Windows).
It is inspired by Python pathlib and C++ filesystem.

a small sample of “path%” methods include:

expanduser()   ! expand tilde '~' to user home directory
with_suffix(new)  ! replace filename suffix
as_posix()  ! switch "\" for "/"
is_directory()  ! directory and not a file
is_file() ! is a file and not a directory
parent() ! one level up path
11 Likes

Excellent, thanks for sharing! Fortran definitely needs that.

I have needed similar functions ( M_path ) that complement some other modules I have; but quite a few of mine require a POSIX C interface and so do not work in several MSWindows environments. Nice. Wondering if you plan on making it available as an fpm(1) package? I particularly like the expanduser() and as_posix() which I do not have an equivalent of but already have programs where I should use them, and the interface is very intuitive. Thanks!

Is there a reason expanduser() is not named expand_user()?

expanduser was named like Python pathlib and os.path method as well as Meson filesystem method

2 Likes

I’m not sure if fpm can yet do the logic for including different source files based on operating system and compiler. Can fpm do “if” logic like that?

I use Windows CRT or Unix CRT as needed.

Looks great! In addition to the procedures you’ve already implemented I think a relative_to procedure similar to the one in pathlib would be highly appreciated.

Also, I would strongly consider making character(len=:), allocatable :: path private inside the path type. You could then implement a constructor that takes a string like this:

type :: path
    private
    character(len=:), allocatable :: path
end type

interface path
    module procedure new_from_chars
end interface

contains

type(path) pure function new_from_chars(chars) result(p)
    character(len=*), intent(in) :: chars
    this%path = chars
end function

Usage:

type(path) :: p
p = path('/foo/bar')

To get the string back (e.g. for printing/opening files/etc.) you could then implement a procedure :: to_chars that takes the path object and returns a character(len=:), allocatable.

Personally I would also name the type path_t and not path so that we can have a variable type(path_t) :: path, but I don’t think there’s any consensus on such naming conventions at the moment.

EDIT: Oh and I forgot to suggest a join procedure to concatenate two paths :smiley: Would also be very handy!

2 Likes

Thank you Pål Levold, I switched the base type name to “path_t” for clarity as you suggest.

I added getter/setter methods as you suggest also.

Very helpful!!

2 Likes

I don’t think fpm can do that, but you might be able to surround the submodule code with preprocessor directives like #ifdef _WIN32 etc as a workaround.

Update: now Fortran-pathlib is entirely based on C++17 filesystem. This avoids the platform-specific and compiler-specific workarounds I had to use in earlier versions of pathlib.

GCC >= 8, Clang, Intel compiler, Visual Studio, etc. have C++17 to work with the Fortran pathlib interface.

I have retained as a fallback the original pathlib C stdlib-based routines as a fallback for non-C++17 systems.

fpm is not yet supported as “build-script” and/or non-Fortran source are not yet fpm features.

pathlib can build with GNU Make, CMake or Meson.

4 Likes

Thank you! :stars:

I guess the current version does not yet support fpm tests (cloned your repository a few moments ago):

$ fpm test
<INFO> No tests to run
STOP 0

$ fpm --version
Version:     0.5.0, alpha
Program:     fpm(1)
Description: A Fortran package manager and build system
Home Page:   https://github.com/fortran-lang/fpm
License:     MIT
OS Type:     Linux

Yes I was incorrect, fpm is not linking in C++ fs.cpp that is needed. thus libpathlib.a is missing the functions provided by C++ with fpm. I think there are some open PRs/issues for FPM but maybe build/link C++ isn’t yet possible with FPM, without using a Makefile/script

Ok, thanks, but I will note that it builds ok:

$ fpm build
 + mkdir -p build/gfortran_2A42023B310FA28D
 + mkdir -p build/gfortran_2A42023B310FA28D/fortran-pathlib/
 + gfortran -c ./src/pathlib.f90  -Wall -Wextra -Wimplicit-interface -fPIC -fmax-errors=1 -g -fcheck=bounds -fcheck=array-temps -fbacktrace -fcoarray=single -J build/gfortran_2A42023B310FA28D -Ibuild/gfortran_2A42023B310FA28D -o build/gfortran_2A42023B310FA28D/fortran-pathlib/src_pathlib.f90.o
 + gfortran -c ./src/find.f90  -Wall -Wextra -Wimplicit-interface -fPIC -fmax-errors=1 -g -fcheck=bounds -fcheck=array-temps -fbacktrace -fcoarray=single -J build/gfortran_2A42023B310FA28D -Ibuild/gfortran_2A42023B310FA28D -o build/gfortran_2A42023B310FA28D/fortran-pathlib/src_find.f90.o
./src/find.f90:58:0:

   58 | p = expanduser(path)
      | 
Warning: ‘.p’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/find.f90:11:0:

   11 | character(:), allocatable :: path1, suff(:)
      | 
Warning: ‘.suff’ is used uninitialized in this function [-Wuninitialized]
./src/find.f90:27:0:

   27 |     get_filename = get_filename // '/' // name
      | 
Warning: ‘.__var_1_realloc_string’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/find.f90:37:0:

   37 | path1 = get_filename
      | 
Warning: ‘.path1’ may be used uninitialized in this function [-Wmaybe-uninitialized]
 + gfortran -c ./src/io.f90  -Wall -Wextra -Wimplicit-interface -fPIC -fmax-errors=1 -g -fcheck=bounds -fcheck=array-temps -fbacktrace -fcoarray=single -J build/gfortran_2A42023B310FA28D -Ibuild/gfortran_2A42023B310FA28D -o build/gfortran_2A42023B310FA28D/fortran-pathlib/src_io.f90.o
 + gfortran -c ./src/fs_cpp.f90  -Wall -Wextra -Wimplicit-interface -fPIC -fmax-errors=1 -g -fcheck=bounds -fcheck=array-temps -fbacktrace -fcoarray=single -J build/gfortran_2A42023B310FA28D -Ibuild/gfortran_2A42023B310FA28D -o build/gfortran_2A42023B310FA28D/fortran-pathlib/src_fs_cpp.f90.o
./src/fs_cpp.f90:571:0:

  571 | cpath = path // C_NULL_CHAR
      | 
Warning: ‘.cpath’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:504:0:

  504 | s1 = trim(path) // C_NULL_CHAR
      | 
Warning: ‘.s1’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:484:0:

  484 | s1 = a // C_NULL_CHAR
      | 
Warning: ‘.s1’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:485:0:

  485 | s2 = b // C_NULL_CHAR
      | 
Warning: ‘.s2’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:470:0:

  470 | csrc = src // C_NULL_CHAR
      | 
Warning: ‘.csrc’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:471:0:

  471 | cdest = dest // C_NULL_CHAR
      | 
Warning: ‘.cdest’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:456:0:

  456 | cpath = path // C_NULL_CHAR
      | 
Warning: ‘.cpath’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:444:0:

  444 | c1 = path1 // C_NULL_CHAR
      | 
Warning: ‘.c1’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:445:0:

  445 | c2 = path2 // C_NULL_CHAR
      | 
Warning: ‘.c2’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:436:0:

  436 | cpath = path // C_NULL_CHAR
      | 
Warning: ‘.cpath’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:428:0:

  428 | cpath = path // C_NULL_CHAR
      | 
Warning: ‘.cpath’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:421:0:

  421 | cpath = path // C_NULL_CHAR
      | 
Warning: ‘.cpath’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:413:0:

  413 | cpath = path // C_NULL_CHAR
      | 
Warning: ‘.cpath’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:362:0:

  362 | cpath = path // C_NULL_CHAR
      | 
Warning: ‘.cpath’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:352:0:

  352 | ctgt = tgt // C_NULL_CHAR
      | 
Warning: ‘.ctgt’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:353:0:

  353 | clink = link // C_NULL_CHAR
      | 
Warning: ‘.clink’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:344:0:

  344 | cpath = path // C_NULL_CHAR
      | 
Warning: ‘.cpath’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:336:0:

  336 | cpath = path // C_NULL_CHAR
      | 
Warning: ‘.cpath’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/fs_cpp.f90:326:0:

  326 | cpath = path // C_NULL_CHAR
      | 
Warning: ‘.cpath’ may be used uninitialized in this function [-Wmaybe-uninitialized]
 + gfortran -c ./src/iter.f90  -Wall -Wextra -Wimplicit-interface -fPIC -fmax-errors=1 -g -fcheck=bounds -fcheck=array-temps -fbacktrace -fcoarray=single -J build/gfortran_2A42023B310FA28D -Ibuild/gfortran_2A42023B310FA28D -o build/gfortran_2A42023B310FA28D/fortran-pathlib/src_iter.f90.o
./src/iter.f90:26:0:

   26 | wk = as_posix(path)
      | 
Warning: ‘.wk’ may be used uninitialized in this function [-Wmaybe-uninitialized]
./src/iter.f90:43:0:

   43 | if(wk(j:j) == "/") wk = wk(:j-1)
      | 
Warning: ‘.__var_1_realloc_string’ may be used uninitialized in this function [-Wmaybe-uninitialized]
 + ar -rs build/gfortran_2A42023B310FA28D/fortran-pathlib/libfortran-pathlib.a build/gfortran_2A42023B310FA28D/fortran-pathlib/src_find.f90.o build/gfortran_2A42023B310FA28D/fortran-pathlib/src_io.f90.o build/gfortran_2A42023B310FA28D/fortran-pathlib/src_fs_cpp.f90.o build/gfortran_2A42023B310FA28D/fortran-pathlib/src_iter.f90.o build/gfortran_2A42023B310FA28D/fortran-pathlib/src_pathlib.f90.o
ar: creating build/gfortran_2A42023B310FA28D/fortran-pathlib/libfortran-pathlib.a

The error message with fpm test looks more like tests are not found, not that they are found and fail.

I attempt to use Makefile from fpm with parameter:

[library]
build-script="src/Makefile"

I’m not sure why that doesn’t work. Maybe my fpm 0.5.0 is too old?

Yes I think fpm is not recognizing that I’ve specified in fpm.toml to use src/Makefile. Fpm instead tries to build like a default fpm project, and then the test fails to link because it’s missing the fs.cpp content and c++ flags.

Using just the Makefile works from src/ directory make produces pathlib.a, which can be used like

gfortran tests/test_pathlib.f90 pathlib.a -lstdc++

In my last post I was pointing out that, reading the console message

I am under the impression that fpm doesn’t find the tests, not that there is a linking error. I may be mistaken though.

To clarify: fpm does not yet support c++ sources. You can use fpm build --list to check which objects will be compiled.

Because there are no tests detected, fpm doesn’t get to the link step so we won’t see any link errors. (try moving ./src/tests folder to ./test).

Unfortunately fpm doesn’t currently support the build-script option.

But this part of fpm.toml

...
[[ test ]]
name="core"
source-dir = "src/tests"
main="test_pathlib.f90"

made me think that fpm knows where the tests are located… :thinking:

Yes, that should work; I think it’s been updated since I last tried it.

OK thanks I updated the note to show that pathlib can build with Make, CMake or Meson. Thanks!