Characters and Strings

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3 final draft J3/23-007r1) and was checked on 2026-10-18 with GNU Fortran 16.2 and LLVM Flang 23. A Fortran string is not an array of characters and not an object with methods: it is a scalar of type character whose length is a type parameter, and nearly everything about strings follows from that. The length decides whether an assignment pads or truncates, whether two strings are equal, whether a variable can change size at run time and whether the compiler can check a call. The page follows that thread: the character type and its declaration forms, literals and substrings, assignment and comparison, the four ways to give a string a length, the string intrinsics, the Fortran 2023 intrinsics split and tokenize, conversion between numbers and strings, the Fortran 2023 at edit descriptor and the automatic allocation of iomsg variables, character kinds and UTF-8, C strings, arrays of strings and the string_type of the Fortran standard library.

Several Fortran 2023 string features are not implemented by both compilers. Every such feature below is labelled with the compiler that accepts it and the real diagnostic or failure of the one that does not, and the portable replacement is shown next to it. The compiler-support table at the end of the page collects them. Format specifications and internal files are covered on Format Specifications and Edit Descriptors and Internal Files, Namelist and Non-Advancing I/O; the kind system on Intrinsic Types and Kinds; allocation on Allocatable Arrays and Dynamic Memory; and the C side of strings on Interoperability with C.

Every example belongs to the heatflow scenario (a two-dimensional heat-diffusion solver) and uses its vocabulary: boundary condition names, solver names, namelist-style key=value specifications and CSV file names. Each one is a complete, self-contained file. Unless a paragraph says otherwise, every example compiles without warnings under gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all, was also built and run with LLVM Flang 23, and the output shown is what both printed. Examples that are deliberately wrong, or that use a feature one compiler lacks, are labelled in the text and in a comment on their first line, and the compiler messages or run-time failures they produce are shown.

The character type

The type character has a set of values that are strings of characters. A string is a sequence of characters numbered from 1, and its length is the number of characters, which may be zero. The length is a type parameter, written character(len=n); the other type parameter is the kind, which selects the character set and its representation (J3/23-007r1, 7.4.4.1 and 7.4.4.2). The default kind is the one of the literal 'a', and on both compilers used here it holds one byte per character; kinds for other character sets are the subject of the section on character kinds below.

The declaration forms are the following. They differ in who decides the length, and when.

Form Length Where it is allowed

character

1.

Everywhere. A missing length means 1, so character :: c is a single character.

character(len=10) or character(10)

Fixed at compile time by a constant expression.

Variables, components, function results, named constants.

character(len=n) with n not constant

Automatic length: evaluated each time the procedure is entered, from a specification expression such as len(text) or a dummy argument.

Local variables and function results of a procedure; not module variables.

character(len=*)

Assumed length: taken from the actual argument at each call.

Dummy arguments, named constants (character(len=*), parameter :: banner = 'text'), and the type-spec of allocate for an assumed-length dummy (constraint C726).

character(len=:), allocatable

Deferred length: set by an allocate statement or by assignment, and able to change during the run.

Variables, components, function results, dummy arguments (Fortran 2003).

character(len=:), pointer

Deferred length taken from the pointer target.

Pointers; see Pointers and Targets.

character*10

Same as character(len=10).

Obsolescent: Annex B.3.8 of J3/23-007r1 calls the form redundant. Appears only in old code.

Three rules of that clause are easy to miss. A negative length is not an error: the entity has length zero. A length of * is not allowed for a local variable. And an entity can carry its own length that overrides the one in the type specifier (character(len=8) :: a, b*20), which is legal and best avoided.

A character literal is delimited by apostrophes or quotation marks, and the delimiter inside the literal is written twice: 'It''s' and "It's" are the same string. The length of a literal is the number of characters between the delimiters, a literal of a kind other than the default is written with the kind name and an underscore before the delimiter (ucs4_'text'), and the empty literal '' has length zero. In free source form a character literal can continue across lines with & at the end of the line and & at the start of the next, and Fortran 2023 raises the limit on line length to 10,000 characters (see Program Structure and Source Form).

The first example declares the main forms, shows what an assignment does when the lengths differ, and uses substrings, which the next section explains. Its helper store has two assumed-length dummies, so the lengths are known only at run time and the compiler cannot warn about the truncation it performs.

module str_helpers
  implicit none
contains
  ! Both lengths are known only at run time, so the compiler cannot warn about truncation.
  subroutine store(dst, src)
    character(len=*), intent(out) :: dst
    character(len=*), intent(in) :: src
    dst = src            ! padded with blanks if src is shorter, truncated if it is longer
  end subroutine store
end module str_helpers

program str_basics
  use str_helpers, only: store
  implicit none
  character(len=8) :: solver          ! fixed length: always exactly 8 characters
  character(8) :: same_thing          ! shorthand for len=8
  character :: one                    ! len=1 by default
  character(len=0) :: nothing         ! legal: a zero-length string
  character(len=*), parameter :: banner = "heatflow's solver"   ! length taken from the constant
  character(len=20) :: title

  call store(solver, 'ftcs')                 ! padded: 'ftcs    '
  call store(same_thing, 'jacobi-method')    ! truncated on the right: 'jacobi-m'
  call store(one, 'xyz')                     ! keeps 'x'
  call store(nothing, 'abc')                 ! keeps nothing
  title = 'It''s "hot"'                      ! doubled delimiter inside a literal

  print '(a, i0)', 'len(solver)      = ', len(solver)
  print '(a, i0)', 'len_trim(solver) = ', len_trim(solver)
  print '(3a)',    '[', solver, ']'
  print '(3a)',    '[', same_thing, ']'
  print '(3a)',    '[', one, ']'
  print '(a, i0)', 'len(nothing)     = ', len(nothing)
  print '(a, i0)', 'len(banner)      = ', len(banner)
  print '(3a)',    '[', title, ']'
  print '(a, i0)', 'len(title)       = ', len(title)
  print '(a, i0)', 'len_trim(title)  = ', len_trim(title)
  print '(3a)',    '[', solver(2:3), ']'                   ! substring
  print '(3a)',    '[', banner(:8), ']'                    ! start defaults to 1
  print '(3a)',    '[', banner(10:), ']'                   ! end defaults to len
  print '(a, i0)', 'len(solver(5:4)) = ', len(solver(5:4)) ! start > end: length zero
  print '(3a)',    '[', 'abcdef'(2:4), ']'                 ! substring of a literal
  solver(1:1) = 'F'                                        ! assignment to a substring
  print '(3a)',    '[', solver, ']'
end program str_basics
len(solver)      = 8
len_trim(solver) = 4
[ftcs    ]
[jacobi-m]
[x]
len(nothing)     = 0
len(banner)      = 17
[It's "hot"          ]
len(title)       = 20
len_trim(title)  = 10
[tc]
[heatflow]
[s solver]
len(solver(5:4)) = 0
[bcd]
[Ftcs    ]

Writing the same assignments with constants of known length, same_thing = 'jacobi-method', makes GNU Fortran warn CHARACTER expression will be truncated in assignment (8/13) under -Wall (the option is -Wcharacter-truncation); Flang does not warn. The truncation is not an error in either case, which is why a dedicated procedure that checks len_trim(src) > len(dst) is a better way to copy data that must not be cut.

Substrings

A substring is a contiguous part of a string, written parent(first:last). The parent can be a variable, an array element, a structure component, a coindexed object or a constant. Both bounds are optional: the first defaults to 1 and the last to the length of the parent. The length of the substring is max(last - first + 1, 0). If first is greater than last the substring has length zero, and that is legal for any values, even outside the string; otherwise both bounds must lie between 1 and the length of the parent (J3/23-007r1, 9.4.1). A substring is a variable when its parent is, so it can be assigned, passed to a procedure and used as an internal file. As the standard’s own note points out, the right-hand side is evaluated before any part of the left-hand side is defined, so overlapping assignment such as s(2:5) = s(1:4) behaves as if a temporary were used.

The bounds are not checked by default. A substring that runs past the end of the string reads or writes the neighbouring memory. With -fcheck=all (or -fcheck=bounds) GNU Fortran detects it at run time:

! Deliberately non-conforming: the substring end point exceeds the length of the string.
program substring_overrun
  implicit none
  character(len=8) :: solver
  integer :: last
  solver = 'ftcs'
  last = 12                      ! not a constant, so the compiler cannot reject it at compile time
  print '(3a)', '[', solver(5:last), ']'
end program substring_overrun
At line 8 of file substring_overrun.f90
Fortran runtime error: Substring out of bounds: upper bound (12) of 'solver' exceeds string length (8)
Error termination. Backtrace:

LLVM Flang 23 has no such check in the build used here, built with its default options. The program ran to completion and the substring was printed with the bytes that follow the 8-byte variable in memory (the output contained non-printable characters). Only a compiler option that inserts the check, or a tool such as a sanitizer, finds the error; this is the same situation as for array bounds, discussed on Testing, Debugging and Profiling.

Assignment, concatenation and comparison

Assignment to a character variable of fixed length copies the value; if the value is shorter it is extended on the right with blanks, and if it is longer it is truncated from the right (J3/23-007r1, 10.2.1.3). The padding character of a nondefault kind is processor dependent. Concatenation uses the operator //, which joins two strings of the same kind into a string whose length is the sum of the lengths (10.1.5.3). It is the only character operator, it is associative, and the precedence of // is lower than that of the arithmetic operators and higher than that of the relational ones, so 'n = ' // to_string(n) // ' steps' needs no parentheses.

Comparison with ==, /=, <, <=, > and >= (or their older spellings .eq. and so on) compares one character at a time from the left. If the operands have unequal length, the shorter one is treated as if it were extended with blanks on the right to the length of the longer one (J3/23-007r1, 10.1.5.5.1). Two consequences matter every day:

  • Trailing blanks never affect a comparison: 'ftcs' equals 'ftcs ', and a zero-length string equals any string of blanks. Leading blanks do count.

  • The padding character takes part in the ordering. Because the shorter operand is padded with a blank, 'a' compares as 'a ', so it is less than 'a!' (the blank, code 32, precedes !, code 33) and greater than a string whose next character has a code below 32, such as 'a'//char(1).

The order of two different characters is given by the collating sequence. For the ASCII character kind it is the ASCII sequence and for the ISO 10646 kind it is the order of the code points (7.4.4.4). For default character the sequence is processor dependent, but the equality operators do not depend on it. The four intrinsic functions lge, lgt, lle and llt compare with the ASCII sequence whatever the processor uses, and the standard guarantees portability for the Fortran character set. They exist because, on a processor whose default sequence is not ASCII, < and lgt could differ.

program str_compare
  implicit none
  character(len=8) :: a, b
  character(len=:), allocatable :: c

  a = 'ftcs'
  b = 'ftcs    '
  c = 'ftcs'
  print '(a, l1)', "a == b            : ", a == b          ! trailing blanks never matter
  print '(a, l1)', "a == c            : ", a == c          ! c has length 4, a has length 8
  print '(a, l1)', "'ab' == 'ab   '   : ", 'ab' == 'ab   '
  print '(a, l1)', "'ab' == ' ab'     : ", 'ab' == ' ab'   ! leading blanks do matter
  print '(a, l1)', "'' == '   '       : ", '' == '   '     ! a zero-length string equals blanks
  print '(a, l1)', "'Z' < 'a'         : ", 'Z' < 'a'       ! ASCII: upper case precedes lower case
  print '(a, l1)', "'a' < 'B'         : ", 'a' < 'B'
  print '(a, l1)', "'a' < 'a!'        : ", 'a' < 'a!'      ! shorter operand is padded with a blank (code 32)
  print '(a, l1)', "'a' < 'a'//char(1): ", 'a' < 'a'//char(1)
  print '(a, l1)', "lgt('b', 'A')     : ", lgt('b', 'A')
  print '(a, l1)', "lle('abc', 'abd') : ", lle('abc', 'abd')
  print '(a, l1)', "a(1:4) == c       : ", a(1:4) == c
end program str_compare
a == b            : T
a == c            : T
'ab' == 'ab   '   : T
'ab' == ' ab'     : F
'' == '   '       : T
'Z' < 'a'         : T
'a' < 'B'         : F
'a' < 'a!'        : T
'a' < 'a'//char(1): F
lgt('b', 'A')     : T
lle('abc', 'abd') : T
a(1:4) == c       : T

Equality with trailing blanks ignored is convenient for fixed-length fields and a trap for deferred-length ones: after name = 'ftcs ' the length of name is 7, name == 'ftcs' is true, and yet name and trim(name) have different lengths. Case is significant in comparisons. There is no case-insensitive comparison in the standard; the section on intrinsics shows a portable one built from iachar and achar.

Four ways to give a string a length

A procedure that handles strings has to decide how the length is determined. The choice follows from who knows it and whether it changes.

flowchart TD start(["a string is needed"]) --> known{"is the length a compile-time constant?"} known -- yes --> fixed["character(len=10): fixed length"] known -- no --> dummy{"is it a dummy argument or a named constant?"} dummy -- yes --> assumed["character(len=*): assumed length"] dummy -- no --> fromargs{"is the length a function of the arguments, fixed during the call?"} fromargs -- yes --> automatic["character(len=len(text)): automatic length"] fromargs -- no --> deferred["character(len=:), allocatable: deferred length"] deferred --> many{"array of strings with different lengths?"} many -- yes --> wrapper["array of a derived type with an allocatable character component"] many -- no --> common["one length for every element: character(len=:), allocatable :: a(:)"]

The next two subsections show automatic length and deferred length, the two forms that are newest to people coming from other languages; assumed length appears in every example because every helper takes character(len=*) arguments.

Assumed and automatic length

An assumed-length dummy argument has the length of the actual argument, and len tells the procedure what it is. The procedure does not own the memory, so it can neither lengthen the string nor see how much of the actual argument is significant: it sees blanks, and len_trim is the way to ignore the trailing ones. A function result cannot be of assumed length in new code (the form is obsolescent, Annex B.3.6); the replacement is an automatic length, a length computed from the arguments by a specification expression. This is the usual shape of a function that transforms a string and returns a string of the same size:

module heatflow_text
  implicit none
  private
  public :: to_upper, same_name, reversed, is_digit

contains

  ! Automatic length: the result length is a specification expression, here len(text).
  pure function to_upper(text) result(upper)
    character(len=*), intent(in) :: text
    character(len=len(text)) :: upper
    integer :: i, code

    upper = text
    do i = 1, len(text)
      code = iachar(text(i:i))
      if (code >= iachar('a') .and. code <= iachar('z')) upper(i:i) = achar(code - 32)
    end do
  end function to_upper

  ! Case-insensitive comparison; trailing blanks are ignored by == anyway.
  pure logical function same_name(a, b)
    character(len=*), intent(in) :: a, b
    same_name = to_upper(a) == to_upper(b)
  end function same_name

  pure function reversed(text) result(back)
    character(len=*), intent(in) :: text
    character(len=len(text)) :: back
    integer :: i, n

    n = len(text)
    do i = 1, n
      back(i:i) = text(n - i + 1:n - i + 1)
    end do
  end function reversed

  ! An elemental function over characters works on whole arrays of characters.
  elemental logical function is_digit(c)
    character(len=1), intent(in) :: c
    is_digit = c >= '0' .and. c <= '9'
  end function is_digit

end module heatflow_text

program auto_length
  use heatflow_text, only: to_upper, same_name, reversed, is_digit
  implicit none
  character(len=*), parameter :: spec = 'nx=41,ny=21'
  character(len=:), allocatable :: s
  integer :: i

  s = 'Dirichlet'
  print '(3a)', '[', to_upper(s), ']'
  print '(3a)', '[', to_upper('neumann  '), ']'                ! trailing blanks are kept: length is len(text)
  print '(a, l1)', "same_name('Periodic', 'PERIODIC  '): ", same_name('Periodic', 'PERIODIC  ')
  print '(3a)', '[', reversed(s), ']'
  print '(a, i0)', 'digits in spec: ', count(is_digit([(spec(i:i), i = 1, len(spec))]))
end program auto_length
[DIRICHLET]
[NEUMANN  ]
same_name('Periodic', 'PERIODIC  '): T
[telhciriD]
digits in spec: 4

to_upper('neumann ') has the length of its argument, so the two trailing blanks survive. A function whose result length is not known from the arguments, such as one that reads or builds text, uses a deferred-length result instead.

Deferred length

A variable declared character(len=:), allocatable has no length until it is allocated, and an intrinsic assignment s = expr allocates it with the length of expr if it is unallocated, and deallocates and reallocates it if the lengths differ (J3/23-007r1, 10.2.1.3, with the standard’s own note for NAME = 'Dr. ' // FIRST_NAME // SURNAME). This automatic reallocation is on by default in GNU Fortran for every standard from Fortran 2003 on (the manual’s option is -frealloc-lhs). Four things are worth stating exactly:

  • s(:) = expr does not reallocate. A substring or section on the left keeps the existing length: the value is padded or truncated, and the variable must already be allocated. s(1:3) = text on an unallocated variable is an error.

  • allocate (character(len=n) :: s) allocates with an explicit length and allocate (s, source=expr) allocates with the length and value of expr; mold=expr takes only the length. After a plain allocate the content is undefined.

  • call move_alloc(from, to) transfers the allocation, length included, without copying.

  • Elements of a deferred-length array share one length. character(len=:), allocatable :: a(:) has a single len(a) for all elements; for strings of different lengths see the section on arrays of strings.

The following program uses the function that heatflow_boundary provides, boundary_label, with a deferred-length result, and shows each of the points above:

module heatflow_boundary_names
  implicit none
  private
  public :: boundary_label, describe

contains

  ! A function result with deferred length: the length is whatever the result is assigned.
  pure function boundary_label(code) result(label)
    integer, intent(in) :: code
    character(len=:), allocatable :: label
    select case (code)
    case (1)
      label = 'dirichlet'
    case (2)
      label = 'neumann'
    case (3)
      label = 'periodic'
    case default
      label = 'unknown'
    end select
  end function boundary_label

  ! An assumed-length dummy accepts any length; len(text) is the caller's length.
  subroutine describe(name, text)
    character(len=*), intent(in) :: name
    character(len=*), intent(in) :: text
    print '(4a, i0, a, i0)', name, ' = [', text, '] len=', len(text), ' len_trim=', len_trim(text)
  end subroutine describe

end module heatflow_boundary_names

program deferred_length
  use heatflow_boundary_names, only: boundary_label, describe
  implicit none
  character(len=:), allocatable :: s, t, u
  character(len=:), allocatable :: pieces(:)
  integer :: n

  print '(a, l1)', 'allocated(s) before assignment: ', allocated(s)
  s = 'ftcs'                              ! automatic allocation with length 4
  call describe('s', s)
  s = s // ' solver'                      ! reallocated to length 11
  call describe('s', s)
  s = boundary_label(1)                   ! reallocated to length 9
  call describe('s', s)

  s(:) = 'neu'                            ! no reallocation: the old length is kept, padding added
  call describe('s(:)', s)
  s = boundary_label(3)
  s(:) = 'a very long replacement'        ! no reallocation: truncated to the existing length
  call describe('s(:)', s)

  n = 5
  allocate (character(len=n) :: t)        ! explicit length at allocation; t is undefined
  print '(a, i0)', 'len(t) after allocate = ', len(t)
  t = 'abcdefgh'                          ! reallocated on assignment: the length 5 becomes 8
  call describe('t', t)
  deallocate (t)
  allocate (t, source=boundary_label(2))  ! length and value taken from the source expression
  call describe('t', t)

  call move_alloc(t, u)                   ! u takes over the allocation, t is now unallocated
  print '(a, l1, a, l1)', 'allocated(t)=', allocated(t), ' allocated(u)=', allocated(u)
  call describe('u', u)

  allocate (character(len=9) :: pieces(3)) ! all elements of one array share one length
  pieces = [character(len=9) :: 'dirichlet', 'neumann', 'periodic']
  print '(a, i0, a, i0)', 'len(pieces)=', len(pieces), ' size(pieces)=', size(pieces)
  do n = 1, size(pieces)
    print '(i0, 2a)', n, ': ', trim(pieces(n))
  end do
end program deferred_length
allocated(s) before assignment: F
s = [ftcs] len=4 len_trim=4
s = [ftcs solver] len=11 len_trim=11
s = [dirichlet] len=9 len_trim=9
s(:) = [neu      ] len=9 len_trim=3
s(:) = [a very l] len=8 len_trim=8
len(t) after allocate = 5
t = [abcdefgh] len=8 len_trim=8
t = [neumann] len=7 len_trim=7
allocated(t)=F allocated(u)=T
u = [neumann] len=7 len_trim=7
len(pieces)=9 size(pieces)=3
1: dirichlet
2: neumann
3: periodic

A deferred-length variable must be allocated before it is read, and a substring assignment never allocates:

! Deliberately non-conforming: the deferred-length variable is used before it is allocated.
program unallocated_string
  implicit none
  character(len=:), allocatable :: s
  character(len=:), allocatable :: t

  t = 'abc'
  s(1:3) = t                  ! substring assignment never allocates
  print '(3a)', '[', s, ']'
end program unallocated_string
unallocated_string.f90:8:3:
    8 |   s(1:3) = t                  ! substring assignment never allocates
      |   ^~~~~~~
Warning: '.s' may be used uninitialized [-Wmaybe-uninitialized]
unallocated_string.f90:2:26:
    2 | program unallocated_string
      |                          ^
note: '.s' was declared here
At line 8 of file unallocated_string.f90
Fortran runtime error: Substring out of bounds: upper bound (3) of 's' exceeds string length (0)
Error termination. Backtrace:

LLVM Flang 23 ended the same program with Segmentation fault. The diagnostic from GNU Fortran comes from -fcheck=all; without it the program writes through a null pointer, and the -Wmaybe-uninitialized warning above is the only compile-time hint.

A dummy argument with the allocatable attribute and a deferred length lets a procedure replace the caller’s string: character(len=:), allocatable, intent(out) :: text is deallocated on entry and the procedure assigns it. The interface must be explicit (a module procedure is the simplest way), and the actual argument must itself be a deferred-length allocatable; a fixed-length actual argument is rejected:

! Deliberately non-conforming call: a fixed-length actual argument for a deferred-length dummy.
module m
contains
  subroutine make_label(text)
    character(len=:), allocatable, intent(out) :: text
    text = 'dirichlet'
  end subroutine make_label
end module m

program alloc_dummy
  use m, only: make_label
  implicit none
  character(len=:), allocatable :: ok
  character(len=20) :: fixed
  call make_label(ok)
  print '(a, i0)', 'len(ok)=', len(ok)
  call make_label(fixed)
end program alloc_dummy

GNU Fortran 16.2, then LLVM Flang 23:

alloc_dummy.f90:17:18:
   17 |   call make_label(fixed)
      |                  1
Error: Actual argument at (1) to allocatable or pointer dummy argument 'text' must have a deferred length type parameter if and only if the dummy has one
error: Semantic errors in alloc_dummy.f90
alloc_dummy.f90:17:19: error: ALLOCATABLE dummy argument 'text=' must be associated with an ALLOCATABLE actual argument
    call make_label(fixed)
                    ^^^^^

An intent(in) assumed-length dummy accepts both fixed-length and allocatable actual arguments and is the better choice for strings the procedure only reads.

A last length-related error is passing a shorter actual argument to a dummy of explicit length. It is reported when the compiler can see both lengths, and silently ignored otherwise:

! Deliberately non-conforming: the actual argument is shorter than the explicit-length dummy.
module m
contains
  subroutine show(text)
    character(len=20), intent(in) :: text
    print '(3a)', '[', text, ']'
  end subroutine show
end module m

program short_actual
  use m, only: show
  implicit none
  character(len=10) :: name
  name = 'ftcs'
  call show(name)
end program short_actual
short_actual.f90:15:12:
   15 |   call show(name)
      |            1
Error: Character length of actual argument shorter than of dummy argument 'text' (10/20) at (1)

LLVM Flang 23 accepted the program without a diagnostic and printed the 10 bytes of name followed by whatever followed them in memory. Use assumed length for dummy arguments that only read a string.

String intrinsic procedures

The intrinsic functions below are all elemental, so they apply to arrays of strings element by element, except where the table says otherwise. The Since column is the Fortran standard that introduced the procedure or the argument named.

Procedure Clause Since Result

len(string [, kind])

16.9.122

F77

Length of the string. For an unallocated variable of deferred length it is undefined. The kind argument is Fortran 2003.

len_trim(string [, kind])

16.9.123

F90

Length without trailing blanks; 0 for a string of blanks.

trim(string)

16.9.214

F90

The string without trailing blanks, with the length len_trim(string).

adjustl(string), adjustr(string)

16.9.8, 16.9.9

F90

Same length: leading blanks are removed and put at the end (adjustl), or trailing blanks are put at the start (adjustr).

index(string, substring [, back, kind])

16.9.109

F77

Start of the first (back=.true.: last) occurrence of substring, or 0. index(s, '') is 1, or len(s)+1 with back.

scan(string, set [, back, kind])

16.9.179

F90

Position of the first (last with back) character of string that is in set, or 0.

verify(string, set [, back, kind])

16.9.218

F90

Position of the first (last with back) character of string that is not in set, or 0 if all are in it.

repeat(string, ncopies)

16.9.174

F90

ncopies copies concatenated; ncopies is scalar and not negative. Not elemental.

achar(i [, kind]), iachar(c [, kind])

16.9.3, 16.9.98

F90

Conversion between an ASCII code and its character, independent of the processor’s default sequence.

char(i [, kind]), ichar(c [, kind])

16.9.52, 16.9.105

F77

Conversion using the processor’s own code for the kind. For codes 0 to 127 on a processor whose default kind is ASCII they equal achar and iachar.

lge, lgt, lle, llt (string_a, string_b)

16.9.124 to 16.9.127

F77

Comparison using the ASCII sequence.

new_line(a)

16.9.150

F2003

The newline character of the kind of a; achar(10) on both compilers used here. The standard defines its record-ending effect only for formatted stream files (13.7.4); both compilers also end the line when it is written to standard output, as the example shows.

selected_char_kind(name)

16.9.180

F2003

Kind for 'default', 'ascii' or 'iso_10646'; -1 when not supported. Not elemental.

split(string, set, pos [, back])

16.9.196

F2023

Subroutine: finds the next delimiter. See the next section.

tokenize(string, set, tokens [, separator]) or tokenize(string, set, first, last)

16.9.210

F2023

Subroutine: all tokens at once. See the next section.

max, min, maxval, minval, maxloc, minloc, findloc

16.9.135 to 16.9.144 and 16.9.88

see text

Accept character arguments and use the collating comparison. max and min extend shorter results with blanks to the longest length.

merge, pack, transfer, reshape, spread, cshift

16.9

F90

Work on arrays and strings of any character type, which is how a string is converted to and from an array of single characters.

The program below exercises each of the first group, including the edge cases in the table (the empty substring, the zero copies of repeat, the three outcomes of verify). The case conversion at the end is the portable idiom, since there is no intrinsic for it:

program str_intrinsics
  implicit none
  character(len=32) :: path
  character(len=:), allocatable :: key, text
  character(len=*), parameter :: digits = '0123456789'
  integer :: i

  path = '   output/heatflow.csv'
  print '(a, i0, a, i0)', 'len=', len(path), ' len_trim=', len_trim(path)
  print '(3a)', '[', trim(path), ']'                   ! removes trailing blanks only
  print '(3a)', '[', adjustl(path), ']'                ! moves leading blanks to the end
  print '(3a)', '[', adjustr('ftcs    '), ']'          ! moves trailing blanks to the front
  print '(3a)', '[', trim(adjustl(path)), ']'          ! the usual idiom

  text = 'bc=dirichlet,solver=ftcs,solver=jacobi'
  print '(a, i0)', "index(text, 'solver')          = ", index(text, 'solver')
  print '(a, i0)', "index(text, 'solver', back=.true.) = ", index(text, 'solver', back=.true.)
  print '(a, i0)', "index(text, 'missing')         = ", index(text, 'missing')
  print '(a, i0)', "index(text, '')                = ", index(text, '')
  print '(a, i0)', "scan(text, ',=')               = ", scan(text, ',=')        ! first of any listed character
  print '(a, i0)', "scan(text, ',=', back=.true.)  = ", scan(text, ',=', back=.true.)
  print '(a, i0)', "verify('2000', digits)         = ", verify('2000', digits)  ! 0: every character is a digit
  print '(a, i0)', "verify('20x0', digits)         = ", verify('20x0', digits)  ! position of the first non-digit
  print '(a, i0)', "verify('20x0 ', digits, .true.)= ", verify('20x0 ', digits, .true.)

  print '(2a)', 'repeat: ', repeat('=-', 6)
  print '(2a)', 'repeat with 0 copies: [', repeat('ab', 0) // ']'

  key = 'Solver'
  print '(a, i0, a, a)', 'iachar(', iachar(key(1:1)), ') = ', key(1:1)
  print '(a, a)', 'achar(97 + 1) = ', achar(97 + 1)
  print '(a, i0)', "ichar('A') = ", ichar('A')
  print '(a, a)', "char(66)  = ", char(66)
  do i = 1, len(key)                                   ! upper-case conversion with ASCII codes
    if (key(i:i) >= 'a' .and. key(i:i) <= 'z') key(i:i) = achar(iachar(key(i:i)) - 32)
  end do
  print '(2a)', 'upper-cased: ', key

  print '(a)', 'line one' // new_line('a') // 'line two'
  print '(a, i0)', "iachar(new_line('a')) = ", iachar(new_line('a'))
end program str_intrinsics
len=32 len_trim=22
[   output/heatflow.csv]
[output/heatflow.csv             ]
[    ftcs]
[output/heatflow.csv]
index(text, 'solver')          = 14
index(text, 'solver', back=.true.) = 26
index(text, 'missing')         = 0
index(text, '')                = 1
scan(text, ',=')               = 3
scan(text, ',=', back=.true.)  = 32
verify('2000', digits)         = 0
verify('20x0', digits)         = 3
verify('20x0 ', digits, .true.)= 5
repeat: =-=-=-=-=-=-
repeat with 0 copies: []
iachar(83) = S
achar(97 + 1) = b
ichar('A') = 65
char(66)  = B
upper-cased: SOLVER
line one
line two
iachar(new_line('a')) = 10

trim and adjustl are the pair behind most of the code in this page: trim(adjustl(buffer)) turns a fixed-length buffer that was written with a format into the value without padding. The functions that take a set (scan, verify) treat it as a set of characters, not as a substring; scan(text, ',=') finds the first comma or equals sign, and index(text, ',=') would look for the two-character sequence.

The ordering intrinsics work on strings too. maxval and minval of an array of strings return the greatest and least in the collating sequence, maxloc and minloc give the position, and max and min of several strings return the largest extended with blanks to the longest length:

program char_maxmin
  implicit none
  character(len=9) :: names(3) = [character(len=9) :: 'jacobi', 'ftcs', 'multigrid']
  print '(3a)', '[', maxval(names), ']'
  print '(3a)', '[', minval(names), ']'
  print '(a, i0, a, i0)', 'maxloc=', maxloc(names, 1), ' minloc=', minloc(names, 1)
  print '(3a)', '[', max('ftcs', 'jacobi', 'abc'), ']'
end program char_maxmin
[multigrid]
[ftcs     ]
maxloc=3 minloc=2
[jacobi]

Because a string is a scalar, string(i) is not an element access and string(i:i) is how a single character is read. When an algorithm is simpler on an array, transfer(string, [' ']) converts a string to an array of character(len=1), and transfer(array, repeat(' ', n)) converts back. The pack call below also shows that chars /= ',' is an elemental comparison on single characters:

program char_arrays
  implicit none
  character(len=*), parameter :: spec = 'nx=41,ny=21'
  character(len=1), allocatable :: chars(:)
  character(len=:), allocatable :: digits_only, back
  integer :: i

  chars = transfer(spec, [' '])                       ! string -> array of character(len=1)
  print '(a, i0, a, i0)', 'size(chars)=', size(chars), ' len(spec)=', len(spec)

  digits_only = ''
  do i = 1, size(chars)
    if (index('0123456789', chars(i)) > 0) digits_only = digits_only // chars(i)
  end do
  print '(2a)', 'digits: ', digits_only

  back = transfer(pack(chars, chars /= ','), repeat(' ', count(chars /= ',')))   ! array -> string
  print '(2a)', 'without commas: ', back
  print '(a, i0)', 'commas: ', count(chars == ',')
  print '(a, i0)', 'first "=" at element ', findloc(chars, '=', 1)
  print '(a, *(a))', 'reversed: ', chars(size(chars):1:-1)
end program char_arrays
size(chars)=11 len(spec)=11
digits: 4121
without commas: nx=41ny=21
commas: 1
first "=" at element 3
reversed: 12=yn,14=xn

Splitting a string: split and tokenize

Parsing key=value lists, comma-separated numbers and file paths means cutting a string at delimiters. Before Fortran 2023 this was written by hand with index and scan, as in the portable function later in the page. Fortran 2023 added two intrinsic subroutines for the task. In both, set is a string whose characters are all delimiters, and a token is the text between two delimiters or between a delimiter and an end of the string. Consecutive delimiters, or a delimiter at either end, produce a token of length zero; nothing is discarded or merged.

  • split(string, set, pos [, back]) finds one delimiter at a time. On entry pos says where to start (0 to start at the beginning), on return it is the position of the next delimiter, or len(string)+1 if there is none. The token that was just passed runs from the old pos+1 to the new pos-1. With back=.true. the search goes right to left: pos starts at len(string)+1 and returns the previous delimiter or 0. No memory is allocated, so it suits loops and fixed-size buffers.

  • tokenize(string, set, tokens [, separator]) finds all tokens and returns them in an allocatable, rank-one, deferred-length array whose length is that of the longest token, so shorter tokens are padded with blanks. The optional separator receives the delimiter that follows each token. The second form, tokenize(string, set, first, last), allocates two integer arrays with the start and end positions of every token; a zero-length token has last = first - 1. Both forms allocate the arguments they define.

Both subroutines are simple procedures (J3/23-007r1, 16.9.196 and 16.9.210), which means they can be called from do concurrent and from other pure procedures. The loops below use split; the start of the loop is always pos = 0 and the test is pos > len(string), so a trailing delimiter correctly produces a final empty token. Both a forward and a backward scan are shown, with a string that has empty tokens at both ends:

program split_loop
  implicit none
  character(len=*), parameter :: delimiters = ',;'

  call show_forward('nx=41,,ny=21;alpha=1.0')
  call show_forward(',a,')                      ! empty tokens at both ends
  call show_backward('nx=41,,ny=21;alpha=1.0')

contains

  subroutine show_forward(spec)
    character(len=*), intent(in) :: spec
    integer :: first, pos, count

    print '(3a)', 'forward: "', spec, '"'
    pos = 0                                     ! 0 means: before the first character
    count = 0
    do
      if (pos > len(spec)) exit                 ! pos = len+1 after the last token
      first = pos + 1
      call split(spec, delimiters, pos)         ! pos becomes the next delimiter, or len+1
      count = count + 1
      print '(i0, a, i0, a, i0, 3a)', count, ': positions ', first, ' to ', pos - 1, ' = [', spec(first:pos - 1), ']'
    end do
  end subroutine show_forward

  subroutine show_backward(spec)
    character(len=*), intent(in) :: spec
    integer :: last, pos

    print '(3a)', 'backward: "', spec, '"'
    pos = len(spec) + 1                         ! len+1 means: after the last character
    do
      last = pos - 1
      call split(spec, delimiters, pos, back=.true.)   ! pos becomes the previous delimiter, or 0
      print '(3a)', '  [', spec(pos + 1:last), ']'
      if (pos == 0) exit
    end do
  end subroutine show_backward

end program split_loop
forward: "nx=41,,ny=21;alpha=1.0"
1: positions 1 to 5 = [nx=41]
2: positions 7 to 6 = []
3: positions 8 to 12 = [ny=21]
4: positions 14 to 22 = [alpha=1.0]
forward: ",a,"
1: positions 1 to 0 = []
2: positions 2 to 2 = [a]
3: positions 4 to 3 = []
backward: "nx=41,,ny=21;alpha=1.0"
  [alpha=1.0]
  [ny=21]
  []
  [nx=41]

split is available in both compilers. tokenize is not: the GNU Fortran manual has no page for it, and the compiler treated the call as a call to an external subroutine named tokenize, which fails at link time.

program tokenize_probe
  implicit none
  character(len=:), allocatable :: tokens(:)
  call tokenize('a,b;c', ',;', tokens)
  print '(i0)', size(tokens)
end program tokenize_probe
/work/tokenize_probe.f90:4:(.text+0xd4): undefined reference to `tokenize_'
collect2: error: ld returned 1 exit status

Because the call is treated as an implicit external, GNU Fortran also rejects a file that uses the different forms of tokenize together, with messages about the argument lists of a procedure the programmer never wrote:

Error: More actual than formal arguments in procedure call at (1)
Error: Type mismatch between actual argument at (1) and actual argument at (2) (INTEGER(4)/CHARACTER(*)).

LLVM Flang 23 implements both forms. The program below runs on Flang; the GNU Fortran messages above are the ones it produces for it:

! Fortran 2023 TOKENIZE. Accepted by LLVM Flang 23; GNU Fortran 16.2 compiles the call but
! fails to link it (see the page text for the real diagnostic).
program tokenize_f2023
  implicit none
  character(len=*), parameter :: spec = 'nx=41,,ny=21;alpha=1.0'
  character(len=:), allocatable :: tokens(:), seps(:)
  integer, allocatable :: first(:), last(:)
  integer :: i

  call tokenize(spec, ',;', tokens)                ! form 1: tokens, padded to the longest
  print '(a, i0, a, i0)', 'tokens: size=', size(tokens), ' len=', len(tokens)
  do i = 1, size(tokens)
    print '(i0, 3a)', i, ': [', trim(tokens(i)), ']'
  end do

  call tokenize(spec, ',;', tokens, seps)          ! form 1 with SEPARATOR
  print '(a, i0, *(1x, a))', 'separators: ', size(seps), seps

  call tokenize(spec, ',;', first, last)           ! form 2: positions only, no copy
  do i = 1, size(first)
    print '(i0, a, i0, a, i0, 3a)', i, ': ', first(i), ':', last(i), ' [', spec(first(i):last(i)), ']'
  end do
end program tokenize_f2023
tokens: size=4 len=9
1: [nx=41]
2: []
3: [ny=21]
4: [alpha=1.0]
separators: 3 , , ;
1: 1:5 [nx=41]
2: 7:6 []
3: 8:12 [ny=21]
4: 14:22 [alpha=1.0]

The portable replacement for tokenize is a function written with index or scan, shown in the section on arrays of strings below; it returns an array of a wrapper type so that tokens keep their exact length, which tokenize cannot do. When only split is needed it is the better primitive: it works with both compilers and does not allocate.

Numbers and strings

Fortran has no to_string or parse_float in the language. The conversion is done by internal I/O: a write whose unit is a character variable converts values to text, and a read from a character variable converts text to values, with the same format machinery as for files (J3/23-007r1, 12.4). The variable is the internal file; a scalar variable is one record, an array is a file of as many records as it has elements, and an internal write fails if the text does not fit in the record (GNU Fortran reports an end-of-file error).

Two idioms cover nearly every case. To convert a value to text, write it into a buffer that is large enough, with a format that does not pad (i0, f0.3, g0), and use trim(adjustl(buffer)). To convert text to a value, read from the string with iostat= and check it; a list-directed read (read (text, *) x) accepts any valid number with surrounding blanks and stops at a comma or a blank, while an explicit format is stricter. Both appear in this module, which also shows a generic to_string:

module heatflow_convert
  use, intrinsic :: iso_fortran_env, only: int32, real64
  implicit none
  private
  public :: to_string, parse_real, parse_int

  interface to_string
    module procedure int_to_string, real_to_string
  end interface to_string

contains

  ! Conversion to a string: write into a buffer big enough for any value, then trim.
  function int_to_string(value) result(text)
    integer(int32), intent(in) :: value
    character(len=:), allocatable :: text
    character(len=16) :: buffer              ! 11 characters are enough for an int32

    write (buffer, '(i0)') value              ! i0: minimum width, no blanks
    text = trim(buffer)
  end function int_to_string

  function real_to_string(value, fmt) result(text)
    real(real64), intent(in) :: value
    character(len=*), intent(in), optional :: fmt
    character(len=:), allocatable :: text
    character(len=64) :: buffer

    if (present(fmt)) then
      write (buffer, fmt) value
    else
      write (buffer, '(g0)') value            ! g0: shortest field that shows the value
    end if
    text = trim(adjustl(buffer))
  end function real_to_string

  ! Conversion from a string: list-directed internal read, errors reported through iostat.
  subroutine parse_real(text, value, stat, msg)
    character(len=*), intent(in) :: text
    real(real64), intent(out) :: value
    integer, intent(out) :: stat
    character(len=*), intent(inout) :: msg      ! fixed length: portable before Fortran 2023

    read (text, *, iostat=stat, iomsg=msg) value
  end subroutine parse_real

  subroutine parse_int(text, value, stat)
    character(len=*), intent(in) :: text
    integer(int32), intent(out) :: value
    integer, intent(out) :: stat

    read (text, '(i10)', iostat=stat) value
  end subroutine parse_int

end module heatflow_convert

program number_string
  use, intrinsic :: iso_fortran_env, only: int32, real64
  use heatflow_convert, only: to_string, parse_real, parse_int
  implicit none
  character(len=:), allocatable :: label
  character(len=128) :: msg
  real(real64) :: x
  integer(int32) :: n
  integer :: stat

  label = 'nx=' // to_string(41_int32) // ' alpha=' // to_string(1.0_real64)
  print '(2a)', 'label: ', label
  print '(2a)', 'g0 of 0.1:      ', to_string(0.1_real64)
  print '(2a)', 'g0 of 1.0e-7:   ', to_string(1.0e-7_real64)
  print '(2a)', 'g0 of 123456.5: ', to_string(123456.5_real64)
  print '(2a)', 'f8.3 of pi:     ', to_string(3.14159265_real64, '(f8.3)')
  print '(2a)', 'es12.4 of 1e6:  ', to_string(1.0e6_real64, '(es12.4)')

  msg = ''
  call parse_real('  0.25 ', x, stat, msg)
  print '(a, g0, a, i0)', 'parsed ', x, ' stat=', stat
  call parse_real('1.5e-3,7', x, stat, msg)               ! list-directed: stops at the comma
  print '(a, g0, a, i0)', 'parsed ', x, ' stat=', stat
  call parse_real('abc', x, stat, msg)
  print '(a, i0, 2a)', 'bad input: stat=', stat, ' msg=', trim(msg)
  call parse_real('', x, stat, msg)
  print '(a, i0, 2a)', 'empty input: stat=', stat, ' msg=', trim(msg)
  call parse_int('  42', n, stat)
  print '(a, i0, a, i0)', 'int ', n, ' stat=', stat
  call parse_int('4x2', n, stat)
  print '(a, i0)', 'bad int stat=', stat
end program number_string

The output of GNU Fortran 16.2:

label: nx=41 alpha=1.0000000000000000
g0 of 0.1:      0.10000000000000001
g0 of 1.0e-7:   0.99999999999999995E-007
g0 of 123456.5: 123456.50000000000
f8.3 of pi:     3.142
es12.4 of 1e6:  1.0000E+06
parsed 0.25000000000000000 stat=0
parsed 0.15000000000000000E-002 stat=0
bad input: stat=5010 msg=Bad real number in item 1 of list input
empty input: stat=-1 msg=End of file
int 42 stat=0
bad int stat=5010

LLVM Flang 23 printed different text for the lines that depend on processor-defined behaviour; everything else was identical:

label: nx=41 alpha=1.
g0 of 0.1:      .1
g0 of 1.0e-7:   .1E-6
g0 of 123456.5: 123456.5
f8.3 of pi:     3.142
es12.4 of 1e6:  1.0000E+06
parsed .25 stat=0
parsed .15E-2 stat=0
bad input: stat=1030 msg=Bad real input data at column 2 of record 1
empty input: stat=-1 msg=End of file during input
int 42 stat=0
bad int stat=1001

Three differences between the compilers show up there and deserve attention:

  • g0 for reals is processor dependent. The standard says only that G0 follows the rules for Gw.dEe with reasonable processor-dependent w, d and e and with blanks removed (13.7.5.2.3). GNU Fortran prints enough digits to round-trip the value (0.10000000000000001), and Flang prints the shortest representation and omits the leading zero (.1, and 1. for 1.0). A program that must produce the same text on both compilers uses an explicit format such as f8.3 or es12.4.

  • f0.3 omits the leading zero on both compilers (.500), which is allowed by the standard. Fortran 2023 added the lz, lzs and lzp control edit descriptors and the leading_zero= specifier for this (Introduction of J3/23-007r1). Flang accepted lzp with a fixed width (0.500 with f8.3) but still printed .500 for f0.3, and GNU Fortran 16.2 rejected lzp with Legacy Extension: Missing comma in FORMAT string.

  • The iostat values and the iomsg texts are processor-defined. The standard fixes only the sign: positive for an error condition, negative for the end of file or end of record, and zero for success. Test stat /= 0 and print the message; do not compare with a number.

A format that builds a file name or a CSV row follows the same pattern. Zero-padding is i6.6, a field without padding is i0 or f0.d (with the leading-zero caveat above), and the unlimited repeat *(...) writes a variable number of items. The colon edit descriptor : stops the format at the first item that has no data, which removes the trailing separator:

program build_strings
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  character(len=48) :: buffer
  character(len=:), allocatable :: filename, row
  integer :: step, i
  real(real64) :: u(3)

  step = 125
  write (buffer, '(a, i6.6, a)') 'heatflow_step_', step, '.csv'    ! zero-padded step number
  filename = trim(buffer)
  print '(2a)', 'file name: ', filename

  u = [0.5_real64, 12.25_real64, 100.0_real64]
  row = ''
  do i = 1, size(u)
    write (buffer, '(f0.3)') u(i)                  ! f0: smallest field, sign and digits only
    if (i > 1) row = row // ','
    row = row // trim(buffer)
  end do
  print '(2a)', 'row with f0.3: ', row

  write (buffer, '(*(g0, :, ","))') step, 'ftcs', .true., 2             ! g0 on integer, character, logical
  print '(2a)', 'row with g0:   ', trim(buffer)
  write (buffer, '(*(f8.3, :, ","))') u
  print '(2a)', 'row with f8.3: ', trim(buffer)
  write (buffer, '(*(es12.4e3, :, ";"))') u
  print '(2a)', 'row with es:   ', trim(buffer)
end program build_strings
file name: heatflow_step_000125.csv
row with f0.3: .500,12.250,100.000
row with g0:   125,ftcs,T,2
row with f8.3:    0.500,  12.250, 100.000
row with es:    5.0000E-001; 1.2250E+001; 1.0000E+002

The g0 descriptor is convenient for building heterogeneous rows because it applies the edit descriptor that suits the type: an integer prints as i0, a logical as l1 and a character as a without padding (13.7.5.2.2, 13.7.5.3 and 13.7.5.4). For real values the choice of digits is the processor’s, as seen above.

The AT edit descriptor (Fortran 2023)

The a edit descriptor writes a string with its full length, including trailing blanks, and that is why trim appears in every print of a fixed-length variable. Fortran 2023 adds the at edit descriptor, which writes the value with trailing blanks removed and with no field width; on a string of blanks it writes nothing. It is for output only (J3/23-007r1, 13.7.4).

! Fortran 2023 AT edit descriptor (clause 13.7.4). Accepted by LLVM Flang 23.
! GNU Fortran 16.2 rejects it (see the page text for the real diagnostic).
program at_edit
  implicit none
  character(len=12) :: solver = 'ftcs'
  character(len=12) :: blank = ' '
  integer :: n = 41

  write (*, '("[", a, "]")') solver              ! a: the whole field, 12 characters
  write (*, '("[", at, "]")') solver             ! at: trailing blanks removed
  write (*, '("[", at, "]")') blank              ! all blanks: no output at all
  write (*, '(at, "=", i0)') solver, n           ! ftcs=41, no trim() needed
end program at_edit

LLVM Flang 23 accepts it:

[ftcs        ]
[ftcs]
[]
ftcs=41

GNU Fortran 16.2 does not recognise at as an edit descriptor. It reads a followed by the position descriptor t and complains that the comma is missing:

at_edit.f90:10:19:
   10 |   write (*, '("[", at, "]")') solver             ! at: trailing blanks removed
      |                   1
Error: Legacy Extension: Missing comma in FORMAT string at (1)
at_edit.f90:11:19:
   11 |   write (*, '("[", at, "]")') blank              ! all blanks: no output at all
      |                   1
Error: Legacy Extension: Missing comma in FORMAT string at (1)
at_edit.f90:12:14:
   12 |   write (*, '(at, "=", i0)') solver, n           ! ftcs=41, no trim() needed
      |              1
Error: Legacy Extension: Missing comma in FORMAT string at (1)

The Flang documentation of Fortran 2023 support (read on 2026-10-18) lists the at edit descriptor as not implemented, but the build checked here accepted and executed it; the table is behind the compiler. Until both compilers agree, write a with trim (write (*, '(a, "=", i0)') trim(solver), n), which is valid in every standard and costs nothing.

Automatic allocation of iomsg, errmsg and internal-file variables (Fortran 2023)

Before Fortran 2023 the iomsg= variable of an I/O statement and the errmsg= variable of allocate, deallocate and the image-control statements had to be a character variable that already had a length, and the message was truncated or padded to it. That forced code to guess a size: character(len=256) :: msg. Fortran 2023 lets the variable be a deferred-length allocatable scalar, in which case the processor allocates it with the length of the message (Introduction, "Data usage and computation" and "Input/output"). The same release lets a deferred-length allocatable scalar be the unit of an internal write, in which case it is allocated to the length of the record written (12.4). Both are behaviour changes: code that relied on a fixed-length msg keeps working, but an allocatable one now has a defined meaning where it used to be an error.

The three programs below use the new behaviour. Neither compiler implements it. In GNU Fortran 16.2 and in LLVM Flang 23 the variable stays unallocated, and then the compiler either crashes or reports an end of file:

! Fortran 2023: a deferred-length allocatable iomsg variable is allocated by the processor.
! Neither GNU Fortran 16.2 nor LLVM Flang 23 implements this yet (see the page text).
program iomsg_f2023
  implicit none
  character(len=:), allocatable :: message
  integer :: stat, unit

  open (newunit=unit, file='no-such-file.nml', status='old', action='read', iostat=stat, iomsg=message)
  print '(a, i0)', 'stat=', stat
  print '(a, l1)', 'allocated(message)=', allocated(message)
  if (allocated(message)) print '(a, i0, 3a)', 'len=', len(message), ' [', message, ']'
end program iomsg_f2023

GNU Fortran 16.2 (with -fcheck=all), then LLVM Flang 23:

iomsg_f2023.f90:8:103:
    8 |   open (newunit=unit, file='no-such-file.nml', status='old', action='read', iostat=stat, iomsg=message)
      |                                                                                                       ^
Warning: '.message' may be used uninitialized [-Wmaybe-uninitialized]
iomsg_f2023.f90:3:19:
    3 | program iomsg_f2023
      |                   ^
note: '.message' was declared here
Program received signal SIGSEGV: Segmentation fault - invalid memory reference.
Backtrace for this error:
Segmentation fault
stat=2
allocated(message)=F
! Fortran 2023: a deferred-length allocatable scalar internal file in WRITE is allocated
! to the length of the record written. Not implemented by GNU Fortran 16.2 or LLVM Flang 23.
program internal_write_f2023
  implicit none
  character(len=:), allocatable :: line

  write (line, '(a, i0, a, f6.2)') 'nx=', 41, ' alpha=', 1.0
  print '(a, i0, 3a)', 'len=', len(line), ' [', line, ']'
end program internal_write_f2023

GNU Fortran 16.2, then LLVM Flang 23:

internal_write_f2023.f90:7:60:
    7 |   write (line, '(a, i0, a, f6.2)') 'nx=', 41, ' alpha=', 1.0
      |                                                            ^
Warning: '.line' may be used uninitialized [-Wmaybe-uninitialized]
internal_write_f2023.f90:3:28:
    3 | program internal_write_f2023
      |                            ^
note: '.line' was declared here
At line 7 of file internal_write_f2023.f90
Fortran runtime error: End of file
Error termination. Backtrace:
fatal Fortran runtime error(/work/internal_write_f2023.f90:7): Internal write overran available records
Aborted

The same happens for errmsg= in a deallocate statement:

! Fortran 2023: a deferred-length allocatable errmsg variable is allocated by the processor.
! Neither GNU Fortran 16.2 nor LLVM Flang 23 does it yet (see the page text).
program errmsg_f2023
  implicit none
  character(len=:), allocatable :: message
  real, allocatable :: u(:)
  integer :: stat

  deallocate (u, stat=stat, errmsg=message)         ! u is not allocated: an error condition
  print '(a, l1)', 'stat /= 0: ', stat /= 0
  print '(a, l1)', 'allocated(message)=', allocated(message)
  if (allocated(message)) print '(a, i0, 3a)', 'len=', len(message), ' [', message, ']'
end program errmsg_f2023

GNU Fortran 16.2, then LLVM Flang 23:

errmsg_f2023.f90:9:92:
    9 |   deallocate (u, stat=stat, errmsg=message)         ! u is not allocated: an error condition
      |                                                                                            ^
Warning: '.message' may be used uninitialized [-Wmaybe-uninitialized]
errmsg_f2023.f90:3:20:
    3 | program errmsg_f2023
      |                    ^
note: '.message' was declared here
Program received signal SIGSEGV: Segmentation fault - invalid memory reference.
Backtrace for this error:
Segmentation fault
stat /= 0: T
allocated(message)=F

The portable form is a fixed-length buffer that is large enough, trimmed after use, which is what the parse_real routine in the previous section does with its msg argument. For an internal write, use the buffer-and-trim idiom shown there; the heatflow_config module of the running example reads its namelist with a character(len=*) message argument for the same reason. A message longer than the buffer is truncated without an error.

Character kinds, Unicode and UTF-8

Each processor provides a default character kind and may provide others. selected_char_kind(name) returns the kind number for 'default', 'ascii' or 'iso_10646' (case and trailing blanks are ignored) and -1 if the processor lacks it, and a character literal of a given kind is written with the kind name and an underscore before the delimiter. The ASCII kind is the character set of ISO/IEC 646, and the ISO 10646 kind is UCS-4, the full Unicode repertoire with one character per code point (J3/23-007r1, 7.4.4.1 and 16.9.180). Both compilers used here provide kind 1 for default and ASCII, and kind 4 for ISO 10646; the kind numbers are not portable (the standard does not fix them), so use the values returned by selected_char_kind rather than numbers.

Three layers have to be kept apart:

  • The kind of the variable decides the character set in memory. With the ISO 10646 kind, len counts code points, and iachar and achar work on ASCII codes only. The kind-4 characters have their own char(i, kind) and ichar.

  • The encoding of a file is a property of the connection: open (unit=u, ..., encoding='utf-8') makes a formatted file Unicode. Characters of the ISO 10646 kind are then converted to and from UTF-8 on the way out and in (12.5.6.9), and inquire (unit=u, encoding=e) returns 'UTF-8' for such a connection. Only a formatted connection can have this option; an unformatted file is a sequence of bytes.

  • The bytes of the source file. A literal such as 'café' in a default-kind variable holds the bytes of the source file’s encoding: with UTF-8 source the accented character occupies two bytes, so len is 5.

The program writes a string of the ISO 10646 kind to a UTF-8 file, reads the raw bytes back through a stream connection and reads the text again through a Unicode connection:

program char_kinds
  use, intrinsic :: iso_fortran_env, only: int8
  implicit none
  integer, parameter :: ascii = selected_char_kind('ascii')
  integer, parameter :: ucs4 = selected_char_kind('iso_10646')
  character(kind=ucs4, len=:), allocatable :: label
  character(kind=ucs4, len=20) :: readback
  character(len=:), allocatable :: plain
  integer(int8), allocatable :: bytes(:)
  integer :: unit, stat, size_in_bytes, i

  print '(a, i0)', "selected_char_kind('default')   = ", selected_char_kind('default')
  print '(a, i0)', "selected_char_kind('ascii')     = ", ascii
  print '(a, i0)', "selected_char_kind('iso_10646') = ", ucs4
  print '(a, i0)', "selected_char_kind('klingon')   = ", selected_char_kind('klingon')
  print '(a, i0)', "kind('a') = ", kind('a')

  ! The degree sign and the Greek letter alpha come from their Unicode code points.
  label = ucs4_'Temperature ' // char(int(z'00B0'), ucs4) // ucs4_'C, ' // char(int(z'03B1'), ucs4)
  print '(a, i0)', 'len(label) in characters = ', len(label)

  open (newunit=unit, file='label.txt', status='replace', action='write', encoding='utf-8')
  write (unit, '(a)') label
  close (unit)

  open (newunit=unit, file='label.txt', status='old', action='read', access='stream', form='unformatted')
  inquire (unit, size=size_in_bytes)
  allocate (bytes(size_in_bytes))
  read (unit) bytes
  close (unit)
  print '(a, i0)', 'file size in bytes = ', size_in_bytes
  print '(a, *(z2.2, 1x))', 'bytes = ', (iand(int(bytes(i)), 255), i = 13, size_in_bytes)

  open (newunit=unit, file='label.txt', status='old', action='read', encoding='utf-8')
  read (unit, '(a)', iostat=stat) readback
  close (unit)
  print '(a, i0, a, i0)', 'stat=', stat, ' len_trim(readback)=', len_trim(readback)
  print '(a, l1)', 'round trip equals label: ', trim(readback) == label

  plain = 'caf' // char(int(z'C3')) // char(int(z'A9'))   ! the two UTF-8 bytes of an accented e
  print '(a, i0)', 'default-kind string holding UTF-8: len = ', len(plain)
end program char_kinds
selected_char_kind('default')   = 1
selected_char_kind('ascii')     = 1
selected_char_kind('iso_10646') = 4
selected_char_kind('klingon')   = -1
kind('a') = 1
len(label) in characters = 17
file size in bytes = 20
bytes = C2 B0 43 2C 20 CE B1 0A
stat=0 len_trim(readback)=17
round trip equals label: T
default-kind string holding UTF-8: len = 5

The file has 20 bytes for 17 characters because the degree sign (C2 B0) and the Greek letter (CE B1) take two bytes each, and the final byte is the newline that ends the record. A default-kind literal that contains non-ASCII text behaves as bytes on both compilers:

program utf8_literal
  implicit none
  character(len=:), allocatable :: s
  s = 'café'
  print '(a, i0, 2a)', 'len=', len(s), ' ', s
end program utf8_literal
len=5 café

Mixing kinds follows strict rules. A formatted transfer to an internal file of default kind cannot carry a character item of a nondefault kind, an internal file of the ISO 10646 kind accepts items of ISO 10646 and ASCII kinds, and comparison and concatenation need operands of the same kind (12.6.3). Assignment between kinds converts, and it is a conversion that can lose information. Programs that handle Unicode text beyond display purposes, normalisation or grapheme clusters need a library; the language knows only code points.

C strings (Fortran 2023)

A C string is an array of char ended by a null character, while a Fortran string carries its length and may contain null characters. The iso_c_binding module has c_char and c_null_char for building such strings by hand, as in trim(name) // c_null_char. Fortran 2023 adds two procedures to remove that boilerplate (J3/23-007r1, 18.2.3.9 and 18.2.3.5): f_c_string(string [, asis]) returns the string followed by a null character, with trailing blanks removed unless asis is true, and c_f_strpointer makes a deferred-length character pointer from a C address or an array of c_char, with length up to the first null character. f_c_string is accepted by both compilers:

! Fortran 2023 F_C_STRING (clause 18.2.3.9): accepted by GNU Fortran 16.2 and LLVM Flang 23.
program f_c_string_demo
  use, intrinsic :: iso_c_binding, only: c_char, f_c_string
  implicit none
  character(kind=c_char, len=16) :: padded = 'heatflow.csv'
  character(kind=c_char, len=:), allocatable :: c_text, c_text_asis

  c_text = f_c_string(padded)                    ! trailing blanks removed, then a null appended
  c_text_asis = f_c_string(padded, asis=.true.)  ! every character kept, then a null appended
  print '(a, i0, a, i0)', 'default: len=', len(c_text), ' last code=', iachar(c_text(len(c_text):len(c_text)))
  print '(a, i0)', 'asis=.true.: len=', len(c_text_asis)
end program f_c_string_demo
default: len=13 last code=0
asis=.true.: len=17

c_f_strpointer is accepted by LLVM Flang 23 but not by GNU Fortran 16.2:

! Fortran 2023 C_F_STRPOINTER (clause 18.2.3.5): accepted by LLVM Flang 23, not by GNU Fortran 16.2.
program c_f_strpointer_demo
  use, intrinsic :: iso_c_binding, only: c_char, c_null_char, c_ptr, c_loc, c_f_strpointer
  implicit none
  character(kind=c_char, len=:), allocatable, target :: c_text
  character(kind=c_char, len=:), pointer :: view
  type(c_ptr) :: address

  c_text = 'heatflow.csv' // c_null_char
  address = c_loc(c_text(1:1))
  call c_f_strpointer(address, view, len(c_text))
  print '(a, i0)', 'len(c_text) = ', len(c_text)
  print '(a, i0)', 'len(view)   = ', len(view)
end program c_f_strpointer_demo

LLVM Flang 23 printed:

len(c_text) = 13
len(view)   = 13

GNU Fortran 16.2 stopped at the use statement:

c_f_strpointer.f90:3:75:
    3 |   use, intrinsic :: iso_c_binding, only: c_char, c_null_char, c_ptr, c_loc, c_f_strpointer
      |                                                                           1
Error: Symbol 'c_f_strpointer' referenced at (1) not found in intrinsic module ISO_C_BINDING

The Flang result differs from the standard text, whose rule is that the length of the pointer becomes the largest value for which no null character appears in the sequence, which for this string is 12: the pointer length printed by Flang includes the null character. Treat the length from this procedure as unverified until the compilers agree, and use strlen through an interface to the C library when it matters. The Flang documentation also lists these two procedures as not implemented; the build checked here accepts them. The C side, including descriptors for character arguments, is covered on Interoperability with C and C Descriptors and ISO_Fortran_binding.

Arrays of strings

A character(len=10) :: names(3) is an array of three strings, each exactly 10 characters, and every element has the same length. That is the right type for a list of fixed-size items, such as the solver names of the heatflow configuration. All the array machinery applies: any(names == 'jacobi'), findloc(names, 'jacobi', 1), maxval(len_trim(names)), array constructors and sections, and a substring of an element is written names(2)(1:3).

Two rules concern array constructors. In Fortran 90 every element of a constructor of strings must have the same length, so ['ftcs', 'jacobi'] is an error; since Fortran 2003 a type specification fixes the length and shorter elements are padded: [character(len=10) :: 'ftcs', 'jacobi'].

! Deliberately non-conforming: the elements of the array constructor have different lengths.
program ctor_lengths
  implicit none
  character(len=10) :: solvers(3)
  solvers = ['ftcs', 'jacobi', 'multigrid']
  print '(a)', solvers
end program ctor_lengths

GNU Fortran 16.2 rejects it:

ctor_lengths.f90:5:20:
    5 |   solvers = ['ftcs', 'jacobi', 'multigrid']
      |                    1
Error: Different CHARACTER lengths (4/6) in array constructor at (1)

LLVM Flang 23 accepts it as a documented extension (the longest element determines the length) and prints the three names padded to 10 characters. Add the type specification so the file is also valid for GNU Fortran and every conforming compiler.

When the strings have different lengths and the lengths matter (tokens, lines of a file, key and value), an array of character(len=:) is not possible, because all elements of an array share one type parameter. The standard answer is a derived type with a deferred-length allocatable component, and an array of that type. The module below defines it together with split_string, a portable replacement for tokenize that returns exact-length tokens, and with join, the inverse:

module heatflow_strings
  implicit none
  private
  public :: string_t, split_string, join

  ! Elements of a character array share one length; this wrapper gives each element its own.
  type :: string_t
    character(len=:), allocatable :: value
  end type string_t

contains

  ! Portable stand-in for TOKENIZE: works with every Fortran 2003 compiler.
  function split_string(text, delimiters) result(parts)
    character(len=*), intent(in) :: text, delimiters
    type(string_t), allocatable :: parts(:)
    integer :: n, i, start, k

    n = 1
    do i = 1, len(text)
      if (index(delimiters, text(i:i)) > 0) n = n + 1
    end do
    allocate (parts(n))
    start = 1
    do k = 1, n
      i = scan(text(start:), delimiters)            ! 0 when no delimiter is left
      if (i == 0) then
        parts(k)%value = text(start:)
      else
        parts(k)%value = text(start:start + i - 2)  ! may be zero-length
        start = start + i
      end if
    end do
  end function split_string

  function join(parts, separator) result(text)
    type(string_t), intent(in) :: parts(:)
    character(len=*), intent(in) :: separator
    character(len=:), allocatable :: text
    integer :: i

    text = ''
    do i = 1, size(parts)
      if (i > 1) text = text // separator
      text = text // parts(i)%value
    end do
  end function join

end module heatflow_strings

program string_arrays
  use heatflow_strings, only: string_t, split_string, join
  implicit none
  character(len=10) :: solvers(3)                  ! fixed length: every element has 10 characters
  character(len=:), allocatable :: line
  type(string_t), allocatable :: parts(:), pair(:)
  integer :: i

  solvers = ['ftcs      ', 'jacobi    ', 'multigrid ']
  print '(a, i0, a, i0)', 'len(solvers)=', len(solvers), ' size=', size(solvers)
  print '(a, i0)', 'longest name: ', maxval(len_trim(solvers))
  print '(a, l1)', "any(solvers == 'jacobi'): ", any(solvers == 'jacobi')
  print '(a, i0)', "findloc(solvers, 'jacobi', 1): ", findloc(solvers, 'jacobi', 1)
  print '(3a)', 'element 2 is [', solvers(2), ']'
  print '(3a)', 'its substring (1:3) is [', solvers(2)(1:3), ']'    ! substring of an array element

  ! A constructor with differing lengths needs a type-spec, or the lengths must match.
  solvers = [character(len=10) :: 'ftcs', 'jacobi', 'multigrid']
  print '(*(a, :, "|"))', (trim(solvers(i)), i = 1, size(solvers))

  line = 'nx=41,ny=21,,solver=ftcs'
  parts = split_string(line, ',')
  print '(a, i0, a)', 'split into ', size(parts), ' pieces:'
  do i = 1, size(parts)
    print '(i0, a, i0, 3a)', i, ': len=', len(parts(i)%value), ' [', parts(i)%value, ']'
  end do

  pair = split_string(parts(4)%value, '=')         ! key and value of the last piece
  print '(5a)', 'key=', pair(1)%value, ' value=', pair(2)%value
  print '(2a)', 'joined: ', join(parts, ';')
  print '(a, i0)', 'characters in all pieces: ', sum([(len(parts(i)%value), i = 1, size(parts))])
end program string_arrays
len(solvers)=10 size=3
longest name: 9
any(solvers == 'jacobi'): T
findloc(solvers, 'jacobi', 1): 2
element 2 is [jacobi    ]
its substring (1:3) is [jac]
ftcs|jacobi|multigrid
split into 4 pieces:
1: len=5 [nx=41]
2: len=5 [ny=21]
3: len=0 []
4: len=11 [solver=ftcs]
key=solver value=ftcs
joined: nx=41;ny=21;;solver=ftcs
characters in all pieces: 21

The derived type costs one allocation per element and cannot be printed directly (a derived type with an allocatable component needs a defined output procedure, see Derived-Type and Asynchronous I/O), but it is the only form in which strings of different lengths can live in one array. Derived types themselves are covered on Derived Types.

The Fortran standard library

The Fortran standard library (stdlib) supplies what the language lacks for strings, in the modules stdlib_ascii (character classification and case conversion), stdlib_strings (strip, chomp, starts and ends tests, slicing, search, replacement, padding, zero-fill, join, to_string for numbers and to_c_char), stdlib_string_type (a string_type derived type that hides its representation and overloads len, trim, adjustl, comparison, concatenation and formatted I/O) and stdlib_stringlist_type (a list of strings). The specification pages mark these procedures experimental on the date of the check (2026-10-18), so their interfaces can still change; they are listed in more detail on Fortran stdlib.

The program below was built with fpm against the stdlib-fpm branch of stdlib, which is a generated fpm-ready snapshot of the library (commit 5465df3 on 2026-10-04, library version 0.8.1), under fpm’s default compiler flags rather than the strict flag set used for the other examples, since stdlib itself is not built under them. It is a checked example, not part of the standalone set:

program main
  use stdlib_string_type, only: string_type, char, len, len_trim, to_upper, assignment(=), operator(//), operator(==)
  use stdlib_strings, only: strip, starts_with, ends_with, replace_all, join, to_string, find, count, slice, padl, zfill
  implicit none
  type(string_type) :: s, t, parts(3)
  integer :: i
  s = '  ftcs solver  '
  t = strip(s)
  print '(3a)', '[', char(t), ']'
  print '(a, i0, a, i0)', 'len=', len(s), ' len_trim=', len_trim(s)
  print '(2a)', 'upper: ', char(to_upper(t))
  print '(2a)', 'concat: ', char(t // ' v2')
  print '(a, l1)', 'equal: ', t == 'ftcs solver'
  print '(a, l1, l1)', 'starts/ends: ', starts_with(char(t), 'ftcs'), ends_with(char(t), 'solver')
  print '(2a)', 'replace_all: ', replace_all(char(t), 'ftcs', 'jacobi')
  print '(a, i0)', 'find: ', find(char(t), 'solver')
  print '(a, i0)', 'count: ', count('a,b,c,d', ',')
  print '(2a)', 'slice: ', slice(char(t), 1, 4)
  print '(2a)', 'to_string: ', to_string(41)
  print '(2a)', 'padl: ', padl('41', 6, '0')
  print '(2a)', 'zfill: ', zfill('41', 6)
  parts(1) = 'nx'; parts(2) = 'ny'; parts(3) = 'alpha'
  print '(2a)', 'join: ', char(join(parts, ','))
end program
Project is up to date
[ftcs solver]
len=15 len_trim=13
upper: FTCS SOLVER
concat: ftcs solver v2
equal: T
starts/ends: TT
replace_all: jacobi solver
find: 6
count: 3
slice: ftcs
to_string: 41
padl: 000041
zfill: 000041
join: nx,ny,alpha

A string_type cannot be given directly to a print list unless the module’s formatted-write procedure is imported (write(formatted) in the only list), which is why the example converts with char before printing. Whether to use it is a design decision: string_type makes the code read like a string class and removes the trimming and reallocation details shown in this page, at the price of a dependency, a type that hides its length and a conversion at every interface with the rest of Fortran, where character(len=*) is the common currency.

Where the books are outdated

The treatment of strings in the book Modern Fortran Explained (sections 2.12 and 3.7) and of the Fortran 2023 additions (sections 23.2.2 and 23.3.1) is organised by language feature and gives no information about which compilers implement them, which is what a programmer needs today. The points below are the ones where a reader following the book alone would go wrong on current toolchains.

Book content Current state

split and tokenize described as Fortran 2023 intrinsics

split is implemented by GNU Fortran 16.2 and Flang 23; tokenize only by Flang 23 (checked above).

The at edit descriptor and the automatic allocation of iomsg=, errmsg= and internal-file variables

Not implemented by GNU Fortran 16.2. Flang 23 accepts at and does not allocate the variables.

f_c_string and c_f_strpointer

f_c_string works in both; c_f_strpointer only in Flang 23, with a length that differs from the standard text.

Source form limits for long string literals

The 132-character line and the 255 continuation lines of the Fortran 2018 limits are replaced in Fortran 2023 by 10,000 characters per line, no limit on continuation lines and 1,000,000 characters per statement; see Program Structure and Source Form.

character*n, fixed form and H edit descriptors in older material

character*n is obsolescent (Annex B.3.8), fixed form is obsolescent (B.3.7) and the H edit descriptor is a deleted feature (Annex B.1). New code uses character(len=n) in free form.

Assumed-length character functions

Obsolescent (Annex B.3.6); use an automatic length or a deferred-length result.

err= and end= on a failed read of a number

They are not obsolescent in the standard. iostat= and iomsg= are the preferred style because they work for every kind of error and give a message.

Standard version provenance and compiler support

Feature Introduced GNU Fortran 16.2 LLVM Flang 23 (checked 2026-10-18)

character(len=n), substrings, //, comparison, lge family, index, len, char, ichar

Fortran 77 and earlier

yes

yes

trim, adjustl, adjustr, len_trim, repeat, scan, verify, achar, iachar, array constructors

Fortran 90

yes

yes

len=: allocatable and pointer, automatic reallocation on assignment, new_line, selected_char_kind, kind= in len and index, ISO 10646 kind, encoding='utf-8', iomsg=, type-spec in array constructors

Fortran 2003

yes

yes (constructor with different lengths accepted as an extension)

findloc

Fortran 2008

yes

yes

split

Fortran 2023

yes

yes

tokenize

Fortran 2023

no: link error undefined reference to tokenize_

yes, both forms and separator

at edit descriptor

Fortran 2023

no: Legacy Extension: Missing comma in FORMAT string

yes (documentation table says not yet)

Deferred-length allocatable iomsg=, errmsg= and internal write

Fortran 2023

no: segmentation fault or end-of-file error

no: variable stays unallocated or Internal write overran available records

f_c_string

Fortran 2023

yes

yes (documentation table says not yet)

c_f_strpointer

Fortran 2023

no: not found in ISO_C_BINDING

yes, but the pointer length differs from the standard text

lz, lzs, lzp edit descriptors

Fortran 2023

no: Legacy Extension: Missing comma in FORMAT string

accepted; no effect on f0.3

Not checked here, because no compiler for them is installed: Intel ifx, NAG, HPE Cray and NVIDIA HPC compilers. See History, Standards and Compilers for the per-compiler support table, and What’s New and Fortran 2028 Preview for the features added by Fortran 2023 and the ones planned for Fortran 2028.

Pitfalls

  • Trailing blanks are invisible and never compared. 'ftcs' equals 'ftcs ', so use len and len_trim, not ==, to tell them apart, and trim before concatenating, or the padding becomes part of the result.

  • s = 'x' // t and s(:) = 'x' // t are different statements. The first reallocates s, the second pads or truncates to the existing length, and on an unallocated s the second is an error.

  • A fixed-length buffer silently truncates. character(len=16) :: name followed by an assignment of 20 characters keeps 16 and reports nothing. Check len_trim(src) <= len(dst) where data matters.

  • A substring is not range-checked by default. s(i:j) outside the string reads other memory. Compile test builds with -fcheck=all.

  • len(unallocated) is undefined. Test allocated(s) before using a deferred-length variable that a branch may not have set, and remember that an allocated zero-length string is also legal.

  • index and scan are not the same. index looks for a substring, scan for any character of a set, and verify for any character that is not in a set. Passing a one-character set to the wrong one gives wrong positions but valid code.

  • Case conversion does not exist as an intrinsic, and achar(iachar(c) - 32) changes any character, not only letters. Test the range first, and do not use it for non-ASCII text.

  • g0 of a real is not portable. The digits and the leading zero differ between compilers; use a fixed format when the text is compared or parsed by another program.

  • f0.d may omit the leading zero (.500); a CSV consumer that rejects it needs f8.3 plus adjustl, or the Fortran 2023 lzp where both compilers accept it.

  • Internal read stops at a blank or comma with *. read (text, *) x accepts '0.25' and '0.25 trailing'; use an explicit format and check iostat when the whole field must be a number.

  • iostat values differ between compilers. 5010 from GNU Fortran and 1030 from Flang were both a bad number; compare with zero only.

  • The Fortran 2023 conveniences are not universal. tokenize, at, automatic allocation of iomsg and c_f_strpointer fail on one or both compilers checked here; keep the trim and buffer idioms for code that must build everywhere.

  • A default-kind string with UTF-8 text counts bytes. len('café') is 5, index and substrings cut in the middle of a character, and upper-casing by ASCII code corrupts the text. Use the ISO 10646 kind when characters matter.

  • An array of strings has one length. character(len=:), allocatable :: a(:) cannot hold tokens of different lengths; use a wrapper derived type.

  • Do not pass a shorter actual argument to a dummy of explicit length. Use character(len=*) for dummies that only read.

  • character*n, fixed form and Hollerith constants belong to old code; see Legacy Features and Modernization.

See also Best Practices and Style, Common Gotchas and Pitfalls, Expressions and Assignment for the assignment and operator rules, Intrinsic Procedures Overview for the other intrinsic classes and Input/Output: Units and Files for the connection options mentioned above.