-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: Add std::io::inputln() #3196
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
base: master
Are you sure you want to change the base?
Conversation
d1b96ae
to
211cae9
Compare
211cae9
to
1309352
Compare
I think that this might be a better idea. In my RFC I proposed a more advanced approach which ultimately evolved to have formatting, however that is a lot more complex to implement and might be better to leave to crates. I think the standard library would benefit more from a simpler implementation like the one suggested here. |
Since it's a function it can be added to std::prelude.
I'm personally all for this method, but I think that the method should be called |
Since there is already a |
|
I think a specific example might help: https://doc.rust-lang.org/std/fs/fn.read_to_string.html One |
Note that print!() and println!() are "specialised" variant of write!() and writeln!(), where all of them are based on io::Write but the first two specifically writes to stdout. So analogously, we should have something that provides a more convenient interface to io::Read first, then to provide a more specific one such as inputln(). Introducing inputln() without something like read() or readln() just feels weird logically. |
Thanks Clar, that is a really good observation! I however see a different consistency axis: for any function named
So having a |
I am not sure this is a big deal, but I tried using your function in a test project and noticed that it doesn't work with parsing to other values. For example: let age: i32 = io::inputln()?.parse().expect("Invalid age!"); This will always fail here, because when it reads it uses the pub fn read_line() -> Result<String, std::io::Error> {
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
Ok(line.trim_end_matches('\n').to_string())
} I might be wrong, but I see no reason to include the newline character at the end. EDIT: Why does |
Good observation @undersquire! I however don't think that this is necessary. You can just do: let age: i32 = io::inputln()?.trim().parse().expect("Invalid age!"); which also has the benefit that it does not fail when the user accidentally enters other whitespace before or after their input.
Please stop talking about breaking backwards compatability 😅 |
Given that the entire purpose of This is the same reason that |
That's a very good point! I updated the RFC to include the same newline trimming as |
What does this function do when it hits EOF or reads a non-newline-terminated line? Python's fn main() -> std::io::Result<()> {
loop {
dbg!(inputln()?);
}
}
|
I would expect
|
The proposed |
@not-my-profile Is the function being renamed to |
Thanks @m-ou-se, you're of course right, when the function performs newline trimming it also has to convert zero reads to UnexpectedEof errors, or otherwise users have no chance of detecting EOF. I added EOF handling to the RFC (along with rationales for both newline trimming and EOF handling).
If the function would just allocate a string, call But now that the function is additionally trimming newlines and handling EOF, I am thinking that a distinct function name might be a better choice, so that API users aren't mislead into thinking that the function returns the string/error unchanged, as is the case with the Do we want two functions in the same standard library module with the same name but subtly different behavior? Is there precedent for that? Is that desirable? |
Rather than returning an error on EOF, I personally would expect this to be an expected possibility to handle: |
The RFC notes that contrary to println! and friends, that are macros, inputln is a function and that printing a prompt with print doesn't flush. Couldn't both be addressed by instead of making inputln a function it would be a macro.
This would add to the namespace but the RFC seems to propose that the function is available in prelude in any case. |
not if you have any kind of redirection or piping going on. For example if stdin is a pipe, and the process on the other end terminates, you will get an I/O error (if your process isn't killed by SIGPIPE). And if you want to avoid |
actually, afaict you just get EOF when reading from a pipe where the write end was closed (after reading any cached data). you get SIGPIPE and/or EPIPE when writing where the read end was closed. |
@programmerjake considering this do you think a result should be returned from the function? |
yes, because EOF can be considered an error and is quite common. after all, you want to be able to distinguish between reading an infinite sequence of empty lines and reaching the end of a file. |
@programmerjake any thoughts on how what type we should return or parse, and where we should draw the line? |
pub fn inputln() -> io::Result<String> {...} Python's usage: let foo: i32 = inputln()?.parse(); alternatively, have it return usage: loop {
let Some(line) = inputln()? else {
break;
};
let v: i32 = line.parse()?;
println!("{v} * 2 == {}", v * 2);
} |
@programmerjake Also you think this should also be a macro? Do you have a preference or intuition on if Option String or expected String in your example of if it's more likely to be considered acceptable to rusts team? Just collecting your thoughts while I have your attention :) |
Also everyone don't forget to cast your vote from this old RFC https://strawpoll.com/zxds5jye6 This will be a good community study for a considered proposal |
What would own the underlying data? Or perhaps written another way, what would be the lifetime of the returned The possibilities I see for that are:
|
I've linked this before but I personally think my ACP suggestion is a better option for easy input. It can entirely avoid any ownership problems because you're just using the buffer from stdin and parsing directly from there. You probably want to parse quite often into a different type than |
Wow now that's some real code hombre. |
It's an ACP, not an RFC. I was advised to use the ACP process instead of the RFC process because it was just a single function to It is currently still waiting for the libs team to look at it and give a respond and it's unfortunately taking a long time. Or at least that's what I think but I can't seem to find any documentation on the ACP process any more and now I'm curious what happened to it. |
After a lot of deliberation, I aimed at simplification and producing minimal code changes while maximizing velocity for newcomers to I/O related problems. This is made somewhat with the general rust dev in mind, but not to the point it becomes an advance level feature, or fails in solving the original problem: making the Rust intro smoother. What this board learned:
Things this does:
Things this does NOT DO:
use std::io::{self, Write};
use std::str::FromStr;
/// Custom error type for input parsing.
pub enum InputError {
IoError(io::Error),
ParseError(String),
}
/// Function to get input and parse it into the desired type.
pub fn parse_input<T: FromStr>(prompt: &str) -> Result<T, InputError>
where
<T as FromStr>::Err: ToString,
{
print!("{}", prompt);
io::stdout().flush().map_err(InputError::IoError)?;
let mut input = String::new();
io::stdin().read_line(&mut input).map_err(InputError::IoError)?;
input.trim().parse::<T>().map_err(|e| InputError::ParseError(e.to_string()))
}
/// Macro to simplify calling the parse_input function.
#[macro_export]
macro_rules! parse_input {
($prompt:expr) => {
$crate::parse_input::<_>($prompt)
};
}
#[cfg(test)]
mod tests {
// Example test for successful parsing
#[test]
fn test_parse_input_success() {
let result = "42".parse::<f64>();
assert_eq!(result, Ok(42.));
}
// Example test for failed parsing
#[test]
fn test_parse_input_failure() {
let result = "abc".parse::<i32>();
assert!(result.is_err());
}
} |
The enum InputError<E> {
Io(io::Error),
Parse(E),
}
pub fn parse_input<T: FromStr>(prompt: &str) -> Result<T, InputError<T::Err>> { ... } |
To add on to @RustyYato 's comment, when I first saw the definition of InputError, I thought that the content of the Parse variant was the value of the string that was read but failed to parse, not the stringification of the parse error. And actually, it might be worth including the string read in that variant. |
Maybe it's better to separate the topics. I said that because in my opinion, we are talking about different cases. However, this doesn't solve the first problem that Mara said.
Besides we must start with just one easy case (Maybe) to replace the current let mut input = String::new();
stdin().read_line(&mut input) |
@bazylhorsey in the interest of beginner-friendliness, I would recommend having input work similarly to how it does in Python. I don't think newbies would have any trouble understanding this program: fn main() {
let name = input!("Enter your name: ");
if let Ok(age) = input!("Nice to meet you, {name}. Enter your age: ").parse::<u64>() {
println!("You are {age} years old.");
} else {
println!("Invalid input.");
}
}
But this is just one possible implementation — anything would be better than the current situation where you have to manage heap allocation and low-level details of |
Here's my latest version hearing everyone's feedback. I included methods to have input with and without eof as wanted by the community. This includes zero dependencies outside std, elegant compared to competitors text_io and read_input (in my humble opinion). Find the repo here |
@bazylhorsey I noticed that your implementation contains the comment: // Consider an empty line as EOF, returning None What's the justification for this? I think there are situations where someone would want to accept an empty string as legitimate input. Other than that I like it a lot. |
When prompted for an input, if the user just press the carriage returns, the program should get "\n" instead of "". To have the program to receive empty input, the user needs to press Ctrl-D to close the input steam. That's to say:
|
So the big question is, how do we get this moved forward and approved to PR to rust? |
@bazylhorsey having read the discussion I believe there's consensus that:
So the only questions remaining are:
If anyone feels strongly about these we could have a quick vote. I'm not sure who has the power to move this forward, but I would encourage you to create a PR at some point. |
I sent you a PR to add formatting into the prompt
Okay it could be logic to me, we can leave the task of trim it to the user.
We just make a new RFC with the example code of the library I guess Edit:
IMO |
I've updated the repo to have an inputln! macro 100% FromStr, because you can always construct it as a String when you called the macro. Questions
enum ReadOutcome<T, E> {
Eof,
Ok(T),
Err(E),
}
// Then you define something like:
fn read_input<T: FromStr>() -> ReadOutcome<T, InputError<T::Err>> {
// ...
}
|
1/2: I would just use a enum InputErr<T: FromStr> {
Eof,
Parse(T::Err),
Io(io::Error),
} 3: I think always flushing would be fine for this. |
Yet another release, consolodated EOF into a more generic error response the user can handle. |
@bazylhorsey I created an RFC inspired by your library and moved the discussion to the other RFC in case you'd like to share your perspective. The main change I made is that |
I think the best way to drive the addition of this function forward is to have a dedicated RFC for it.
Rendered
Previous discussions:
std::io::input
simple input function. rust#75435