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