Skip to content

Cookbook ​

Short answers to "how do I ...?". Each recipe is a complete program with its real output; the reference has the details. The files they read are in docs/examples/files (case.xml and coupled.xml of the tutorial, broken.xml, station-*.xml), or are written by an earlier program: results.xml by chapter 5 of the tutorial, appended.xml by Write binary data inside a document.

Reading ​

Parse a string ​

f90
program parse_string
use foxy
implicit none
type(xml_file) :: doc

call doc%parse(string='<point x="1" y="2">origin</point>')
print '(A)', doc%content('point')//' at '//doc%attribute(1, 'x')//','//doc%attribute(1, 'y')
endprogram parse_string
$ parse_string
origin at 1,2

attribute(1, name): the root tag of a parsed document is the tag 1.

Parse a file, and stop on errors ​

f90
program parse_file
use foxy
implicit none
type(xml_file)     :: doc
integer(I4P)       :: iostat
character(len=256) :: iomsg

call doc%parse(filename='case.xml', iostat=iostat, iomsg=iomsg)
if (iostat/=0) error stop 'case.xml: '//trim(iomsg)
print '(A,I0,A)', 'case.xml: ', doc%tags_number(), ' tags'
endprogram parse_file
$ parse_file
case.xml: 18 tags

The file is read in chunks, never loaded whole: parse(..., chunk_size=) sets their size.

Check that a document is well-formed ​

f90
program check_wellformed
use foxy
implicit none
type(xml_file)     :: doc
integer(I4P)       :: iostat
character(len=256) :: iomsg
character(len=32)  :: sources(6), shown(6)
integer            :: s

sources = [character(len=32) :: '<a><b/></a>', '<a x="1" x="2"/>', '<a/><b/>', '<a>1 < 2</a>', '<a>'//achar(255)//'</a>', &
           '<a>R&D</a>']
shown = sources
shown(5) = '<a>\xFF</a>'                       ! the byte 0xFF, not UTF-8
do s=1, size(sources)
   call doc%parse(string=trim(sources(s)), iostat=iostat, iomsg=iomsg)
   if (iostat==0) then
      print '(A)', trim(shown(s))//': well-formed'
   else
      print '(A)', trim(shown(s))//': '//trim(iomsg)
   endif
enddo
endprogram check_wellformed
$ check_wellformed
<a><b/></a>: well-formed
<a x="1" x="2"/>: line 1: duplicate attribute "x" in tag <a>
<a/><b/>: line 1: second root tag <b>
<a>1 < 2</a>: line 1: character '<' not starting a tag
<a>\xFF</a>: line 1: invalid UTF-8 byte 0xFF
<a>R&D</a>: line 1: malformed reference "&D"

iostat is XML_MALFORMED and iomsg holds the first problem, with its line; the document is built anyway.

Read a number, an integer or a logical ​

f90
program read_number
use foxy
implicit none
type(xml_file) :: doc
real(R8P)      :: dt
integer(I4P)   :: steps
logical        :: adaptive

call doc%parse(filename='case.xml')
call doc%get_content(id=doc%find(path='case/time/dt'), value=dt)
call doc%get_content(id=doc%find(path='case/time/steps'), value=steps)
call doc%get_content(id=doc%find(path='case/time/adaptive'), value=adaptive)
print '(ES10.3,1X,I0,1X,L1)', dt, steps, adaptive
endprogram read_number
$ read_number
 1.000E-03 500 T

Logicals are true, false, 1 or 0; any integer kind (I1P…I8P), R4P and R8P work.

Read a list of numbers ​

f90
program read_list
use foxy
implicit none
type(xml_file)         :: doc
real(R8P), allocatable :: domain(:)

call doc%parse(filename='case.xml')
call doc%get_content(id=doc%find(path='case/mesh/domain'), values=domain)   ! allocated to the list size
print '(I0,A,6F5.2)', size(domain), ' values:', domain
endprogram read_list
$ read_list
6 values: 0.00 1.00 0.00 0.50 0.00 0.00

values= is an allocatable rank-1 array, allocated to the number of blank-separated tokens.

Read an attribute, as text or as a number ​

f90
program read_attribute
use foxy
implicit none
type(xml_file) :: doc
integer(I4P)   :: grid, ni, nj

call doc%parse(filename='case.xml')
grid = doc%find(name='grid')
print '(A)', 'ni as text: '//doc%attribute(grid, 'ni')
call doc%get_attribute(id=grid, name='ni', value=ni)
call doc%get_attribute(id=grid, name='nj', value=nj)
print '(A,I0)', 'cells: ', ni*nj
endprogram read_attribute
$ read_attribute
ni as text: 64
cells: 2048

Use a default when a setting is missing ​

f90
program default_value
use foxy
implicit none
type(xml_file) :: doc
integer(I4P)   :: iostat, nk, cfl_steps

call doc%parse(filename='case.xml')
cfl_steps = 10                                                  ! the default
call doc%get_content(id=doc%find(path='case/time/cfl_steps'), value=cfl_steps, iostat=iostat)
print '(A,I0,A,L1)', 'cfl_steps = ', cfl_steps, ', from the file: ', iostat==0
nk = 1
call doc%get_attribute(id=doc%find(name='grid'), name='nk', value=nk, iostat=iostat)
print '(A,I0,A,L1)', 'nk = ', nk, ', from the file: ', iostat==0
endprogram default_value
$ default_value
cfl_steps = 10, from the file: F
nk = 1, from the file: T

On error the value is not touched: set the default first, check iostat if you need to know.

Report a bad or missing value ​

f90
program report_bad_value
use foxy
implicit none
type(xml_file)     :: doc
integer(I4P)       :: iostat, n
character(len=256) :: iomsg

call doc%parse(string='<run><steps>5OO</steps></run>')
call doc%get_content(id=doc%find(name='steps'), value=n, iostat=iostat, iomsg=iomsg)
if (iostat==XML_BAD_VALUE) print '(A)', trim(iomsg)
call doc%get_content(id=doc%find(name='step'), value=n, iostat=iostat, iomsg=iomsg)
if (iostat==XML_NOT_FOUND) print '(A)', trim(iomsg)
endprogram report_bad_value
$ report_bad_value
invalid character "O" in "5OO" in the content of tag "steps"
no tag with ID 0

Tokens are checked before conversion: ,, /, quotes and repeat counts (2*5) are rejected, not interpreted.

Keep the spaces around attribute values ​

f90
program keep_attribute_spaces
use foxy
implicit none
type(xml_file) :: doc

call doc%parse(string='<a label="  padded  "/>')
print '(A)', '['//doc%attribute(1, 'label')//']'                     ! trimmed by default
call doc%parse(string='<a label="  padded  "/>', sanitize_attributes_value=.false.)
print '(A)', '['//doc%attribute(1, 'label')//']'                     ! kept, as XML requires
endprogram keep_attribute_spaces
$ keep_attribute_spaces
[padded]
[  padded  ]

Read text with markup around it ​

f90
program mixed_content
use foxy
implicit none
type(xml_file) :: doc

call doc%parse(string='<p>Use <code>dt</code> &lt; 1e-2, see <![CDATA[<ref/>]]><!-- todo -->.</p>')
print '(A)', '['//doc%content('p')//']'           ! direct text: children excluded, comment removed, CDATA literal
print '(A)', '['//doc%content('code')//']'
print '(A)', doc%stringify()                       ! written back as read
endprogram mixed_content
$ mixed_content
[Use  < 1e-2, see <ref/>.]
[dt]
<p>Use <code>dt</code> &lt; 1e-2, see <![CDATA[<ref/>]]><!-- todo -->.</p>

content returns the direct text: references decoded, CDATA literal, comments and child tags out.

Read a UTF-16 or Latin-1 file ​

f90
program read_encodings
use foxy
implicit none
type(xml_file)     :: doc
type(xml_index)    :: idx
integer(I4P)       :: iostat
character(len=200) :: iomsg

call doc%parse(filename='station-utf16.xml', iostat=iostat)    ! UTF-16, little endian, with its byte order mark
print '(A,I0,1X,A)', 'UTF-16: ', iostat, doc%attribute(doc%find(name='station'), 'name')
call doc%parse(filename='station-latin1.xml', iostat=iostat)   ! declared encoding="ISO-8859-1"
print '(A,I0,1X,A)', 'ISO-8859-1: ', iostat, doc%attribute(doc%find(name='station'), 'name')// &
                     ' '//doc%content(name='reading')//' '//doc%attribute(doc%find(name='reading'), 'unit')
call doc%save(filename='station.xml')                          ! written in UTF-8, declared so
call idx%scan(filename='station-utf16.xml', iostat=iostat, iomsg=iomsg) ! the index reads the file bytes
print '(L1,1X,A)', iostat==XML_MALFORMED, trim(iomsg)
endprogram read_encodings
$ read_encodings
UTF-16: 0 Zürich
ISO-8859-1: 0 Zürich 21.5 °C
T line 1: a UTF-16LE file cannot be indexed: the index reads the file bytes as UTF-8 (parse it with xml_file or xml_handler)
$ head -1 station.xml
<?xml version="1.0" encoding="UTF-8"?>

Nothing to do: the encoding is detected (byte order mark, declaration) and the text converted to UTF-8; save writes UTF-8 and declares it. The index reads the file bytes (its positions are file positions), so it refuses a UTF-16 file: parse it with xml_file or a handler. See Encodings.

Finding ​

Check that a tag exists ​

f90
program check_exists
use foxy
implicit none
type(xml_file) :: doc
integer(I4P)   :: id

call doc%parse(filename='case.xml')
id = doc%find(path='case/time/dt')                              ! 0 if there is no such tag
if (id>0) print '(A)', 'dt is set: '//doc%content(id=id)
id = doc%find(path='case/time/cfl')
if (id==0) print '(A)', 'no cfl: the default is used'
print '(A,L1)', 'the west boundary has a velocity: ', size(doc%select('case/boundary[@side="west"]/velocity'))>0
print '(A,L1)', 'the case has a name: ', doc%attribute(id=doc%find(name='case'), name='name')/=''
endprogram check_exists
$ check_exists
dt is set: 1.0e-3
no cfl: the default is used
the west boundary has a velocity: T
the case has a name: T

find returns 0 when there is no such tag, select an empty array, attribute an empty string; the accessors also take 0 safely (content(id=0) is '', get_content(id=0, ...) returns XML_NOT_FOUND).

Loop over repeated tags ​

f90
program loop_repeated
use foxy
implicit none
type(xml_file)            :: doc
integer(I4P), allocatable :: walls(:)
integer(I4P)              :: b

call doc%parse(filename='case.xml')
walls = doc%find_all(path='case/boundary')
do b=1, size(walls)
   if (doc%attribute(walls(b), 'type')=='wall') print '(A)', doc%attribute(walls(b), 'side')//' is a wall'
enddo
endprogram loop_repeated
$ loop_repeated
north is a wall
south is a wall

find_all returns the IDs in document order, find only the first one.

Read the children of a tag ​

f90
program children
use foxy
implicit none
type(xml_file)            :: doc
integer(I4P), allocatable :: kids(:)
integer(I4P)              :: k, time

call doc%parse(filename='case.xml')
time = doc%find(path='case/time')
kids = doc%children(time)
do k=1, size(kids)
   print '(A)', doc%name(kids(k))//' = '//doc%content(id=kids(k))
enddo
print '(A,I0,A,I0)', 'parent of dt: ', doc%parent(doc%find(name='dt')), ', level of dt: ', doc%level(doc%find(name='dt'))
endprogram children
$ children
dt = 1.0e-3
steps = 500
adaptive = true
parent of dt: 5, level of dt: 3

List the attributes of a tag ​

f90
program list_attributes
use foxy
implicit none
type(xml_file) :: doc
integer(I4P)   :: grid, a

call doc%parse(filename='case.xml')
grid = doc%find(name='grid')
do a=1, doc%attributes_number(grid)
   print '(A)', doc%attribute_name(grid, a)//' = '//doc%attribute(grid, doc%attribute_name(grid, a))
enddo
endprogram list_attributes
$ list_attributes
ni = 64
nj = 32
nk = 1

Visit every tag ​

f90
program walk_tree
use foxy
implicit none
type(xml_file) :: doc
integer(I4P)   :: t

call doc%parse(filename='case.xml')
do t=1, doc%tags_number()                 ! every tag, in document order
   print '(A)', repeat('. ', doc%level(t)-1)//doc%name(t)
enddo
endprogram walk_tree
$ walk_tree
case
. mesh
. . grid
. . domain
. time
. . dt
. . steps
. . adaptive
. fluid
. . density
. . viscosity
. boundary
. . velocity
. boundary
. boundary
. boundary
. output
. notes

In a parsed document IDs follow the document order, so a plain loop visits the tree depth first; children and recursion give the structure. A tag appended later with add_tag(parent=) gets the next free ID: after such edits, walk with children.

Select tags with a path expression ​

f90
program select_path
use foxy
implicit none
type(xml_file)            :: doc
integer(I4P), allocatable :: ids(:)
integer(I4P)              :: iostat, i
character(len=200)        :: iomsg

call doc%parse(filename='case.xml')
ids = doc%select('case/boundary[@type="wall"]')                ! attribute value
print '(A,*(1X,A))', 'walls:', (doc%attribute(ids(i), 'side'), i=1, size(ids))
ids = doc%select('//*[@unit]')                                 ! any name, at any depth
print '(A,*(1X,A))', 'with a unit:', (doc%name(ids(i)), i=1, size(ids))
ids = doc%select('/case/boundary[2]')                          ! the second boundary child
print '(A,*(1X,A))', 'second boundary:', (doc%attribute(ids(i), 'side'), i=1, size(ids))
ids = doc%select('//boundary[@side~="^(east|west)$"]/velocity') ! regular expression
print '(A,*(1X,A))', 'east/west velocity:', (doc%content(id=ids(i)), i=1, size(ids))
ids = doc%select('case/boundary[@type=wall]', iostat=iostat, iomsg=iomsg)
print '(A,L1,1X,A)', 'malformed: ', iostat==XML_MALFORMED, trim(iomsg)
endprogram select_path
$ select_path
walls: north south
with a unit: dt density viscosity
second boundary: east
east/west velocity: 1.0 0.0 0.0
malformed: T select: a quoted value expected at column 21 of "case/boundary[@type=wall]"

One call for what would be find_all plus loops: steps (/ children, // descendants), *, attribute tests ([@k], [@k="v"], [@k~="re"]) and positions among siblings ([n]). See Querying.

Filter tags with a regular expression ​

f90
program regex_filter
use foxy
implicit none
type(xml_file)            :: doc
type(regex)               :: re
integer(I4P), allocatable :: ids(:)
integer(I4P)              :: iostat, i

call doc%parse(filename='case.xml')
call re%compile('^(density|viscosity)$', iostat=iostat)        ! StringiFor syntax, as Python re
ids = doc%matching(re)                                         ! on names
print '(A,*(1X,A))', 'fluid properties:', (doc%name(ids(i)), i=1, size(ids))
call re%compile('^(wall|outflow)$', iostat=iostat)
ids = doc%matching(re, ids=doc%find_all(name='boundary'), attribute='type')
print '(A,*(1X,A))', 'closed sides:', (doc%attribute(ids(i), 'side'), i=1, size(ids))
call re%compile('e-[0-9]+$', iostat=iostat)
ids = doc%matching(re, on='content')                           ! contents, decoded
print '(A,*(1X,A))', 'small values:', (doc%name(ids(i))//'='//doc%content(id=ids(i)), i=1, size(ids))
endprogram regex_filter
$ regex_filter
fluid properties: density viscosity
closed sides: east north south
small values: dt=1.0e-3 viscosity=1.8e-5

Compile the pattern once; matching takes the IDs of any query (ids=) and keeps those whose name, content or attribute value matches. See Regular expressions.

Match a namespace, whatever the prefix ​

f90
program namespaces_clark
use foxy
implicit none
type(xml_file) :: doc
integer(I4P)   :: t

call doc%parse(filename='coupled.xml')
t = doc%find(name='{https://example.org/thermal}dt')
print '(A)', doc%name(t)//' in '//doc%namespace(t)//' = '//doc%content(id=t)
endprogram namespaces_clark
$ namespaces_clark
th:dt in https://example.org/thermal = 5.0e-3

{uri}local is the Clark notation: a local name in a namespace.

Query with your own prefixes ​

f90
program namespaces_map
use foxy
implicit none
type(xml_file)            :: doc
integer(I4P), allocatable :: ids(:)

call doc%parse(filename='coupled.xml')
ids = doc%find_all(path='flow:case/thermal:case', namespaces='xmlns:flow="https://example.org/flow" '// &
                                                             'xmlns:thermal="https://example.org/thermal"')
print '(A)', 'found '//doc%name(ids(1))//' named '//doc%attribute(ids(1), 'name')
endprogram namespaces_map
$ namespaces_map
found th:case named plate

The prefixes of namespaces= replace the ones of the file for this query only.

Editing ​

Build a document in code ​

f90
program build_document
use foxy
implicit none
type(xml_file) :: doc
integer(I4P)   :: root, solver

call doc%add_tag(xml_tag(name='config'), id=root)
call doc%add_tag(xml_tag(name='dt', content='0.01', attributes_stream='unit="s"'), parent=root)
call doc%add_tag(xml_tag(name='solver', attributes_stream='kind="cg" tol="1e-6"'), parent=root, id=solver)
call doc%add_tag(xml_tag(name='restart', is_self_closing=.true.), parent=solver)
print '(A)', doc%stringify()
endprogram build_document
$ build_document
<config>
  <dt unit="s">0.01</dt>
  <solver kind="cg" tol="1e-6">
    <restart/>
  </solver>
</config>

Attributes go in as a stream (attributes_stream=) or as name/value pairs; parent= nests, id= returns the new ID.

Insert a tag before or after another, rename a tag ​

f90
program insert_tags
use foxy
implicit none
type(xml_file) :: doc

call doc%parse(string='<list><a/><c/></list>')
call doc%add_tag(xml_tag(name='b', is_self_closing=.true.), after=doc%find(name='a'))      ! after a sibling
call doc%add_tag(xml_tag(name='first', is_self_closing=.true.), before=doc%find(name='a'))  ! before a sibling
call doc%set_name(id=doc%find(name='c'), name='last')     ! an insertion renumbers the tags after it: find them again
print '(A)', doc%stringify()
endprogram insert_tags
$ insert_tags
<list>
  <first/>
  <a/>
  <b/>
  <last/>
</list>

An insertion takes the place of the sibling in the document order: the tags after it are renumbered.

Change a value and save ​

f90
program edit_and_save
use foxy
implicit none
type(xml_file) :: doc
integer(I4P)   :: steps, iostat

call doc%parse(filename='case.xml')
steps = doc%find(path='case/time/steps')
call doc%set_content(id=steps, value=2000_I4P)
call doc%set_attribute(id=steps, name='edited', value='yes')
call doc%save(filename='case_edited.xml', iostat=iostat)
call doc%parse(filename='case_edited.xml')
print '(A)', 'steps = '//doc%content('steps')//', edited = '//doc%attribute(doc%find(name='steps'), 'edited')
endprogram edit_and_save
$ edit_and_save
steps = 2000, edited = yes

Set numbers, logicals and lists ​

f90
program set_numbers
use foxy
implicit none
type(xml_file) :: doc
integer(I4P)   :: grid

call doc%parse(string='<mesh><grid/><domain/><origin/></mesh>')
grid = doc%find(name='grid')
call doc%set_attribute(id=grid, name='ni', value=128_I4P)                     ! an integer
call doc%set_attribute(id=grid, name='spacing', value=0.1_R8P)                ! a real: its shortest text
call doc%set_attribute(id=grid, name='periodic', value=.true.)                ! a logical
call doc%set_content(id=doc%find(name='domain'), values=[0._R8P, 1._R8P, 0._R8P, 0.5_R8P]) ! a list
call doc%set_content(id=doc%find(name='origin'), values=[0._R8P, 0.25_R8P], fmt='(F5.2)')  ! formatted
print '(A)', doc%stringify()
endprogram set_numbers
$ set_numbers
<mesh>
  <grid ni="128" spacing="0.1" periodic="true"/>
  <domain>0.0 1.0 0.0 0.5</domain>
  <origin>0.00 0.25</origin>
</mesh>

set_attribute and set_content take numbers, logicals and lists as the writer does: integers with their digits, reals with the shortest text that reads back exactly, unless fmt= formats them. See Editing.

Delete tags ​

f90
program delete_tags
use foxy
implicit none
type(xml_file) :: doc

call doc%parse(string='<r><keep/><drop><child/></drop><keep/><drop/></r>')
call doc%delete_tag(id=doc%find(name='drop'))       ! the first one, with its child
call doc%delete_tag(name='drop')                    ! the next first one
print '(A)', doc%stringify()
endprogram delete_tags
$ delete_tags
<r>
  <keep/>
  <keep/>
</r>

A deletion renumbers the following tags: look IDs up again after it.

Save with an XML declaration ​

f90
program save_declaration
use foxy
implicit none
type(xml_file) :: doc
character(len=64) :: line
integer :: unit

call doc%add_tag(xml_tag(name='empty', is_self_closing=.true.))
call doc%save(filename='declared.xml', is_declared=.true.)     ! adds <?xml version="1.0"?> if missing
open(newunit=unit, file='declared.xml')
read(unit, '(A)') line ; print '(A)', trim(line)
read(unit, '(A)') line ; print '(A)', trim(line)
close(unit)
endprogram save_declaration
$ save_declaration
<?xml version="1.0"?>
<empty/>

Store user text safely ​

f90
program escape_text
use foxy
implicit none
type(xml_file)                :: doc
character(len=:), allocatable :: user_text

user_text = 'p < 1 & "fast"'
call doc%add_tag(xml_tag(name='note', content=xml_escape(user_text)))   ! contents are stored as they are
print '(A)', doc%stringify()
print '(A)', doc%content('note')                                          ! read back decoded
endprogram escape_text
$ escape_text
<note>p &lt; 1 &amp; &quot;fast&quot;</note>
p < 1 & "fast"

Contents are stored as given (they may hold markup): escape text that is not markup. set_attribute and the writer escape for you.

Parse, build and edit a single tag ​

f90
program single_tag
use foxy
implicit none
type(xml_tag)                 :: tag
character(len=:), allocatable :: value

call tag%parse(source='junk <grid ni="64" nj="32"/> more')     ! the first tag of a string
print '(A)', tag%name()//': ni='//tag%get_attribute('ni')//', '//tag%stringify()
tag = xml_tag(name='dt', content='1e-3', attributes_stream='unit="s"')
call tag%add_attributes(attributes_stream='min="0" max="1"')
call tag%delete_attributes(name='min')
print '(A)', tag%stringify()
call tag%get_content(name='dt', content=value)
print '(A)', 'content: '//value
endprogram single_tag
$ single_tag
grid: ni=64, <grid ni="64" nj="32"/>
<dt unit="s" max="1">1e-3</dt>
content: 1e-3

Writing ​

Stream a document to a file ​

f90
program writer_file
use foxy
implicit none
type(xml_writer) :: w
integer(I4P)     :: iostat, i

call w%open(filename='series.xml', iostat=iostat)
call w%declaration()
call w%start('series')
do i=1, 3
   call w%start('point')
   call w%attribute('i', i)
   call w%attribute('x', 0.5_R8P*i, fmt='(F4.2)')
   call w%end('point')
enddo
call w%close(iostat=iostat)
print '(A,I0)', 'iostat = ', iostat
endprogram writer_file
$ writer_file
iostat = 0
$ cat series.xml
<?xml version="1.0"?>
<series>
  <point i="1" x="0.50"/>
  <point i="2" x="1.00"/>
  <point i="3" x="1.50"/>
</series>

fmt= formats a number attribute; without it, integers get their digits (a sign only if negative) and reals the shortest text that reads back exactly.

Write numbers, logicals and lists ​

f90
program writer_numbers
use foxy
implicit none
type(xml_writer) :: w

call w%open()
call w%start('cell')
call w%attribute('dt', 0.1_R8P)                        ! the shortest text that reads back: 0.1
call w%attribute('extent', [0_I4P, 63_I4P, 0_I4P, 31_I4P])
call w%start('volume')
call w%text(1._R8P/3._R8P)
call w%end('volume')
call w%start('center')
call w%text([0.5_R8P, 0.25_R8P, 0._R8P], fmt='(F5.3)')
call w%end('center')
call w%start('active')
call w%text(.true.)
call w%end('active')
call w%close()
write(*, '(A)', advance='no') w%string()        ! the string ends with a new line
endprogram writer_numbers
$ writer_numbers
<cell dt="0.1" extent="0 63 0 31">
  <volume>0.3333333333333333</volume>
  <center>0.500 0.250 0.000</center>
  <active>true</active>
</cell>

Without fmt=, integers are written with their digits and reals with the shortest text that reads back exactly (0.1, 0.3333333333333333); lists are separated by one space. The DOM does the same: set_content(id, value=), set_attribute(id, name, values=).

Write a document to memory ​

f90
program writer_memory
use foxy
implicit none
type(xml_writer) :: w

call w%open(indent=0_I4P)              ! no target: memory, no layout
call w%start('msg')
call w%attribute('ok', .true.)
call w%text('a < b')
call w%close()
print '(A)', w%string()
endprogram writer_memory
$ writer_memory
<msg ok="true">a &lt; b</msg>

open() without a file or a unit writes to memory; indent=0 writes no layout.

Catch a writer misuse ​

f90
program writer_errors
use foxy
implicit none
type(xml_writer)   :: w
integer(I4P)       :: iostat
character(len=256) :: iomsg

call w%open()
call w%start('data')
call w%attribute('n', 1_I4P)
call w%attribute('n', 2_I4P)          ! a duplicate: the first error, kept; the next calls do nothing
call w%start('more')
call w%close(iostat=iostat, iomsg=iomsg)
print '(A,L1,A)', 'malformed: ', iostat==XML_MALFORMED, ', '//trim(iomsg)
endprogram writer_errors
$ writer_errors
malformed: T, duplicate attribute "n" in <data>

The first error is kept and the following calls do nothing; close(iostat=) returns it. Without iostat, close stops the program on an error nobody saw.

Write binary data inside a document ​

f90
program binary_appended
use foxy
implicit none
type(xml_writer) :: w
integer          :: unit

open(newunit=unit, file='appended.xml', access='stream', form='unformatted', status='replace')
call w%open(unit=unit)
call w%start('VTKFile')
call w%start('AppendedData')
call w%attribute('encoding', 'raw')
call w%flush()                         ! the start tag is complete: write to the unit directly
write(unit) '_', 3_I4P, 1.0_R4P
call w%end('AppendedData')
call w%close()
close(unit)
print '(A)', 'appended.xml written'
endprogram binary_appended
$ binary_appended
appended.xml written

flush completes the pending start tag so that you can write to the unit yourself; the end tag follows on its own line.

Large files ​

Process a huge file in one pass ​

f90
module pressure_handler
!< A handler summing the values of every <pressure> of a results file, in one pass.
use foxy
implicit none
private
public :: pressure_sum

type, extends(xml_handler) :: pressure_sum
   character(len=:), allocatable :: text          !< Text of the current <pressure>, collected in pieces.
   logical                       :: is_inside = .false.
   real(R8P)                     :: total = 0._R8P
   integer                       :: values_number = 0
   contains
      procedure :: start_element => on_start
      procedure :: characters    => on_text
      procedure :: end_element   => on_end
endtype pressure_sum

contains
   subroutine on_start(self, name, attributes)
   class(pressure_sum), intent(inout) :: self
   character(*),        intent(in)    :: name
   type(xml_attribute), intent(in)    :: attributes(:)
   self%is_inside = name=='pressure'
   if (self%is_inside) self%text = ''
   endsubroutine on_start

   subroutine on_text(self, text)                  ! the text may come in pieces: collect it
   class(pressure_sum), intent(inout) :: self
   character(*),        intent(in)    :: text
   if (self%is_inside) self%text = self%text//text
   endsubroutine on_text

   subroutine on_end(self, name)                   ! the text is complete: convert it
   class(pressure_sum), intent(inout) :: self
   character(*),        intent(in)    :: name
   real(R8P), allocatable             :: values(:)
   integer(I4P)                       :: iostat
   character(len=:), allocatable      :: iomsg
   if (name/='pressure') return
   call to_values(self%text, values, iostat, iomsg)
   if (iostat==0) then
      self%total = self%total + sum(values)
      self%values_number = self%values_number + size(values)
   endif
   self%is_inside = .false.
   endsubroutine on_end
endmodule pressure_handler

program stream_handler
use foxy
use pressure_handler, only : pressure_sum
implicit none
type(pressure_sum) :: summer
integer(I4P)       :: iostat

call summer%parse(filename='results.xml', iostat=iostat)   ! written by the tutorial, chapter 5
print '(A,I0,A,F8.3,A,I0)', 'values: ', summer%values_number, ', sum: ', summer%total, ', iostat: ', iostat
endprogram stream_handler
$ stream_handler
values: 9, sum:    2.811, iostat: 0

Only the callbacks you override are called; the file is read in chunks, never whole. See Streaming parser.

Stop a streaming parse early, and see its problems ​

f90
module wall_finder
!< A handler looking for the first wall boundary: it stops there, and prints the problems it meets.
use foxy
implicit none
private
public :: first_wall

type, extends(xml_handler) :: first_wall
   character(len=:), allocatable :: side
   contains
      procedure :: start_element => on_start
      procedure :: problem       => on_problem
endtype first_wall

contains
   subroutine on_start(self, name, attributes)
   class(first_wall),   intent(inout) :: self
   character(*),        intent(in)    :: name
   type(xml_attribute), intent(in)    :: attributes(:)
   integer                            :: a
   logical                            :: is_wall
   if (name/='boundary') return
   is_wall = .false.
   do a = 1, size(attributes)
      if (attributes(a)%name=='type') is_wall = attributes(a)%value=='wall'
      if (attributes(a)%name=='side') self%side = attributes(a)%value
   enddo
   if (is_wall) call self%stop_parsing()                  ! found: no more callbacks, the rest is not read
   endsubroutine on_start

   subroutine on_problem(self, message)
   class(first_wall), intent(inout) :: self
   character(*),      intent(in)    :: message
   print '(A)', 'problem: '//message                      ! parsing goes on after a problem
   endsubroutine on_problem
endmodule wall_finder

program stream_stop
use foxy
use wall_finder, only : first_wall
implicit none
type(first_wall) :: finder
integer(I4P)     :: iostat

call finder%parse(filename='case.xml', iostat=iostat)
print '(A,I0)', 'first wall: '//finder%side//', iostat ', iostat
call finder%parse(filename='broken.xml', iostat=iostat)
print '(A,L1)', 'broken.xml malformed: ', iostat==XML_MALFORMED
endprogram stream_stop
$ stream_stop
first wall: north, iostat 0
problem: line 5: end tag </time> closes open tag <steps>
broken.xml malformed: T

stop_parsing ends the parse from a callback: no more callbacks, the rest of the file is not read. Override problem to see each problem as it is met; parse also returns the first one in iomsg.

Index a file and stop before its binary data ​

f90
program index_stop
use foxy
implicit none
type(xml_index) :: idx
integer(I8P)    :: start

call idx%scan(filename='appended.xml', stop_at='AppendedData')   ! the binary data are not parsed
start = idx%stop_position()                                      ! the byte after <AppendedData ...>
print '(A,I0,A,I0)', 'tags: ', idx%tags_number(), ', binary data from byte ', start + 1 ! after the new line of flush
print '(A)', 'encoding: '//idx%attribute(idx%find(name='AppendedData'), 'encoding')
endprogram index_stop
$ index_stop
tags: 2, binary data from byte 43
encoding: raw

stop_position() is the first byte after the > of the start tag; there the writer's flush put a new line, so the data start one byte later. What follows is not parsed, and the scan reads nothing beyond its current chunk (64 KiB by default): read the binary data yourself from there.

Read only the contents you need ​

f90
program index_content
use foxy
implicit none
type(xml_index)        :: idx
real(R8P), allocatable :: domain(:)
integer(I8P)           :: first, last

call idx%scan(filename='case.xml')
print '(A)', 'dt: '//idx%content(idx%find(path='case/time/dt'))
call idx%get_content(id=idx%find(name='domain'), values=domain)
call idx%text_range(id=idx%find(name='domain'), first=first, last=last)
print '(A,I0,A,I0,A,I0)', 'domain: ', size(domain), ' values at bytes ', first, '-', last
endprogram index_content
$ index_content
dt: 1.0e-3
domain: 6 values at bytes 206-228

content, get_content and get_attribute read from the file on demand; text_range and content_range give byte ranges.