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