Skip to content
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

io: getfile #939

Merged
merged 26 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
38 changes: 38 additions & 0 deletions doc/specs/stdlib_io.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,41 @@ Provides formats for all kinds as defined in the `stdlib_kinds` module.
```fortran
{!example/io/example_fmt_constants.f90!}
```

## `getfile` - Read a whole ASCII file into a string variable

### Status

Experimental

### Description

This function reads the entirety of a specified ASCII file and returns its content as a string. The function provides an optional error-handling mechanism via the `state_type` class. If the `err` argument is not provided, exceptions will trigger an `error stop`. The function also supports an optional flag to delete the file after reading.

### Syntax

`call [[stdlib_io(module):getfile(function)]] (fileName [, err] [, delete=.false.])`

### Class
Function

### Arguments

`fileName`: Shall be a character input containing the path to the ASCII file to read. It is an `intent(in)` argument.

`err` (optional): Shall be a `type(state_type)` variable. It is an `intent(out)` argument used for error handling.

`delete` (optional): Shall be a `logical` flag. If `.true.`, the file is deleted after reading. Default is `.false.`. It is an `intent(in)` argument.

### Return values

The function returns a `string_type` variable containing the full content of the specified file.

Raises `STDLIB_IO_ERROR` if the file is not found, cannot be opened, read, or deleted.
Exceptions trigger an `error stop` unless the optional `err` argument is provided.

### Example

```fortran
{!example/io/example_getfile.f90!}
```
1 change: 1 addition & 0 deletions example/io/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
ADD_EXAMPLE(fmt_constants)
#ADD_EXAMPLE(getline)
ADD_EXAMPLE(getfile)
ADD_EXAMPLE(loadnpy)
ADD_EXAMPLE(loadtxt)
ADD_EXAMPLE(open)
Expand Down
20 changes: 20 additions & 0 deletions example/io/example_getfile.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
! Demonstrate usage of `getfile`
program example_getfile
use stdlib_io, only: getfile
use stdlib_string_type, only: string_type
use stdlib_error, only: state_type
implicit none

character(*), parameter :: fileName = "example.txt"
type(string_type) :: fileContent
type(state_type) :: err

! Read a file into a string
fileContent = getfile(fileName, err=err)

if (err%error()) then
print *, err%print()
else
print *, "Success! File "//fileName//" imported."
end if
end program example_getfile
115 changes: 113 additions & 2 deletions src/stdlib_io.fypp
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,34 @@ module stdlib_io
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_error, only: error_stop, state_type, STDLIB_IO_ERROR
use stdlib_optval, only: optval
use stdlib_ascii, only: is_blank
use stdlib_string_type, only : string_type
use stdlib_string_type, only : string_type, assignment(=), move
implicit none
private
! Public API
public :: loadtxt, savetxt, open, getline

!! version: experimental
!!
!! Reads a whole ASCII file and loads its contents into a string variable.
!! ([Specification](../page/specs/stdlib_io.html#getfile-read-a-whole-ascii-file-into-a-string-variable))
!!
!!### Summary
!! Function interface for reading the content of a file into a string.
!!
!!### Description
!!
!! This function reads the entirety of a specified ASCII file and returns it as a string. The optional
!! `err` argument allows for handling errors through the library's `state_type` class.
!! An optional `logical` flag can be passed to delete the file after reading.
!!
!!@note Handles errors using the library's `state_type` error-handling class. If not provided,
!! exceptions will trigger an `error stop`.
!!
public :: getfile

! Private API that is exposed so that we can test it in tests
public :: parse_mode

Expand Down Expand Up @@ -528,4 +547,96 @@ contains
call getline(input_unit, line, iostat, iomsg)
end subroutine getline_input_string

!> Version: experimental
!>
!> Reads a whole ASCII file and loads its contents into a string variable.
!> The function handles error states and optionally deletes the file after reading.
type(string_type) function getfile(fileName,err,delete) result(file)
!> Input file name
character(*), intent(in) :: fileName
!> [optional] State return flag. On error, if not requested, the code will stop.
type(state_type), optional, intent(out) :: err
!> [optional] Delete file after reading? Default: do not delete
logical, optional, intent(in) :: delete

! Local variables
type(state_type) :: err0
character(len=:), allocatable :: fileString
character(len=512) :: iomsg
integer :: lun,iostat
integer(int64) :: errpos,fileSize
logical :: is_present,want_deleted

! Initializations
file = ""

!> Check if the file should be deleted after reading
if (present(delete)) then
want_deleted = delete
else
want_deleted = .false.
end if

!> Check file existing
inquire(file=fileName, exist=is_present)
if (.not.is_present) then
err0 = state_type('getfile',STDLIB_IO_ERROR,'File not present:',fileName)
call err0%handle(err)
return
end if

!> Retrieve file size
inquire(file=fileName,size=fileSize)

invalid_size: if (fileSize<0) then

err0 = state_type('getfile',STDLIB_IO_ERROR,fileName,'has invalid size=',fileSize)
call err0%handle(err)
return

endif invalid_size

! Read file
open(newunit=lun,file=fileName, &
form='unformatted',action='read',access='stream',status='old', &
iostat=iostat,iomsg=iomsg)

if (iostat/=0) then
err0 = state_type('getfile',STDLIB_IO_ERROR,'Cannot open',fileName,'for read:',iomsg)
call err0%handle(err)
return
end if

allocate(character(len=fileSize) :: fileString)

read_data: if (fileSize>0) then

read(lun, pos=1, iostat=iostat, iomsg=iomsg) fileString

! Read error
if (iostat/=0) then

inquire(unit=lun,pos=errpos)
err0 = state_type('getfile',STDLIB_IO_ERROR,iomsg,'(',fileName,'at byte',errpos,')')
call err0%handle(err)
return

endif

end if read_data

if (want_deleted) then
close(lun,iostat=iostat,status='delete')
if (iostat/=0) err0 = state_type('getfile',STDLIB_IO_ERROR,'Cannot delete',fileName,'after reading')
else
close(lun,iostat=iostat)
if (iostat/=0) err0 = state_type('getfile',STDLIB_IO_ERROR,'Cannot close',fileName,'after reading')
endif

! Process output
call move(from=fileString,to=file)
call err0%handle(err)

end function getfile

end module stdlib_io
81 changes: 78 additions & 3 deletions test/io/test_getline.f90
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module test_getline
use stdlib_io, only : getline
use stdlib_string_type, only : string_type, len
use stdlib_io, only : getline, getfile
use stdlib_error, only: state_type
use stdlib_string_type, only : string_type, len, len_trim
use testdrive, only : new_unittest, unittest_type, error_type, check
implicit none
private
Expand All @@ -20,7 +21,10 @@ subroutine collect_getline(testsuite)
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.) &
new_unittest("no-unit", test_no_unit, should_fail=.true.), &
new_unittest("getfile-no", test_getfile_missing), &
new_unittest("getfile-empty", test_getfile_empty), &
new_unittest("getfile-non-empty", test_getfile_non_empty) &
]
end subroutine collect_getline

Expand Down Expand Up @@ -139,6 +143,77 @@ subroutine test_no_unit(error)
call check(error, stat, msg)
end subroutine test_no_unit

subroutine test_getfile_missing(error)
!> Test for a missing file.
type(error_type), allocatable, intent(out) :: error

type(string_type) :: fileContents
type(state_type) :: err

fileContents = getfile("nonexistent_file.txt", err)

! Check that an error was returned
call check(error, err%error(), "Error not returned on a missing file")
if (allocated(error)) return

end subroutine test_getfile_missing

subroutine test_getfile_empty(error)
!> Test for an empty file.
type(error_type), allocatable, intent(out) :: error

integer :: ios
character(len=:), allocatable :: filename
type(string_type) :: fileContents
type(state_type) :: err

! Get a temporary file name
filename = "test_getfile_empty.txt"

! Create an empty file
open(newunit=ios, file=filename, action="write", form="formatted", access="sequential")
close(ios)

! Read and delete it
fileContents = getfile(filename, err, delete=.true.)

call check(error, err%ok(), "Should not return error reading an empty file")
if (allocated(error)) return

call check(error, len_trim(fileContents) == 0, "String from empty file should be empty")
if (allocated(error)) return

end subroutine test_getfile_empty

subroutine test_getfile_non_empty(error)
!> Test for a non-empty file.
type(error_type), allocatable, intent(out) :: error

integer :: ios
character(len=:), allocatable :: filename
type(string_type) :: fileContents
type(state_type) :: err

! Get a temporary file name
filename = "test_getfile_size5.txt"

! Create a fixed-size file
open(newunit=ios, file=filename, action="write", form="unformatted", access="stream")
write(ios) "12345"
close(ios)

! Read and delete it
fileContents = getfile(filename, err, delete=.true.)

call check(error, err%ok(), "Should not return error reading a non-empty file")
if (allocated(error)) return

call check(error, len_trim(fileContents) == 5, "Wrong string size returned")
if (allocated(error)) return

end subroutine test_getfile_non_empty


end module test_getline


Expand Down
Loading