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
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,2attribute(1, name): the root tag of a parsed document is the tag 1.
Parse a file, and stop on errors
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 tagsThe file is read in chunks, never loaded whole: parse(..., chunk_size=) sets their size.
Check that a document is well-formed
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
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 TLogicals are true, false, 1 or 0; any integer kind (I1P…I8P), R4P and R8P work.
Read a list of numbers
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.00values= is an allocatable rank-1 array, allocated to the number of blank-separated tokens.
Read an attribute, as text or as a number
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: 2048Use a default when a setting is missing
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: TOn error the value is not touched: set the default first, check iostat if you need to know.
Report a bad or missing value
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 0Tokens are checked before conversion: ,, /, quotes and repeat counts (2*5) are rejected, not interpreted.
Keep the spaces around attribute values
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
program mixed_content
use foxy
implicit none
type(xml_file) :: doc
call doc%parse(string='<p>Use <code>dt</code> < 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> < 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
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
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: Tfind 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
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 wallfind_all returns the IDs in document order, find only the first one.
Read the children of a tag
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: 3List the attributes of a tag
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 = 1Visit every tag
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
. notesIn 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
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
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-5Compile 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
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
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 plateThe prefixes of namespaces= replace the ones of the file for this query only.
Editing
Build a document in code
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
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
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 = yesSet numbers, logicals and lists
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
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
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
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 < 1 & "fast"</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
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-3Writing
Stream a document to a file
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
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
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 < b</msg>open() without a file or a unit writes to memory; indent=0 writes no layout.
Catch a writer misuse
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
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 writtenflush 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
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: 0Only 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
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: Tstop_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
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: rawstop_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
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-228content, get_content and get_attribute read from the file on demand; text_range and content_range give byte ranges.