Fortran File Operations

Fortran, like most programming languages, uses the refrigerator model for file access. Like a refrigerator, a file must be opened before we can put anything into it (write) or take anything out (read). After we're done reading or writing, it must be closed.

A file is essentially a sequence of bytes stored on a disk or other non-volatile storage device.

Access to files is, for the most part, sequential, meaning we start reading a file at the first byte, and go forward from there. After we read the first byte, the next read operation automatically gets the second, and so on. It is possible to control the order in which we read or write data to a file, but doing non-sequential access is a bit more cumbersome than it is with an array. Unlike array access, it takes separate statements to go to a different location within the file and then read or write data.

Open and Close

Before we can access a file from Fortran, we must open it. The open statement creates a new unit, which can then be used in read and write statements to input from or output to the file.

As you saw in the section called “Fortran Terminal Input/Output (I/O)”, all input and output in Fortran uses unit numbers.

The unit number is Fortran's way of representing the more general concept of a file handle. The term "file handle" is a metaphor for a mechanism used to grasp or control something. Just like a suitcase or door has a handle that allows you to manipulate it, files in Fortran and other languages have conceptual "handles" for manipulating them.

You might be wondering, isn't this what the filename is for? To some extent, yes, but when we're reading or writing a file, we need more than just the name. We need to keep track of where we are in the file, for example.

The open statement creates a structure containing information about the file, such as its name, whether we're reading or writing to is, where we are in the file at a given moment, etc. As a Fortran programmer, you do not need to keep track of this information yourself. The compiler and operating system take care of all of this for you. All you need in order to work with an open file is the unit number. This unit number is your handle to the file.

It is your job as a Fortran programmer to choose a unique unit number for each file you open. Low-numbered unit numbers such as 0, 1, and 2 are reserved for special units like INPUT_UNIT, OUTPUT_UNIT, and ERROR_UNIT. Most Fortran programmers use 10 as the lowest unit number, and go up from there if they need more than one file open at a time.

            module constants
                integer, parameter :: &
                    MAX_PATH_LEN = 1024, &
                    CHROMOSOME_UNIT = 10
            end module
            
            program files
                implicit none
                integer open_status, close status
                character(MAX_PATH_LEN) :: filename
                
                filename = 'input.txt'
                
                open (unit=CHROMOSOME_UNIT, file=filename, status='old', &
                    iostat=open_status, action='read', position='rewind')
                if ( open_status /= 0 ) then
                    print *, 'Could not open ',filename,' for reading.', &
                        'unit = ', unit
                    stop
                endif
                
                ...
            
                close(CHROMOSOME_UNIT, iostat=close_status)
                if ( close_status /= 0 ) then
                    print *, 'Error: Attempt to close a file that is not open.', &
                        'unit = ', CHROMOSOME_UNIT
                    stop
                endif
            end program
            

The open statement uses a number of tags to specify which file to open and how it will be used.

Required tags:

  • unit: integer unit number to be used by read, write, and close.
  • filename: String constant, variable, or expression representing the absolute or relative pathname of the file.
  • status: String constant, variable, or expression that reduces to:
    • 'old': For existing files, usually used when reading.
    • 'new': Used when writing to a file that does not yet exist.
    • 'replace': Used to overwrite a file that already exists.
  • iostat: Integer variable to receive the status of the open operation. If the file is opened successfully, the variable is set to 0. Otherwise, it will contain a non-zero error code that indicates why the file could not be opened. (Does not exist, no permission, etc.)

Optional tags:

  • action: String
    • 'read': Open for reading only
    • 'write': Open for writing only
    • 'readwrite': Allow both reading and writing
  • position: String
    • 'rewind': Start at beginning of file
    • 'append': Writes add to file rather than overwrite

The close statement makes sure any writes to a file opened for writing are complete, and then disables the unit. About the only way a close can fail is if the unit does not represent an open file. This generally means there's a bug in the program, since a close statement should only be attempted if the open succeeded.

Read

The read statement works for files exactly as it does for the standard input. Recall that you can actually make the standard input refer to a file instead of the keyboard by using redirection in the Unix shell:

            shell> asg02 < input.txt
            

The above example reads from standard input using a statement such as read (*,*) variable. Recall that the first '*' represents the default unit (standard input) and the second represents the default format.

When reading from a file that was opened by the program, we simply replace the first '*' with an explicit unit number.

We also introduce here the use of the iostat tag. The iostat variable will receive 0 is the read is successful, and a non-zero error code if it failed (at end of file, file is not open, etc.) Technically, the iostat tag could and should be used when reading from the standard input as well, but it was omitted in Chapter 18, Basic Terminal Input/Output (I/O) for simplicity.

            integer :: read_status
            character(MAX_CHROMOSOME_LEN) :: chromosome1
            
            read (CHROMOSOME_UNIT, *, iostat=read_status) chromosome1
            if ( read_status /= 0 ) then
                print *, 'Error reading file, unit = ', CHROMOSOME_UNIT
                stop
            endif
            
Write

The same ideas apply to write as to read.

            integer :: write_status
            character(MAX_CHROMOSOME_LEN) :: chromosome1
            
            write (CHROMOSOME_UNIT, *, iostat=read_status) chromosome1
            if ( write_status /= 0 ) then
                print *, 'Error writing file, unit = ', CHROMOSOME_UNIT
                stop
            endif