📁 Fortran Programming: Files, Arrays, and Memory Management

Andrew J. Pounds, Ph.D.

🎯 Learning Objectives

After completing this exercise, you will be able to:

1️⃣ Command-Line Arguments

Command-line arguments allow users to pass information to a program when it is executed. In Fortran, command-line arguments are accessed through intrinsic subroutines provided by the Fortran 2003 standard.

Basic Command-Line Argument Access

Fortran provides several intrinsic procedures for accessing command-line arguments:

Procedure Description
command_argument_count() Returns the number of command-line arguments (excluding program name)
get_command_argument(i, arg) Retrieves the i-th argument into the character variable arg
get_command(cmd) Retrieves the entire command line
program cmdargs
    implicit none
    integer :: num_args, i
    character(len=256) :: arg
    
    ! Get the number of command-line arguments
    num_args = command_argument_count()
    
    print *, "Number of arguments: ", num_args
    
    ! Loop through and print each argument
    do i = 0, num_args
        call get_command_argument(i, arg)
        print '(A, I0, A, A)', "args(", i, ") = ", trim(arg)
    end do
end program cmdargs

Compile and run with arguments:

gfortran cmdargs.f90 -o cmdargs
./cmdargs hello world 123

Output:

Number of arguments: 3 args(0) = ./cmdargs args(1) = hello args(2) = world args(3) = 123
Note: Argument index 0 is the program name itself. User-provided arguments start at index 1. The command_argument_count() function returns only the count of user arguments (excluding the program name).

Processing Command-Line Arguments

Here's an example that expects a filename as a command-line argument:

program process_args
    implicit none
    integer :: num_args, count, ios
    character(len=256) :: filename, count_str
    
    num_args = command_argument_count()
    
    ! Check if correct number of arguments provided
    if (num_args < 1) then
        print *, "Usage: ./process_args <filename> [count]"
        stop
    end if
    
    ! Get the filename (first argument)
    call get_command_argument(1, filename)
    print *, "Processing file: ", trim(filename)
    
    ! If a second argument is provided, treat it as a number
    if (num_args >= 2) then
        call get_command_argument(2, count_str)
        read(count_str, *, iostat=ios) count
        if (ios == 0) then
            print *, "Count parameter: ", count
        else
            print *, "Warning: Invalid count parameter"
        end if
    end if
end program process_args

2️⃣ Reading from Files

Fortran provides powerful file I/O capabilities through the open, read, write, and close statements.

Opening and Reading a File

The basic approach uses unit numbers to identify open files:

program read_file
    implicit none
    integer :: unit_num, ios, num_args
    character(len=256) :: filename, line
    
    num_args = command_argument_count()
    
    if (num_args < 1) then
        print *, "Usage: ./read_file <filename>"
        stop
    end if
    
    call get_command_argument(1, filename)
    
    ! Open file for reading
    unit_num = 10
    open(unit=unit_num, file=trim(filename), status='old', &
         action='read', iostat=ios)
    
    if (ios /= 0) then
        print *, "Error: Cannot open file '", trim(filename), "'"
        stop
    end if
    
    ! Read line by line until end of file
    do
        read(unit_num, '(A)', iostat=ios) line
        if (ios /= 0) exit  ! Exit on EOF or error
        print *, trim(line)
    end do
    
    ! Close the file
    close(unit_num)
end program read_file

File Open Specifiers

Specifier Description Common Values
unit= Unit number for file reference Any positive integer (avoid 5, 6)
file= Name of the file Character string
status= File existence status 'old', 'new', 'replace', 'unknown'
action= Read/write permission 'read', 'write', 'readwrite'
iostat= I/O status variable Returns 0 on success, nonzero on error
Note: Unit numbers 5 and 6 are traditionally reserved for standard input and standard output, respectively. Use other unit numbers (like 10, 20, etc.) for your files.

Using newunit for Automatic Unit Assignment

Fortran 2008 introduced newunit to automatically assign an available unit number:

program read_newunit
    implicit none
    integer :: unit_num, ios, num_args
    character(len=256) :: filename, line
    
    num_args = command_argument_count()
    
    if (num_args < 1) then
        print *, "Usage: ./read_newunit <filename>"
        stop
    end if
    
    call get_command_argument(1, filename)
    
    ! Open with automatic unit number assignment
    open(newunit=unit_num, file=trim(filename), status='old', &
         action='read', iostat=ios)
    
    if (ios /= 0) then
        print *, "Error: Cannot open file"
        stop
    end if
    
    print *, "Assigned unit number: ", unit_num
    
    do
        read(unit_num, '(A)', iostat=ios) line
        if (ios /= 0) exit
        print *, trim(line)
    end do
    
    close(unit_num)
end program read_newunit
Using newunit is the recommended modern approach as it avoids potential conflicts with hardcoded unit numbers.

Checking if a File Exists

Use the inquire statement to check file properties before opening:

program check_file
    implicit none
    integer :: num_args
    character(len=256) :: filename
    logical :: file_exists
    
    num_args = command_argument_count()
    
    if (num_args < 1) then
        print *, "Usage: ./check_file <filename>"
        stop
    end if
    
    call get_command_argument(1, filename)
    
    ! Check if file exists
    inquire(file=trim(filename), exist=file_exists)
    
    if (.not. file_exists) then
        print *, "Error: File '", trim(filename), "' not found."
        stop
    end if
    
    print *, "File exists: ", trim(filename)
end program check_file

3️⃣ Tokenizing Strings

Tokenization is the process of breaking a string into smaller pieces (tokens) based on delimiters. Unlike D or Python, Fortran doesn't have a built-in split() function, but we can create one or use internal reads.

Tokenizing with Internal Reads

For simple whitespace-delimited data, internal reads work well:

program tokenize_simple
    implicit none
    character(len=100) :: line
    character(len=20) :: word1, word2, word3, word4, word5
    
    line = "The quick brown fox jumps"
    
    ! Read tokens from the string
    read(line, *) word1, word2, word3, word4, word5
    
    print *, "Token 1: ", trim(word1)
    print *, "Token 2: ", trim(word2)
    print *, "Token 3: ", trim(word3)
    print *, "Token 4: ", trim(word4)
    print *, "Token 5: ", trim(word5)
end program tokenize_simple

Parsing CSV Data

For comma-separated data, we need to manually find and extract tokens using the index() function:

program parse_csv
    implicit none
    character(len=256) :: csv_line, remaining
    character(len=50) :: name, major
    integer :: age, comma_pos
    real :: gpa
    
    csv_line = "Alice,25,Computer Science,3.85"
    
    ! Extract name (first field)
    comma_pos = index(csv_line, ",")
    name = csv_line(1:comma_pos-1)
    remaining = csv_line(comma_pos+1:)
    
    ! Extract age (second field)
    comma_pos = index(remaining, ",")
    read(remaining(1:comma_pos-1), *) age
    remaining = remaining(comma_pos+1:)
    
    ! Extract major (third field)
    comma_pos = index(remaining, ",")
    major = remaining(1:comma_pos-1)
    remaining = remaining(comma_pos+1:)
    
    ! Extract GPA (fourth field - last field)
    read(remaining, *) gpa
    
    print *, "Name: ", trim(name)
    print *, "Age: ", age
    print *, "Major: ", trim(major)
    print '(A, F5.2)', " GPA: ", gpa
end program parse_csv

A Reusable Tokenization Subroutine

Here's a subroutine that splits a string on a delimiter:

program tokenize_demo
    implicit none
    character(len=256) :: line
    character(len=50) :: tokens(10)
    integer :: num_tokens, i
    
    line = "apple,banana,cherry,date,elderberry"
    
    call split_string(line, ",", tokens, num_tokens)
    
    print *, "Number of tokens: ", num_tokens
    do i = 1, num_tokens
        print '(A, I0, A, A)', "  Token ", i, ": ", trim(tokens(i))
    end do

contains

    subroutine split_string(str, delimiter, tokens, num_tokens)
        character(len=*), intent(in) :: str, delimiter
        character(len=*), intent(out) :: tokens(:)
        integer, intent(out) :: num_tokens
        
        character(len=256) :: remaining
        integer :: pos
        
        num_tokens = 0
        remaining = trim(str)
        
        do while (len_trim(remaining) > 0)
            pos = index(remaining, delimiter)
            
            if (pos > 0) then
                num_tokens = num_tokens + 1
                tokens(num_tokens) = remaining(1:pos-1)
                remaining = remaining(pos+1:)
            else
                ! Last token (no more delimiters)
                num_tokens = num_tokens + 1
                tokens(num_tokens) = trim(remaining)
                exit
            end if
        end do
    end subroutine split_string

end program tokenize_demo
The index(string, substring) function returns the starting position of substring within string, or 0 if not found. This is essential for manual tokenization.

Tokenizing File Contents

Combining file reading with tokenization:

program tokenize_file
    implicit none
    integer :: unit_num, ios, num_args, total_tokens
    character(len=256) :: filename, line
    character(len=50) :: tokens(100)
    integer :: num_tokens, i
    logical :: file_exists
    
    num_args = command_argument_count()
    
    if (num_args < 1) then
        print *, "Usage: ./tokenize_file <filename>"
        stop
    end if
    
    call get_command_argument(1, filename)
    
    inquire(file=trim(filename), exist=file_exists)
    if (.not. file_exists) then
        print *, "Error: File not found"
        stop
    end if
    
    open(newunit=unit_num, file=trim(filename), status='old', action='read')
    
    total_tokens = 0
    
    do
        read(unit_num, '(A)', iostat=ios) line
        if (ios /= 0) exit
        
        ! Count words (space-separated) in this line
        call split_on_spaces(line, tokens, num_tokens)
        total_tokens = total_tokens + num_tokens
    end do
    
    close(unit_num)
    
    print *, "Total tokens in file: ", total_tokens

contains

    subroutine split_on_spaces(str, tokens, num_tokens)
        character(len=*), intent(in) :: str
        character(len=*), intent(out) :: tokens(:)
        integer, intent(out) :: num_tokens
        
        integer :: i, start, length
        logical :: in_word
        
        num_tokens = 0
        in_word = .false.
        length = len_trim(str)
        
        do i = 1, length
            if (str(i:i) /= ' ') then
                if (.not. in_word) then
                    in_word = .true.
                    start = i
                end if
            else
                if (in_word) then
                    num_tokens = num_tokens + 1
                    tokens(num_tokens) = str(start:i-1)
                    in_word = .false.
                end if
            end if
        end do
        
        ! Don't forget the last word if line doesn't end with space
        if (in_word) then
            num_tokens = num_tokens + 1
            tokens(num_tokens) = str(start:length)
        end if
    end subroutine split_on_spaces

end program tokenize_file

4️⃣ Arrays in Fortran

Fortran is renowned for its powerful array handling capabilities. Arrays can be static (fixed size at compile time) or allocatable (dynamic size at runtime).

Static Arrays

Static arrays have a fixed size determined at compile time:

program static_arrays
    implicit none
    
    ! Declare static arrays
    integer :: numbers(5)                   ! Array of 5 integers (1 to 5)
    real :: matrix(3, 3)                   ! 3x3 matrix
    character(len=20) :: names(4)         ! Array of 4 strings
    
    ! Initialize with values
    integer :: primes(5) = [2, 3, 5, 7, 11]
    
    ! Explicit bounds (useful for 0-based or custom indexing)
    integer :: zero_based(0:4)             ! Indices 0, 1, 2, 3, 4
    
    integer :: i
    
    ! Assign values
    numbers = [10, 20, 30, 40, 50]
    names = ["Alice", "Bob", "Charlie", "Diana"]
    
    print *, "Numbers array:"
    do i = 1, 5
        print '(A, I0, A, I0)', "  numbers(", i, ") = ", numbers(i)
    end do
    
    print *, ""
    print *, "Names array:"
    do i = 1, 4
        print '(A, I0, A, A)', "  names(", i, ") = ", trim(names(i))
    end do
    
    print *, ""
    print *, "Primes: ", primes
end program static_arrays
Important: By default, Fortran arrays are 1-indexed (first element is at index 1), unlike C and D which are 0-indexed. You can change this behavior by specifying explicit bounds.

Allocatable (Dynamic) Arrays

Allocatable arrays can change size at runtime:

program allocatable_arrays
    implicit none
    
    ! Declare allocatable arrays
    integer, allocatable :: numbers(:)
    real, allocatable :: matrix(:,:)
    character(len=50), allocatable :: words(:)
    
    integer :: n, i
    
    print *, "Enter the size of the array: "
    read *, n
    
    ! Allocate memory
    allocate(numbers(n))
    allocate(matrix(n, n))
    allocate(words(5))
    
    ! Initialize
    do i = 1, n
        numbers(i) = i * 10
    end do
    
    print *, "Array size: ", size(numbers)
    print *, "Array contents: ", numbers
    
    ! Deallocate when done (good practice, but automatic at program end)
    deallocate(numbers)
    deallocate(matrix)
    deallocate(words)
end program allocatable_arrays

Array Operations and Intrinsic Functions

Fortran provides many built-in operations for arrays:

program array_operations
    implicit none
    integer :: arr(5) = [3, 1, 4, 1, 5]
    real :: reals(5) = [1.5, 2.5, 3.5, 4.5, 5.5]
    
    ! Array intrinsic functions
    print *, "Array: ", arr
    print *, "Size: ", size(arr)
    print *, "Sum: ", sum(arr)
    print *, "Product: ", product(arr)
    print *, "Minimum: ", minval(arr)
    print *, "Maximum: ", maxval(arr)
    print *, "Location of min: ", minloc(arr)
    print *, "Location of max: ", maxloc(arr)
    
    print *, ""
    print *, "Real array: ", reals
    print '(A, F8.2)', " Average: ", sum(reals) / size(reals)
    
    ! Whole-array operations
    print *, ""
    print *, "Doubled: ", arr * 2
    print *, "Plus 10: ", arr + 10
end program array_operations

Common Array Intrinsic Functions

Function Description Example
size(array) Returns total number of elements size(arr) → 5
sum(array) Returns sum of all elements sum(arr) → 14
product(array) Returns product of all elements product(arr) → 60
minval(array) Returns minimum value minval(arr) → 1
maxval(array) Returns maximum value maxval(arr) → 5
minloc(array) Returns location of minimum minloc(arr) → [2]
maxloc(array) Returns location of maximum maxloc(arr) → [5]
lbound(array) Returns lower bound lbound(arr) → 1
ubound(array) Returns upper bound ubound(arr) → 5

Array Slicing

Fortran supports powerful array slicing syntax:

program array_slicing
    implicit none
    integer :: arr(10) = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    
    print *, "Full array: ", arr
    print *, "Elements 2 to 5: ", arr(2:5)
    print *, "First 3: ", arr(1:3)
    print *, "Last 3: ", arr(8:10)
    print *, "Every other: ", arr(1:10:2)   ! Start:End:Step
    print *, "Reversed: ", arr(10:1:-1)
end program array_slicing

Storing Tokens in an Array

A complete example that reads a file, tokenizes it, and stores tokens in an array:

program file_tokens
    implicit none
    integer :: unit_num, ios, num_args
    character(len=256) :: filename, line
    character(len=50), allocatable :: all_tokens(:)
    character(len=50) :: line_tokens(100)
    integer :: num_tokens, total_tokens, i
    logical :: file_exists
    
    num_args = command_argument_count()
    
    if (num_args < 1) then
        print *, "Usage: ./file_tokens <filename>"
        stop
    end if
    
    call get_command_argument(1, filename)
    
    inquire(file=trim(filename), exist=file_exists)
    if (.not. file_exists) then
        print *, "Error: File not found."
        stop
    end if
    
    ! First pass: count tokens
    open(newunit=unit_num, file=trim(filename), status='old', action='read')
    total_tokens = 0
    
    do
        read(unit_num, '(A)', iostat=ios) line
        if (ios /= 0) exit
        call count_words(line, num_tokens)
        total_tokens = total_tokens + num_tokens
    end do
    
    close(unit_num)
    
    ! Allocate array for all tokens
    allocate(all_tokens(total_tokens))
    
    ! Second pass: store tokens
    open(newunit=unit_num, file=trim(filename), status='old', action='read')
    total_tokens = 0
    
    do
        read(unit_num, '(A)', iostat=ios) line
        if (ios /= 0) exit
        call split_on_spaces(line, line_tokens, num_tokens)
        
        do i = 1, num_tokens
            total_tokens = total_tokens + 1
            all_tokens(total_tokens) = line_tokens(i)
        end do
    end do
    
    close(unit_num)
    
    ! Display results
    print *, "=== File Token Analysis ==="
    print *, "Total tokens: ", size(all_tokens)
    
    if (size(all_tokens) > 0) then
        print *, "First token: ", trim(all_tokens(1))
        print *, "Last token: ", trim(all_tokens(size(all_tokens)))
    end if
    
    print *, ""
    print *, "Tokens longer than 5 characters:"
    do i = 1, size(all_tokens)
        if (len_trim(all_tokens(i)) > 5) then
            print '(A, I0, A, A, A, I0, A)', "  [", i, "]: ", &
                  trim(all_tokens(i)), " (length: ", len_trim(all_tokens(i)), ")"
        end if
    end do
    
    ! Clean up
    deallocate(all_tokens)

contains

    subroutine count_words(str, count)
        character(len=*), intent(in) :: str
        integer, intent(out) :: count
        integer :: i, length
        logical :: in_word
        
        count = 0
        in_word = .false.
        length = len_trim(str)
        
        do i = 1, length
            if (str(i:i) /= ' ') then
                if (.not. in_word) then
                    in_word = .true.
                    count = count + 1
                end if
            else
                in_word = .false.
            end if
        end do
    end subroutine count_words

    subroutine split_on_spaces(str, tokens, num_tokens)
        character(len=*), intent(in) :: str
        character(len=*), intent(out) :: tokens(:)
        integer, intent(out) :: num_tokens
        
        integer :: i, start, length
        logical :: in_word
        
        num_tokens = 0
        in_word = .false.
        length = len_trim(str)
        
        do i = 1, length
            if (str(i:i) /= ' ') then
                if (.not. in_word) then
                    in_word = .true.
                    start = i
                end if
            else
                if (in_word) then
                    num_tokens = num_tokens + 1
                    tokens(num_tokens) = str(start:i-1)
                    in_word = .false.
                end if
            end if
        end do
        
        if (in_word) then
            num_tokens = num_tokens + 1
            tokens(num_tokens) = str(start:length)
        end if
    end subroutine split_on_spaces

end program file_tokens

5️⃣ Memory Management in Fortran

Unlike languages with garbage collection (like D, Java, or Python), Fortran requires explicit memory management for allocatable arrays. This gives programmers direct control but also places responsibility for proper allocation and deallocation.

What is Manual Memory Management?

Manual memory management means:

The allocate and deallocate Statements

program memory_demo
    implicit none
    integer, allocatable :: numbers(:)
    real, allocatable :: matrix(:,:)
    integer :: n, alloc_stat
    
    print *, "Enter array size: "
    read *, n
    
    ! Allocate memory with error checking
    allocate(numbers(n), stat=alloc_stat)
    if (alloc_stat /= 0) then
        print *, "Error: Failed to allocate memory for numbers"
        stop
    end if
    
    allocate(matrix(n, n), stat=alloc_stat)
    if (alloc_stat /= 0) then
        print *, "Error: Failed to allocate memory for matrix"
        deallocate(numbers)  ! Clean up already allocated memory
        stop
    end if
    
    print *, "Successfully allocated:"
    print *, "  numbers: ", size(numbers), " elements"
    print *, "  matrix: ", size(matrix, 1), " x ", size(matrix, 2)
    
    ! Use the arrays...
    numbers = 0
    matrix = 0.0
    
    ! Deallocate when done
    deallocate(numbers)
    deallocate(matrix)
    
    print *, "Memory deallocated successfully"
end program memory_demo

Checking Allocation Status

Use the allocated() function to check if an array is currently allocated:

program check_allocated
    implicit none
    integer, allocatable :: arr(:)
    
    print *, "Before allocation:"
    print *, "  allocated(arr) = ", allocated(arr)
    
    allocate(arr(10))
    
    print *, "After allocation:"
    print *, "  allocated(arr) = ", allocated(arr)
    
    ! Safe deallocation pattern
    if (allocated(arr)) then
        deallocate(arr)
        print *, "Array deallocated"
    end if
    
    print *, "After deallocation:"
    print *, "  allocated(arr) = ", allocated(arr)
end program check_allocated
Warning: Attempting to deallocate an array that is not allocated, or accessing a deallocated array, causes runtime errors. Always use allocated() to check before deallocating.

Memory Leaks and How to Avoid Them

A memory leak occurs when allocated memory is never deallocated:

program memory_leak_example
    implicit none
    integer :: i
    
    ! BAD: This causes a memory leak!
    do i = 1, 1000
        call leaky_subroutine()  ! Allocates but never deallocates
    end do
    
    print *, "Warning: This program leaks memory!"

contains

    subroutine leaky_subroutine()
        integer, allocatable :: temp(:)
        
        allocate(temp(10000))
        temp = 0
        ! PROBLEM: temp is never deallocated!
        ! Memory is lost when subroutine returns
    end subroutine leaky_subroutine

end program memory_leak_example

The correct version:

program no_memory_leak
    implicit none
    integer :: i
    
    ! GOOD: Memory is properly managed
    do i = 1, 1000
        call proper_subroutine()
    end do
    
    print *, "No memory leak!"

contains

    subroutine proper_subroutine()
        integer, allocatable :: temp(:)
        
        allocate(temp(10000))
        temp = 0
        
        ! Always deallocate before returning
        deallocate(temp)
    end subroutine proper_subroutine

end program no_memory_leak
Fortran 2003+ Automatic Deallocation: In modern Fortran, local allocatable arrays are automatically deallocated when they go out of scope. However, it's still good practice to explicitly deallocate, especially for large arrays or in long-running programs.

Comparing Manual vs. Garbage-Collected Memory Management

Feature Manual (Fortran) Garbage Collection (D, Java)
Memory Allocation Explicit with allocate Automatic with new
Memory Deallocation Explicit with deallocate Automatic by GC
Memory Leaks Possible if deallocate forgotten Prevented automatically
Dangling Pointers Possible after deallocate Prevented automatically
Programmer Control Full control Limited control
Performance No GC overhead Small GC overhead
Predictability Deterministic timing GC pauses possible

Practical Example: Processing Large Data Files

program large_file_processor
    implicit none
    integer :: num_args
    character(len=256) :: filename
    
    num_args = command_argument_count()
    
    if (num_args < 1) then
        print *, "Usage: ./large_file_processor <file1> [file2] [file3] ..."
        stop
    end if
    
    print *, "=== Multi-File Processor ==="
    print *, ""
    
    call process_all_files()
    
    print *, ""
    print *, "Done! All memory has been properly deallocated."

contains

    subroutine process_all_files()
        integer :: i, num_args
        character(len=256) :: filename
        logical :: file_exists
        
        num_args = command_argument_count()
        
        do i = 1, num_args
            call get_command_argument(i, filename)
            
            inquire(file=trim(filename), exist=file_exists)
            
            if (file_exists) then
                call analyze_file(trim(filename))
            else
                print *, "Skipping (not found): ", trim(filename)
            end if
        end do
    end subroutine process_all_files

    subroutine analyze_file(filename)
        character(len=*), intent(in) :: filename
        character(len=256) :: line
        character(len=50), allocatable :: words(:)
        character(len=50) :: temp_words(100)
        integer :: unit_num, ios, word_count, total_words
        integer :: long_word_count, num_tokens, i
        
        print *, "Analyzing: ", filename
        
        ! First pass: count words
        open(newunit=unit_num, file=filename, status='old', action='read')
        total_words = 0
        
        do
            read(unit_num, '(A)', iostat=ios) line
            if (ios /= 0) exit
            call count_words_in_line(line, word_count)
            total_words = total_words + word_count
        end do
        
        close(unit_num)
        
        ! Allocate array for all words
        allocate(words(total_words))
        
        ! Second pass: store words
        open(newunit=unit_num, file=filename, status='old', action='read')
        total_words = 0
        
        do
            read(unit_num, '(A)', iostat=ios) line
            if (ios /= 0) exit
            call extract_words(line, temp_words, num_tokens)
            
            do i = 1, num_tokens
                total_words = total_words + 1
                words(total_words) = temp_words(i)
            end do
        end do
        
        close(unit_num)
        
        ! Analyze
        long_word_count = 0
        do i = 1, size(words)
            if (len_trim(words(i)) > 6) then
                long_word_count = long_word_count + 1
            end if
        end do
        
        print *, "  Words found: ", size(words)
        print *, "  Long words (>6 chars): ", long_word_count
        
        ! IMPORTANT: Deallocate before returning!
        deallocate(words)
        print *, "  Memory deallocated"
        print *, ""
    end subroutine analyze_file

    subroutine count_words_in_line(str, count)
        character(len=*), intent(in) :: str
        integer, intent(out) :: count
        integer :: i
        logical :: in_word
        
        count = 0
        in_word = .false.
        
        do i = 1, len_trim(str)
            if (str(i:i) /= ' ') then
                if (.not. in_word) then
                    in_word = .true.
                    count = count + 1
                end if
            else
                in_word = .false.
            end if
        end do
    end subroutine count_words_in_line

    subroutine extract_words(str, tokens, num_tokens)
        character(len=*), intent(in) :: str
        character(len=*), intent(out) :: tokens(:)
        integer, intent(out) :: num_tokens
        integer :: i, start, length
        logical :: in_word
        
        num_tokens = 0
        in_word = .false.
        length = len_trim(str)
        
        do i = 1, length
            if (str(i:i) /= ' ') then
                if (.not. in_word) then
                    in_word = .true.
                    start = i
                end if
            else
                if (in_word) then
                    num_tokens = num_tokens + 1
                    tokens(num_tokens) = str(start:i-1)
                    in_word = .false.
                end if
            end if
        end do
        
        if (in_word) then
            num_tokens = num_tokens + 1
            tokens(num_tokens) = str(start:length)
        end if
    end subroutine extract_words

end program large_file_processor
Best Practice: Always pair every allocate with a corresponding deallocate. Consider using a consistent pattern: allocate early, use, then deallocate before returning from subroutines.

6️⃣ Complete Example: Word Frequency Analyzer

This comprehensive example combines all the concepts: command-line arguments, file reading, tokenization, arrays, and proper memory management:

program word_frequency
    implicit none
    integer :: num_args
    character(len=256) :: filename
    logical :: file_exists
    
    num_args = command_argument_count()
    
    if (num_args < 1) then
        print *, "Word Frequency Analyzer"
        print *, "Usage: ./word_frequency <filename>"
        stop
    end if
    
    call get_command_argument(1, filename)
    
    inquire(file=trim(filename), exist=file_exists)
    if (.not. file_exists) then
        print *, "Error: File '", trim(filename), "' not found."
        stop
    end if
    
    call analyze_word_frequency(trim(filename))

contains

    subroutine analyze_word_frequency(filename)
        character(len=*), intent(in) :: filename
        character(len=256) :: line
        character(len=50), allocatable :: unique_words(:)
        integer, allocatable :: word_counts(:)
        character(len=50) :: temp_words(100), word
        integer :: unit_num, ios, num_tokens, i, j
        integer :: num_unique, total_tokens
        integer :: max_count, max_idx
        logical :: found
        
        ! Initial allocation for unique words (will reallocate if needed)
        allocate(unique_words(1000))
        allocate(word_counts(1000))
        
        num_unique = 0
        total_tokens = 0
        
        open(newunit=unit_num, file=filename, status='old', action='read')
        
        do
            read(unit_num, '(A)', iostat=ios) line
            if (ios /= 0) exit
            
            ! Convert to lowercase
            call to_lowercase(line)
            
            ! Tokenize the line
            call extract_words(line, temp_words, num_tokens)
            
            do i = 1, num_tokens
                word = temp_words(i)
                call remove_punctuation(word)
                
                if (len_trim(word) == 0) cycle
                
                total_tokens = total_tokens + 1
                
                ! Check if word already exists
                found = .false.
                do j = 1, num_unique
                    if (trim(unique_words(j)) == trim(word)) then
                        word_counts(j) = word_counts(j) + 1
                        found = .true.
                        exit
                    end if
                end do
                
                ! Add new word
                if (.not. found) then
                    num_unique = num_unique + 1
                    unique_words(num_unique) = word
                    word_counts(num_unique) = 1
                end if
            end do
        end do
        
        close(unit_num)
        
        ! Display results
        print *, "=== Word Frequency Analysis ==="
        print *, "File: ", filename
        print *, "Total tokens: ", total_tokens
        print *, "Unique words: ", num_unique
        print *, ""
        
        print *, "Words appearing more than once:"
        print *, "--------------------------------"
        do i = 1, num_unique
            if (word_counts(i) > 1) then
                print '(A, A, A, I0)', "  ", trim(unique_words(i)), " : ", word_counts(i)
            end if
        end do
        
        ! Find most common word
        max_count = 0
        max_idx = 1
        do i = 1, num_unique
            if (word_counts(i) > max_count) then
                max_count = word_counts(i)
                max_idx = i
            end if
        end do
        
        print *, ""
        print '(A, A, A, I0, A)', "Most common word: '", &
              trim(unique_words(max_idx)), "' (", max_count, " occurrences)"
        
        ! Clean up memory
        deallocate(unique_words)
        deallocate(word_counts)
    end subroutine analyze_word_frequency

    subroutine to_lowercase(str)
        character(len=*), intent(inout) :: str
        integer :: i, ic
        
        do i = 1, len_trim(str)
            ic = ichar(str(i:i))
            if (ic >= ichar('A') .and. ic <= ichar('Z')) then
                str(i:i) = char(ic + 32)
            end if
        end do
    end subroutine to_lowercase

    subroutine remove_punctuation(str)
        character(len=*), intent(inout) :: str
        character(len=20) :: punct = ".,!?;:""'()-"
        integer :: i, pos
        
        do i = 1, len_trim(punct)
            do
                pos = index(str, punct(i:i))
                if (pos == 0) exit
                str = str(1:pos-1) // str(pos+1:)
            end do
        end do
    end subroutine remove_punctuation

    subroutine extract_words(str, tokens, num_tokens)
        character(len=*), intent(in) :: str
        character(len=*), intent(out) :: tokens(:)
        integer, intent(out) :: num_tokens
        integer :: i, start, length
        logical :: in_word
        
        num_tokens = 0
        in_word = .false.
        length = len_trim(str)
        
        do i = 1, length
            if (str(i:i) /= ' ') then
                if (.not. in_word) then
                    in_word = .true.
                    start = i
                end if
            else
                if (in_word) then
                    num_tokens = num_tokens + 1
                    tokens(num_tokens) = str(start:i-1)
                    in_word = .false.
                end if
            end if
        end do
        
        if (in_word) then
            num_tokens = num_tokens + 1
            tokens(num_tokens) = str(start:length)
        end if
    end subroutine extract_words

end program word_frequency

7️⃣ Quick Reference

Task Fortran Syntax
Get argument count n = command_argument_count()
Get argument value call get_command_argument(i, arg)
Open file for reading open(newunit=u, file=fname, status='old', action='read')
Read a line read(u, '(A)', iostat=ios) line
Check if file exists inquire(file=fname, exist=exists)
Close file close(u)
Find substring position pos = index(str, substring)
Declare static array integer :: arr(10)
Declare allocatable array integer, allocatable :: arr(:)
Allocate memory allocate(arr(n))
Deallocate memory deallocate(arr)
Check if allocated if (allocated(arr)) ...
Array size size(arr)
Array slice arr(2:5) or arr(1:10:2)

📝 Programming Exercise: CSV Data Processor

Write a Fortran program that processes a CSV (Comma-Separated Values) file containing student grade data. Your program should:

  1. Accept the filename as a command-line argument
    • Display a usage message if no filename is provided
    • Display an error if the file doesn't exist
  2. Read and parse the CSV file where each line has the format:
    StudentName,Score1,Score2,Score3
    Example: Alice,85,92,78
  3. Store the data in arrays:
    • A character allocatable array for student names
    • A real allocatable array for each student's average score
  4. Calculate and display:
    • Each student's name and average score
    • The class average (average of all student averages)
    • The highest and lowest scoring students
  5. Demonstrate proper memory management by deallocating all arrays before the program ends, and adding a comment explaining your memory management strategy

Sample Input File (grades.csv)

Alice,85,92,78
Bob,90,88,95
Charlie,72,68,75
Diana,95,98,92

Sample Output

=== Student Grade Analyzer === File: grades.csv Students loaded: 4 Student Averages: Alice : 85.00 Bob : 91.00 Charlie : 71.67 Diana : 95.00 Class Statistics: Class Average : 85.67 Highest Score : Diana (95.00) Lowest Score : Charlie (71.67) Memory deallocated successfully.

Hints

Submission

Save your program as grade_analyzer.f90 and compile with:

gfortran grade_analyzer.f90 -o grade_analyzer
./grade_analyzer grades.csv