Loading...
Searching...
No Matches
include.f90
Go to the documentation of this file.
1!> @file
2!! @defgroup group_include Include
3!! Include file handling and resolution for the fpx Fortran preprocessor
4!!
5!! This module implements robust and standard-compliant processing of `#include` directives
6!! with full support for:
7!! - Both forms: `#include "file.h"` (local/user) and `#include <file.h>` (system)
8!! - Proper search order: quotes search source dir first, angle brackets skip source dir
9!! - Relative paths resolved against the directory of the parent source file
10!! - Search in user-defined include directories (`global%includedir`)
11!! - Search in system PATH environment variable directories
12!! - Fallback to current working directory
13!! - Proper error reporting with file name and line number context
14!! - Recursion safety through integration with the main preprocessor loop
15!! - Seamless integration via the abstract `preprocess` procedure pointer
16!!
17!! The routine correctly strips quotes or angle brackets, performs path resolution,
18!! checks file existence, opens the file, and recursively invokes the main preprocessing
19!! engine on the included content using the same macro environment.
20!!
21!! <h2 class="groupheader">Search Order</h2>
22!!
23!! For `#include "file"`:
24!! 1. Directory of the parent source file
25!! 2. Directories specified by -I or -Y options (global%includedir)
26!! 3. Directories in INCLUDE environment variable
27!! 4. Current working directory
28!!
29!! For `#include <file>`:
30!! 1. Directories specified by -I or -Y options (global%includedir)
31!! 2. Directories in INCLUDE environment variable
32!! 3. Current working directory
33!!
34!! <h2 class="groupheader">Examples</h2>
35!!
36!! 1. Include a local header from the same directory using quotes:
37!! @code{.f90}
38!! #include "config.h"
39!! !> fpx will look for ./config.h relative to the current source file first
40!! @endcode
41!!
42!! 2. Include a system header using angle brackets:
43!! @code{.f90}
44!! #include <stdlib.h>
45!! !> fpx will skip the source directory and search -I paths, then PATH
46!! @endcode
47!!
48!! 3. Using from the driver program (adding include paths):
49!! @code{.f90}
50!! global%includedir = ['/usr/include', './include', './headers']
51!! call preprocess('main.F90', 'main.f90')
52!! !> All #include <...> will search these directories in order
53!! @endcode
54!!
55!! 4. Verbose error reporting when a file is not found:
56!! @code{.txt}
57!! $ fpx -v src/utils.F90
58!! Error: Cannot find include file 'missing.h' at src/utils.F90:27
59!! @endcode
60module fpx_include
61 use iso_fortran_env, only : iostat_end
62 use fpx_constants
63 use fpx_logging
64 use fpx_path
65 use fpx_string
66 use fpx_macro
67 use fpx_global
68
69 implicit none; private
70
71 public :: handle_include
72
73 ! Include directive types
74 integer, parameter, private :: INCLUDE_TYPE_SYSTEM = 1 ! < >
75 integer, parameter, private :: INCLUDE_TYPE_LOCAL = 2 ! " "
76#ifdef _WIN32
77 integer, parameter, private :: MAX_PATH_LEN = 256
78#else
79 integer, parameter, private :: MAX_PATH_LEN = 4096
80#endif
81 !> Abstract interface for the main preprocessing routine (used for recursion)
82 !! Allows handle_include to recursively call the top-level preprocess_unit routine
83 !! without creating circular module dependencies.
84 !!
85 !! @b Remarks
86 !! @ingroup group_include
87 interface
88 subroutine read_unit(iunit, ounit, macros, from_include)
89 import macro
90 implicit none
91 integer, intent(in) :: iunit
92 integer, intent(in) :: ounit
93 type(macro), allocatable, intent(inout) :: macros(:)
94 logical, intent(in) :: from_include
95 end subroutine
96 end interface
97
98contains
99
100 !> Process a #include directive encountered during preprocessing
101 !! Resolves the include file name (quoted or angle-bracketed), searches for the file
102 !! using standard C preprocessor rules:
103 !! - Quoted includes search: parent directory, -I paths, PATH, cwd
104 !! - Angle bracket includes search: -I paths, PATH, cwd (skips parent directory)
105 !! Opens the file and recursively preprocesses its contents into the output unit.
106 !!
107 !! @param[in] input Full line containing the #include directive
108 !! @param[in] ounit Output unit where preprocessed content is written
109 !! @param[in] parent_file Path of the file containing the #include
110 !! @param[in] iline Line number in parent file (for error messages)
111 !! @param[in] preprocess Procedure pointer to the main line-by-line preprocessor
112 !! @param[inout] macros Current macro table (shared across recursion levels)
113 !! @param[in] token Usually 'INCLUDE' – the directive keyword
114 !!
115 !! @b Remarks
116 !! @ingroup group_include
117 recursive subroutine handle_include(input, ounit, parent_file, iline, preprocess, macros, token)
118 character(*), intent(in) :: input
119 integer, intent(in) :: ounit
120 character(*), intent(in) :: parent_file
121 integer, intent(in) :: iline
122 procedure(read_unit) :: preprocess
123 type(macro), allocatable, intent(inout) :: macros(:)
124 character(*), intent(in) :: token
125 !private
126 character(:), allocatable :: include_file
127 character(:), allocatable :: dir, ifile
128 character(:), allocatable :: sys_paths(:)
129 integer :: i, iunit, ierr, pos
130 integer :: include_type
131 logical :: exists
132
133 ! Extract the directory of the parent file
134 dir = dirpath(parent_file)
135 ! Find the position after the #include token
136 pos = index(uppercase(input), token) + len(token)
137 include_file = trim(adjustl(input(pos:)))
138
139 ! Determine include type and extract filename
140 if (include_file(1:1) == '"') then
141 include_type = include_type_local
142 include_file = include_file(2:index(include_file(2:), '"'))
143 else if (include_file(1:1) == '<') then
144 include_type = include_type_system
145 include_file = include_file(2:index(include_file(2:), '>'))
146 else
147 ! Malformed include directive
148 if (verbose) print *, 'Error: Malformed #include directive at ', trim(parent_file), ':', iline
149 return
150 end if
151
152 ! Handle absolute/rooted paths (same for both types)
153 ifile = include_file
154 if (is_rooted(ifile)) then
155 inquire(file=ifile, exist=exists)
156 if (exists) then
157 include_file = ifile
158 else
159 if (verbose) then
160 print *, "Error: Cannot find include file '", trim(include_file), "' at ", trim(parent_file), ":", iline
161 return
162 end if
163 end if
164 else
165 ! Relative path - search according to include type
166 exists = .false.
167 ! For quoted includes (#include "file"), search parent directory first
168 ifile = join(dir, include_file)
169 if (include_type == include_type_local) then
170 ifile = join(dir, include_file)
171 inquire(file=ifile, exist=exists)
172 if (exists) then
173 include_file = ifile
174 end if
175 end if
176
177 ! If not found yet, search user-specified include directories (-I paths)
178 if (.not. exists .and. allocated(global%includedir)) then
179 do i = 1, size(global%includedir)
180 ifile = join(global%includedir(i), include_file)
181 inquire(file=ifile, exist=exists)
182 if (exists) then
183 include_file = ifile
184 exit
185 end if
186 end do
187 end if
188
189 ! If still not found, try the INCLUDE environmental variable
190 if (.not. exists) then
191 block
192 character(:), allocatable :: ipaths(:)
193
194 ipaths = get_system_paths()
195 do i = 1, size(ipaths)
196 ifile = join(ipaths(i), include_file)
197 inquire(file=ifile, exist=exists)
198 if (exists) then
199 include_file = ifile
200 end if
201 end do
202 end block
203 end if
204
205 ! If still not found, try current working directory as last resort
206 if (.not. exists) then
207 ifile = join(cwd(), include_file)
208 inquire(file=ifile, exist=exists)
209 if (exists) then
210 include_file = ifile
211 end if
212 end if
213
214 ! If file was not found anywhere, report error
215 if (.not. exists) then
216 if (verbose) print *, "Error: Cannot find include file '", trim(include_file), "' at ", trim(parent_file), ":", iline
217 return
218 end if
219 end if
220
221 ! Open and preprocess the include file
222 open(newunit=iunit, file=include_file, status='old', action='read', iostat=ierr)
223 if (ierr /= 0) then
224 if (verbose) print *, "Error: Cannot open include file '", trim(include_file), "' at ", trim(parent_file), ":", iline
225 return
226 end if
227
228 call preprocess(iunit, ounit, macros, .true.)
229 close(iunit)
230 end subroutine
231
232 !> Get system include paths from PATH environment variable
233 !! Returns an array of directory paths found in PATH
234 !! @return Array of path strings, empty if PATH not set
235 !!
236 !! @b Remarks
237 !! @ingroup group_include
238 function get_system_paths() result(paths)
239 character(:), allocatable :: paths(:)
240 !private
241 character(:), allocatable :: path_env, tmp(:)
242 integer :: lpath, i, n_paths, start_pos, end_pos, count
243 character(len=1) :: path_sep
244
245#ifdef _WIN32
246 path_sep = ';' ! Windows path separator
247#else
248 path_sep = ':' ! Unix/Linux/Mac path separator
249#endif
250
251 ! Get PATH environment variable length
252 call get_environment_variable('INCLUDE', length=lpath)
253 if (lpath <= 0) then
254 allocate(character(len=0) :: paths(0)); return
255 end if
256
257 ! Allocate and retrieve PATH value
258 allocate(character(len=lpath) :: path_env)
259 call get_environment_variable('INCLUDE', value=path_env)
260
261 ! Count number of paths (number of separators + 1)
262 n_paths = 1
263 do i = 1, len(path_env)
264 if (path_env(i:i) == path_sep) n_paths = n_paths + 1
265 end do
266
267 ! Allocate temporary array with maximum size
268 allocate(character(len=MAX_PATH_LEN) :: tmp(n_paths))
269
270 ! Split INCLUDE into individual directories
271 count = 0
272 start_pos = 1
273 do i = 1, len(path_env) + 1
274 if (i > len(path_env) .or. path_env(i:i) == path_sep) then
275 if (i > len(path_env)) then
276 end_pos = i - 1
277 else
278 end_pos = i - 1
279 end if
280
281 if (end_pos >= start_pos) then
282 count = count + 1
283 tmp(count) = trim(adjustl(path_env(start_pos:end_pos)))
284 end if
285 start_pos = i + 1
286 end if
287 end do
288
289 ! Allocate result array with actual count
290 if (count > 0) then
291 allocate(character(len=MAX_PATH_LEN) :: paths(count))
292 paths(:) = tmp(1:count)
293 else
294 allocate(character(len=0) :: paths(0))
295 end if
296 end function
297
298end module
type(global_settings), public global
The single global instance used throughout fpx Initialized automatically with sensible defaults value...
Definition global.f90:92
recursive subroutine, public handle_include(input, ounit, parent_file, iline, preprocess, macros, token)
Process a include directive encountered during preprocessing Resolves the include file name (quoted o...
Definition include.f90:118
character(:) function, dimension(:), allocatable get_system_paths()
Get system include paths from PATH environment variable Returns an array of directory paths found in ...
Definition include.f90:239
logical, public verbose
Master switch for verbose diagnostic output Default value is .false. (quiet mode)....
Definition logging.f90:56
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 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 character(len_trim(str)) function, public uppercase(str)
Convert string to upper case (respects contents of quotes).
Definition string.f90:583
Abstract interface for the main preprocessing routine (used for recursion) Allows handle_include to r...
Definition include.f90:88
Generic interface for joining two path components Supports all combinations of character and string a...
Definition path.f90:101
Return the length of a string.
Definition string.f90:130
Return the trimmed string.
Definition string.f90:146
Derived type representing a single preprocessor macro Extends string with macro-specific fields: rep...
Definition macro.f90:103