How to stop Fortran list directed read from separating string on '/'

Viewed 152

Is it possible to use list-directed reads to read a file path from a line, when the input may have a / in it? The application of this is to easily parse commands from an input file for my program.

An example problem which shows the behavior I don't want is:

program test

    implicit none

    character(len=100) :: line, label, fname

    line="command file/path"
    read(line, *) label, fname
    print *, fname

end program test

This program will print file instead of printing file/path. The reason I need list directed output is because I would like to have users be able to provide optional commands after. Such a command input would be:

command file/path arg1 arg2 

where the number of optional arguments is variable. If it's a matter of it just not being that easy to do then I'll implement some kind of tokenizing subroutine.

This is free form using the latest version of gfortran if that makes a difference; I'm not limited to any specific standard.

2 Answers

In list-directed input for a character item, we can avoid *, /, ,/;, or being treated as value separators by delimiting them. For example, a / may appear in a file path:

program test

    implicit none

    character(len=100) :: line, label, fname

    line="command 'file/path'"
    read(line, *) label, fname
    print *, fname

end program test

Recall how comma-separated value files require quotes around values with commas.

However, in general you will want to avoid using list-directed input and use more advanced input parsing methods.

As far as I am aware, there is no solution exclusively via list-directed IO. However, you could solve the problem by reading the input as a formatted string and tokenizing it, as you said, like the following implementation,

module String_mod

    use iso_fortran_env, only: IK => int32
    implicit none
    public

    type :: CharVec_type
        character(:)    , allocatable   :: record
    end type CharVec_type

    type :: String_type
        character(:)      , allocatable   :: value          !< The string value.
        type(CharVec_type), allocatable   :: Parts(:)       !< The string parts.
        integer(IK)                       :: nPart = 0_IK   !< The number of parts in the string.
    contains
        procedure, nopass :: split
    end type String_type

contains

    function split(string,delim,npart) result(Parts)

        implicit none
        character(len=*)    , intent(in)            :: string, delim

        integer(IK)         , intent(out), optional :: npart

        type(CharVec_type)  , allocatable           :: Parts(:)
        integer(IK)         , allocatable           :: PartEnd(:)
        integer(IK)         , allocatable           :: PartBegin(:)
        integer(IK)                                 :: dlmlenMinusOne
        integer(IK)                                 :: strlen, dlmlen, npartMax, ipart, ibeg, iend, i
        logical                                     :: npartIsPresent

        dlmlen = len(delim)
        strlen = len(string)
        npartIsPresent = present(npart)

        ! if dlm is empty, return the whole string split character by character

        if (dlmlen==0_IK) then
            allocate(Parts(strlen))
            do ipart = 1, strlen
                Parts(ipart)%record = string(ipart:ipart)
            end do
            if (npartIsPresent) npart = strlen
            return
        end if

        npartMax = 1_IK + strlen / dlmlen ! There can be at most strlen + 1 splits
        allocate(PartBegin(npartMax), PartEnd(npartMax)) ! This will contain the beginning and the ends of the splits.
        dlmlenMinusOne = dlmlen - 1_IK

        ibeg = 0_IK
        ipart = 1_IK
        PartBegin(ipart) = 1_IK
        loopParseString: do

            ibeg = ibeg + 1_IK
            iend = ibeg + dlmlenMinusOne

            if (strlen<iend) then ! the remaining part of the string is shorter than the delim
                PartEnd(ipart) = strlen
                exit loopParseString
            elseif ( string(ibeg:iend) == delim ) then
                PartEnd(ipart) = ibeg - 1_IK
                ipart = ipart + 1_IK
                PartBegin(ipart) = iend + 1_IK
                ibeg = iend
            end if

        end do loopParseString

        allocate(Parts(ipart))
        do i = 1, ipart
            Parts(i)%record = string(PartBegin(i):PartEnd(i))
        end do
        if (present(npart)) npart = ipart

        deallocate(PartBegin, PartEnd)

    end function split
    
end module String_mod

program test

use, intrinsic :: iso_fortran_env, only: output_unit
use String_mod, only: String_type
implicit none
type(String_type) :: string

string%value = "command file/path"
string%Parts = string%split(string = string%value, delim = " ")
write(output_unit,"(*(g0,:,' '))") "fname =", string%Parts(2)%record

end program test

You can test it here online: https://www.tutorialspoint.com/compile_fortran_online.php If you take such an approach, you will have to be explicit with the user about the kinds of delimiters they have to use in their input. The default choice of black is good as long as the file path does not contain blanks.

Related