• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gyrokinetics / utils / 2078150095

03 Oct 2025 09:14AM UTC coverage: 6.981% (+0.1%) from 6.855%
2078150095

push

gitlab-ci

David Dickinson
Merged in experimental/user_controlled_output_base_name_in_file_utils (pull request #204)

741 of 10615 relevant lines covered (6.98%)

16211.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

37.65
/file_utils.fpp
1
# include "define.inc"
2

3
!> Various utilities for working with input and output files, as well as some
4
!> shims/backfills for standard features not present in older compilers.
5
!>
6
!> This module is responsible for setting up the label for the run,
7
!> [[file_utils:run_name]], as well as opening the main input file, the error
8
!> file, and the exit reason file.
9
!>
10
!> Most of the procedures in this module deal with files, and are therefore not
11
!> MPI/thread safe, and you must manually guard any opens/closes.
12
module file_utils
13
  use constants, only: run_name_size
14
  use iso_fortran_env, only: std_error_unit => error_unit, stdout_unit => output_unit
15
  implicit none
16

17
  private
18

19
  public :: init_file_utils, init_job_name, finish_file_utils, initialized
20
  public :: run_name, list_name, dirname, basename, split_dir_and_base, mkdir
21
  public :: input_unit_exist, input_unit, error_unit, stdout_unit
22
  public :: init_error_unit, init_exit_reason_unit, init_input_unit, exit_reason_unit
23
  public :: get_input_unit, append_output_file, open_output_file, close_output_file
24
  public :: get_indexed_namelist_unit, num_input_lines, replace_all_tabs
25

26
  !> Label for the run. Usually this is the input file without any
27
  !> extensions. Set by [[init_run_name]]. Used to derive output file names
28
  character(run_name_size) :: run_name = 'unknown'
29
  !> The full input file name, set by [[get_input_filename]] or what
30
  !> is passed to init_file_utils
31
  character(run_name_size) :: arun_name = 'unknown'
32
  !> The current job name if we've forked -- the actual list job or
33
  !> ensemble case we're running. See [[job_manage(module)]]
34
  character(run_name_size) :: job_name = 'unknown'
35
  !> Label for the list run, taken from the command line typically. Only
36
  !> set if ends in .list, otherwise 'unknown'
37
  character(run_name_size) :: list_name = 'unknown'
38
  !> Input base name. Name of the actual input file for this job.
39
  character(run_name_size) :: input_name = 'unknown'
40
  !> Note, as we should always use newunit and this always returns negative units
41
  !> we just pick some positive value here. This relies a bit on us never manually
42
  !> specifying the unit elsewhere.
43
  integer, parameter :: invalid_unit = 49
44
  !> Unit number for main input file (actually a copy we make which has comments
45
  !> stripped and `!include` files included).
46
  integer :: input_unit_no = invalid_unit
47
  !> Unit number for main error file
48
  integer :: error_unit_no = std_error_unit
49
  !> Unit number for exit reason file
50
  integer :: exit_reason_unit_no = stdout_unit
51
  !> Number of lines in input file
52
  integer :: num_input_lines
53
  !> Is module initialized?
54
  logical :: initialized = .false.
55

56
  !> Local type for recording the unit and file name of files. Used in the
57
  !> include functionality to track the heirarchy of included files.
58
  type stack_type
59
     character(len=:), allocatable :: file_name
60
     integer :: unit = invalid_unit
61
  end type stack_type
62

63
contains
64

65
  !> Read the [[file_utils:run_name]] from the command line (if not given), and determine
66
  !> from the extension whether this is a list run (i.e. a list of runs has been
67
  !> given). If so or if this is a run with multiple ensembles, open the list
68
  !> description. If not, open the error file and call [[init_input_unit]]
69
  subroutine init_file_utils (list, input, error, trin_run, name, output_base_name, &
1✔
70
       n_ensembles, input_file, remove_comments)
71
    use optionals, only: get_option_with_default
72
    implicit none
73
    !> True if the input file is a list run file (see [[is_input_file_list]])
74
    logical, intent (out) :: list
75
    !> If true, open the input file and strip comments (default: true)
76
    logical, intent (in), optional :: input
77
    !> If true, open the error output file (default: true)
78
    logical, intent (in), optional :: error
79
    !> If present, regardless of value, sets `list` to false
80
    logical, intent (in), optional :: trin_run
81
    !> Set [[file_utils:run_name]] if `input` is false or `trin_run` is present
82
    character(len = *), intent (in), optional :: name
83
    !> Set [[file_utils:output_name]] used as the base for output file. If not present
84
    !> then default to run_name
85
    character(len = *), intent (in), optional :: output_base_name
86
    !> Number of run ensembles. If greater than 1, sets [[file_utils:list_name]] and
87
    !> doesn't open any files
88
    integer, intent (in), optional :: n_ensembles
89
    !> Use given input filename instead of reading from command line
90
    character(len = *), intent (in), optional :: input_file
91
    !> Do we want to strip comments from the input file as we read it.
92
    logical, intent(in), optional :: remove_comments
93
    logical :: inp, err
94

95
    inp = get_option_with_default(input, .true.)
1✔
96
    err = get_option_with_default(error, .true.)
1✔
97
    arun_name = trim(get_option_with_default(name, "unknown"))
1✔
98

99
    if (inp .and. .not. get_option_with_default(trin_run, .false.)) then
1✔
100
       ! Note we can't (easily) use get_option_with_default here
101
       ! as the default, would be the return of get_input_filename()
102
       ! but this function can call mp_abort and this could trigger
103
       ! even if input_file is passed and hence we don't need get_input_filename
104
       if (present(input_file)) then
1✔
105
          arun_name = input_file
1✔
106
       else
107
          arun_name = get_input_filename()
×
108
       endif
109
    end if
110

111
    list = is_input_file_list(arun_name)
1✔
112

113
    if (list .or. (get_option_with_default(n_ensembles, 1) > 1)) then
1✔
114
       list_name = arun_name
×
115
    else
116
       call init_run_name(arun_name, output_base_name)
1✔
117
       if (run_name /= input_name) call mkdir(run_name)
1✔
118
       call init_error_unit (err)
1✔
119
       call init_exit_reason_unit (err)
1✔
120
       call init_input_unit (inp, remove_comments)
1✔
121
    end if
122
    initialized = .true.
1✔
123
  end subroutine init_file_utils
1✔
124

125
  !> Return the input filename as set by the command line
126
  function get_input_filename () result(input_filename)
×
127
    use mp, only : mp_abort
128
    implicit none
129
    character(run_name_size) :: input_filename
130
    character(len=50) :: error_message
131
    integer :: filename_length, ierr
132

133
    if (command_argument_count() == 0) call mp_abort("No input file provided", .true.)
×
134

135
    call get_command_argument(1, input_filename, filename_length, ierr)
×
136
    if (ierr /= 0) then
×
137
      write(error_message, '(A, I0)') "Couldn't get input file name, error code: ", ierr
×
138
      call mp_abort(error_message, to_screen=.true.)
×
139
    end if
140
  end function get_input_filename
×
141

142
  !> Returns true if [[input_filename]] is a list input: that is, if it ends in `.list`
143
  pure logical function is_input_file_list(input_filename)
1✔
144
    character(len=*), intent(in) :: input_filename
145
    integer :: filename_length
146
    filename_length = len_trim(input_filename)
1✔
147
    if (filename_length > 5) then
1✔
148
       is_input_file_list = (input_filename(filename_length-4:filename_length) == ".list")
1✔
149
    else
150
       is_input_file_list = .false.
×
151
    endif
152
  end function is_input_file_list
2✔
153

154
  !> This is called for a non Trinity or list run - it checks that the input
155
  !> file name ends in `.in`, chops the extension off and stores it in
156
  !> [[file_utils:arun_name]]. It also assigns the pointer
157
  !> [[file_utils:run_name]] to [[file_utils:arun_name]].
158
  subroutine init_run_name(arun_name, output_base_name)
1✔
159
    use optionals, only: get_option_with_default
160
    implicit none
161
    character(len = *), intent(in out) :: arun_name
162
    character(len = *), intent(in), optional :: output_base_name
163
    integer :: l
164
    l = len_trim (arun_name)
1✔
165
    if (l > 3)then
1✔
166
       if (arun_name(l-2:l) == ".in") arun_name = arun_name(1:l-3)
1✔
167
    end if
168
    input_name = arun_name
1✔
169
    run_name = get_option_with_default(output_base_name, arun_name)
1✔
170
  end subroutine init_run_name
1✔
171

172
  !> Set [[file_utils:run_name]] and [[file_utils:job_name]] to `jobname`. Used
173
  !> by [[job_manage(module)]] when forking (i.e. list mode or ensembles)
174
  subroutine init_job_name (jobname)
×
175
    implicit none
176
    character(len=*), intent (in) :: jobname
177
    job_name = trim(jobname)
×
178
    call init_run_name(job_name)
×
179
  end subroutine init_job_name
×
180

181
  !> Returns a modified file path of form <dir><prefix><base><extension> for input file
182
  !> path of form <dir><base>.
183
  pure function get_modified_file_path(file_path, prefix, extension) result(file_name)
4✔
184
    use optionals, only: get_option_with_default
185
    character(len = *), intent(in) :: file_path
186
    character(len = *), intent (in), optional :: extension, prefix
187
    character(len = :), allocatable :: file_name
188
    file_name = dirname(file_path) // get_option_with_default(prefix, '') // &
189
         basename(file_path) // get_option_with_default(extension, '')
4✔
190
  end function get_modified_file_path
4✔
191

192
  !> Open an output file to write (replacing any existing) whose name is
193
  !> `<run_name>.<ext>`, and set `unit` to the unit number of that output
194
  !> file. If the binary flag is true, a binary file is opened
195
  subroutine open_output_file (unit, ext, prefix, binary)
2✔
196
    use optionals, only: get_option_with_default
197
    implicit none
198
    !> Unit number of opened file
199
    integer, intent (out) :: unit
200
    !> ext is file extension to open, appended to [[file_utils:run_name]]
201
    character (len = *), intent(in) :: ext
202
    !> Prefix is added to the start of the filename, after any directory part
203
    character (len = *), intent(in), optional :: prefix
204
    !> If true, then open a binary (unformatted) file
205
    logical, intent(in), optional :: binary
206
    character(len = :), allocatable :: file_name
2✔
207
    character(len=11) :: formtxt
208
    file_name = get_modified_file_path(run_name, prefix, ext)
2✔
209
    formtxt = "formatted"
2✔
210
    if (get_option_with_default(binary, .false.)) formtxt = "unformatted"
2✔
211
    open(newunit=unit, file=file_name, status="replace", action="write", form=trim(formtxt))
2✔
212
  end subroutine open_output_file
4✔
213

214
  !> Open an output file to write (appending if existing) whose name is
215
  !> `<run_name>.<ext>`, and set `unit` to the unit number of that
216
  !> output file. If the optional `run_name_in` variable is present, this
217
  !> replaces run_name as the root of the output file.
218
  subroutine append_output_file (unit, ext, prefix, run_name_in)
×
219
    use optionals, only: get_option_with_default
220
    implicit none
221
    !> Unit number of opened file, in append mode
222
    integer, intent (out) :: unit
223
    !> File extension to open, appended to [[file_utils:run_name]]
224
    character (*), intent (in) :: ext, prefix
225
    !> Optional root name for the output file. If not specified,
226
    !> [[file_utils:run_name]] is used as the root.
227
    character (*), intent (in), optional :: run_name_in
228
    character(len=:), allocatable :: file_name
×
229
    logical :: exists
230
    file_name = get_modified_file_path(get_option_with_default(run_name_in, run_name), &
231
         prefix, ext)
×
232
    inquire(file=file_name, exist=exists)
×
233
    if (exists) then
×
234
       open(newunit=unit, file=trim(file_name), status="old", position="append", &
235
            action="write")
×
236
    else
237
      open(newunit=unit, file=trim(file_name), status="new", action="write")
×
238
    end if
239
  end subroutine append_output_file
×
240

241
  !> Close the file associated with `unit`
242
  !>
243
  !> FIXME: Remove
244
  subroutine close_output_file (unit)
×
245
    implicit none
246
    !> Unit of file to close
247
    integer, intent (in) :: unit
248
    close (unit=unit)
×
249
  end subroutine close_output_file
×
250

251
  !> Open error file and record associated lun/unit
252
  subroutine init_error_unit (open_it)
1✔
253
    use mp, only : set_default_error_file_unit
254
    implicit none
255
    !> If true, open the file, otherwise do nothing
256
    logical, intent (in) :: open_it
257
    error_unit_no = invalid_unit
1✔
258
    if (run_name /= "unknown" .and. open_it) &
1✔
259
       call open_output_file (error_unit_no, ".error")
1✔
260
    ! Set mp's error file so that we don't have to set it in every
261
    ! call to mp_abort
262
    call set_default_error_file_unit(error_unit_no)
1✔
263
  end subroutine init_error_unit
1✔
264

265
  !> Open exit_reason file and record associated lun/unit
266
  subroutine init_exit_reason_unit (open_it)
1✔
267
    implicit none
268
    !> If true, open the file, otherwise do nothing
269
    logical, intent (in) :: open_it
270
    exit_reason_unit_no = invalid_unit
1✔
271
    if (run_name /= "unknown" .and. open_it) &
1✔
272
       call open_output_file (exit_reason_unit_no, ".exit_reason")
1✔
273
  end subroutine init_exit_reason_unit
1✔
274

275
  !> Replaces each horizontal tab with a single space
276
  subroutine replace_all_tabs (line)
×
277
    use iso_c_binding, only: C_HORIZONTAL_TAB
278
    implicit none
279
    !> Text to modify in-place
280
    character(*), intent (in out) :: line
281
    integer :: tab_location, length, counter
282
    tab_location = scan(line, C_HORIZONTAL_TAB)
×
283
    length = len(line)
×
284
    counter = 1
×
285
    do while (tab_location > 0 .and. counter <= length)
×
286
       !Replace tab with a single space
287
       line(tab_location:tab_location) = ' '
×
288
       !Update tab_location
289
       tab_location = scan(line, C_HORIZONTAL_TAB)
×
290
       counter = counter+1
×
291
    end do
292
  end subroutine replace_all_tabs
×
293

294
  !> Replaces all leading tabs with space.
295
  !> Note we consider any tabs appearing before the first non-space/tab character.
296
  subroutine replace_leading_tabs (line)
×
297
    use iso_c_binding, only: C_HORIZONTAL_TAB
298
    implicit none
299
    !> Text to modify in-place
300
    character(*), intent (in out) :: line
301
    integer :: tab_location, i, length
302
    tab_location = scan(line, C_HORIZONTAL_TAB)
×
303
    ! If there are no tabs in the line then return immediately
304
    if (tab_location == 0) return
×
305

306
    length = len(line)
×
307

308
    ! Consider each character in turn
309
    do i = 1, length
×
310
       if (line(i:i) == ' ') then
×
311
          ! If it is a space then just move to next character
312
          cycle
×
313
       else if (line(i:i) == C_HORIZONTAL_TAB) then
×
314
          ! If it is a tab replace with a space and move to next character
315
          line(i:i) = ' '
×
316
          cycle
×
317
       else
318
          ! We've reached a non-space/tab character so can stop checking
319
          exit
×
320
       end if
321
    end do
322
  end subroutine replace_leading_tabs
×
323
  
324
  !> Remove Fortran comments (`!`) from `line`
325
  subroutine strip_comments (line)
×
326
    implicit none
327
    !> Text to modify in-place
328
    character(*), intent (in out) :: line
329
    logical :: in_single_quotes, in_double_quotes
330
    integer :: i, length
331

332
    length = len_trim(line)
×
333
    i = 1
×
334
    in_single_quotes = .false.
×
335
    in_double_quotes = .false.
×
336
    loop: do
×
337
       if (in_single_quotes) then
×
338
          if (line(i:i) == "'") in_single_quotes = .false.
×
339
       else if (in_double_quotes) then
×
340
          if (line(i:i) == '"') in_double_quotes = .false.
×
341
       else
342
          select case (line(i:i))
×
343
          case ("'")
344
             in_single_quotes = .true.
×
345
          case ('"')
346
             in_double_quotes = .true.
×
347
          case ("!")
348
             i = i - 1
×
349
             exit loop
×
350
          end select
351
       end if
352
       if (i >= length) exit loop
×
353
       i = i + 1
×
354
    end do loop
355
    line = line(1:i)
×
356
  end subroutine strip_comments
×
357

358
  !> Create a directory based on the passed path. We obtain the dirname
359
  !> from path_like so one may pass the path to a file and only the directory
360
  !> up to the file will be created.
361
  subroutine mkdir(path_like, make_parents)
×
362
    use optionals, only: get_option_with_default
363
    character(len = *), intent(in) :: path_like
364
    logical, intent(in), optional :: make_parents
365
    character(len = :), allocatable :: command, dir
×
366
    ! Note here we hard code the command name, this _could_ be
367
    ! defined at compile time or determined based on run time properties
368
    command = 'mkdir'
×
369
    dir = dirname(path_like)
×
370
    if (len_trim(dir) == 0) return
×
371
    if (get_option_with_default(make_parents, .true.)) command = command // " -p "
×
372
    call execute_command_line(command // dir)
×
373
  end subroutine mkdir
×
374

375
  !> Takes a path like string and tries to split it into a dirname and basename
376
  pure subroutine split_dir_and_base(path_like, separator, dirname, basename)
×
377
    use optionals, only: get_option_with_default
378
    character(len = *), intent(in) :: path_like
379
    character, intent(in), optional :: separator
380
    character(len = :), allocatable, intent(out), optional :: dirname, basename
381
    integer :: ind_separator
382
    ind_separator = index(path_like, get_option_with_default(separator, '/'), .true.)
18✔
383

384
    if (ind_separator == 0) then
18✔
385
       ! No separator found
386
       if (present(dirname)) dirname = ''
12✔
387
       if (present(basename)) basename = trim(path_like)
12✔
388
    else
389
       if (present(dirname)) dirname = trim(path_like(1:ind_separator))
6✔
390
       if (present(basename)) then
6✔
391
          ! Check that path_like isn't just a directory path, i.e. that there _is_ a file
392
          if (ind_separator < len_trim(path_like)) then
3✔
393
             basename = trim(path_like(ind_separator+1:))
2✔
394
          else
395
             basename = ''
1✔
396
          end if
397
       end if
398
    end if
399
  end subroutine split_dir_and_base
36✔
400

401
  !> Returns the dirname of a passed path like string. Will be '' if path just a file
402
  pure function dirname(path_like, separator)
×
403
    use optionals, only: get_option_with_default
18✔
404
    character(len = *), intent(in) :: path_like
405
    character, intent(in), optional :: separator
406
    character(len = :), allocatable :: dirname
407
    call split_dir_and_base(path_like, separator, dirname = dirname)
9✔
408
  end function dirname
18✔
409

410
  !> Returns the basename of a passed path like string. Will be '' if path just a dir
411
  pure function basename(path_like, separator)
×
412
    use optionals, only: get_option_with_default
9✔
413
    character(len = *), intent(in) :: path_like
414
    character, intent(in), optional :: separator
415
    character(len = :), allocatable :: basename
416
    call split_dir_and_base(path_like, separator, basename = basename)
9✔
417
  end function basename
18✔
418

419
  !> Opens the input file, strip out any comments and write them into the file
420
  !> `.<run_name>.in`. Check for includes, read any lines from the includes,
421
  !> strip any comments from them and add them to the same file.
422
  subroutine init_input_unit (open_it, remove_comments)
1✔
423
    use optionals, only: get_option_with_default
9✔
424
    implicit none
425
    !> If true, open the file, otherwise do nothing
426
    logical, intent(in) :: open_it
427
    logical, intent(in), optional :: remove_comments
428
    integer :: in_unit, out_unit, iostat, stack_ptr
429
    character(500) :: line
430
    integer, parameter :: stack_size = 10
431
    type(stack_type), dimension(0:stack_size) :: stack
25✔
432
    logical :: already_opened
433
    character(len=75) :: non_end_error_message
434
    character(len=:), allocatable :: include_file
1✔
435

436
    if (.not. open_it) then
1✔
437
       input_unit_no = invalid_unit
×
438
       return
×
439
    end if
440

441
    open(newunit=in_unit, file=get_modified_file_path(trim(input_name), extension = ".in"), &
442
         status="old", action="read", iostat=iostat)
1✔
443
    if (iostat /= 0) call close_stack_and_abort( &
1✔
444
         "Couldn't open input file: " // trim(input_name) // ".in")
×
445

446
    open(newunit = out_unit, file=get_modified_file_path(run_name, ".", ".in"))
1✔
447
    input_unit_no = out_unit
1✔
448

449
    iostat = 0 ;  num_input_lines = 0 ; stack_ptr = 0
1✔
450
    stack(stack_ptr) = stack_type(unit = in_unit, file_name = trim(input_name))
1✔
451

452
    do
×
453
       read (unit=in_unit, fmt="(a)", iostat=iostat) line
1✔
454

455
       ! It appears that we're using iostat / =0 to identify when
456
       ! we've reached the end of the current file. This therefore
457
       ! ignores any other possible errors. We should perhaps instead
458
       ! first check if iostat_is_end(iostat), if so proceed as we
459
       ! have here else if iostat /= 0 we have some other error and
460
       ! should consider aborting. Currently we just display an error
461
       ! message but carry on as usual.
462
       if (iostat /= 0) then
1✔
463
          if (.not. is_iostat_end(iostat)) then
1✔
464
             write(non_end_error_message, '(A, I0)') &
465
                  "Error encountered whilst reading input file with code ", iostat
×
466
             call close_stack_and_abort(non_end_error_message)
×
467
          end if
468
          if (stack_ptr <= 0) exit
1✔
469
          close (unit=in_unit)
×
470
          iostat = 0
×
471
          stack_ptr = stack_ptr - 1
×
472
          in_unit = stack(stack_ptr)%unit
×
473
          cycle
×
474
       end if
475

476
       if (line(1:9) == "!include ") then
×
477
          if (stack_ptr >= stack_size) call close_stack_and_abort( &
×
478
               "!include ignored: nesting too deep: " // trim(line))
×
479

480
          ! Check if we've already opened this file, if so assume this
481
          ! is a circular dependency and abort. Note we add the dirname of the
482
          ! input file (and not run_name) in order to allow relative paths which
483
          ! are resolved relative to the input file and not relative to where we
484
          ! have run from.
485
          include_file = dirname(stack(stack_ptr)%file_name) // trim(line(10:))
×
486
          inquire(file = include_file, opened = already_opened)
×
487
          if (already_opened) call close_stack_and_abort( &
×
488
               "Circular dependency with " // trim(line))
×
489

490
          open(newunit=in_unit, file=include_file, status="old", action="read", iostat=iostat)
×
491

492
          stack_ptr = stack_ptr + 1
×
493
          stack(stack_ptr)%unit = in_unit ; stack(stack_ptr)%file_name = include_file
×
494

495
          ! Here we assume that any problem with the above open is due
496
          ! to the file being "unreadable". This could also end up
497
          ! catching circular dependencies in our includes as the
498
          ! fortran standard forbids having the same file open with
499
          ! different file units (although some compilers may allow
500
          ! this as an extension). To help the user we first detect if
501
          ! this file has already been opened and if so warn for
502
          ! circular dependency. Here we therefore deal with all other
503
          ! errors.
504
          if (iostat /= 0) call close_stack_and_abort( &
×
505
               "!include ignored: file unreadable: " // trim(line))
×
506
          cycle
×
507
       end if
508
       if (get_option_with_default(remove_comments, .false.)) call strip_comments (line)
×
509
       call replace_leading_tabs(line)
×
510
       write (unit=out_unit, fmt="(a)") trim(line)
×
511
       num_input_lines = num_input_lines + 1
×
512
    end do
513
    close (unit=in_unit)
2✔
514
  contains
515
    !> Close all open files and abort run
516
    subroutine close_stack_and_abort(error_message)
×
517
      use mp, only : mp_abort
518
      !> Error message to print to screen and log in error file
519
      character(len=*), intent(in) :: error_message
520
      integer :: i
521
      do i = 0, stack_ptr
×
522
        if (unit_is_open(stack(i)%unit)) close(stack(i)%unit)
×
523
      end do
524
      call mp_abort(error_message, to_screen=.true.)
×
525
    end subroutine close_stack_and_abort
×
526
  end subroutine init_input_unit
527

528
  !> Close any files opened by [[init_file_utils]]
529
  subroutine finish_file_utils
16✔
530
    implicit none
531
    if (unit_is_open(input_unit_no)) then
16✔
532
       close (unit=input_unit_no, status = 'delete')
1✔
533
       input_unit_no = invalid_unit
1✔
534
    end if
535
    if (unit_is_open(error_unit_no) .and. error_unit_no /= std_error_unit) then
16✔
536
       close (unit=error_unit_no)
1✔
537
       error_unit_no = std_error_unit
1✔
538
    end if
539
    if (unit_is_open(exit_reason_unit_no) .and. exit_reason_unit_no /= stdout_unit) then
16✔
540
       close (unit=exit_reason_unit_no)
1✔
541
       exit_reason_unit_no = stdout_unit
1✔
542
    end if
543
    initialized = .false.
16✔
544
  end subroutine finish_file_utils
16✔
545

546
  !> Small wrapper to determine if a unit is open
547
  logical function unit_is_open(unit) result(opened)
48✔
548
    integer, intent(in) :: unit
549
    inquire(unit = unit, opened = opened)
48✔
550
  end function unit_is_open
48✔
551

552
  !> Rewind the input file to start of namelist `nml`, and return the unit of
553
  !> the file opened by [[init_input_unit]]
554
  integer function input_unit(nml, exist)
×
555
    implicit none
556
    !> Name of namelist to find start of
557
    character(*), intent (in) :: nml
558
    !> Was `nml` was found in the input file?
559
    logical, intent(out), optional :: exist
560
    integer :: iostat
561
    character(500) :: line
562
    input_unit = input_unit_no
×
563
    if (present(exist)) exist = .true.
×
564
    if (unit_is_open(input_unit_no)) then
×
565
       rewind (unit=input_unit_no)
×
566
       do
×
567
          read (unit=input_unit_no, fmt="(a)", iostat=iostat) line
×
568
          if (iostat /= 0) then
×
569
             rewind (unit=input_unit_no)
×
570
             exit
×
571
          end if
572
          if (trim(adjustl(line)) == "&"//nml) then
×
573
             backspace (unit=input_unit_no)
×
574
             return
×
575
          end if
576
       end do
577
    end if
578
    if (present(exist)) then
×
579
       exist = .false.
×
580
    else
581
       write (unit=error_unit_no, fmt="('Couldn''t find namelist: ',a)") nml
×
582
       write (unit=*, fmt="('Couldn''t find namelist: ',a)") nml
×
583
    end if
584
  end function input_unit
×
585

586
  !> Similar to [[input_unit]] but set `exist` to true if `nml` was found in the
587
  !> input file, and false otherwise
588
  integer function input_unit_exist (nml, exist)
×
589
    implicit none
590
    !> Name of namelist to find start of
591
    character(*), intent (in) :: nml
592
    !> Was `nml` was found in the input file?
593
    logical, intent(out) :: exist
594
    input_unit_exist = input_unit(nml, exist)
×
595
  end function input_unit_exist
×
596

597
  !> Returns the file unit number associated with the error file
598
  pure integer function error_unit ()
×
599
    implicit none
600
    error_unit = error_unit_no
×
601
  end function error_unit
×
602

603
  !> Returns the file unit number associated with the exit_reason file
604
  pure integer function exit_reason_unit ()
×
605
    implicit none
606
    exit_reason_unit = exit_reason_unit_no
×
607
  end function exit_reason_unit
×
608

609
  !> Returns the file unit number associated with the input file
610
  !>
611
  !> @note This is a subroutine unlike the functions used for error_unit
612
  !> and exit_reason_unit. We should think about making these consistent.
613
  pure subroutine get_input_unit (unit)
×
614
    implicit none
615
    integer, intent (out) :: unit
616
    unit = input_unit_no
×
617
  end subroutine get_input_unit
×
618

619
  !> Copy namelist, `<nml>_<index_in>`, from the input file to namelist `NML` in
620
  !> a temporary file with unit `unit`
621
  subroutine get_indexed_namelist_unit (unit, nml, index_in, exist)
×
622
    implicit none
623
    !> Unit of new temporary file containing the indexed namelist
624
    integer, intent (out) :: unit
625
    !> Name of indexed namelist to copy
626
    character (*), intent (in) :: nml
627
    !> Index number of namelist to copy
628
    integer, intent (in) :: index_in
629
    !> Does the indexed namelist exist or not?
630
    logical, intent (out) :: exist
631
    character(500) :: line
632
    integer :: iunit, iostat
633
    write (line, '(A,"_",I0)') nml, index_in
×
634
    iunit = input_unit_exist(trim(line), exist)
×
635
    unit = invalid_unit
×
636
    if (.not. exist) then
×
637
       write(stdout_unit,*) "get_indexed_namelist: following namelist not found ",trim(line)
×
638
       return
×
639
    end if
640

641
    open(newunit = unit, file=get_modified_file_path(run_name, ".", ".scratch"))
×
642
    read (unit=iunit, fmt="(a)") line
×
643
    write (unit=unit, fmt="('&',a)") nml
×
644

645
    do
×
646
       read (unit=iunit, fmt="(a)", iostat=iostat) line
×
647
       if (iostat /= 0 .or. trim(adjustl(line)) == "/") exit
×
648
       write (unit=unit, fmt="(a)") trim(line)
×
649
    end do
650
    write (unit=unit, fmt="('/')")
×
651
    rewind (unit=unit)
×
652
  end subroutine get_indexed_namelist_unit
×
653
end module file_utils
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc