Cookbook
Short answers to "how do I ...?". Each recipe is a complete program with its real output; the reference has the details.
Print a string
program print_a_string
!< Three ways to print a string.
use stringifor
implicit none
type(string) :: s
s = 'Hello World'
print '(A)', s//'' ! concatenation gives a character
print '(A)', s%chars() ! the raw characters
print '(DT)', s ! defined I/O
endprogram print_a_string$ print_string
Hello World
Hello World
Hello World//'' is the idiom used in all these pages: it works wherever a character is expected.
Assign, concatenate, compare
program operators
!< Assignment, concatenation and comparison.
use stringifor
implicit none
type(string) :: a, b, c
character(:), allocatable :: chars
a = 'Hello' ! from a character
b = a ! from a string
c = a//' '//'World' ! // gives a character, assigned to a string
chars = a//' again' ! or kept as a character
c = a .cat. ' World' ! .cat. gives a string
print '(A)', c//''
print '(A)', chars
print '(3(L1,1X))', a == b, a == 'Hello', a /= c
print '(3(L1,1X))', a < c, 'Apple' < a, a >= b
endprogram operators$ operators
Hello World
Hello again
T T T
T T T// gives a character, .cat. a string; the comparisons accept a character on either side.
Change the case
program case_conversion
!< Upper, lower and the like.
use stringifor
implicit none
type(string) :: s
s = 'Hello World'
print '(A)', s%upper()//''
print '(A)', s%lower()//''
print '(A)', s%swapcase()//''
print '(A)', s%capitalize()//''
print '(2(L1,1X))', s%is_upper(), s%is_lower()
endprogram case_conversion$ letter_case
HELLO WORLD
hello world
hELLO wORLD
Hello world
F FcamelCase, snake_case, Start Case
program word_case
!< Word-level case styles.
use stringifor
implicit none
type(string) :: s
s = 'the quick brown fox'
print '(A)', s%startcase()//''
print '(A)', s%camelcase()//''
print '(A)', s%snakecase()//''
s = 'the-quick-brown-fox'
print '(A)', s%camelcase(sep='-')//''
endprogram word_case$ wordcase
The Quick Brown Fox
TheQuickBrownFox
the_quick_brown_fox
TheQuickBrownFoxThe words are separated by blanks, or by sep.
Remove blanks, or other characters, around a string
program strip_blanks
!< Remove what surrounds a string.
use stringifor
implicit none
type(string) :: s
s = ' hello world '
print '(A)', '['//s%strip()//']' ! both ends
print '(A)', '['//s%trim()//']' ! the end only
print '(A)', '['//s%adjustl()//']' ! moved to the left, same length
s = '--=hello world=--'
print '(A)', '['//s%strip(remove='-=')//']' ! any character of a set
print '(A)', '['//s%lstrip(remove='-=')//']' ! the beginning only
print '(A)', '['//s%rstrip(remove='-=')//']' ! the end only
s = achar(9)//' hello world'//new_line('a')
print '(A)', '['//s%strip(whitespace=.true.)//']' ! tabs and new lines too
endprogram strip_blanks$ strip
[hello world]
[ hello world]
[hello world ]
[hello world]
[hello world=--]
[--=hello world]
[hello world]strip removes the blanks at both ends, or the characters of the set remove; lstrip and rstrip one end only. whitespace=.true. removes tabs, new lines and the other whitespace too, as Python str.strip().
Replace, collapse, insert
program replace_text
!< Replace, collapse, insert.
use stringifor
implicit none
type(string) :: s
s = 'a-b-c-d'
print '(A)', s%replace(old='-', new=' + ')//''
print '(A)', s%replace(old='-', new='', count=2)//'' ! the first two only
s = 'too many blanks'
print '(A)', s%unique(' ')//''
s = 'Helo'
print '(A)', s%insert(substring='l', pos=3)//''
endprogram replace_text$ replace
a + b + c + d
abc-d
too many blanks
HelloTidy whitespace, repeats, tabs and quotes
program tidy
!< Whitespace, repeated characters, tabs, character sets and quotes.
use stringifor
implicit none
type(string) :: s
s = ' too many'//achar(9)//'blanks '
print '(A)', '['//s%compact()//']'
print '(A)', '['//s%compact(sep='_')//']'
s = 'Mississippi --- yes!!!'
print '(A)', s%squeeze()//''
print '(A)', s%squeeze(set='-!')//''
s = 'id'//achar(9)//'name'//achar(9)//'value'
print '(A)', s%expand_tabs(8)//''
s = 'hello world'
print '(A)', s%transliterate('lo', 'LO')//''
print '(A)', s%transliterate('aeiou', '')//''
s = 'say "hi"'
s = s%quote()
print '(A)', s//''
print '(A)', s%unquote()//''
endprogram tidy$ tidy
[too many blanks]
[too_many_blanks]
Misisipi - yes!
Mississippi - yes!
id name value
heLLO wOrLd
hll wrld
"say ""hi"""
say "hi"compact collapses whitespace, squeeze repeated characters, expand_tabs aligns on tab stops, transliterate maps a set of characters onto another; quote and unquote add and remove quotes, doubling the inner ones.
Tell letters, digits, punctuation and blanks apart
program char_classes
!< The class of the characters of some strings.
use stringifor
implicit none
type(string) :: words(5)
integer :: w
words(1) = 'Fortran'
words(2) = 'F2023'
words(3) = 'c0ffee'
words(4) = '?!,'
words(5) = ' '
print '(A)', 'word alpha alnum xdigit punct space'
do w = 1, size(words)
print '(A,5L6)', words(w)%ljust(10)//'', words(w)%is_alpha(), words(w)%is_alnum(), words(w)%is_xdigit(), &
words(w)%is_punct(), words(w)%is_space()
enddo
endprogram char_classes$ char_classes
word alpha alnum xdigit punct space
Fortran T T F F F
F2023 F T T F F
c0ffee F T T F F
?!, F F F T F
F F F F TEach test is true if all the characters of the string are in the class, false for a null string.
Split a line into words
program split_words
!< Split a string into tokens.
use stringifor
implicit none
type(string) :: s
type(string), allocatable :: tokens(:)
integer :: t
s = 'the quick brown fox'
call s%split(tokens=tokens) ! on blanks, repeated ones count as one
print '(I0,A)', size(tokens), ' words, the last one is "'//tokens(size(tokens))//'"'
s = '2001-07-14'
call s%split(tokens=tokens, sep='-')
print '(*(A,:,"/"))', (tokens(t)//'', t=size(tokens), 1, -1)
s = 'key = value = with = equals'
call s%split(tokens=tokens, sep=' = ', max_tokens=1) ! split once
print '(A)', '['//tokens(1)//'] ['//tokens(2)//']'
endprogram split_words$ split_words
4 words, the last one is "fox"
14/07/2001
[key] [value = with = equals]Repeated separators count as one, and the ones at the ends are ignored. max_tokens is the number of splits. To keep the empty fields, as in a CSV record, pass keep_empty=.true..
Join an array of strings
program join_strings
!< Join an array into one string.
use stringifor
implicit none
type(string) :: glue, words(3), joined
character(5) :: chars(3)
words(1) = 'one' ; words(2) = 'two' ; words(3) = 'three'
glue = ', '
joined = glue%join(array=words) ! the string is the separator
print '(A)', joined//''
print '(A)', glue%join(array=words, sep='-')//'' ! or pass one
chars = ['alpha', 'beta ', 'gamma']
joined = strjoin(array=chars, sep='+') ! no separator string needed; characters are trimmed
print '(A)', joined//''
endprogram join_strings$ join_strings
one, two, three
one-two-three
alpha+beta+gammajoin is a method, its string is the default separator; strjoin is a function of the module.
Parse comma-separated values
program csv_line
!< Parse a line of comma-separated values, empty cells included.
use stringifor
implicit none
type(string) :: line
type(string), allocatable :: cells(:)
real(R8P) :: value, total
integer :: c, iostat, bad
line = ' 12.5 , 7 , n/a,, 30.25 '
call line%split(tokens=cells, sep=',', keep_empty=.true.) ! the empty cell keeps its column
cells = cells%strip() ! every cell at once
total = 0._R8P
bad = 0
do c = 1, size(cells)
call cells(c)%read_number(value, iostat=iostat)
if (iostat == 0) then
total = total + value
else
bad = bad + 1
endif
enddo
print '(I0,A,F0.2,A,I0,A)', size(cells), ' cells, the numbers sum to ', total, ', ', bad, ' are not numbers'
endprogram csv_line$ csvline
5 cells, the numbers sum to 49.75, 2 are not numberskeep_empty=.true. keeps the empty cell in its column, and read_number reports, with a positive iostat, a cell that is not a number instead of returning 0 or NaN.
Split a very long string
program split_big
!< Split a long string in a fixed number of chunks of work, not one big scan.
use stringifor
implicit none
type(string) :: text
type(string), allocatable :: tokens(:)
text = repeat('field,', 5000)
call text%split_chunked(tokens=tokens, sep=',', chunks=100)
print '(I0,1X,A)', size(tokens), tokens(5000)%chars()
endprogram split_big$ split_big
5000 fieldsplit_chunked gives the tokens of split, working through the string in chunks pieces, which saves memory on very long strings. Repeated separators are collapsed.
Split once, at the first separator
program partition_once
!< Split at the first separator, keeping the three parts.
use stringifor
implicit none
type(string) :: s, parts(3)
s = 'name = John = Smith'
parts = s%partition(sep=' = ')
print '(A)', '['//parts(1)//'] ['//parts(2)//'] ['//parts(3)//']'
endprogram partition_once$ partition
[name] [ = ] [John = Smith]Take a part of a string
program slice_a_string
!< Substrings, with a stride too.
use stringifor
implicit none
type(string) :: s
s = 'Hello World'
print '(A)', s%slice(first=1, last=5)
print '(A)', s%slice(first=7) ! to the end
print '(A)', s%slice(last=5) ! from the beginning
print '(A)', s%slice(stride=2) ! every other character
print '(A)', s%slice(stride=-1) ! backwards
print '(A)', '['//s%slice(first=20, last=30)//']' ! out of the string: clamped, never out of bounds
endprogram slice_a_string$ slice
Hello
World
Hello
HloWrd
dlroW olleH
[]slice returns a character; its three arguments are all optional.
Reverse the characters, or the words
program reverse_things
!< Reverse the characters or the words.
use stringifor
implicit none
type(string) :: s
s = 'the sky is blue'
print '(A)', s%reverse()//''
print '(A)', s%reverse_words()//''
s = 'usr/local/lib'
print '(A)', s%reverse_words(sep='/')//''
endprogram reverse_things$ reverse
eulb si yks eht
blue is sky the
lib/local/usrPad to a width
program pad_a_string
!< Pad to a width.
use stringifor
implicit none
type(string) :: s
s = '42'
print '(A)', s%fill(width=6)//'' ! zeros on the left
print '(A)', s%fill(width=6, right=.true.)//''
print '(A)', s%fill(width=6, filling_char='.')//''
s = 'name'
print '(A)', '['//s%fill(width=10, right=.true., filling_char=' ')//']'
endprogram pad_a_string$ pad
000042
420000
....42
[name ]Center or justify in a width
program align
!< Strings centered or justified in a given width.
use stringifor
implicit none
type(string) :: s
s = 'title'
print '(A)', '['//s%center(11)//']'
print '(A)', '['//s%center(11, '=')//']'
print '(A)', '['//s%ljust(11, '.')//']'
print '(A)', '['//s%rjust(11)//']'
print '(A)', '['//s%center(3)//']'
endprogram align$ align
[ title ]
[===title===]
[title......]
[ title]
[title]center, ljust and rjust work as the Python methods: a string already as wide as the width is unchanged.
Find something in a string
program search_text
!< Look for something in a string.
use stringifor
implicit none
type(string) :: s
s = 'report_2001_final.tar.gz'
print '(2(L1,1X))', s%start_with('report'), s%end_with('.gz')
print '(I0)', s%count('_')
print '(I0)', s%index('2001')
print '(I0)', s%index('.', back=.true.)
print '(I0)', s%scan('0123456789') ! the first digit
print '(I0)', s%verify('abcdefghijklmnopqrstuvwxyz') ! the first character that is not a letter
s = 'aaaa'
print '(I0,1X,I0)', s%count('aa'), s%count('aa', overlapping=.true.)
s = 'a.b.c.d'
print '(I0,1X,I0)', s%index('.', occurrence=2), s%index('.', back=.true., occurrence=2)
endprogram search_text$ search
T T
2
8
22
8
7
2 3
4 4count and index look for occurrences that do not overlap, as Python: 'aaaa' holds 2 'aa', 3 with overlapping=.true.. index(substring, occurrence=k) is the k-th occurrence, counted from the end with back=.true., 0 if there are fewer.
Read the settings of a key = value file
program regex_keyvalue
!< Read the settings of a key = value file, comments and blank lines skipped.
use stringifor
implicit none
type(string) :: lines(5)
type(regex) :: setting
type(regex_match) :: m
integer :: i
lines = [string('# solver settings'), string(' cfl = 0.8 # stable'), string(''), string('scheme=WENO5'), &
string('bad line')]
call setting%compile('^\s*(?P<key>\w+)\s*=\s*(?P<value>[^#]*?)\s*(?:#.*)?$')
do i = 1, size(lines)
m = setting%match(lines(i))
if (m%found) then
print '(A,"=[",A,"]")', m%group('key')//'', m%group('value')//''
elseif (lines(i)%len() > 0 .and. .not.lines(i)%start_with('#')) then
print '(A,I0,2A)', 'line ', i, ' is not a setting: ', lines(i)%chars()
endif
enddo
endprogram regex_keyvalue$ regex_keyvalue
cfl=[0.8]
scheme=[WENO5]
line 5 is not a setting: bad linematch anchors the pattern at the start of each line; [^#]*? is lazy, so the trailing blanks and the comment are left to the rest of the pattern. See Regular Expressions.
Extract the numbers of a text
program regex_numbers
!< Extract the numbers of a text, with their sign, fraction and exponent, and convert them.
use stringifor
implicit none
type(string), allocatable :: numbers(:)
real(R8P) :: total
integer :: i
numbers = re_findall('dt = 1.5e-3 s, steps: 2000, residual -4.2E+01 (x2)', &
'[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?')
total = 0
do i = 1, size(numbers)
total = total + numbers(i)%to_number(kind=1._R8P)
print '(A)', numbers(i)%chars()
enddo
print '(A,F12.4)', 'sum:', total
endprogram regex_numbers$ regex_numbers
1.5e-3
2000
-4.2E+01
2
sum: 1960.0015re_findall returns the text of every match; the pattern takes a sign, a fraction and an exponent, and (?:...) groups without capturing, so findall returns the whole matches.
Rename files that follow a pattern
program regex_rename
!< Plan the renaming of files: IMG_YYYYMMDD_hhmm.jpg becomes YYYY-MM-DD_hhmm.jpg, the other names are kept.
use stringifor
implicit none
type(string) :: names(4)
type(string) :: renamed
type(regex) :: photo
integer :: n
integer :: i
names = [string('IMG_20261030_0915.jpg'), string('img_20261031_1802.JPG'), string('notes.txt'), &
string('IMG_2026_x.jpg')]
call photo%compile('^img_(\d{4})(\d\d)(\d\d)_(\d{4})\.jpg$', flags='i')
do i = 1, size(names)
renamed = photo%sub('\1-\2-\3_\4.jpg', names(i), nsub=n)
if (n > 0) print '(3A)', names(i)%chars(), ' -> ', renamed%chars()
enddo
endprogram regex_rename$ regex_rename
IMG_20261030_0915.jpg -> 2026-10-30_0915.jpg
img_20261031_1802.JPG -> 2026-10-31_1802.jpgsub rebuilds the name from the groups \1 to \4; nsub tells whether the pattern matched, so the other names are left alone. To rename for real, pass the two names to rename (a common extension) or to execute_command_line.
Search or replace once, without compiling
m = re_search('key = 42', '(\w+)\s*=\s*(\d+)')
print '(A,1X,A)', m%group(1)//'', m%group(2)//''
s = re_sub('x=1, y=22', '(\w)=(\d+)', '\2=\1')
print '(A)', s%chars()$ regex_calls
key 42
1=x, 22=y
re_fullmatch 2026-10-31: T
fullmatch on 2026-10-31: F
search on 2026-10-31: T
groups: 3, m is number 2
2026 10 F
NAME 10 13
T
2 Fre_search, re_fullmatch, re_findall, re_split and re_sub take the subject first and the pattern as a character, compiled at each call: in a loop, compile a regex once and pass it instead. A malformed pattern stops the program; compile it yourself with iostat to handle the error.
Check that a whole string has a shape
m = re_fullmatch('2026-10-31', '\d{4}-\d\d-\d\d')
print '(A,L2)', 're_fullmatch 2026-10-31:', m%found
call re%compile('\d{4}-\d\d-\d\d')
m = re%fullmatch('on 2026-10-31')
print '(A,L2)', 'fullmatch on 2026-10-31:', m%found
m = re%search('on 2026-10-31')
print '(A,L2)', 'search on 2026-10-31: ', m%foundsearch finds the pattern anywhere, match only at the start, fullmatch and re_fullmatch only on the whole string: that is the one for validating input.
Work with the groups of a match
call re%compile('(?P<y>\d{4})-(?P<m>\d\d)(-\d\d)?')
print '(A,I0,A,I0)', 'groups: ', re%group_count(), ', m is number ', re%group_index('m')
m = re%search('2026-10')
g = m%groups()
print '(3A,L2)', g(1)%chars(), ' ', g(2)%chars(), g(3)%is_allocated()group_count and group_index ask the regex; groups returns the texts of all the groups of a match, and a group that did not take part is a not allocated string, not an empty one.
Ignore case, match lines, and catch a bad pattern
call re%compile('^name', flags='im')
m = re%search('# header'//new_line('a')//'NAME = x')
print '(A,2I4)', m%group(0)//'', m%span(0)
call re%compile('(?s)a.b')
m = re%search('a'//new_line('a')//'b')
print '(L1)', m%found
call re%compile('(', iostat=iostat)
print '(I0,L2)', iostat, re%is_compiled()i ignores the ASCII case, m makes ^ and $ match at each line, s lets the dot match a line feed; they go in flags, or at the start of the pattern. With iostat a malformed pattern gives the byte of the error and leaves the regex not compiled (is_compiled), and such a regex matches nothing.
Get the text between two tags
program tagged_text
!< The text between two tags.
use stringifor
implicit none
type(string) :: s, found
integer :: first, last
s = '<b>bold</b> plain <b>bold again</b>'
found = s%search(tag_start='<b>', tag_end='</b>', istart=first, iend=last)
print '(A,2(1X,I0))', found//'', first, last
endprogram tagged_text$ tags
<b>bold</b> 1 11search returns the first tagged text, tags included, and optionally where it starts and ends.
Check whether a string is a number
program number_check
!< Is it a number?
use stringifor
implicit none
type(string) :: s(6)
integer :: i
s(1) = '42' ; s(2) = '-3.14' ; s(3) = '6.02e23' ; s(4) = '1.' ; s(5) = '12 apples' ; s(6) = '007'
print '(A)', 'string number integer real digit'
do i = 1, size(s)
print '(A,4(L1,6X))', s(i)%fill(width=12, right=.true., filling_char=' ')//'', &
s(i)%is_number(), s(i)%is_integer(), s(i)%is_real(), s(i)%is_digit()
enddo
endprogram number_check$ number_check
string number integer real digit
42 T T F T
-3.14 T F T F
6.02e23 T F T F
1. T F T F
12 apples F F F F
007 T T F Tis_digit is true when every character is a digit; a real has a decimal point or an exponent, so an integer is not a real.
Convert a string to a number
program number_cast
!< From a string to a number.
use stringifor
implicit none
type(string) :: s
real(R8P) :: x
integer(I4P) :: n
s = '3.4e2'
if (s%is_number()) x = s%to_number(kind=1._R8P) ! the kind of the argument selects the result
print '(F0.1)', x
s = '42'
if (s%is_integer()) n = s%to_number(kind=1_I4P)
print '(I0)', n + 1
x = s%to_number(kind=1._R8P) ! an integer string is a valid real
print '(F0.1)', x
endprogram number_cast$ number_cast
340.0
43
42.0Check first: on a string that is not a number to_number gives 0 (integer kinds) or NaN (real kinds).
Convert a string to a number, with an error status
program checked_cast
!< Numbers read from strings, with an error status.
use stringifor
implicit none
type(string) :: fields(3)
real(R8P) :: value
integer :: iostat
character(len=99) :: iomsg
integer :: f
fields(1) = '3.25'
fields(2) = 'n/a'
fields(3) = '1e3'
do f = 1, size(fields)
call fields(f)%read_number(value, iostat=iostat, iomsg=iomsg)
if (iostat == 0) then
print '(A,F8.2)', fields(f)//' -> ', value
else
print '(A)', fields(f)//' -> error: '//trim(iomsg)
endif
enddo
endprogram checked_cast$ checked_cast
3.25 -> 3.25
n/a -> error: the string is not a number
1e3 -> 1000.00read_number reports the failure as a read statement: iostat is positive and iomsg says why.
Convert a number to a string
program number_to_string
!< From a number to a string.
use stringifor
implicit none
type(string) :: s
s = 42
print '(A)', s//''
s = -127_I1P
print '(A)', s//''
s = 2.5_R4P
print '(A)', s//''
s = 1._R8P/3._R8P
print '(A)', s//''
s = 255
print '(A)', s%hex()//' '//s%hex(uppercase=.true.)
s = -1
print '(A)', s%hex(bits=16)//'' ! two's complement on 16 bits
endprogram number_to_string$ number_string
+42
-127
+0.250000000E+01
+0.33333333333333331E+000
ff FF
ffffA real is written with all the significant digits of its kind. hex reads the integer in the string.
Write a real as briefly as it reads back
program shortest_text
!< A real as the shortest text that reads back as exactly that number, against the assignment.
use stringifor
implicit none
real(R8P) :: values(5)
type(string) :: full
type(string) :: short
integer :: v
values = [0.1_R8P, 100._R8P, 1.e23_R8P, 1.5e-7_R8P, -2.5_R8P]
do v = 1, size(values)
full = values(v)
short = shortest(values(v))
print '(A,1X,A)', full%chars(), short%chars()
enddo
if (short%to_number(1._R8P) == values(5)) print '(A)', 'it reads back as the same number'
endprogram shortest_text$ shortest_text
+0.10000000000000001E+000 0.1
+0.10000000000000000E+003 100.0
+0.99999999999999992E+023 1e+23
+0.14999999999999999E-006 1.5e-07
-0.25000000000000000E+001 -2.5
it reads back as the same numbershortest gives the fewest digits that read back as exactly that number; the assignment gives all the digits of the kind.
Read reals written by Fortran, infinities and NaNs
program number_forms
!< The forms a real can take in a string: what Fortran writes, and the special values.
use stringifor
implicit none
type(string) :: fields(6)
real(R8P) :: value
integer :: iostat
integer :: f
fields(1) = '1.5-101' ! as E and ES edit descriptors write a 3 digit exponent
fields(2) = '1.5d3'
fields(3) = 'inf'
fields(4) = '-Infinity'
fields(5) = 'nan'
fields(6) = '2026-11' ! not a number
do f = 1, size(fields)
call fields(f)%read_number(value, iostat=iostat)
print '(A10,1X,L1,1X,ES12.4)', fields(f)%chars(), iostat == 0, value
enddo
endprogram number_forms$ number_forms
1.5-101 T 1.5000-101
1.5d3 T 1.5000E+03
inf T Infinity
-Infinity T -Infinity
nan T NaN
2026-11 F NaN1.5-101 is how E and ES edit descriptors write a three digit exponent. 2026-11 stays not a number.
Suggest the nearest word
program did_you_mean
!< The nearest of a list of known words, for a "did you mean" message.
use stringifor
implicit none
character(len=8), parameter :: KNOWN(4) = [character(len=8) :: 'build', 'clean', 'install', 'rule']
type(string) :: typed
integer :: distances(size(KNOWN))
integer :: k
typed = 'instal'
do k = 1, size(KNOWN)
distances(k) = typed%edit_distance(trim(KNOWN(k)))
enddo
k = minloc(distances, dim=1)
print '(A)', 'unknown command '//typed//', did you mean '//trim(KNOWN(k))//'?'
typed = 'caf'//char(195)//char(169)
print '(I0,1X,I0)', typed%edit_distance('cafe'), typed%edit_distance('cafe', by=SF_CHARS)
endprogram did_you_mean$ did_you_mean
unknown command instal, did you mean install?
2 1edit_distance counts the insertions, deletions and substitutions, in bytes or, with by=SF_CHARS, in characters.
Read a whole file
program read_a_file
!< Read a whole file.
use stringifor
implicit none
type(string), allocatable :: lines(:)
type(string) :: content
call read_file(file='notes.txt', lines=lines) ! one string for each line
print '(I0,A)', size(lines), ' lines, the last is "'//lines(size(lines))//'"'
call content%read_file(file='notes.txt') ! the whole file in one string
print '(I0,A,I0,A)', content%len(), ' characters, ', content%count(new_line('a')), ' line ends'
endprogram read_a_file$ readfile
3 lines, the last is "third line"
34 characters, 3 line endsThe first form is a subroutine of the module, the second a method reading the file as one string, line ends included.
Read a file line by line
program read_line_by_line
!< Read a file one line at a time.
use stringifor
implicit none
type(string) :: line
integer :: unit, iostat, n
n = 0
open(newunit=unit, file='poem.txt', status='old', action='read')
do
call line%read_line(unit=unit, iostat=iostat)
if (iostat /= 0) exit ! the end of the file, or an error
n = n + 1
print '(I0,A)', n, ': ['//line//']'
enddo
close(unit)
endprogram read_line_by_line$ readline
1: [roses are red]
2: []
3: [violets are blue]iostat is zero for every line read, empty ones included, and not zero at the end of the file.
Read a file saved by a Windows editor
call text%read_file(file='windows.txt')
print '(A,L1,A,L1)', 'has a BOM: ', text%has_bom(), ', starts with key: ', text%start_with('key')
call text%read_file(file='windows.txt', skip_bom=.true.)
print '(A,L1,A,L1)', 'has a BOM: ', text%has_bom(), ', starts with key: ', text%start_with('key')
call read_file(file='windows.txt', lines=lines, skip_bom=.true.)
print '(A)', lines(1)//'|'$ bom_file
has a BOM: T, starts with key: F
has a BOM: F, starts with key: T
key = value|$ od -c windows.txt
0000000 357 273 277 k e y = v a l u e \n o
0000020 t h e r = 1 \n
0000031Some editors start a UTF-8 file with a byte order mark, the bytes EF BB BF (357 273 277 in octal): read as is, they are the first three characters of the first line, so a key or a number there is not found. skip_bom=.true. drops them (the module subroutines read_file and read_lines take it too), has_bom() tells whether a string starts with them, and write_file(..., bom=.true.) writes them.
Write a file
program write_a_file
!< Write strings to a file.
use stringifor
implicit none
type(string) :: lines(3)
lines(1) = 'bread'
lines(2) = 'milk'
lines(3) = 'coffee'
call write_file(file='shopping.txt', lines=lines)
print '(A)', 'written'
endprogram write_a_file$ writefile
written$ cat shopping.txt
bread
milk
coffeeWrite and read lines through an open unit
program unit_io
!< Lines written to and read from a unit already open.
use stringifor
implicit none
type(string) :: line, text
type(string), allocatable :: lines(:)
integer :: unit, l
open(newunit=unit, file='notes.txt', status='replace')
line = 'first line'
call line%write_line(unit=unit) ! one string, one line
text = 'second line'//new_line('a')//'third line'
call text%write_lines(unit=unit) ! one string, a line for each line it holds
call read_lines(unit=unit, lines=lines) ! rewinds the unit, one string for each line
close(unit, status='delete')
do l = 1, size(lines)
print '(I0,A)', l, ': '//lines(l)
enddo
endprogram unit_io$ unit_io
1: first line
2: second line
3: third linewrite_line writes a string as one line, write_lines writes each line it holds as a line. read_lines rewinds the unit and reads every line: as a method into one string, as the procedure of the module into an array.
Save strings in a binary file
program binary_io
!< Strings saved to and loaded from an unformatted file.
use stringifor
implicit none
type(string) :: saved(3), loaded(3)
integer :: unit, s
saved(1) = 'Venture'
saved(2) = ' blanks kept '
saved(3) = ''
open(newunit=unit, file='names.bin', form='unformatted', access='stream', status='replace')
write(unit) saved ! each string: its length, then its characters
close(unit)
open(newunit=unit, file='names.bin', form='unformatted', access='stream', status='old')
read(unit) loaded
close(unit, status='delete')
do s = 1, size(loaded)
print '(I2,A)', loaded(s)%len(), ' characters: ['//loaded(s)//']'
enddo
endprogram binary_io$ binary_io
7 characters: [Venture]
15 characters: [ blanks kept ]
0 characters: []An unformatted write stores each string as its length followed by its characters, and read gets it back exactly, blanks and empty strings included. The format is the same as that of string_type in the Fortran stdlib.
Read strings from the standard input
program read_standard_input
!< Read strings with list-directed input.
use stringifor
implicit none
type(string) :: first, second
read *, first, second
print '(A)', '['//first//'] ['//second//']'
endprogram read_standard_input$ echo '"two words" three' | read_stdin
[two words] [three]A list-directed read takes one blank-delimited word, or a quoted text, for each string.
Take a path apart
program path_pieces
!< The pieces of a file name.
use stringifor
implicit none
type(string) :: path
path = '/home/user/data/archive.tar.gz'
print '(A)', path%basedir()//''
print '(A)', path%basename()//''
print '(A)', path%extension()//''
print '(A)', path%basename(strip_last_extension=.true.)//''
print '(A)', path%basename(extension='.tar.gz')//''
endprogram path_pieces$ paths
/home/user/data
archive.tar.gz
.gz
archive.tar
archiveList the files matching a pattern
program list_files
!< The files matching a pattern.
use stringifor
implicit none
type(string) :: s
type(string), allocatable :: files(:)
integer :: f
call s%glob(pattern='data/*.csv', list=files)
do f = 1, size(files)
print '(A)', files(f)//''
enddo
endprogram list_files$ glob
data/cars.csvglob runs the ls command: it works on Unix-like systems only. Only the wildcards *, ? and [...] are special: any other character of the pattern is part of the names searched.
Filter names with a wildcard pattern
program wildcards
!< The names matching a wildcard pattern.
use stringifor
implicit none
type(string) :: names(5)
integer :: n
names(1) = 'report-2025.csv'
names(2) = 'report-2026.csv'
names(3) = 'report-2026.txt'
names(4) = 'notes.txt'
names(5) = 'Report-2026.csv'
do n = 1, size(names)
if (names(n)%match('report-202[5-9].csv')) print '(A)', names(n)//''
enddo
print '(5L2)', names%match('*.txt')
endprogram wildcards$ wildcards
report-2025.csv
report-2026.csv
F F T T Fmatch needs no file system: it works on any system, on names already in memory, and it is elemental.
Get a name for a temporary file
program temporary_name
!< A name for a temporary file.
use stringifor
implicit none
type(string) :: s, name
name = s%tempname(prefix='scratch-') ! a name not used by any file of the directory
print '(L1,1X,L1)', name%start_with('scratch-'), name%len() > len('scratch-')
endprogram temporary_name$ tempname
T TThe name is not used by any file of the directory; it changes at each call, so the example prints what does not.
Wrap a paragraph in justified lines
program justify_text
!< Wrap a paragraph in justified lines.
use stringifor
implicit none
type(string) :: text
type(string), allocatable :: lines(:)
integer :: l
text = 'Fortran is a general-purpose, compiled imperative programming language '// &
'that is especially suited to numeric computation and scientific computing.'
call text%justify(lines=lines, width=36)
do l = 1, size(lines)
print '(A)', '|'//lines(l)//'|'
enddo
print '(A,I0)', 'the last word has length ', text%len_last_word()
endprogram justify_text$ justify
|Fortran is a general-purpose,|
|compiled imperative programming|
|language that is especially suited|
|to numeric computation and|
|scientific computing. |
the last word has length 10The last line is left-justified and padded; a word longer than the width stays alone on its line.
Find what several strings start with
program common_prefix
!< What several strings start with.
use stringifor
implicit none
type(string) :: files(3), prefix
files(1) = 'src/lib/stringifor.F90'
files(2) = 'src/lib/stringifor_string_t.F90'
files(3) = 'src/tests/stringifor/test.f90'
prefix = files(1)%common_prefix(files(2)) ! of two strings
print '(A)', prefix//''
prefix = files(1)%common_prefix(array=files) ! of an array
print '(A)', prefix//''
endprogram common_prefix$ prefix
src/lib/stringifor
src/Compare two version numbers
program compare_versions
!< Which version is newer?
use stringifor
implicit none
type(string) :: version
version = '1.10.0'
print '(I0)', version%compare_version('1.9.3') ! 1: newer
print '(I0)', version%compare_version('1.10') ! 0: the same, missing fields are zero
print '(I0)', version%compare_version('2.0') ! -1: older
version = '2024-03'
print '(I0)', version%compare_version('2024-11', sep='-')
endprogram compare_versions$ version
1
0
-1
-1Fields of digits are compared as integers of any size, the others as text. It is not semantic versioning: 1.0.0-rc1 is greater than 1.0.0.
Sort an array of strings
names = [string('run10.dat'), string('Run2.dat'), string('run2.dat'), string('run1.dat'), string('README'), &
string('run02.dat')]
call strsort(names)
print '(6(A,1X))', (names(i)%chars(), i=1, 6)
call strsort(names, less=strless_natural)
print '(6(A,1X))', (names(i)%chars(), i=1, 6)
call strsort(names, less=strless_nocase)
print '(6(A,1X))', (names(i)%chars(), i=1, 6)names = [string('c'), string('a'), string('b'), string('a'), string('c'), string('b')]
sizes = [3.0, 1.0, 2.0, 1.5, 3.5, 2.5]
call strsort(names, index=perm)
sizes = sizes(perm) ! the data follows its key; equal keys keep their order
print '(6(A,1X))', (names(i)%chars(), i=1, 6)
print '(6(F3.1,1X))', sizesnames = [string('bb'), string('a'), string('ccc'), string('ab'), string('e'), string('fff')]
call strsort(names, less=longer_first)
print '(6(A,1X))', (names(i)%chars(), i=1, 6)$ sorting
README Run2.dat run02.dat run1.dat run10.dat run2.dat
README Run2.dat run1.dat run02.dat run2.dat run10.dat
README run02.dat run1.dat run10.dat Run2.dat run2.dat
a a b b c c
1.0 1.5 2.0 2.5 3.0 3.5
ccc fff ab bb a estrsort is a stable sort in place. less chooses the order: strless, the default, is byte order; strless_natural puts run2 before run10; strless_nocase ignores the ASCII case; or write your own pure function with the interface strsort_less. index returns the permutation, so that other arrays can follow the strings.
Encode and decode in base64
program base64
!< Encode and decode in base64.
use stringifor
implicit none
type(string) :: s, code
s = 'Hello World'
code = s%encode(codec='base64')
print '(A)', code//''
print '(A)', code%decode(codec='base64')//''
endprogram base64$ encode_base64
SGVsbG8gV29ybGQ=
Hello WorldEscape a character
program escape_characters
!< Escape a character, and back.
use stringifor
implicit none
type(string) :: s, escaped
s = 'C:\temp\new'
escaped = s%escape(to_escape='\')
print '(A)', escaped//''
print '(A)', escaped%unescape(to_unescape='\')//''
endprogram escape_characters$ escape
C:\\temp\\new
C:\temp\newEscape text for XML or HTML
text = 'Q&A: "x < y" isn''t'//achar(10)//'always true'
print '(A)', '<note title="'//text%escape_xml(attribute=.true.)//'">'
print '(A)', text%escape_xml()//''
print '(A)', '</note>'note = 'café ☕ <b> AT&T'
text = note%unescape_xml()
print '(A)', text%chars()
note = 'R&D � A'
print '(A,I0)', 'first malformed reference at byte ', note%xml_error()
print '(A)', note%unescape_xml()//''$ xml_escape
<note title="Q&A: "x < y" isn't always true">
Q&A: "x < y" isn't
always true
</note>
café ☕ <b> AT&T
first malformed reference at byte 2
R&D � Aescape_xml protects & < > " '; pass attribute=.true. for an attribute value, so that its line ends survive. unescape_xml reads the entities back, and xml_error gives the byte of the first malformed reference: is one, as XML does not define it.
Log any bytes readably
text = 'tab'//achar(9)//'nul'//achar(0)//'latin-1 '//char(233)//' utf-8 '//char(195)//char(169)
print '(A)', text%escape_c()//''
print '(A)', text%escape_c(ascii=.false.)//''
text = text%escape_c()
print '(A,L1)', 'read back: ', text%unescape_c()=='tab'//achar(9)//'nul'//achar(0)//'latin-1 '//char(233)// &
' utf-8 '//char(195)//char(169)text = 'ok\n, then \q and \u12'
print '(A,I0)', 'first malformed escape at byte ', text%escapes_error()
print '(A)', text%unescape_c()//''$ c_escapes
Grüße, 你好! 😀
tab\tnul\x00latin-1 \xe9 utf-8 \u00e9
tab\tnul\x00latin-1 \xe9 utf-8 é
read back: T
first malformed escape at byte 12
ok
, then \q and \u12escape_c writes any bytes as printable ASCII and unescape_c reads them back exactly; escapes_error gives the byte of the first malformed escape. See 8. Unicode text.
Print in colour
program colours
!< Colours and styles for the terminal.
use stringifor
implicit none
type(string) :: s
s = 'error: the file is missing'
print '(A)', s%colorize(color_fg='red', style='bold_on')
s = 'warning: the file is empty'
print '(A)', s%colorize(color_fg='yellow')
s = 'done'
print '(A)', s%colorize(color_fg='green', style='underline_on')
endprogram coloursA method on every element of an array
program arrays_of_strings
!< A method applied to every element of an array.
use stringifor
implicit none
type(string) :: names(3), glue
integer :: n
names(1) = 'ada' ; names(2) = 'grace hopper' ; names(3) = 'linus' ! elements of different lengths
names = names%startcase() ! the methods are elemental
print '(A)', glue%join(array=names, sep=', ')//''
print '(3(I0,1X))', names%len()
print '(3(L1,1X))', names%start_with('G')
n = maxloc(names%len(), dim=1)
print '(A)', 'the longest is '//names(n)
endprogram arrays_of_strings$ arrays
Ada, Grace Hopper, Linus
3 12 5
F T F
the longest is Grace HopperThe elements of an array of strings have each its own length.
Use the Fortran intrinsics on a string
program intrinsics_on_strings
!< The Fortran intrinsics, on strings.
use stringifor
implicit none
type(string) :: s
s = ' hello '
print '(A)', '['//trim(s)//']'
print '(A)', '['//adjustl(s)//']'
print '(I0)', len_trim(s)
print '(I0)', index(s, 'll')
print '(A)', repeat(s%strip(), 3)//''
endprogram intrinsics_on_strings$ intrinsics
[ hello]
[hello ]
7
5
hellohellohelloadjustl, adjustr, count, index, len_trim, repeat, scan, trim and verify accept a string. trim removes the trailing blanks only.