Operations on external files
Overview
Teaching: 20 min
Exercises: 20 minQuestions
How do I open and close files for input and output?
How do I handle any errors that might occur during I/O?
Objectives
Learn how to open and close files for input and output using a unit number.
Be able to read and write to an open file and handle errors that might arise.
Move around an open file using
read
to move forwards andbackspace()
andrewind()
to move back.
Operations on external files
Writing data to, or reading data from, an external file is an essential part of a useful application.
Writing a file
File handles, or unit numbers are obtained using open()
, and are
used to direct the output of write()
to the relevant file.
integer :: myunit
open(newunit = myunit, file = 'filename.dat', form = 'formatted', &
action = 'write', status = 'new')
write(unit = myunit, *) data1 ! write data first record
write(unit = myunit, *) data2 ! second record ... and so on
close(unit = myunit, status = 'keep')
To read back the same data, we might use:
open(newunit = myunit, file = 'filename.dat', form = 'formatted', &
action = "read", status = "old")
read(unit = myunit, *) data1
read(unit = myunit, *) data2
close (unit = myunit, status = 'keep')
Unit numbers
A valid unit number can be assigned by using the newunit
option to open()
–
this has been available since Fortran 2008. There is no need to choose your own
(and it can be error-prone to do so).
Note that a given file can only be connected to one unit number at any given time.
open()
The arguments seen above include:
file
: literal string or character variable or expression with the name of the file in the file system;form
:formatted
orunformatted
;action
:read
,write
orreadwrite
. An error may occur if inappropriate actions are performed;status
: one ofold
,new
,replace
,scratch
, orunknown
.
A file with status old
is expected to exist, while an attempt to create a
new
file when one already exists with the same name will result in an error.
If replace
is specified, any existing file will be overwritten by a new
file. If scratch
is specified, a temporary file is created which will
be deleted when close()
is executed (or at the end of the program).
The system will automatically choose a name for a scratch file if no
file
argument is present. The default status is unknown
, which
means the status is system dependent.
Formally, only the unit number is mandatory (and must appear first), but there is no reason not to provide as much information as possible.
close()
The unit number is again mandatory. The status
is either keep
or
delete
. If the status is omitted, the default is keep
, except for
scratch files.
inquire()
The inquire
statement offers a way to obtain information on the
current state of either unit numbers or files. There are a large
number of optional arguments. One common usage is to check whether
a file exists:
logical :: exists
inquire( file = 'filename.dat', exist = exists)
Internal files
In some situations, it may be convenient to use a formatted read to
generate a new string in memory (in the same way as sprintf
in C).
Fortran uses a so-called internal file, which is usually a
character string. E.g.,
character (len = 10) :: buffer
integer :: ival
...
read(buffer, fmt = "i10") ival
Here buffer
takes the place of the input unit. This can be used, e.g.,
to create format strings at run time.
Recovery from errors
Operations on external files can be error-prone. While there is no formal exception mechanism in Fortran, some ability to recover is available.
Consider the following schematic example:
subroutine read_my_file_format(myunit, ..., ierr)
integer, intent(in) :: myunit
integer, intent(out) :: ierr ! error code
character (len = 256) :: msg
read (myunit, ..., err = 999, iomsg = msg) ...
! Everything was ok
ierr = 0
return
999 continue
ierr = -1
print *, "Error reading file: ", trim(msg)
return
end subroutine real_my_file_format
We assume the relevant file has been opened successfully, and is
connected to unit number myunit
. If an error occurs at the
point of the read statement, the err
argument directs control
to be transferred to the statement will label 999
. In this
case the system should provide a meaningful message describing the
error.
If the read is successful, the routine completes at the first return
statement with intent out ierr = 0
.
Error handling for open()
and close()
Both open
and close
statements provide optional arguments for
error handling, illustrated schematically here with open()
:
integer :: ierr
character (len = 128) :: msg
open( newunit = myunit, ..., err = 900, iostat = ierr, iomsg = msg)
err
: is a label in the same scope to which control is transferred on error;iostat
: an integer variable which is zero on success but positive if an error has occurred;iomsg
: if an error occurs, a system-dependent message will be assigned tomsg
.
If neither err
nor iostat
arguments are present, then an error may
result in immediate termination of the program.
The variable msg
should be a scalar character variable; the message will be
truncated or padded appropriately.
Error handling for read()
and write()
Error handling facilities for read()
and write()
are similar, and
are illustrated here:
write (myunit, myformat, end = 999, err = 998, iostat = ierr, iomsg = msg) ..
end
: label in same scope to which control is transferred on end-of-file;err
: label in same scope to which control is transferred on any other error; the label may be the same asend
;iostat
: the integer error code is negative if end-of-record or end-of-file, or positive if another error (e.g., bad format conversion) or zero on success;iomsg
: should return a useful message on error.
The two negative iostat
cases can be distinguished via calls to
the intrinsic functions
is_iostat_end(ierr)
is_iostat_eor(ierr)
stop
statement
If one really can’t continue, then execution can be terminated immediately
via the stop
statement. This has an optional message string argument.
stop "Cannot continue"
In general one should try to recover by returning control to the caller,
so stop
is a last resort.
Moving around an open file
It is sometimes useful to be able to reposition oneself in an open file
so that a given record can be read (or written) more than once. This
can be done using backspace()
with the relevant unit number. Formally
backspace([unit = ] unit-number [, iostat = iostat] [, err = label])
This steps back one record.
It is also possible to reposition to the beginning of the file, again
with the relevant connected unit number using rewind()
.
rewind([unit = ] unit-number [, iostat = iostat] [, err = label])
Exercise (20 minutes)
Writing a simple image
Portable bit map (PBM, PGM, PPM) is a very simple image format which can be expressed as an ASCII file. See the description at Wikipedia.
In the template program1.f90, we establish a two-dimensional logical array of a fixed size, and we want to write out a file that can be viewed in “P1” format (file extension
.pbm
) by an image viewer.The data is initialised with a suitable pattern to check that the image is correct (it is the right way up and is not a mirror). A little care is required in the rows and columns (look closely at the example).
Provide a module with the subroutine
write_pbm()
with three arguments as in the template program. Note the template program expects a modulesolution_module
.Additional exercises: repeat for integer data (to “P2” format
.pgm
) and floating point data (to “P3” format.ppm
).You can check any image you produce by either downloading it to your machine with
scp
orrsync
, or you can view it directly on ARCHER2 by logging in with X11 forwarding enabled, runningmodule load imagemagick
and thendisplay test49.pbm
.Solution
A solution to this problem appears in the later episode on interfaces.
You should be able to produce an output PBM that looks like the example shown below.
Key Points
Writing data to, or reading data from, an external file is an essential part of a useful application.
Unit numbers are integers which are used as handles to open files.
Errors from I/O can be handled with labels and by examining the returned message.