Loading...
Searching...
No Matches
path.f90
Go to the documentation of this file.
1!> @file
2!! @defgroup group_path Path
3!! A modern, portable Fortran module for path manipulation and basic directory operations.
4!! This module provides a clean interface for working with file system paths
5!! in a platform-independent way. It correctly handles both Unix ('/') and Windows ('\') path
6!! separators through conditional compilation and offers deferred-length character results
7!! for maximum flexibility.
8!!
9!! The module builds upon the @link fpx_string fpx_string @endlink module for @link fpx_string::string string @endlink type support
10!! and provides
11!! overloads of key procedures to accept either intrinsic `character(*)` or `type(string)`
12!! arguments.
13!!
14!! Features include:
15!! - Detection of absolute and rooted paths
16!! - Cross-platform path joining
17!! - Extraction of directory and filename components
18!! - Splitting paths into head/tail elements
19!! - Retrieval and modification of the current working directory
20!! - Support for both intrinsic CHARACTER and type(string) arguments
21!! - Changing the current working directory (`chdir`)
22!!
23!! @note All path-returning functions return allocatable deferred-length characters.
24!! @note The public generic `join` interface works with any combination of `character` and `string`.
25!!
26!! @section path_examples Examples
27!! @code{.f90}
28!! character(:), allocatable :: p1, p2, full
29!!
30!! p1 = '/home/user/docs'
31!! p2 = 'report.pdf'
32!! full = join(p1, p2) ! => '/home/user/docs/report.pdf'
33!!
34!! print *, is_absolute(full) ! .true. (on Unix)
35!! print *, filename(full) ! 'report'
36!! print *, filename(full,.true.) ! 'report.pdf'
37!! print *, dirpath(full) ! '/home/user/docs'
38!! ...
39!! @endcode
40!!
41!! On Windows:
42!! @code{.f90}
43!! character(:), allocatable :: p
44!! p = join('C:\Users', 'Alice', 'Documents')
45!! ! p == 'C:\Users\Alice\Documents'
46!! print *, is_absolute(p) ! .true.
47!! ...
48!! @endcode
49module fpx_path
50 use, intrinsic :: iso_c_binding
51 use fpx_string
52
53 public :: join, &
55 is_rooted, &
56 filename, &
57 dirpath, &
58 dirname, &
59 split_path, &
60 cwd, &
61 chdir
62
63 !! @cond
64#ifdef _WIN32
65 !> Platform-dependent directory separator.
66 !!
67 !! '/' on Unix-like systems,
68 !! '\' on Windows.
69 !!
70 !! @ingroup group_path
71 character, parameter :: separator = '\'
72 character(*), parameter :: alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
73#else
74 !> Platform-dependent directory separator.
75 !!
76 !! '/' on Unix-like systems,
77 !! '\' on Windows.
78 !!
79 !! @ingroup group_path
80 character, parameter :: separator = '/'
81#endif
82
83#ifdef _WIN32
84 interface
85 function getcwd_c(buf, size) bind(C, name='_getcwd') result(r)
86 import
87 implicit none
88 type(c_ptr) :: r
89 ! allow(assumed-size, assumed-size-character-intent)
90 character(kind=c_char), intent(out) :: buf(*)
91 integer(kind=c_size_t), value :: size
92 end function
93 end interface
94#else
95 interface
96 function getcwd_c(buf, size) bind(C, name='getcwd') result(r)
97 import
98 implicit none
99 type(c_ptr) :: r
100 ! allow(assumed-size, assumed-size-character-intent)
101 character(kind=c_char), intent(out) :: buf(*)
102 integer(kind=c_size_t), value :: size
103 end function
104 end interface
105#endif
106
107 interface
108 integer(c_int) function chdir_c(path) bind(C, name='chdir')
109 import
110 implicit none
111 ! allow(assumed-size)
112 character(kind=c_char), intent(in) :: path(*)
113 end function
114 end interface
115 !! @endcond
116
117 !> Join path components using the platform separator.
118 !!
119 !! The generic interface accepts any combination of intrinsic
120 !! `character(*)` and @link fpx_string::string string @endlink
121 !! arguments.
122 !!
123 !! Supported overloads:
124 !! - join(character, character)
125 !! - join(character, string)
126 !! - join(string, character)
127 !! - join(string, string)
128 !!
129 !! @b Examples
130 !! @code{.f90}
131 !! character(:), allocatable :: p
132 !!
133 !! p = join('/usr','bin')
134 !! ! '/usr/bin'
135 !!
136 !! p = join(string('/usr'),'local')
137 !! ! '/usr/local'
138 !! ...
139 !! @endcode
140 !! @ingroup group_path
141 interface join
142 module procedure :: join_character_character
143 module procedure :: join_string_character
144 module procedure :: join_character_string
145 module procedure :: join_string_string
146 end interface
147
148contains
149
150 !> Returns .true. if the path is absolute.
151 !! On Unix a path is absolute when it starts with '/'.
152 !! On Windows a path is absolute when it starts with a drive letter followed by ':\'
153 !! (e.g. 'C:\', 'd:/temp').
154 !!
155 !! @param[in] filepath Path to test
156 !! @return res .true. if filepath is absolute
157 !!
158 !! @code{.f90}
159 !! print *, is_absolute('/home/user') ! .true. (Unix)
160 !! print *, is_absolute('C:\\Temp') ! .true. (Windows)
161 !! print *, is_absolute('docs/..') ! .false.
162 !! ...
163 !! @endcode
164 !!
165 !! @ingroup group_path
166 pure logical function is_absolute(filepath) result(res)
167 character(*), intent(in) :: filepath
168#ifdef _WIN32
169 if (len(filepath) < 2) then
170 res = .false.
171 return
172 end if
173 res = scan(filepath(1:1), alphabet) /= 0 .and. filepath(2:2) == ':'
174#else
175 res = filepath(1:1) == separator
176#endif
177
178 end function
179
180 !> Returns .true. if the path is rooted (starts with a separator) or is absolute.
181 !! A rooted path begins with the platform separator ('\' on Windows, '/' elsewhere)
182 !! even if it is not a full absolute path (e.g. '/temp' on Linux).
183 !!
184 !! @param[in] filepath Path to test
185 !! @return res .true. if filepath is rooted
186 !!
187 !! @ingroup group_path
188 pure logical function is_rooted(filepath) result(res)
189 character(*), intent(in) :: filepath
190 !private
191 integer :: length
192
193 length = len(filepath)
194#ifdef _WIN32
195 res = (length >= 1 .and. filepath(1:1) == separator) .or. is_absolute(filepath)
196#else
197 res = (length > 0 .and. filepath(1:1) == separator)
198#endif
199 end function
200
201 !! Returns the filename component of a path.
202 !!
203 !! The directory portion is discarded. By default, the final
204 !! extension is removed; when `keepext=.true.` the full filename
205 !! is returned unchanged.
206 !! By default the extension is stripped. If keepext=.true. the full filename
207 !! including extension is returned.
208 !!
209 !! @param[in] filepath Full or relative path
210 !! @param[in] keepext Optional; keep extension when .true.
211 !! @return res Filename (without path)
212 !!
213 !! @code{f.90}
214 !! print *, filename('dir/file.txt') ! 'file'
215 !! print *, filename('dir/file.txt',.true.) ! 'file.txt'
216 !! print *, filename('archive.tar.gz') ! 'archive.tar'
217 !! @endcode
218 !!
219 !! @ingroup group_path
220 pure function filename(filepath, keepext) result(res)
221 character(*), intent(in) :: filepath
222 character(:), allocatable :: res
223 logical, intent(in), optional :: keepext
224 !private
225 integer :: ipoint, islash
226
227 ipoint = index(filepath, '.', back=.true.)
228 islash = index(filepath, separator, back=.true.)
229 if (ipoint < islash) ipoint = len_trim(filepath) + 1
230 if (present(keepext)) then
231 if (keepext) then
232 res = filepath(islash + 1:len_trim(filepath))
233 else
234 res = filepath(islash + 1: ipoint - 1)
235 end if
236 else
237 res = filepath(islash + 1: ipoint - 1)
238 end if
239 end function
240
241 !> Implementation of @ref join for character arguments.
242 !!
243 !! @copydetails join
244 !!
245 !! @ingroup group_path
246 pure function join_character_character(path1, path2) result(res)
247 character(*), intent(in) :: path1
248 character(*), intent(in) :: path2
249 character(:), allocatable :: res
250 !private
251 character(:), allocatable :: temp
252
253 temp = trim(adjustl(path1))
254 if (temp(len(temp):len(temp)) == separator) temp = trim(temp(:len(temp) - 1))
255
256 res = temp // separator // trim(adjustl(path2))
257 end function
258
259 !> Implementation of @ref join for character arguments.
260 !!
261 !! @copydetails join
262 !!
263 !! @ingroup group_path
264 pure function join_character_string(path1, path2) result(res)
265 character(*), intent(in) :: path1
266 type(string), intent(in) :: path2
267 character(:), allocatable :: res
268 !private
269 character(:), allocatable :: temp
270
271 temp = trim(adjustl(path1))
272 if (temp(len(temp):len(temp)) == separator) temp = trim(temp(:len(temp) - 1))
273
274 res = temp // separator // trim(adjustl(path2%chars))
275 end function
276
277 !> Implementation of @ref join for character arguments.
278 !!
279 !! @copydetails join
280 !!
281 !! @ingroup group_path
282 pure function join_string_character(path1, path2) result(res)
283 type(string), intent(in) :: path1
284 character(*), intent(in) :: path2
285 character(:), allocatable :: res
286 !private
287 character(:), allocatable :: temp
288
289 temp = trim(adjustl(path1%chars))
290 if (temp(len(temp):len(temp)) == separator) temp = trim(temp(:len(temp) - 1))
291
292 res = temp // separator // trim(adjustl(path2))
293 end function
294
295 !> Implementation of @ref join for character arguments.
296 !!
297 !! @copydetails join
298 !!
299 !! @ingroup group_path
300 pure function join_string_string(path1, path2) result(res)
301 type(string), intent(in) :: path1
302 type(string), intent(in) :: path2
303 character(:), allocatable :: res
304 !private
305 character(:), allocatable :: temp
306
307 temp = trim(adjustl(path1%chars))
308 if (temp(len(temp):len(temp)) == separator) temp = trim(temp(:len(temp) - 1))
309
310 res = temp // separator // trim(adjustl(path2%chars))
311 end function
312
313 !! Returns the directory component of a filesystem path.
314 !!
315 !! This is equivalent to the "head" returned by split_path().
316 !! @param[in] filepath Path to analyse
317 !! @return res Directory component
318 !!
319 !! @code {.f90}
320 !! print *, dirpath('/home/user/file.txt') ! '/home/user'
321 !! @endcode
322 !!
323 !! @ingroup group_path
324 pure function dirpath(filepath) result(res)
325 character(*), intent(in) :: filepath
326 character(:), allocatable :: res
327 !private
328 character(:), allocatable :: temp
329
330 call split_path(filepath, res, temp)
331 end function
332
333 !! Returns the basename component of a filesystem path.
334 !!
335 !! This is equivalent to the "tail" returned by split_path().
336 !! @param[in] filepath Path to analyse
337 !! @return res Base name component
338 !!
339 !! @code{.f90}
340 !! print *, dirname('/home/user/file.txt') ! 'file.txt'
341 !! @endcode
342 !!
343 !! @ingroup group_path
344 pure function dirname(filepath) result(res)
345 character(*), intent(in) :: filepath
346 character(:), allocatable :: res
347 !private
348 character(:), allocatable :: temp
349
350 call split_path(filepath, temp, res)
351 end function
352
353 !> Splits a path into head (directory) and tail (basename) components.
354 !! Special cases:
355 !! - Empty paths return ('.','')
356 !! - Root directories return ('/','')
357 !! - Trailing separators are ignored
358 !! @param[in] filepath Input path
359 !! @param[out] head Directory part (includes trailing separator when appropriate)
360 !! @param[out] tail Base name part
361 !!
362 !! @ingroup group_path
363 pure subroutine split_path(filepath, head, tail)
364 character(*), intent(in) :: filepath
365 character(:), allocatable, intent(out) :: head
366 character(:), allocatable, intent(out) :: tail
367 !private
368 character(:), allocatable :: temp
369 integer :: i, ipoint, isep
370
371 ! Empty string, return (.,'')
372 if (len_trim(filepath) == 0) then
373 head = '.'; tail = ''
374 return
375 end if
376
377 ! Remove trailing path separators
378 temp = trim(adjustl(filepath))
379 if (temp(len(temp):len(temp)) == separator) then
380 temp = trim(temp(:len(temp) - 1))
381 else
382 ipoint = index(filepath, '.', back=.true.)
383 isep = index(filepath, separator, back=.true.)
384 if (ipoint > isep .and. isep > 0) then
385 temp = trim(temp(:isep - 1))
386 end if
387 end if
388
389 if (len_trim(temp) == 0) then
390 head = separator
391 tail = ''
392 return
393 end if
394
395 i = len(temp) - index(temp, separator, back=.true.) + 1
396
397 ! if no `pathsep`, then it probably was a root dir like `C:\`
398 if (i == 0) then
399 head = temp // separator
400 tail = ''
401 return
402 end if
403
404 head = temp(:len(temp) - i)
405
406 ! child of a root directory
407 if (index(temp, separator, back=.true.) == 0) then
408 head = head // separator
409 end if
410
411 tail = temp(len(temp) - i + 2:)
412 end subroutine
413
414 !> Returns the current working directory as a deferred-length character string.
415 !! Returns an empty string if the current directory cannot be determined.
416 !!
417 !! @return res Current working directory
418 !! @code{.f90}
419 !! character(:), allocatable :: here
420 !! here = cwd()
421 !! print *, 'We are in: ', here
422 !! @endcode
423 !!
424 !! @ingroup group_path
425 function cwd() result(res)
426 character(:), allocatable :: res
427 !private
428 character(len=1, kind=c_char) :: buf(256)
429 integer :: i, n
430 integer(c_size_t) :: s
431
432 s = size(buf, kind=c_size_t)
433 if (c_associated(getcwd_c(buf, s))) then
434 n = findloc(buf, achar(0), 1)
435 allocate(character(n - 1) :: res)
436 do i = 1, n - 1
437 res(i:i) = buf(i)
438 end do
439 else
440 res = ''
441 end if
442 end function
443
444 !> Changes the current working directory.
445 !! This is a thin wrapper around the underlying C runtime
446 !! `chdir()` implementation.
447 !! @param[in] path Directory to change to
448 !! @param[out] err Optional integer error code (0 = success, non-zero = failure)
449 !! @code{.f90}
450 !! integer :: ierr
451 !! call chdir('/tmp', ierr)
452 !! if (ierr /= 0) stop 'Failed to change directory'
453 !! @endcode
454 !!
455 !! @ingroup group_path
456 subroutine chdir(path, err)
457 character(*), intent(in) :: path
458 integer, optional, intent(out) :: err
459 integer :: loc_err
460
461 loc_err = chdir_c(path // c_null_char)
462
463 if (present(err)) err = loc_err
464 end subroutine
465end module
pure character(:) function, allocatable join_string_string(path1, path2)
Implementation of join for character arguments.
Definition path.f90:301
pure logical function, public is_rooted(filepath)
Returns .true. if the path is rooted (starts with a separator) or is absolute. A rooted path begins w...
Definition path.f90:189
character(:) function, allocatable, public cwd()
Returns the current working directory as a deferred-length character string. Returns an empty string ...
Definition path.f90:426
pure logical function, public is_absolute(filepath)
Returns .true. if the path is absolute. On Unix a path is absolute when it starts with '/'....
Definition path.f90:167
subroutine, public chdir(path, err)
Changes the current working directory. This is a thin wrapper around the underlying C runtime chdir()...
Definition path.f90:457
pure character(:) function, allocatable join_character_string(path1, path2)
Implementation of join for character arguments.
Definition path.f90:265
pure character(:) function, allocatable join_string_character(path1, path2)
Implementation of join for character arguments.
Definition path.f90:283
pure subroutine, public split_path(filepath, head, tail)
Splits a path into head (directory) and tail (basename) components. Special cases:
Definition path.f90:364
pure character(:) function, allocatable join_character_character(path1, path2)
Implementation of join for character arguments.
Definition path.f90:247
character function, public tail(str)
Returns the last non-blank character of a string.
Definition string.f90:746
character function, public head(str)
Returns the first character of the trimmed string.
Definition string.f90:732
Join path components using the platform separator.
Definition path.f90:141
Locate the position of a substring.
Definition string.f90:395
Return the trimmed length of a string object.
Definition string.f90:201
Return the length of a string object.
Definition string.f90:164
Remove trailing blanks from a string object.
Definition string.f90:240
Represents text as a sequence of ASCII code units. The derived type wraps an allocatable character ar...
Definition string.f90:112