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 use fpx_context
69
70 implicit none; private
71
72 public :: handle_include
73
74 ! Include directive types
75 integer, parameter, private :: INCLUDE_TYPE_SYSTEM = 1 ! < >
76 integer, parameter, private :: INCLUDE_TYPE_LOCAL = 2 ! " "
77#ifdef _WIN32
78 integer, parameter, private :: MAX_PATH_LEN = 256
79#else
80 integer, parameter, private :: MAX_PATH_LEN = 4096
81#endif
82 !> Abstract interface for the main preprocessing routine (used for recursion)
83 !! Allows handle_include to recursively call the top-level preprocess_unit routine
84 !! without creating circular module dependencies.
85 !!
86 !! @b Remarks
87 !! @ingroup group_include
88 interface
89 subroutine read_unit(iunit, ounit, macros, from_include)
90 import macro
91 implicit none
92 integer, intent(in) :: iunit
93 integer, intent(in) :: ounit
94 type(macro), allocatable, intent(inout) :: macros(:)
95 logical, intent(in) :: from_include
96 end subroutine
97 end interface
98
99contains
100
101 !> Process a #include directive encountered during preprocessing
102 !! Resolves the include file name (quoted or angle-bracketed), searches for the file
103 !! using standard C preprocessor rules:
104 !! - Quoted includes search: parent directory, -I paths, PATH, cwd
105 !! - Angle bracket includes search: -I paths, PATH, cwd (skips parent directory)
106 !! Opens the file and recursively preprocesses its contents into the output unit.
107 !!
108 !! @param[in] ctx Context line containing the #include directive
109 !! @param[in] ounit Output unit where preprocessed content is written
110 !! @param[in] preprocess Procedure pointer to the main line-by-line preprocessor
111 !! @param[inout] macros Current macro table (shared across recursion levels)
112 !! @param[in] token Usually 'INCLUDE' – the directive keyword
113 !!
114 !! @b Remarks
115 !! @ingroup group_include
116 recursive subroutine handle_include(ctx, ounit, preprocess, macros, token)
117 type(context), intent(in) :: ctx
118 integer, intent(in) :: ounit
119 procedure(read_unit) :: preprocess
120 type(macro), allocatable, intent(inout) :: macros(:)
121 character(*), intent(in) :: token
122 !private
123 character(:), allocatable :: include_file
124 character(:), allocatable :: dir, ifile
125 character(:), allocatable :: sys_paths(:)
126 integer :: i, iunit, ierr, pos
127 integer :: include_type
128 logical :: exists
129
130 ! Extract the directory of the parent file
131 dir = dirpath(ctx%path)
132 ! Find the position after the #include token
133 pos = index(lowercase(ctx%content), token) + len(token)
134 include_file = trim(adjustl(ctx%content(pos:)))
135
136 ! Determine include type and extract filename
137 if (include_file(1:1) == '"') then
138 include_type = include_type_local
139 include_file = include_file(2:index(include_file(2:), '"'))
140 else if (include_file(1:1) == '<') then
141 include_type = include_type_system
142 include_file = include_file(2:index(include_file(2:), '>'))
143 else
144 ! Malformed include directive
145 call printf(render(diagnostic_report(level_error, &
146 message='Malformed #include directive', &
147 label=label_type('Filepath should either be delimited by "<...>" or "..."', index(ctx%content, include_file), len(include_file)), &
148 source=trim(ctx%path)), &
149 ctx%content, ctx%line))
150 return
151 end if
152
153 ! Handle absolute/rooted paths (same for both types)
154 ifile = include_file
155 if (is_rooted(ifile)) then
156 inquire(file=ifile, exist=exists)
157 if (exists) then
158 include_file = ifile
159 else
160 if (verbose) then
161 call printf(render(diagnostic_report(level_error, &
162 message='File not found', &
163 label=label_type('Cannot find include file ' // trim(include_file), index(ctx%content, include_file), len(include_file)), &
164 source=trim(ctx%path)), &
165 ctx%content, ctx%line))
166 return
167 end if
168 end if
169 else
170 ! Relative path - search according to include type
171 exists = .false.
172 ! For quoted includes (#include "file"), search parent directory first
173 ifile = join(dir, include_file)
174 if (include_type == include_type_local) then
175 ifile = join(dir, include_file)
176 inquire(file=ifile, exist=exists)
177 if (exists) then
178 include_file = ifile
179 end if
180 end if
181
182 ! If not found yet, search user-specified include directories (-I paths)
183 if (.not. exists .and. allocated(global%includedir)) then
184 do i = 1, size(global%includedir)
185 ifile = join(global%includedir(i), include_file)
186 inquire(file=ifile, exist=exists)
187 if (exists) then
188 include_file = ifile
189 exit
190 end if
191 end do
192 end if
193
194 ! If still not found, try the INCLUDE environmental variable
195 if (.not. exists) then
196 block
197 character(:), allocatable :: ipaths(:)
198
199 ipaths = get_system_paths()
200 do i = 1, size(ipaths)
201 ifile = join(ipaths(i), include_file)
202 inquire(file=ifile, exist=exists)
203 if (exists) then
204 include_file = ifile
205 end if
206 end do
207 end block
208 end if
209
210 ! If still not found, try current working directory as last resort
211 if (.not. exists) then
212 ifile = join(cwd(), include_file)
213 inquire(file=ifile, exist=exists)
214 if (exists) then
215 include_file = ifile
216 end if
217 end if
218
219 ! If file was not found anywhere, report error
220 if (.not. exists) then
221 call printf(render(diagnostic_report(level_error, &
222 message='File not found', &
223 label=label_type('Cannot find include file ' // trim(include_file), index(ctx%content, include_file), len(include_file)), &
224 source=trim(ctx%path)), &
225 ctx%content, ctx%line))
226 return
227 end if
228 end if
229
230 ! Open and preprocess the include file
231 open(newunit=iunit, file=include_file, status='old', action='read', iostat=ierr)
232 if (ierr /= 0) then
233 call printf(render(diagnostic_report(level_error, &
234 message='File not found', &
235 label=label_type('Cannot open include file ' // trim(include_file), index(ctx%content, include_file), len(include_file)), &
236 source=trim(ctx%path)), &
237 ctx%content, ctx%line))
238 return
239 end if
240
241 call preprocess(iunit, ounit, macros, .true.)
242 close(iunit)
243 end subroutine
244
245 !> Get system include paths from PATH environment variable
246 !! Returns an array of directory paths found in PATH
247 !! @return Array of path strings, empty if PATH not set
248 !!
249 !! @b Remarks
250 !! @ingroup group_include
251 function get_system_paths() result(paths)
252 character(:), allocatable :: paths(:)
253 !private
254 character(:), allocatable :: path_env, tmp(:)
255 integer :: lpath, i, n_paths, start_pos, end_pos, count
256 character(len=1) :: path_sep
257
258#ifdef _WIN32
259 path_sep = ';' ! Windows path separator
260#else
261 path_sep = ':' ! Unix/Linux/Mac path separator
262#endif
263
264 ! Get PATH environment variable length
265 call get_environment_variable('INCLUDE', length=lpath)
266 if (lpath <= 0) then
267 allocate(character(len=0) :: paths(0)); return
268 end if
269
270 ! Allocate and retrieve PATH value
271 allocate(character(len=lpath) :: path_env)
272 call get_environment_variable('INCLUDE', value=path_env)
273
274 ! Count number of paths (number of separators + 1)
275 n_paths = 1
276 do i = 1, len(path_env)
277 if (path_env(i:i) == path_sep) n_paths = n_paths + 1
278 end do
279
280 ! Allocate temporary array with maximum size
281 allocate(character(len=MAX_PATH_LEN) :: tmp(n_paths))
282
283 ! Split INCLUDE into individual directories
284 count = 0
285 start_pos = 1
286 do i = 1, len(path_env) + 1
287 if (i > len(path_env) .or. path_env(i:i) == path_sep) then
288 if (i > len(path_env)) then
289 end_pos = i - 1
290 else
291 end_pos = i - 1
292 end if
293
294 if (end_pos >= start_pos) then
295 count = count + 1
296 tmp(count) = trim(adjustl(path_env(start_pos:end_pos)))
297 end if
298 start_pos = i + 1
299 end if
300 end do
301
302 ! Allocate result array with actual count
303 if (count > 0) then
304 allocate(character(len=MAX_PATH_LEN) :: paths(count))
305 paths(:) = tmp(1:count)
306 else
307 allocate(character(len=0) :: paths(0))
308 end if
309 end function
310
311end module
type(global_settings), public global
The single global instance used throughout fpx Initialized automatically with sensible defaults value...
Definition global.f90:93
recursive subroutine, public handle_include(ctx, ounit, preprocess, macros, token)
Process a include directive encountered during preprocessing Resolves the include file name (quoted o...
Definition include.f90:117
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:252
logical, public verbose
Master switch for verbose diagnostic output Default value is .false. (quiet mode)....
Definition logging.f90:108
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 lowercase(str)
Convert string to lower case (respects contents of quotes).
Definition string.f90:640
Abstract interface for the main preprocessing routine (used for recursion) Allows handle_include to r...
Definition include.f90:89
Interface to render diagnostic messages and labels.
Definition logging.f90:185
Generic interface for joining two path components Supports all combinations of character and string a...
Definition path.f90:101
Index operator.
Definition string.f90:178
Return the length of a string.
Definition string.f90:133
Return the trimmed string.
Definition string.f90:149
Source location and content snapshot for precise diagnostics Instances of this type are created for e...
Definition context.f90:99
Definition of diagnostic message.
Definition logging.f90:269
Represents text as a sequence of ASCII code units. The derived type wraps an allocatable character ar...
Definition logging.f90:246
Derived type representing a single preprocessor macro Extends string with macro-specific fields: rep...
Definition macro.f90:94