From 15362151ad2c07b70374ffe01ef79a851b4035b4 Mon Sep 17 00:00:00 2001 From: Sebastian Ehlert <28669218+awvwgk@users.noreply.github.com> Date: Sun, 12 Dec 2021 21:14:53 +0100 Subject: [PATCH 1/5] Add getline to read whole line from formatted unit --- doc/specs/stdlib_io.md | 48 +++++++++++++++++++ src/Makefile.manual | 1 + src/stdlib_io.fypp | 66 +++++++++++++++++++++++++- src/tests/io/CMakeLists.txt | 1 + src/tests/io/Makefile.manual | 1 + src/tests/io/test_getline.f90 | 88 +++++++++++++++++++++++++++++++++++ 6 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 src/tests/io/test_getline.f90 diff --git a/doc/specs/stdlib_io.md b/doc/specs/stdlib_io.md index c33511c5e..8f53cd27c 100644 --- a/doc/specs/stdlib_io.md +++ b/doc/specs/stdlib_io.md @@ -223,3 +223,51 @@ 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])` + +### Arguments + +`unit`: Formatted input unit. + This argument is `intent(in)`. + +`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 +``` diff --git a/src/Makefile.manual b/src/Makefile.manual index 57c681fd7..d75a6bd20 100644 --- a/src/Makefile.manual +++ b/src/Makefile.manual @@ -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 diff --git a/src/stdlib_io.fypp b/src/stdlib_io.fypp index 3fc42b873..4e7720b1d 100644 --- a/src/stdlib_io.fypp +++ b/src/stdlib_io.fypp @@ -11,10 +11,11 @@ module stdlib_io 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 @@ -31,6 +32,14 @@ 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 + end interface getline + interface loadtxt !! version: experimental !! @@ -331,4 +340,59 @@ 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 = 512 + character(len=bufsize) :: buffer, msg + integer :: chunk, stat + + allocate(character(len=0) :: line) + do + read(unit, '(a)', advance='no', iostat=stat, iomsg=msg, size=chunk) buffer + if (stat > 0) exit + line = line // buffer(:chunk) + if (stat < 0) then + if (is_iostat_eor(stat)) stat = 0 + exit + end if + end do + + 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 + end module stdlib_io diff --git a/src/tests/io/CMakeLists.txt b/src/tests/io/CMakeLists.txt index bfac1b257..98794cd88 100644 --- a/src/tests/io/CMakeLists.txt +++ b/src/tests/io/CMakeLists.txt @@ -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) diff --git a/src/tests/io/Makefile.manual b/src/tests/io/Makefile.manual index b07c0ee47..b6335cf82 100644 --- a/src/tests/io/Makefile.manual +++ b/src/tests/io/Makefile.manual @@ -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 \ diff --git a/src/tests/io/test_getline.f90 b/src/tests/io/test_getline.f90 new file mode 100644 index 000000000..b67daa944 --- /dev/null +++ b/src/tests/io/test_getline.f90 @@ -0,0 +1,88 @@ +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) & + ] + 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) + call check(error, len(line), 3*10**i) + 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) + call check(error, len(line), 3*10**i) + end do + close(io) + end subroutine test_read_string + +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 From 0c27fcdea7f698abebfcadb8953decde6b0d367e Mon Sep 17 00:00:00 2001 From: Sebastian Ehlert <28669218+awvwgk@users.noreply.github.com> Date: Tue, 14 Dec 2021 22:24:21 +0100 Subject: [PATCH 2/5] Use automatic LHS allocation Co-authored-by: Milan Curcic --- src/stdlib_io.fypp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stdlib_io.fypp b/src/stdlib_io.fypp index 4e7720b1d..e1efd2bdc 100644 --- a/src/stdlib_io.fypp +++ b/src/stdlib_io.fypp @@ -357,7 +357,7 @@ contains character(len=bufsize) :: buffer, msg integer :: chunk, stat - allocate(character(len=0) :: line) + line = "" do read(unit, '(a)', advance='no', iostat=stat, iomsg=msg, size=chunk) buffer if (stat > 0) exit From 1535e19689eeeef8923a1641f95d3980b4e68e8d Mon Sep 17 00:00:00 2001 From: Sebastian Ehlert <28669218+awvwgk@users.noreply.github.com> Date: Sat, 18 Dec 2021 11:32:04 +0100 Subject: [PATCH 3/5] Handle disconnected units and pad='no' --- doc/specs/stdlib_io.md | 4 +- src/stdlib_io.fypp | 52 +++++++++++++++++++--- src/tests/io/test_getline.f90 | 83 ++++++++++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 7 deletions(-) diff --git a/doc/specs/stdlib_io.md b/doc/specs/stdlib_io.md index 8f53cd27c..3635ec149 100644 --- a/doc/specs/stdlib_io.md +++ b/doc/specs/stdlib_io.md @@ -236,12 +236,14 @@ 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)]] (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)`. diff --git a/src/stdlib_io.fypp b/src/stdlib_io.fypp index e1efd2bdc..577118424 100644 --- a/src/stdlib_io.fypp +++ b/src/stdlib_io.fypp @@ -6,6 +6,7 @@ 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 @@ -38,6 +39,8 @@ module stdlib_io 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 @@ -356,17 +359,28 @@ contains integer, parameter :: bufsize = 512 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 + do while (stat == 0) read(unit, '(a)', advance='no', iostat=stat, iomsg=msg, size=chunk) buffer if (stat > 0) exit line = line // buffer(:chunk) - if (stat < 0) then - if (is_iostat_eor(stat)) stat = 0 - exit - end if end do + if (is_iostat_eor(stat)) stat = 0 if (stat /= 0 .and. present(iomsg)) iomsg = trim(msg) if (present(iostat)) then @@ -395,4 +409,32 @@ contains 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 diff --git a/src/tests/io/test_getline.f90 b/src/tests/io/test_getline.f90 index b67daa944..e035a904f 100644 --- a/src/tests/io/test_getline.f90 +++ b/src/tests/io/test_getline.f90 @@ -16,7 +16,11 @@ subroutine collect_getline(testsuite) testsuite = [ & new_unittest("read-char", test_read_char), & - new_unittest("read-string", test_read_string) & + 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 @@ -34,7 +38,9 @@ subroutine test_read_char(error) 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 @@ -53,11 +59,86 @@ subroutine test_read_string(error) 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 From 854d7a8bcbdd0bc6b2e5fe534733bb8fc00f0cd4 Mon Sep 17 00:00:00 2001 From: Sebastian Ehlert <28669218+awvwgk@users.noreply.github.com> Date: Sat, 18 Dec 2021 20:45:08 +0100 Subject: [PATCH 4/5] Increase buffersize Co-authored-by: Milan Curcic --- src/stdlib_io.fypp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stdlib_io.fypp b/src/stdlib_io.fypp index 577118424..a1af943d4 100644 --- a/src/stdlib_io.fypp +++ b/src/stdlib_io.fypp @@ -356,7 +356,7 @@ contains !> Error message character(len=:), allocatable, optional :: iomsg - integer, parameter :: bufsize = 512 + integer, parameter :: bufsize = 4096 character(len=bufsize) :: buffer, msg integer :: chunk, stat logical :: opened From 22c88389da2edf97215f382f600859dc1d5dda6a Mon Sep 17 00:00:00 2001 From: Sebastian Ehlert <28669218+awvwgk@users.noreply.github.com> Date: Sun, 19 Dec 2021 10:19:31 +0100 Subject: [PATCH 5/5] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da425d130..319e80071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`