diff --git a/doc/specs/stdlib_io.md b/doc/specs/stdlib_io.md index 46befe2ea..3081a00b1 100644 --- a/doc/specs/stdlib_io.md +++ b/doc/specs/stdlib_io.md @@ -13,11 +13,12 @@ title: io Experimental ### Description + Loads a rank-2 `array` from a text file. ### Syntax -`call ` [[stdlib_io(module):loadtxt(interface)]] `(filename, array [, skiprows] [, max_rows] [, fmt] [, delimiter])` +`call` [[stdlib_io(module):loadtxt(interface)]] `(filename, array [, skiprows] [, max_rows] [, fmt] [, delimiter])` ### Arguments @@ -29,7 +30,7 @@ Loads a rank-2 `array` from a text file. `max_rows` (optional): Read `max_rows` lines of content after `skiprows` lines. A negative value results in reading all lines. A value of zero results in no lines to be read. The default value is -1. -`fmt` (optional): Fortran format specifier for the text read. Defaults to the write format for the data type. Setting fmt='*' will specify list directed read. +`fmt` (optional): Fortran format specifier for the text read. Defaults to the write format for the data type. Setting fmt='*' will specify list directed read. `delimiter` (optional): Shall be a character expression of length 1 that contains the delimiter used to separate the columns. The default is `' '`. @@ -43,7 +44,6 @@ Returns an allocated rank-2 `array` with the content of `filename`. {!example/io/example_loadtxt.f90!} ``` - ## `open` - open a file ### Status @@ -57,7 +57,7 @@ Text files are opened using a sequential access, while binary files are opened u ### Syntax -`u = ` [[stdlib_io(module):open(function)]] `(filename [, mode] [, iostat])` +`u =` [[stdlib_io(module):open(function)]] `(filename [, mode] [, iostat])` ### Arguments @@ -65,7 +65,6 @@ Text files are opened using a sequential access, while binary files are opened u `mode` (optional): Shall be a character expression containing characters describing the way in which the file will be used. The available modes are: - | Character | Meaning | | --------- | ------- | | `'r'` | open for reading (default) | @@ -76,14 +75,12 @@ Text files are opened using a sequential access, while binary files are opened u | `'b'` | binary mode | | `'t'` | text mode (default) | - The default `mode` is `'rt'` (i.e. open for reading a text file). The `mode` may include one of the four different methods for opening a file (i.e., `'r'`, `'w'`, `'x'`, and `'a'`). These four methods can be associated with the character `'+'` to open the file for updating. In addition, it can be specified if the file should be handled as a binary file (`'b'`) or a text file (`'t'`). `iostat` (optional): Shall be a scalar of type `integer` that receives the error status of `open`, if provided. If no error exists, `iostat` is zero. `u`: Shall be a scalar of type `integer` that specifies the unit number associated with the file `filename`. - ### Return value The result is a scalar of type `integer`. @@ -94,7 +91,6 @@ The result is a scalar of type `integer`. {!example/io/example_open.f90!} ``` - ## `savetxt` - save a 2D array into a text file ### Status @@ -102,11 +98,12 @@ The result is a scalar of type `integer`. Experimental ### Description + Saves a rank-2 `array` into a text file. ### Syntax -`call ` [[stdlib_io(module):savetxt(interface)]] `(filename, array [, delimiter])` +`call` [[stdlib_io(module):savetxt(interface)]] `(filename, array [, delimiter])` ### Arguments @@ -126,7 +123,6 @@ Provides a text file called `filename` that contains the rank-2 `array`. {!example/io/example_savetxt.f90!} ``` - ## `load_npy` ### Status @@ -139,7 +135,7 @@ Loads an `array` from a npy formatted binary file. ### Syntax -`call ` [[stdlib_io_npy(module):load_npy(interface)]] `(filename, array[, iostat][, iomsg])` +`call` [[stdlib_io_npy(module):load_npy(interface)]] `(filename, array[, iostat][, iomsg])` ### Arguments @@ -167,7 +163,6 @@ Returns an allocated `array` with the content of `filename` in case of success. {!example/io/example_loadnpy.f90!} ``` - ## `save_npy` ### Status @@ -180,7 +175,7 @@ Saves an `array` into a npy formatted binary file. ### Syntax -`call ` [[stdlib_io_npy(module):save_npy(interface)]] `(filename, array[, iostat][, iomsg])` +`call` [[stdlib_io_npy(module):save_npy(interface)]] `(filename, array[, iostat][, iomsg])` ### Arguments @@ -220,9 +215,9 @@ Read a whole line from a formatted unit into a string variable ### Syntax -`call ` [[stdlib_io(module):get_line(interface)]] ` (unit, line[, iostat][, iomsg])` +`call` [[stdlib_io(module):get_line(interface)]] `(unit, line[, iostat][, iomsg])` -`call ` [[stdlib_io(module):get_line(interface)]] ` (line[, iostat][, iomsg])` +`call` [[stdlib_io(module):get_line(interface)]] `(line[, iostat][, iomsg])` ### Arguments @@ -272,7 +267,7 @@ Experimental ### Description -This subroutine interface reads the entirety of a specified ASCII file and returns its content as a string or an allocatable `character` variable. +This subroutine interface reads the entirety of a specified ASCII file and returns its content as a string or an allocatable `character` variable. 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 @@ -280,6 +275,7 @@ The function provides an optional error-handling mechanism via the `state_type` `call [[stdlib_io(module):get_file(subroutine)]] (filename, file [, err] [, delete=.false.])` ### Class + Function ### Arguments @@ -287,7 +283,7 @@ Function `filename`: Shall be a character input containing the path to the ASCII file to read. It is an `intent(in)` argument. `file`: Shall be a `type(string_type)` or an allocatable `character` variable containing the full content of the specified file. It is an `intent(out)` 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. @@ -296,7 +292,7 @@ Function Output variable `file` will contain the full content of the specified file. -Raises `STDLIB_IO_ERROR` if the file is not found, cannot be opened, read, or deleted. +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 @@ -305,3 +301,47 @@ Exceptions trigger an `error stop` unless the optional `err` argument is provide {!example/io/example_get_file.f90!} ``` +## `input` - Read user input with an optional prompt + +### Status + +Experimental + +### Description + +Reads a line of user input from standard input with an optional prompt. +The prompt, if provided, is displayed on the same line as where the input will be entered. + +### Syntax + +`str =` [[stdlib_io(module):input(function)]] `([prompt] [, iostat])` + +### Arguments + +`prompt` (optional): Shall be a `character` expression containing the prompt text to be displayed before reading input. + The prompt is displayed without a trailing newline, allowing input on the same line. + This argument is `intent(in)`. + +`iostat` (optional): Shall be a scalar of type `integer` that receives the I/O status. + Zero indicates success, non-zero indicates an error or end-of-file condition. + This argument is `intent(out)`. + If not provided, an error will cause trigger an `error stop`. + +### Return value + +Returns a deferred-length allocatable `character` string containing the line read from standard input. +Trailing newline characters are automatically removed. + +### Notes + +- If no prompt is provided, the function will wait for user input without displaying any text. +- Empty input (just pressing Enter) returns an empty string `""` of length equal to 0. +- The function reads a complete line of input, including any whitespace. +- For error handling, use the optional `iostat` parameter to detect end-of-file or other I/O exceptions. +- The function uses `get_line` internally for consistency with other stdlib I/O operations. + +### Example + +```fortran +{!example/io/example_input.f90!} +``` diff --git a/example/io/example_input.f90 b/example/io/example_input.f90 new file mode 100644 index 000000000..9d94f7470 --- /dev/null +++ b/example/io/example_input.f90 @@ -0,0 +1,13 @@ +program example_input + use stdlib_io, only: input + implicit none + + character(len=:), allocatable :: name + + ! Get user's name with a prompt + name = input('Enter your name: ') + + ! Display greeting + print '(a,a)', 'Hello, ', name + +end program example_input diff --git a/example_input_test b/example_input_test new file mode 100755 index 000000000..454852c5a Binary files /dev/null and b/example_input_test differ diff --git a/src/io/stdlib_io.fypp b/src/io/stdlib_io.fypp index 92d4306c0..f39b210a2 100644 --- a/src/io/stdlib_io.fypp +++ b/src/io/stdlib_io.fypp @@ -6,7 +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, intrinsic :: iso_fortran_env, only : input_unit, output_unit use stdlib_kinds, only: sp, dp, xdp, qp, & int8, int16, int32, int64 use stdlib_io_aux, only: FMT_INT, FMT_REAL_SP, FMT_REAL_DP, FMT_REAL_XDP, FMT_REAL_QP, & @@ -18,7 +18,7 @@ module stdlib_io implicit none private ! Public API - public :: loadtxt, savetxt, open, get_line, get_file + public :: loadtxt, savetxt, open, get_line, get_file, input !! version: experimental !! @@ -61,6 +61,14 @@ module stdlib_io module procedure :: get_line_input_string end interface get_line + !> Version: experimental + !> + !> Reads user input with an optional prompt + !> Returns a deferred-length character string + interface input + module procedure :: input_char + end interface input + interface loadtxt !! version: experimental !! @@ -705,4 +713,53 @@ contains end subroutine get_file_char + !> Version: experimental + !> + !> Reads user input with an optional prompt into a deferred-length character variable + function input_char(prompt, iostat) result(str) + !> Optional prompt to display before reading input + character(len=*), intent(in), optional :: prompt + !> Optional status of operation + integer, intent(out), optional :: iostat + !> The input string read from the user + character(len=:), allocatable :: str + + integer :: stat + character(len=:), allocatable :: iomsg + + ! Display prompt if provided (without newline) + if (present(prompt)) then + write(output_unit, '(a)', advance='no') prompt + flush(output_unit) + end if + + ! Read the input line + call get_line(input_unit, str, stat, iomsg) + + ! Handle iostat + if (present(iostat)) then + iostat = stat + else if (stat /= 0) then + call error_stop(trim(iomsg)) + end if + end function input_char + + !> Version: experimental + !> + !> Reads user input with an optional prompt into a string variable of type `string_type` + recursive function input_string(prompt, iostat) result(str) + !> Optional prompt to display before reading input + character(len=*), intent(in), optional :: prompt + !> Optional status of operation + integer, intent(out), optional :: iostat + !> The input string read from the user + type(string_type) :: str + + character(len=:), allocatable :: buffer + + ! Use the character version and convert to string_type + buffer = input_char(prompt, iostat) + str = string_type(buffer) + end function input_string + end module stdlib_io diff --git a/test/io/test_input.f90 b/test/io/test_input.f90 new file mode 100644 index 000000000..357750f00 --- /dev/null +++ b/test/io/test_input.f90 @@ -0,0 +1,66 @@ +module test_input + use stdlib_io, only: input + use testdrive, only: new_unittest, unittest_type, error_type, check + implicit none + + private + public :: collect_input +contains + + !> Collect all exported unit tests + subroutine collect_input(testsuite) + !> Collection of tests + type(unittest_type), allocatable, intent(out) :: testsuite(:) + + testsuite = [ & + new_unittest("input_basic", test_input_basic) & + ] + + end subroutine collect_input + + subroutine test_input_basic(error) + !> Error handling + type(error_type), allocatable, intent(out) :: error + character(len=:), allocatable :: result + + ! Note: This is a basic structure for the test + ! Actual interactive testing would require input redirection or mocking + ! For now, we verify that the function signature is correct + + ! The function should be callable (this verifies compilation) + ! In a real test environment, you would redirect stdin + + ! For now, just verify the module compiles and can be used + call check(error, .true., "input function is available") + + end subroutine test_input_basic + +end module test_input + + +program tester + use, intrinsic :: iso_fortran_env, only : error_unit + use testdrive, only : run_testsuite, new_testsuite, testsuite_type + use test_input, only : collect_input + implicit none + integer :: stat, is + type(testsuite_type), allocatable :: testsuites(:) + character(len=*), parameter :: fmt = '("#", *(1x, a))' + + stat = 0 + + testsuites = [ & + new_testsuite("input", collect_input) & + ] + + 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 tester diff --git a/test/io/test_input_demo.f90 b/test/io/test_input_demo.f90 new file mode 100644 index 000000000..2cf50f291 --- /dev/null +++ b/test/io/test_input_demo.f90 @@ -0,0 +1,31 @@ +program test_input + ! Test program for the new input() function + use stdlib_io, only: input + implicit none + + character(len=:), allocatable :: name, age_str, city + integer :: ios + + ! Test 1: Basic input with prompt + write(*,'(a)') 'Test 1: Basic input with prompt' + name = input('Enter your name: ') + write(*,'(2a)') 'You entered: ', name + write(*,*) + + ! Test 2: Input without prompt + write(*,'(a)') 'Test 2: Input without prompt' + write(*,'(a)',advance='no') 'Enter your age: ' + age_str = input() + write(*,'(2a)') 'You entered: ', age_str + write(*,*) + + ! Test 3: Input with iostat + write(*,'(a)') 'Test 3: Input with iostat (press Ctrl+D to end)' + city = input('Enter your city: ', iostat=ios) + if (ios == 0) then + write(*,'(2a)') 'You entered: ', city + else + write(*,'(a,i0)') 'Input error, iostat = ', ios + end if + +end program test_input