> "Write programs to handle text streams, because that is a universal interface."
Prerequisites
- 3
- 5
- 6
- 7
Learning Objectives
- Distinguish fixed-length from deferred-length (character(:), allocatable) strings, and predict len, len_trim, and allocated for each.
- Apply the character-intrinsic family — len, len_trim, trim, adjustl, adjustr, index, scan, verify — and compute each return value by hand.
- Build strings with concatenation (//) and extract parts with substring notation s(i:j), including the deferred-length growth idiom.
- Convert between numbers and text in memory with internal files, using the edit descriptors from Chapter 7.
- Parse a line of text into tokens with a scan/verify loop, and explain why it handles runs of delimiters correctly.
- Contrast modern string handling with the FORTRAN 77 fixed-length approach and its manual length bookkeeping.
In This Chapter
- Overview
- Learning Paths
- 12.1 Two Kinds of String: Fixed-Length and Deferred-Length
- 12.2 The Character-Intrinsic Family: Measure, Clean, and Search
- 12.3 Concatenation and Substrings
- 12.4 Internal Files: Numbers to Text and Back
- 12.5 Parsing Text: A Small Tokenizer
- 12.6 Modern Strings vs the FORTRAN 77 Nightmare
- Project Checkpoint
- Summary
- Spaced Review
- What's Next
Chapter 12: String Handling, Characters, and Modern Text Processing
"Write programs to handle text streams, because that is a universal interface." — Douglas McIlroy, on the Unix philosophy
Overview
Fortran's reputation for handling text is terrible, and — like most of Fortran's bad reputation — it is forty years out of date. The FORTRAN 77 that people remember gave you exactly one kind of string: a fixed block of characters, declared at a fixed length, that you padded with blanks and whose real content you tracked yourself in a separate integer. Building a filename, trimming a stray blank, or splitting a line into words meant a small ritual of counting and slicing that was genuinely unpleasant. That is the Fortran of the caricature, and if it is the only Fortran you have seen, you would reasonably conclude the language cannot do text.
Modern Fortran can. This chapter gives you three tools that, together, make text handling clean: strings
that size themselves to their contents (the deferred-length string, which is nothing more than the
allocatable idea from Chapter 5 and
Chapter 11 applied to characters); a family of intrinsic
functions that search, trim, justify, and validate text without a hand-written loop in sight; and
internal files, which let you use the full formatting machinery of
Chapter 7 to turn numbers into text and text
back into numbers, entirely in memory. None of this is exotic. It is the everyday plumbing of scientific
code — reading a configuration line, labeling a plot, writing the filename heat_000123.vtk for the
123rd frame of your simulation — and by the end of the chapter you will do all of it comfortably.
In this chapter, you will learn to:
- Declare and use fixed-length and deferred-length strings, and know exactly what
len,len_trim,trim, andallocatedreport for each. - Reach for the right character intrinsic —
index,scan,verifyto search;trim,adjustl,adjustrto clean up — and compute what each one returns before you compile. - Concatenate with
//, slice out a substring withs(i:j), and grow a deferred-length string by assignment. - Convert numbers to text and text to numbers with internal files, the in-memory cousin of file I/O.
- Write a small tokenizer that splits a line into words, and understand why it survives extra spaces.
- See, side by side, why modern Fortran's strings are a genuine pleasure and the FORTRAN 77 way was a chore — one more instance of the theme that modern Fortran is a modern language.
Learning Paths
How to read this chapter by track. - 🔬 Scientist ("I just need to read inputs and label outputs") — §12.1, §12.2, and §12.4 are your core: deferred-length strings, the trim/search intrinsics, and internal files for number↔text. The Project Checkpoint (indexed filenames) is exactly what your output loop needs. - 📖 Standard — read straight through; strings are a small, self-contained corner of the language and this is the whole of it. - 🔧 Legacy ("I inherited old code") — §12.1 and §12.6 are your orientation: you will meet fixed-length buffers with manual length counters everywhere in old code, and here is what they become. - ⚡ HPC ("I need parallel code") — text is rarely your bottleneck, so skim, but read §12.4: internal files are how you build per-rank, per-timestep output filenames without touching the disk in the hot loop.
12.1 Two Kinds of String: Fixed-Length and Deferred-Length
A Fortran string is a value of the character type you met in
Chapter 3. What is new here is
that a string has a second attribute besides its type — its length, the number of characters it holds
— and there are two ways to decide that length. The choice between them is the whole of this section, and
it echoes a choice you have already made twice: static versus dynamic, fixed versus allocatable.
The old way, and still the right way for a great many uses, is the fixed-length string.
Definition (fixed-length character). A fixed-length string is declared with a length that is fixed for the variable's whole life:
character(len=10) :: name. It always holds exactly that many characters. Assign it a shorter value and it is padded on the right with blanks to fill the declared length; assign it a longer value and the value is truncated to fit. The length is a property of the variable, not of whatever you store in it.
That blank-padding rule is the source of every fixed-length surprise, so meet it directly:
program fixed_length
implicit none
character(len=10) :: name = 'Fortran' ! 7 chars stored, 3 trailing blanks added
print '(a, i0)', 'len(name) = ', len(name) ! 10 — the declared length
print '(a, i0)', 'len_trim(name) = ', len_trim(name) ! 7 — ignoring trailing blanks
print '(a)', 'name = "' // name // '"' ! "Fortran " (three blanks)
end program fixed_length
$ gfortran -std=f2018 -Wall fixed_length.f90 -o fixed && ./fixed
len(name) = 10
len_trim(name) = 7
name = "Fortran "
The variable is length 10 forever. 'Fortran' occupies the first seven characters; the last three are
blanks you did not ask for but got anyway. len(name) reports the declared length, 10 — it is a
compile-time constant here and does not depend on the contents. len_trim(name) reports the length
ignoring trailing blanks, which is the 7 you probably meant. That gap between len and len_trim is
the thing you manage by hand in fixed-length code, and it is exactly the bookkeeping deferred-length
strings abolish.
Definition (deferred-length character). A deferred-length string is declared
character(:), allocatable :: s— the(:)says "the length is not fixed here; defer it." Like anallocatablearray, it starts life unallocated, and it acquires a length either from an explicitallocate(character(len=n) :: s)or, far more commonly, from assignment: writings = 'Fortran'automatically allocatessto length 7 and stores the value with no padding. Assign it again and it reallocates to the new length.len(s)then always reports the current, exact length, andallocated(s)tells you whether it has one yet.
The same program, done the modern way, has no gap to manage:
program deferred_length
implicit none
character(:), allocatable :: s
print '(a, l1)', 'allocated(s) at start = ', allocated(s) ! F — no length yet
s = 'Fortran'
print '(a, i0)', 'len(s) after assign = ', len(s) ! 7 — exactly the content
s = s // ' 2018' ! grow it by concatenation
print '(a, i0)', 'len(s) after grow = ', len(s) ! 12
print '(a)', 's = "' // s // '"' ! "Fortran 2018" — no blanks
end program deferred_length
$ gfortran -std=f2018 -Wall deferred_length.f90 -o deferred && ./deferred
allocated(s) at start = F
len(s) after assign = 7
len(s) after grow = 12
s = "Fortran 2018"
Trace the lengths. Before assignment s is unallocated, so allocated(s) is F and asking for len(s)
would be meaningless — just as for an unallocated array. After s = 'Fortran' the string is allocated to
length 7, holding exactly Fortran with no trailing blanks. Then s = s // ' 2018' builds the
twelve-character value 'Fortran 2018' on the right-hand side and assigns it back; because s is
allocatable, it silently reallocates from length 7 to length 12 to fit. There is no declared length to
overflow and no padding to trim. The string is always precisely as long as its contents.
🚪 Threshold Concept — a string is an allocatable array of characters. Everything that made
allocatablearrays pleasant in Chapter 5 — you do not pre-guess the size, the object owns its memory, it is freed automatically when it dies, and assignment reallocates to fit — is true, unchanged, forcharacter(:), allocatable. A deferred-length string is a dynamically sized, self-managing object; it just happens to be made of characters instead ofreal(dp)s. Once you see strings this way, you stop reaching for oversized fixed buffers "to be safe," because the safe thing already sizes itself.
The automatic-reallocation-on-assignment behavior is the Fortran 2003 rule, and gfortran gives it to you
by default at -std=f2018 (it is the same rule that lets b = a resize an allocatable array). Older code
compiled with -std=f95, or a compiler in a legacy mode, may not do it — a portability wrinkle worth
knowing, and the reason a very old codebase pre-sizes everything.
Passing strings to procedures: assumed length
One more declaration completes the picture, because you will use it in every string routine you write.
When a procedure accepts a string argument, it almost never wants to fix the caller's length; it wants to
accept a string of whatever length the caller has. That is the assumed-length dummy, written
character(len=*):
subroutine announce(label)
character(len=*), intent(in) :: label ! takes the caller's length, whatever it is
print '(a)', 'processing: ' // trim(label)
end subroutine announce
The len=* says "the length is assumed from the actual argument." It is the character analog of the
assumed-shape array argument from Chapter 6:
one routine, any length. You will see character(len=*), intent(in) on essentially every string dummy in
this chapter and the rest of the book.
🐍 Python Comparison: A Python
stris dynamic, immutable, and knows its own length — much like a deferred-length Fortran string, and nothing like a fixed-length one. The FORTRAN 77 fixed-length string, with its trailing blanks and separate length counter, has no real Python equivalent; the closest is a fixed-sizebytearrayyou slice by hand. So the mental model to import from Python ischaracter(:), allocatable, notcharacter(len=n)— the deferred-length string is the one that behaves the way you already expect a string to behave.
12.2 The Character-Intrinsic Family: Measure, Clean, and Search
Fortran gives you a compact set of intrinsic functions for text, and they divide cleanly into three jobs:
measuring a string (len, len_trim), cleaning it up (trim, adjustl, adjustr), and
searching it (index, scan, verify). Learn the eight of them and you can do most of what daily
text handling asks. Because they are intrinsic, the compiler implements them efficiently — there is no
hand-written character loop for you to get wrong.
Definition (the
trim/adjustlfamily — character intrinsics). These operate on acharactervalue and return either an integer position/length or a newcharactervalue: -len(s)— the declared/allocated length, including trailing blanks. -len_trim(s)— the length excluding trailing blanks (0 ifsis all blanks). -trim(s)— a copy ofswith trailing blanks removed (leading blanks are kept); its length islen_trim(s). -adjustl(s)— left-justify: remove leading blanks and append that many blanks at the end; the length is unchanged. -adjustr(s)— right-justify: remove trailing blanks and prepend that many blanks at the front; the length is unchanged. -index(string, sub [, back])— the 1-based position wheresubfirst occurs instring, or 0 if it does not; withback=.true., the last occurrence. -scan(string, set [, back])— the position of the first character ofstringthat is one of the characters inset, or 0 if none; withback, the last. -verify(string, set [, back])— the position of the first character ofstringthat is not inset, or 0 if every character is inset; withback, the last.
That is a lot of behavior stated abstractly, so pin it down with one program whose every line you can check against the definitions:
program string_intrinsics
implicit none
character(len=12) :: s = 'Fortran 90' ! 10 chars stored, 2 trailing blanks
character(len=6) :: pad = ' ab ' ! 2 leading, 'ab', 2 trailing
! measure
print '(a, i0)', 'len(s) = ', len(s) ! 12
print '(a, i0)', 'len_trim(s) = ', len_trim(s) ! 10
! search with index: 1-based position, or 0 if absent
print '(a, i0)', 'index(s,"r") = ', index(s, 'r') ! 3
print '(a, i0)', 'index(s,"r",back) = ', index(s, 'r', .true.) ! 5
print '(a, i0)', 'index(s,"ran") = ', index(s, 'ran') ! 5
print '(a, i0)', 'index(s,"z") = ', index(s, 'z') ! 0
! scan finds a char IN the set; verify finds the first char NOT in the set
print '(a, i0)', 'scan("key=val","=")= ', scan('key=val', '=') ! 4
print '(a, i0)', 'verify("2018",dig) = ', verify('2018', '0123456789') ! 0
print '(a, i0)', 'verify("20x8",dig) = ', verify('20x8', '0123456789') ! 3
! clean up (length is preserved; blanks are moved, not deleted)
print '(a)', 'adjustl(pad) = "' // adjustl(pad) // '"' ! "ab "
print '(a)', 'adjustr(pad) = "' // adjustr(pad) // '"' ! " ab"
print '(a)', 'trim(adjustl) = "' // trim(adjustl(pad)) // '"' ! "ab"
end program string_intrinsics
$ gfortran -std=f2018 -Wall string_intrinsics.f90 -o intr && ./intr
len(s) = 12
len_trim(s) = 10
index(s,"r") = 3
index(s,"r",back) = 5
index(s,"ran") = 5
index(s,"z") = 0
scan("key=val","=")= 4
verify("2018",dig) = 0
verify("20x8",dig) = 3
adjustl(pad) = "ab "
adjustr(pad) = " ab"
trim(adjustl) = "ab"
Walk the tricky lines. The string s is 'Fortran 90' stored in a length-12 variable, so it is
F o r t r a n _ 9 0 followed by two blanks — hence len = 12 but len_trim = 10. For index, number
the characters 1:F 2:o 3:r 4:t 5:r 6:a 7:n: the substring 'r' first appears at position 3 and last
at position 5, and 'ran' (r-a-n) matches at position 5 (there is no earlier r-a-n; position 3
is r-t-r), while 'z' never appears, giving 0. For scan('key=val','='), number 1:k 2:e 3:y 4:=:
the first character that is in the set '=' is the = at position 4. For verify, the set is the ten
digits: '2018' is entirely digits so the first non-digit does not exist and the answer is 0, while
'20x8' has its first non-digit — the x — at position 3. And for the justifiers, pad is
' ab ': adjustl slides the content left, moving the two leading blanks to the tail ('ab ');
adjustr slides it right, moving the two trailing blanks to the front (' ab'); both keep the length
at 6. Wrapping trim around adjustl deletes the now-trailing blanks, leaving 'ab' — the standard
idiom for stripping blanks from both ends at once.
Three of these earn special mention because they are the ones you will reach for constantly.
trim is how you undo blank-padding. Any time you take a value out of a fixed-length variable and want
just its content — to concatenate it, print it, or use it as a filename — trim is the tool. It removes
trailing blanks only; if you also have leading blanks, trim(adjustl(s)) removes both.
index is substring search, and it is 1-based. It answers "where does this appear?" and returns 0 for
"nowhere." That 0 is a genuine position-that-cannot-occur (positions start at 1), so if (index(s,sub) > 0)
reads as "if sub is present." Splitting key=value on its = is index(line, '=').
scan and verify are the underrated pair. scan finds "any character from this set" — the first
delimiter, the first digit, the first vowel. verify finds "the first character not from this set,"
which is exactly how you validate: verify(token, '0123456789') == 0 is the clean test for "this token is
all digits," and verify(s, ' ') is "the first non-blank," i.e. where the content starts. You will build
a tokenizer out of scan and verify in §12.5, and once you have, you will see them everywhere.
⚠️ Common Pitfall —
indexreturns a position; don't test it as a boolean.index(s, sub)returns an integer position, 0 when absent. A frequent slip is to writeif (index(s, sub))— which does not even compile in Fortran, becauseifneeds alogical, not an integer (a mercy; in C the analogous mistake compiles and misbehaves). Write the comparison you mean:if (index(s, sub) > 0)for "present," orif (index(s, sub) == 0)for "absent." Likewisescanandverifyreturn positions, not flags.🐍 Python Comparison: The map to Python's
strmethods is close but not exact, and the differences bite.index(s, sub)is likes.find(sub)— but Fortran is 1-based and returns 0 when absent, while Python is 0-based and returns −1 when absent, so a mechanical port that forgets to shift the base and the sentinel will be wrong in two ways at once.trim(adjustl(s))iss.strip();trim(s)iss.rstrip().scan/verifyhave no single-method Python equivalent — you would reach for a regular expression or a comprehension — which is a rare case of Fortran expressing something more concisely than Python.🔄 Check Your Understanding. 1. For
character(len=8) :: t = 'grid', what arelen(t),len_trim(t), andtrim(t)? 2. What doesindex('a.b.c', '.', back=.true.)return, and what everyday parsing task is that? 3. Write the one-line test for "the stringtokcontains only the digits 0–9."
Answers
1.len(t)= 8 (declared length),len_trim(t)= 4 ('grid'has 4 non-blank chars),trim(t)='grid'(length 4). The variable holds'grid 'with four trailing blanks. 2. It returns 4. Number the characters1:a 2:. 3:b 4:. 5:c: the dots are at positions 2 and 4, so the last one — whatback=.true.asks for — is at position 4. Finding the last dot is how you locate a file extension's separator. 3.verify(tok, '0123456789') == 0— zero means "no character fell outside the digit set."
12.3 Concatenation and Substrings
Two operations let you take strings apart and put them together, and both are pleasantly simple.
Concatenation joins strings with the // operator:
Definition (concatenation). The concatenation operator
//joins twocharactervalues into one whose length is the sum of the two lengths.'heat' // '.vtk'is'heat.vtk'(length 8). It concatenates the operands exactly as given, blanks and all — soname // '.vtk'wherenameis a fixed-length variable will splice inname's trailing blanks unless youtrimit first.
Substrings extract a contiguous run of characters by position:
Definition (substring). A substring is a piece of a string selected by a position range:
s(i:j)is the characters ofsfrom positionithrough positionjinclusive, a string of lengthj - i + 1. The bounds are 1-based, exactly like array indices. Omitting a bound means "to the end" or "from the start":s(i:)runs fromito the last character,s(:j)from the first throughj. Ifi > j, the substring has length zero. A substring is a first-class string value: you can print it, concatenate it, search it, or (ifsis a variable) assign into it.
Both in one short program:
program concat_substring
implicit none
character(len=20) :: filename = 'heat_000123.vtk'
integer :: dot
! concatenation builds a new string
print '(a)', 'joined = "' // 'heat_' // '000123' // '.vtk' // '"' ! "heat_000123.vtk"
! substrings pull pieces out, 1-based and inclusive
print '(a)', 'prefix = "' // filename(1:5) // '"' ! "heat_"
print '(a)', 'stamp = "' // filename(6:11) // '"' ! "000123"
! find the extension by locating the last '.'
dot = index(trim(filename), '.', back=.true.)
print '(a, i0)', 'dot at position ', dot ! 12
print '(a)', 'ext = "' // trim(filename(dot+1:)) // '"' ! "vtk"
end program concat_substring
$ gfortran -std=f2018 -Wall concat_substring.f90 -o cs && ./cs
joined = "heat_000123.vtk"
prefix = "heat_"
stamp = "000123"
dot at position 12
ext = "vtk"
filename holds 'heat_000123.vtk' (15 characters) in a length-20 variable. filename(1:5) is the
five-character prefix 'heat_'; filename(6:11) is the six-character stamp '000123'. To find the
extension we locate the last dot with index(..., back=.true.) — numbering
heat_000123.vtk gives the . at position 12 — then take the substring from dot+1 to the end,
filename(13:), and trim off the padding blanks to get 'vtk'. This is real filename parsing, and
it is three intrinsics.
💡 Intuition: substring notation
s(i:j)is deliberately identical to array-section notationa(i:j)— because a string is an array of characters, and Fortran wants the two to feel the same. Reach for the same instincts: 1-based, inclusive on both ends, and a missing bound means "the rest."⚠️ Common Pitfall — concatenating fixed-length variables splices in the blanks. This is the single most common string bug in scientific code. If
dirischaracter(len=20)holding'output', thendir // '/data.txt'is not'output/data.txt'— it is'output', then fourteen blanks, then'/data.txt', because the fixed-lengthdircarries its padding into the concatenation. Alwaystrima fixed-length variable before you join it:trim(dir) // '/data.txt'. (Deferred-length strings, having no padding, do not have this problem — one more reason to prefer them.)🐛 Find the Bug. A build routine assembles an output path and the file lands in the wrong place:
fortran character(len=20) :: outdir = 'output' character(len=40) :: path path = outdir // '/' // 'heat.vtk' print '(a)', 'writing to: ' // trim(path)The program prints
writing to: output /heat.vtk. What happened, and what is the fix?
Diagnosis
outdiris a fixed-length variable of length 20, so it holds'output'followed by fourteen trailing blanks. Concatenation splices those blanks in verbatim, sooutdir // '/'is'output' + 14 blanks + '/', and the path contains a run of spaces before the slash. The trailingtrim(path)only removes blanks at the very end, not the ones buried in the middle. The fix is totrimthe fixed-length piece before joining:path = trim(outdir) // '/' // 'heat.vtk', giving'output/heat.vtk'. Better still, makeoutdirdeferred-length so there are no blanks to trim.
12.4 Internal Files: Numbers to Text and Back
Sooner or later every program has to cross the line between numbers and their textual representation.
You have an integer step counter and you need the string '000123' to build a filename. You have read
a configuration line as text and you need the number it represents. You want to assemble a log message
mixing words and values. In C this is sprintf and sscanf; in Python it is str, int, float, and
f-strings. Fortran's answer is the internal file, and its great virtue is that it reuses machinery you
already know: the formatted I/O and edit descriptors of
Chapter 7, pointed at a string instead of a
disk file.
Definition (internal file). An internal file is a
charactervariable used as the "unit" in areadorwritestatement. Writing to it —write(s, fmt) values— formats the values into the characters ofsexactly as they would be formatted into a line of a text file, but the result stays in memory as the strings. Reading from it —read(s, fmt) variables— parses the characters ofsas if reading a line from a file, converting text to numbers. No actual file, noopen, noclose, no disk: the string is the file. It is Fortran's in-memory bridge between numbers and text, with the full power of edit descriptors on both sides.
Number → text (internal write)
To turn a number into a string, write it to a character variable with a format:
program num_to_text
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
character(len=32) :: buf
integer :: step
real(dp) :: x
step = 123
write(buf, '(a, i6.6, a)') 'heat_', step, '.vtk' ! -> 'heat_000123.vtk'
print '(a)', 'filename = "' // trim(buf) // '"'
x = 3.14159_dp
write(buf, '(f8.3)') x ! -> ' 3.142' (width 8)
print '(a)', 'x label = "' // trim(adjustl(buf)) // '"' ! strip the leading blanks
write(buf, '(i0)') 255 ! -> '255' (minimum width)
print '(a)', 'count = "' // trim(buf) // '"'
end program num_to_text
$ gfortran -std=f2018 -Wall num_to_text.f90 -o n2t && ./n2t
filename = "heat_000123.vtk"
x label = "3.142"
count = "255"
The first write is the one that matters most, and it is the Project Checkpoint in miniature. The format
'(a, i6.6, a)' says: emit the string 'heat_' (the first a), then the integer step in a field six
wide with a minimum of six digits, zero-padded (i6.6), then the string '.vtk'. With step = 123,
the i6.6 produces '000123' — three significant digits, zero-filled to six — so buf receives
'heat_000123.vtk' (fifteen characters) followed by blanks to fill its length of 32. trim removes the
padding. That zero-padding is precisely what makes filenames sort correctly: heat_000123.vtk sorts
before heat_001000.vtk, whereas the unpadded heat_123.vtk and heat_1000.vtk would not.
The f8.3 conversion formats 3.14159 to three decimals in a field eight wide, right-justified:
' 3.142'. Because the field is wider than the number, it comes out with leading blanks; adjustl
slides it to the left and trim removes the resulting trailing blanks, leaving '3.142'. And i0 is the
integer descriptor with minimum width — no leading blanks at all — giving '255' directly, which is why
i0 is the everyday choice for embedding an integer in a message.
⚠️ Common Pitfall — a too-narrow field prints asterisks. The
i6.6above holds any step from 0 to 999999. Hand it a seven-digit number and the six-wide field cannot contain it, so Fortran fills the field with asterisks —write(buf,'(i6.6)') 1234567yields'******', not a truncated or widened number. This is Fortran refusing to silently corrupt your output, and it is a good failure (a filename full of*is obviously wrong), but it means you must size the field for the largest value you will ever format. For a run of at most a million steps,i6.6is exactly right; for more, widen it. When you want the field to always fit, usei0, which never overflows because it sizes itself.
Text → number (internal read)
The reverse direction parses text into numbers. List-directed input (the * format from Chapter 7) is the
easy way — it splits on blanks and commas and converts each field:
program text_to_num
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
character(len=32) :: line
integer :: a, b, c
real(dp) :: x
line = '3 14 159'
read(line, *) a, b, c ! list-directed: split on blanks
print '(a, i0, 1x, i0, 1x, i0)', 'ints = ', a, b, c ! 3 14 159
line = '2.71828'
read(line, *) x
print '(a, f7.5)', 'real = ', x ! 2.71828
end program text_to_num
$ gfortran -std=f2018 -Wall text_to_num.f90 -o t2n && ./t2n
ints = 3 14 159
real = 2.71828
Reading '3 14 159' list-directed into three integers splits the string on its blanks and converts each
field, giving a = 3, b = 14, c = 159. Reading '2.71828' into a real gives 2.71828. This is how
you consume a data line whose values are separated by spaces without writing a parser — the internal read
is the parser. When the text might be malformed, add the iostat= specifier from Chapter 7 exactly as
you would for a real file, and check it; a bad conversion sets iostat nonzero instead of crashing, and
Chapter 13 makes a discipline of that check.
🔗 Connection: internal files are the same
read/writestatements and the same edit descriptors you met for real files in Chapter 7 — the only change is that the unit is a character variable rather than a unit number fromopen. That is the whole idea: Fortran did not invent a separate string-formatting sublanguage (as C did withprintfformat strings, or Python with its own mini-language); it pointed the I/O system it already had at a string. If you can format it to a file, you can format it to a string, with identical syntax.🔄 Check Your Understanding. 1. What string does
write(buf,'(a,i4.4)') 'run', 7place inbuf(before padding)? 2. Why doesi0never overflow into asterisks, whilei4can? 3. You havecharacter(len=20) :: line = '42 3.5'. Write the statement that reads the42into integernand the3.5intoreal(dp) :: r.
Answers
1.'run0007'— theaemits'run', andi4.4emits7in a 4-wide field zero-padded to 4 digits,'0007'. 2.i0uses the minimum width needed for the value, so it always fits by construction;i4fixes the width at 4, and a value needing 5 or more digits cannot fit, so the field is filled with. 3.read(line, ) n, r— list-directed internal read splits on the blank, converting42to the integer and3.5to the real.
12.5 Parsing Text: A Small Tokenizer
Configuration files, data files, and command lines all arrive as lines of text that you must split into
their pieces — the tokens. A tokenizer is the small workhorse that does this, and building one ties
together everything in the chapter: scan and verify to find boundaries, substrings to extract, and an
assumed-length argument to accept any line.
Definition (tokenizer). A tokenizer scans a string and splits it into a sequence of tokens — the meaningful units (words, numbers, symbols) — separated by delimiters (here, blanks). A good tokenizer treats a run of delimiters as a single separator, so extra spaces between tokens do not produce empty tokens.
The strategy is a two-step loop: use verify to skip any delimiters and find where the next token
starts, then use scan to find where it ends (the next delimiter, or the end of the line). Both
searches run on the remaining substring line(pos:), so we convert their offsets back to absolute
positions:
program tokenizer
implicit none
call tokenize('the quick brown') ! note the double space
contains
subroutine tokenize(line)
character(len=*), intent(in) :: line
character(len=1), parameter :: blank = ' '
integer :: pos, first, last, ntok
pos = 1
ntok = 0
do
if (pos > len(line)) exit
first = verify(line(pos:), blank) ! offset of first non-blank in the remainder
if (first == 0) exit ! only blanks left -> done
first = pos + first - 1 ! absolute start of the token
last = scan(line(first:), blank) ! offset of the next blank after the token
if (last == 0) then
last = len(line) ! no more blanks: token runs to the end
else
last = first + last - 2 ! absolute position of the token's last char
end if
ntok = ntok + 1
print '(a, i0, a, a)', 'token ', ntok, ': ', line(first:last)
pos = last + 1 ! resume just past this token
end do
end subroutine tokenize
end program tokenizer
$ gfortran -std=f2018 -Wall tokenizer.f90 -o tok && ./tok
token 1: the
token 2: quick
token 3: brown
Trace it on 'the quick brown' (length 16, with two blanks between quick and brown), because the
index arithmetic is the kind of thing you should never take on faith:
pos = 1.verify(line(1:), ' ')finds the first non-blank at offset 1 (t), sofirst = 1.scan(line(1:), ' ')finds the first blank at offset 4, solast = 1 + 4 - 2 = 3. Token =line(1:3)='the'. Advancepos = 4.pos = 4.line(4:)is' quick brown';verifyskips the single leading blank and reports offset 2, sofirst = 4 + 2 - 1 = 5(theq).scan(line(5:), ' ')finds the blank afterquickat offset 6, solast = 5 + 6 - 2 = 9(thek). Token =line(5:9)='quick'. Advancepos = 10.pos = 10.line(10:)is' brown';verifyskips both blanks and reports offset 3, sofirst = 10 + 3 - 1 = 12(theb).scan(line(12:), ' ')finds no blank and returns 0, solast = len(line) = 16. Token =line(12:16)='brown'. Advancepos = 17.pos = 17 > 16, so the loop exits. Three tokens.
The double space between quick and brown produced no empty token, because verify skips a run of
blanks in one step — that is the whole reason we search with verify rather than just splitting at every
blank. Change blank to a set like ' ,' and the same loop tokenizes comma-or-space separated data;
that generality is why scan/verify take a set of characters rather than a single one.
💡 Intuition: the two searches answer complementary questions.
verify(line(pos:), blank)asks "where does the next token begin?" (the first non-delimiter);scan(line(first:), blank)asks "where does it end?" (the first delimiter after it). Skip-then-take, skip-then-take, until nothing but delimiters remains. Every hand-rolled tokenizer in every language is some version of this loop; Fortran just hands youscanandverifyso the two searches are one call each.⚠️ Common Pitfall — the off-by-one when converting offsets to positions.
verifyandscanhere search the substringline(pos:), so they return an offset relative topos, not an absolute position inline. The conversion isabsolute = pos + offset - 1. Forgetting the- 1(or the- 2when the end is one before the delimiter) shifts every token by a character — the classic fencepost error. When your tokens come out with a stray leading character or a missing final one, this arithmetic is the first place to look. Hand-tracing three iterations, as above, catches it every time.🔄 Check Your Understanding. 1. Why does the tokenizer use
verifyto find the start of a token butscanto find the end? 2. What single change makes it split on commas as well as spaces? 3. On input' hi'(two leading blanks), what isfirston the first iteration, and what token results?
Answers
1. A token starts at the first character that is not a delimiter — that is exactly whatverifyfinds. A token ends at the first character that is a delimiter — that is whatscanfinds. The two intrinsics are duals, and the tokenizer uses each for the boundary it locates. 2. Change the delimiter to a set containing both: use a two-character set' ,'in place of the single blank (declare itcharacter(len=2), parameter :: seps = ' ,'and search withseps).scan/verifyalready take a set, so nothing else changes. 3.verify(' hi', ' ')returns offset 3 withpos = 1, sofirst = 1 + 3 - 1 = 3;scan(line(3:), ' ')returns 0, solast = 4, and the token isline(3:4) = 'hi'. Leading blanks are skipped, exactly as intended.
12.6 Modern Strings vs the FORTRAN 77 Nightmare
It is worth seeing what all of this replaces, both to appreciate the modern tools and because you will
inherit the old ones. FORTRAN 77 had a CHARACTER type — its addition in the 1977 standard was itself a
big deal, the first time the language handled text as a first-class type — but every string was
fixed-length. There was no deferred-length string, no automatic reallocation, no concatenating your
way to a right-sized result. To build a string whose final length you did not know in advance, you
declared a buffer big enough for the worst case and tracked the used length yourself in a companion
integer.
Here is the FORTRAN 77 way to build a name like heat_000123.vtk and keep its true length — written in
modern free-form so you can read it, but in the old style of manual bookkeeping:
! Legacy style: a fixed buffer plus a hand-maintained length counter.
character(len=64) :: name ! "big enough, I hope"
integer :: n ! the used length, tracked by hand
name = ' ' ! must blank the whole buffer first
name(1:5) = 'heat_'
n = 5
write(name(n+1:n+6), '(i6.6)') step ! place the digits by absolute position
n = n + 6
name(n+1:n+4) = '.vtk'
n = n + 4
! the "real" string is name(1:n); every use must remember to slice it
print '(a)', 'file: ' // name(1:n)
Every line carries risk. The buffer might be too small (silent truncation). You must blank it before use
or leftover characters leak through. You compute every insertion position by hand. And forever after, the
string's true content is name(1:n) — pass name anywhere without the (1:n) and you pass the trailing
blanks too. The separate length counter n is a second variable that must stay in lockstep with the
first, and the day it does not is the day you get a corrupted filename.
The modern version deletes the entire apparatus:
! Modern style: the string sizes itself; no buffer, no counter.
character(:), allocatable :: name
character(len=32) :: buf
write(buf, '(a, i6.6, a)') 'heat_', step, '.vtk'
name = trim(buf) ! name is now EXACTLY 'heat_000123.vtk', length 15
print '(a)', 'file: ' // name
There is no worst-case buffer to size (only the small scratch buf for the one formatted write, and even
that could be avoided), no blanking, no position arithmetic, and — crucially — no companion length
counter, because name knows its own length: len(name) is 15, and it will be whatever the next
assignment makes it. The deferred-length string carries its length with it, which is the single thing
FORTRAN 77 could not do and the reason its string code was such a chore.
🔧 Modern vs Legacy: the two eras of Fortran string building, side by side.
Task FORTRAN 77 (fixed-length) Modern Fortran (deferred-length) Declare a "string" character(len=64) :: s(guess a size)character(:), allocatable :: sKnow its true length track it yourself in an integer nlen(s)— it's carried with the valueBuild up a value write into s(a:b)by hand, updatens = s // piece(reallocates to fit)Pass it somewhere pass s(1:n), or ship the blankspass s— no padding to leakRight-size the result impossible — the buffer is fixed automatic — the string is its content This is the theme of the whole book in one table: modern Fortran is a modern language. The old code is not wrong — much of it still runs, correctly, in production — but the modern tools remove an entire category of bookkeeping and the bugs that came with it.
📜 From History: why did FORTRAN 77 make strings so awkward? For the same reason it made everything static: memory. On the machines of the 1970s, allocating and reallocating storage at run time was a luxury the language deliberately avoided, so every object — arrays and strings alike — had a size fixed at compile time. Deferred-length strings (Fortran 2003) and automatic reallocation are the character-typed face of the same dynamic-memory revolution that gave arrays
allocatablein Fortran 90. When you writecharacter(:), allocatable, you are using a capability that simply did not exist for the language's first forty-six years — which is exactly why the "Fortran can't do strings" reputation formed, and exactly why it is now wrong. And it is one more reason legacy code is not a burden: the fixed-length buffers you find in an old code are not incompetence, they are an artifact of their era, and they modernize cleanly.
Project Checkpoint
Your heat solver will, from Chapter 26
onward, write one output file per timestep so you can watch the plate evolve in ParaView. Those files need
names that are unique and that sort in time order — which means a zero-padded step number, exactly the
heat_000123.vtk pattern this chapter has been circling. This checkpoint builds the small helper that
produces them, using an internal-file write.
Add a function that turns a step number into a frame filename. It returns a deferred-length string, so the caller gets a name of exactly the right length with no trailing blanks to trim:
program checkpoint_frames
implicit none
integer :: k
integer, parameter :: steps(4) = [0, 42, 123, 999999]
do k = 1, size(steps)
print '(a)', frame_name(steps(k))
end do
contains
function frame_name(step) result(name)
integer, intent(in) :: step
character(:), allocatable :: name
character(len=32) :: buf
write(buf, '(a, i6.6, a)') 'heat_', step, '.vtk' ! format into a scratch buffer
name = trim(buf) ! deferred-length: exact size
end function frame_name
end program checkpoint_frames
$ gfortran -std=f2018 -Wall project-checkpoint.f90 -o frames && ./frames
heat_000000.vtk
heat_000042.vtk
heat_000123.vtk
heat_999999.vtk
The i6.6 descriptor zero-pads each step to six digits, so step 0 becomes 000000, step 42 becomes
000042, and step 999999 fills the field exactly as 999999. Because frame_name's result is
character(:), allocatable, the assignment name = trim(buf) sizes it to precisely fifteen characters —
the caller never sees a padding blank, and never has to trim. That is the deferred-length payoff from
§12.1 doing real work: a function can return a string whose length it computes at run time, which a
fixed-length result could not.
Record two things in your heat-solver/ notes. First, the naming convention — heat_NNNNNN.vtk, six
zero-padded digits — because Chapter 26
will call frame_name from the output routine and hand the result to the VTK writer, and ParaView will
rely on the zero-padding to load the frames in order. Second, the ceiling: i6.6 holds up to 999,999
steps; if a production run needs more, widen the field (i8.8) before it silently fills a name with
asterisks. Your solver now knows how to name its output — the last piece of plumbing before, in Chapter
26, it starts producing that output for real.
Summary
Modern Fortran handles text with three tools — self-sizing strings, a family of intrinsics, and internal files — that between them cover almost everything daily scientific code asks of strings.
| Feature | What it does | Key rule |
|---|---|---|
character(len=n) |
fixed-length string | blank-padded on the right; len = declared length |
character(:), allocatable |
deferred-length string | sizes itself from assignment; len = exact content |
character(len=*) |
assumed-length dummy | a procedure takes the caller's length |
len / len_trim |
length with / without trailing blanks | len_trim is usually what you mean |
trim |
drop trailing blanks | trim(adjustl(s)) drops both ends |
adjustl / adjustr |
left- / right-justify | length preserved; blanks moved, not deleted |
index(s, sub[, back]) |
position of sub, or 0 |
1-based; test > 0, never as a boolean |
scan(s, set[, back]) |
first char in set, or 0 |
find a delimiter / a digit |
verify(s, set[, back]) |
first char not in set, or 0 |
== 0 means "all characters are in set" |
// |
concatenation | trim fixed-length operands first, or splice blanks |
s(i:j) |
substring | 1-based, inclusive; a string like any other |
write(s, fmt) / read(s, fmt) |
internal file | number↔text with Chapter 7's edit descriptors |
The three things to memorize. First, character(:), allocatable is a string that sizes itself — it
is the allocatable idea applied to characters, and it is the default you should reach for. Second,
trim before you concatenate or compare a fixed-length string, or its trailing blanks come along for
the ride. Third, an internal file is just a read/write aimed at a string — it is how you convert
between numbers and text, and it is how you build the filename heat_000123.vtk with write(buf,
'(a,i6.6,a)').
Spaced Review
Retrieval practice on the two chapters this one leans on: variables and types (Chapter 3) and I/O (Chapter 7), whose edit descriptors internal files reuse wholesale. Answer before you open the details.
-
(Ch. 7) What does the edit descriptor
i6.6produce for the integer42, and why is the.6part what makes output filenames sort correctly?
Answer
It produces'000042'— a field six characters wide, zero-padded to a minimum of six digits. The zero-padding gives every filename the same digit width, so lexical (alphabetical) sorting matches numeric order:heat_000042.vtksorts beforeheat_000123.vtk. Without padding,heat_42.vtkandheat_123.vtkwould sort with123before42, scrambling a time series. -
(Ch. 7) What is the difference between the
Aedit descriptor and the list-directed*format when writing a character value?
Answer
Ais a formatted descriptor that writes the string's characters exactly, with no extra spacing, giving you precise control (and you can width it,A10, to pad or truncate to a set field). The list-directed*format lets the processor choose the layout — it typically adds a leading blank and separates items with spaces — which is convenient for quick output but not for building an exact string. For assembling filenames and labels you useA(inside an internalwrite); for a fast debug print,*. -
(Ch. 3) What does
selected_real_kind(15, 307)request, and why do we store its result in thedpparameter rather than sprinkling the number through the code?
Answer
It requests a real kind with at least 15 decimal digits of precision and an exponent range reaching at least 10±307 — i.e., IEEE double precision on essentially every machine. Storing it once asinteger, parameter :: dp = selected_real_kind(15, 307)and writingreal(dp)everywhere means the whole program's precision is defined in a single place: change that one line and every real follows, and the code stays portable across compilers that number their kinds differently. -
(Ch. 3) In integer arithmetic, what is
7 / 2, and what would you write instead to get3.5?
Answer
7 / 2is 3 — integer division truncates toward zero, discarding the remainder, because both operands are integers. To get3.5at least one operand must be real:7.0_dp / 2.0_dp(orreal(7, dp) / 2). This is the classic integer-division trap from Chapter 3, and it is worth re-flagging here because internal reads that parse text into integers inherit the same arithmetic afterward. -
(Ch. 7) Reading the line
'3 14 159'list-directed withread(line, *) a, b, cinto three integers — what doa,b, andcbecome, and what plays the role of the separator?
Answer
a = 3,b = 14,c = 159. List-directed input treats runs of blanks (and commas) as separators between values, so the three space-separated fields map to the three variables in order. This is the same list-directed input from Chapter 7, now aimed at a string via an internal file rather than at a real file or the keyboard.
What's Next
You can now build strings, search them, and convert them to and from numbers — which means your programs
can talk: read a configuration line, label an output, name a file. What they cannot yet do is cope
gracefully when that text is wrong — a config line with a typo, a number that will not parse, a grid size
that makes no physical sense. Chapter 13 turns to
exactly that: error handling, debugging, and defensive programming. You will generalize the iostat
mechanism you have used for I/O (and just now for internal reads) into a habit of checking every operation
that can fail, meet error stop and the allocation stat=/errmsg= machinery, and wire up the compiler's
debugging flags — -fcheck=all, -fbacktrace, -ffpe-trap — that turn a silent wrong answer into a loud,
locatable one. Your solver will start validating its inputs and refusing, loudly, to run on a bad
configuration. Text was how the program speaks; error handling is how it tells you when something has gone
wrong.