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` diff --git a/doc/specs/stdlib_io.md b/doc/specs/stdlib_io.md index c33511c5e..3635ec149 100644 --- a/doc/specs/stdlib_io.md +++ b/doc/specs/stdlib_io.md @@ -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 +``` 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..a1af943d4 100644 --- a/src/stdlib_io.fypp +++ b/src/stdlib_io.fypp @@ -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 @@ -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 !! @@ -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 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..e035a904f --- /dev/null +++ b/src/tests/io/test_getline.f90 @@ -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