!> @file path_util.f90 !> @brief Case-relative path resolution for file-valued namelist parameters. !! !! Policy (see doc/superpowers/specs/2026-07-04-case-paths-inline-config-mp-gather): !! every file the namelist references (grid, and later restart/data/output) is !! resolved against the **case directory** — by default the directory of the !! namelist itself — with ABSOLUTE paths honored as-is. This is the OpenFOAM !! `-case` / `#include`-relative model: a case is a movable folder, resolvable !! regardless of the process working directory. module path_util implicit none private public :: resolve_case_path, path_dirname, is_absolute_path, case_base_dir contains !> True if `path` is absolute: POSIX "/…", a Windows drive "X:\…"/"X:/…", !! or a UNC "\\…" path. Used so a user-supplied absolute grid path is left !! untouched while relative paths are anchored to the case directory. pure logical function is_absolute_path(path) result(is_abs) character(len=*), intent(in) :: path integer :: n character :: c1 is_abs = .false. n = len_trim(path) if (n == 0) return c1 = path(1:1) if (c1 == '/' .or. c1 == '\') then is_abs = .true. else if (n >= 3) then ! Windows drive-ABSOLUTE path, e.g. C:\ or C:/. A drive letter followed ! by ':' but NOT a separator (e.g. C:cases) is drive-*relative* — relative ! to the cwd on that drive — so it is deliberately NOT treated as absolute. if (((c1 >= 'A' .and. c1 <= 'Z') .or. (c1 >= 'a' .and. c1 <= 'z')) & .and. path(2:2) == ':' .and. (path(3:3) == '\' .or. path(3:3) == '/')) & is_abs = .true. end if end function is_absolute_path !> The case directory used to anchor a namelist's relative file paths: the !! explicit `override` when supplied and non-empty (the OpenFOAM `-case` / !! GUI-supplied case dir), otherwise the namelist file's own directory. This !! is the single place the "override-or-namelist-dir" rule lives. pure function case_base_dir(nml_path, override) result(dir) character(len=*), intent(in) :: nml_path character(len=*), intent(in), optional :: override character(len=:), allocatable :: dir if (present(override)) then if (len_trim(override) > 0) then dir = trim(override) return end if end if dir = trim(path_dirname(nml_path)) end function case_base_dir !> Directory part of `path` (everything up to the last '/' or '\'); '.' when !! `path` has no directory component. The trailing separator is dropped. pure function path_dirname(path) result(dir) character(len=*), intent(in) :: path character(len=len(path)) :: dir integer :: i, isep isep = 0 do i = len_trim(path), 1, -1 if (path(i:i) == '/' .or. path(i:i) == '\') then isep = i exit end if end do if (isep == 0) then dir = '.' else if (isep == 1) then dir = path(1:1) ! root "/" (or "\") else dir = path(1:isep - 1) end if end function path_dirname !> Resolve a namelist file-valued parameter against a case directory. !! Absolute or empty `value` is returned unchanged; a relative `value` is !! joined onto `base_dir` with a '/' (accepted by the runtime on both POSIX !! and Windows). A '.' or empty `base_dir` leaves `value` unchanged. pure function resolve_case_path(base_dir, value) result(path) character(len=*), intent(in) :: base_dir, value character(len=len_trim(base_dir) + len_trim(value) + 1) :: path integer :: nb character :: last nb = len_trim(base_dir) if (len_trim(value) == 0 .or. is_absolute_path(value) & .or. nb == 0 .or. trim(base_dir) == '.') then path = value else ! Don't double the separator when base_dir already ends in one (e.g. the ! filesystem root '/' or '\'), which on POSIX would make an ! implementation-defined leading '//'. last = base_dir(nb:nb) if (last == '/' .or. last == '\') then path = trim(base_dir)//trim(value) else path = trim(base_dir)//'/'//trim(value) end if end if end function resolve_case_path end module path_util