Description
Proposal
Problem statement
Stdin::read_line
is very commonly used in programs written by beginners in rust. (For example, see the guessing game program in the Book.) Therefore, I think that it should be as easy to use as possible.
Code using read_line
tends to be more verbose than similar code in other languages (because you need to manually create an empty string yourself before using it). Furthermore, it can be error-prone because read_line
returns the string including the trailing newline, tripping up many beginners. (This issue comes up pretty frequently in the rust community discord.) And if you call read_line
in a loop, you need to manually clear the string yourself in order to get the correct strings. This is a lot of complexity to avoid allocations, even though interactive CLI programs are bottlenecked by the human typing the input, so allocations often don't matter.
There exists the Stdin::lines
method. Calling .next()
on the resultant iterator produces Option<io::Result<String>>
. This requires some finangling to properly handle errors. Additionally, it is not very discoverable, and it is not intuitive that doing user input would be done with iterators.
Similar comments also apply to BufRead
.
Solution sketch
I propose adding the following methods:
impl Stdin {
pub fn next_line(&self) -> io::Result<String>;
}
pub trait BufRead {
// existing methods omitted
fn next_line(&mut self) -> io::Result<String> { .... }
}
The methods would behave as follows:
- The input is read into a buffer until a newline (a
0xA
byte) is reached or EOF is reached, whichever is first. - If EOF is reached without any bytes being read, return an
Err
withErrorKind::UnexpectedEof
. - If the buffer ends with
0xD,0xA
, or ends with0xA
, trim those bytes. - Turn the buffer into a
String
, returning anErr
withErrorKind::InvalidData
if it's not valid UTF-8. - Return the resulting
String
.
This behavior is the same as .lines().next()
, except that instead of returning None
to indicate EOF, an error is returned instead.
Alternatives
- The status quo, which would encourage users to avoid allocations at the cost of potentially unnecessary complexity.
- The return value could be of type
io::Result<Option<String>>
, with the exact same behavior as.lines().next().transpose()
. I think that handling the error conditions properly is too cumbersome if this return type is used.
Links and related work
Prior discussion: https://internals.rust-lang.org/t/read-line-is-a-beginner-footgun-how-to-fix-it/20663
What happens now?
This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible responses
The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):
- We think this problem seems worth solving, and the standard library might be the right place to solve it.
- We think that this probably doesn't belong in the standard library.
Second, if there's a concrete solution:
- We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
- We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.
Activity