Skip to content

Add getline to read whole line from formatted unit #597

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Features available from the latest git source
- new module `stdlib_version`
[#579](https://github.com/fortran-lang/stdlib/pull/579)
- new procedure `get_stdlib_version`
- update module `stdlib_io`
[597](https://github.com/fortran-lang/stdlib/pull/597)
- new procedure `getline`
- new module `stdlib_io_npy`
[#581](https://github.com/fortran-lang/stdlib/pull/581)
- new procedures `save_npy`, `load_npy`
Expand Down
50 changes: 50 additions & 0 deletions doc/specs/stdlib_io.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,53 @@ program demo_savenpy
call save_npy('example.npy', x)
end program demo_savenpy
```

## `getline`

### Status

Experimental

### Description

Read a whole line from a formatted unit into a string variable

### Syntax

`call [[stdlib_io(module):getline(interface)]] (unit, line[, iostat][, iomsg])`
`call [[stdlib_io(module):getline(interface)]] (line[, iostat][, iomsg])`

### Arguments

`unit`: Formatted input unit.
This argument is `intent(in)`.
If `unit` is not specified standard input is used.

`line`: Deferred length character or `string_type` variable.
This argument is `intent(out)`.

`iostat`: Default integer, contains status of reading from unit, zero in case of success.
It is an optional argument, in case not present the program will halt for non-zero status.
This argument is `intent(out)`.

`iomsg`: Deferred length character value, contains error message in case `iostat` is non-zero.
It is an optional argument, error message will be dropped if not present.
This argument is `intent(out)`.

### Example

```fortran
program demo_getline
use, intrinsic :: iso_fortran_env, only : input_unit, output_unit
use stdlib_io, only: getline
implicit none
character(len=:), allocatable :: line
integer :: stat

call getline(input_unit, line, stat)
do while(stat == 0)
write(output_unit, '(a)') line
call getline(input_unit, line, stat)
end do
end program demo_getline
```
1 change: 1 addition & 0 deletions src/Makefile.manual
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ stdlib_io.o: \
stdlib_error.o \
stdlib_optval.o \
stdlib_kinds.o \
stdlib_string_type.o \
stdlib_ascii.o
stdlib_io_npy.o: \
stdlib_kinds.o
Expand Down
108 changes: 107 additions & 1 deletion src/stdlib_io.fypp
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ module stdlib_io
!! Provides a support for file handling
!! ([Specification](../page/specs/stdlib_io.html))

use, intrinsic :: iso_fortran_env, only : input_unit
use stdlib_kinds, only: sp, dp, xdp, qp, &
int8, int16, int32, int64
use stdlib_error, only: error_stop
use stdlib_optval, only: optval
use stdlib_ascii, only: is_blank
use stdlib_string_type, only : string_type
implicit none
private
! Public API
public :: loadtxt, savetxt, open
public :: loadtxt, savetxt, open, getline

! Private API that is exposed so that we can test it in tests
public :: parse_mode
Expand All @@ -31,6 +33,16 @@ module stdlib_io
FMT_COMPLEX_XDP = '(*(es26.18e3,1x,es26.18e3))', &
FMT_COMPLEX_QP = '(*(es44.35e4,1x,es44.35e4))'

!> Version: experimental
!>
!> Read a whole line from a formatted unit into a string variable
interface getline
module procedure :: getline_char
module procedure :: getline_string
module procedure :: getline_input_char
module procedure :: getline_input_string
end interface getline

interface loadtxt
!! version: experimental
!!
Expand Down Expand Up @@ -331,4 +343,98 @@ contains

end function parse_mode

!> Version: experimental
!>
!> Read a whole line from a formatted unit into a deferred length character variable
subroutine getline_char(unit, line, iostat, iomsg)
!> Formatted IO unit
integer, intent(in) :: unit
!> Line to read
character(len=:), allocatable, intent(out) :: line
!> Status of operation
integer, intent(out), optional :: iostat
!> Error message
character(len=:), allocatable, optional :: iomsg

integer, parameter :: bufsize = 4096
character(len=bufsize) :: buffer, msg
integer :: chunk, stat
logical :: opened

if (unit /= -1) then
inquire(unit=unit, opened=opened)
else
opened = .false.
end if

if (opened) then
open(unit=unit, pad="yes", iostat=stat, iomsg=msg)
else
stat = 1
msg = "Unit is not connected"
end if

line = ""
do while (stat == 0)
read(unit, '(a)', advance='no', iostat=stat, iomsg=msg, size=chunk) buffer
if (stat > 0) exit
line = line // buffer(:chunk)
end do
if (is_iostat_eor(stat)) stat = 0

if (stat /= 0 .and. present(iomsg)) iomsg = trim(msg)
if (present(iostat)) then
iostat = stat
else if (stat /= 0) then
call error_stop(trim(msg))
end if
end subroutine getline_char

!> Version: experimental
!>
!> Read a whole line from a formatted unit into a string variable
subroutine getline_string(unit, line, iostat, iomsg)
!> Formatted IO unit
integer, intent(in) :: unit
!> Line to read
type(string_type), intent(out) :: line
!> Status of operation
integer, intent(out), optional :: iostat
!> Error message
character(len=:), allocatable, optional :: iomsg

character(len=:), allocatable :: buffer

call getline(unit, buffer, iostat, iomsg)
line = string_type(buffer)
end subroutine getline_string

!> Version: experimental
!>
!> Read a whole line from the standard input into a deferred length character variable
subroutine getline_input_char(line, iostat, iomsg)
!> Line to read
character(len=:), allocatable, intent(out) :: line
!> Status of operation
integer, intent(out), optional :: iostat
!> Error message
character(len=:), allocatable, optional :: iomsg

call getline(input_unit, line, iostat, iomsg)
end subroutine getline_input_char

!> Version: experimental
!>
!> Read a whole line from the standard input into a string variable
subroutine getline_input_string(line, iostat, iomsg)
!> Line to read
type(string_type), intent(out) :: line
!> Status of operation
integer, intent(out), optional :: iostat
!> Error message
character(len=:), allocatable, optional :: iomsg

call getline(input_unit, line, iostat, iomsg)
end subroutine getline_input_string

end module stdlib_io
1 change: 1 addition & 0 deletions src/tests/io/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ADDTEST(savetxt_qp)
set_tests_properties(loadtxt_qp PROPERTIES LABELS quadruple_precision)
set_tests_properties(savetxt_qp PROPERTIES LABELS quadruple_precision)

ADDTEST(getline)
ADDTEST(npy)
ADDTEST(open)
ADDTEST(parse_mode)
1 change: 1 addition & 0 deletions src/tests/io/Makefile.manual
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ SRCGEN = $(SRCFYPP:.fypp=.f90)

PROGS_SRC = test_loadtxt.f90 \
test_savetxt.f90 \
test_getline.f90 \
test_npy.f90 \
test_parse_mode.f90 \
test_open.f90 \
Expand Down
169 changes: 169 additions & 0 deletions src/tests/io/test_getline.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
module test_getline
use stdlib_io, only : getline
use stdlib_string_type, only : string_type, len
use testdrive, only : new_unittest, unittest_type, error_type, check
implicit none
private

public :: collect_getline

contains

!> Collect all exported unit tests
subroutine collect_getline(testsuite)
!> Collection of tests
type(unittest_type), allocatable, intent(out) :: testsuite(:)

testsuite = [ &
new_unittest("read-char", test_read_char), &
new_unittest("read-string", test_read_string), &
new_unittest("pad-no", test_pad_no), &
new_unittest("iostat-end", test_iostat_end), &
new_unittest("closed-unit", test_closed_unit, should_fail=.true.), &
new_unittest("no-unit", test_no_unit, should_fail=.true.) &
]
end subroutine collect_getline

subroutine test_read_char(error)
!> Error handling
type(error_type), allocatable, intent(out) :: error

integer :: io, i, stat
character(len=:), allocatable :: line

open(newunit=io, status="scratch")
write(io, "(a)") repeat("abc", 10), repeat("def", 100), repeat("ghi", 1000)
rewind(io)

do i = 1, 3
call getline(io, line, stat)
call check(error, stat)
if (allocated(error)) exit
call check(error, len(line), 3*10**i)
if (allocated(error)) exit
end do
close(io)
end subroutine test_read_char

subroutine test_read_string(error)
!> Error handling
type(error_type), allocatable, intent(out) :: error

integer :: io, i, stat
type(string_type) :: line

open(newunit=io, status="scratch")
write(io, "(a)") repeat("abc", 10), repeat("def", 100), repeat("ghi", 1000)
rewind(io)

do i = 1, 3
call getline(io, line, stat)
call check(error, stat)
if (allocated(error)) exit
call check(error, len(line), 3*10**i)
if (allocated(error)) exit
end do
close(io)
end subroutine test_read_string

subroutine test_pad_no(error)
!> Error handling
type(error_type), allocatable, intent(out) :: error

integer :: io, i, stat
character(len=:), allocatable :: line

open(newunit=io, status="scratch", pad="no")
write(io, "(a)") repeat("abc", 10), repeat("def", 100), repeat("ghi", 1000)
rewind(io)

do i = 1, 3
call getline(io, line, stat)
call check(error, stat)
if (allocated(error)) exit
call check(error, len(line), 3*10**i)
if (allocated(error)) exit
end do
close(io)
end subroutine test_pad_no

subroutine test_iostat_end(error)
use, intrinsic :: iso_fortran_env, only : iostat_end
!> Error handling
type(error_type), allocatable, intent(out) :: error

integer :: io, i, stat
character(len=:), allocatable :: line

open(newunit=io, status="scratch")
write(io, "(a)") repeat("abc", 10), repeat("def", 100), repeat("ghi", 1000)
rewind(io)

do i = 1, 3
call getline(io, line, stat)
call check(error, stat)
if (allocated(error)) exit
call check(error, len(line), 3*10**i)
if (allocated(error)) exit
end do
if (.not.allocated(error)) then
call getline(io, line, stat)
call check(error, stat, iostat_end)
end if
close(io)
end subroutine test_iostat_end

subroutine test_closed_unit(error)
!> Error handling
type(error_type), allocatable, intent(out) :: error

integer :: io, stat
character(len=:), allocatable :: line, msg

open(newunit=io, status="scratch")
close(io)

call getline(io, line, stat, msg)
call check(error, stat, msg)
end subroutine test_closed_unit

subroutine test_no_unit(error)
!> Error handling
type(error_type), allocatable, intent(out) :: error

integer :: io, stat
character(len=:), allocatable :: line, msg

io = -1
call getline(io, line, stat, msg)
call check(error, stat, msg)
end subroutine test_no_unit

end module test_getline


program tester
use, intrinsic :: iso_fortran_env, only : error_unit
use testdrive, only : run_testsuite, new_testsuite, testsuite_type
use test_getline, only : collect_getline
implicit none
integer :: stat, is
type(testsuite_type), allocatable :: testsuites(:)
character(len=*), parameter :: fmt = '("#", *(1x, a))'

stat = 0

testsuites = [ &
new_testsuite("getline", collect_getline) &
]

do is = 1, size(testsuites)
write(error_unit, fmt) "Testing:", testsuites(is)%name
call run_testsuite(testsuites(is)%collect, error_unit, stat)
end do

if (stat > 0) then
write(error_unit, '(i0, 1x, a)') stat, "test(s) failed!"
error stop
end if
end program