Skip to content

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

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

not-my-profile
Copy link

@not-my-profile not-my-profile commented Nov 16, 2021

I think the best way to drive the addition of this function forward is to have a dedicated RFC for it.

Rendered

Previous discussions:

@not-my-profile not-my-profile changed the title Add RFC for std::inputln() RFC: Add std::inputln() Nov 16, 2021
@undersquire
Copy link

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.

@not-my-profile not-my-profile changed the title RFC: Add std::inputln() RFC: Add std::io::inputln() Nov 16, 2021
Since it's a function it can be added to std::prelude.
@clarfonthey
Copy link

I'm personally all for this method, but I think that the method should be called read_line because it matches the corresponding Stdin method. All of the related FS and IO methods that have standalone equivalents do the same, and the change of name here implies that there's something different happening. This also saves inputln as a name for later if we want a more advanced macro approach.

@undersquire
Copy link

I'm personally all for this method, but I think that the method should be called read_line because it matches the corresponding Stdin method. All of the related FS and IO methods that have standalone equivalents do the same, and the change of name here implies that there's something different happening. This also saves inputln as a name for later if we want a more advanced macro approach.

Since there is already a read_line function, wouldn't it be confusing or even cause conflicts to name this function the same way? Or are you implying that this version replaces the existing read_line implementation?

@not-my-profile
Copy link
Author

not-my-profile commented Nov 16, 2021

std::io::read_line and std::io::Stdin::read_line can both exist perfectly fine without conflicting. Nobody is suggesting to replace any existing function: Rust guarantees backwards compatibility!

@clarfonthey
Copy link

Since there is already a read_line function, wouldn't it be confusing or even cause conflicts to name this function the same way? Or are you implying that this version replaces the existing read_line implementation?

I think a specific example might help: https://doc.rust-lang.org/std/fs/fn.read_to_string.html

One read_to_string is a method on File which accepts a buffer, whereas the other is a standalone function which takes in a filename and allocates a new string.

@lebensterben
Copy link

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.

@not-my-profile
Copy link
Author

not-my-profile commented Nov 16, 2021

Thanks Clar, that is a really good observation! I however see a different consistency axis: for any function named read I expect it to take an argument to specify from where you want to read:

  • the std::io::Read::read* methods read from the passed &mut self
  • std::io::read_to_string reads from the passed reader
  • std::fs::read_to_string reads from the passed file path

So having a std::io::read_line function that does not let you choose where to read from just feels wrong to me.

@undersquire
Copy link

undersquire commented Nov 16, 2021

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 std::io::stdin().readline function, which also includes the \n (newline) in the resulting string. This isn't a huge deal if you expect the user to manually remove the newline, however I think it might be better to do that for them:

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 std::io::stdin().read_line even include the newline? I feel like that should be changed because you really don't need that newline it reads.

@not-my-profile
Copy link
Author

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.

Why even does std::io::stdin().read_line include the newline? I feel like that should be changed because you really don't need that newline it reads.

Please stop talking about breaking backwards compatability 😅

@Diggsey
Copy link
Contributor

Diggsey commented Nov 16, 2021

read_line includes the newline because it's intended to be a lower-level building block, and it may be important for the caller to know how the line was terminated.

Given that the entire purpose of inputln would be as a convenience function, then it shouldn't include the newline.

This is the same reason that read_line is passed a buffer to read into, whereas inputln should just allocate and return a stirng.

@not-my-profile
Copy link
Author

That's a very good point! I updated the RFC to include the same newline trimming as std::io::BufRead::lines has.

@m-ou-se
Copy link
Member

m-ou-se commented Nov 17, 2021

What does this function do when it hits EOF or reads a non-newline-terminated line?

Python's input() throws an exception on a zero read() with no input, but running inputln()? in a loop will run forever when hitting the end of a file that's redirected to stdin:

fn main() -> std::io::Result<()> {
    loop {
        dbg!(inputln()?);
    }
}
$ cargo r < /dev/null
[src/main.rs:16] inputln()? = ""
[src/main.rs:16] inputln()? = ""
[src/main.rs:16] inputln()? = ""
[src/main.rs:16] inputln()? = ""
[...never stops...]

@Diggsey
Copy link
Contributor

Diggsey commented Nov 17, 2021

I would expect inputln to return an error if used again after EOF was already hit. eg.
So:

<EOF> => Ok(""), Err
foo<EOF> => Ok("foo"), Err
foo\n<EOF> => Ok("foo"), Ok(""), Err

@undersquire
Copy link

undersquire commented Nov 17, 2021

What does this function do when it hits EOF or reads a non-newline-terminated line?

The proposed inputln function simply uses std::io::stdin().read_line under the hood, so however std::io::stdin().read_line handles EOF is how inputln will also handle EOF. If it doesn't then maybe it needs to be added to the proposed function.

@yaahc yaahc added the T-libs-api Relevant to the library API team, which will review and decide on the RFC. label Nov 17, 2021
@undersquire
Copy link

@not-my-profile Is the function being renamed to std::io::read_line or are you just keeping it as std::io::inputln?

@not-my-profile
Copy link
Author

not-my-profile commented Nov 18, 2021

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).

Is the function being renamed to std::io::read_line or are you just keeping it as std::io::inputln?

If the function would just allocate a string, call std::io::Stdin::read_line and return the string/error unchanged, then I would agree that it should also be called read_line. In particular because as @clarfonthey remarked there is precedent for that with std::fs::read_to_string and std::io::read_to_string both just allocating a new string, calling Read::read_to_string and returning the resulting string/error.

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 read_to_string functions.

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?

@joshtriplett
Copy link
Member

Rather than returning an error on EOF, I personally would expect this to be an expected possibility to handle: Result<Option<String>>, where Ok(None) indicates an EOF with no available data.

@jsimonss
Copy link

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.

inputln!() would function like the proposed function, inputln!(...) would do a print!(...), flush, and read line.

This would add to the namespace but the RFC seems to propose that the function is available in prelude in any case.

@tmccombs
Copy link

I/O errors reading standard input or writing standard output are truly rare

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 Result altogether, that means you also need to panic if stdin is closed/reached end of stream. Which isn't exactly rare.

@programmerjake
Copy link
Member

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).

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.

@bazylhorsey
Copy link

@programmerjake considering this do you think a result should be returned from the function?

@programmerjake
Copy link
Member

programmerjake commented Nov 21, 2023

@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.

@bazylhorsey
Copy link

bazylhorsey commented Nov 21, 2023

@programmerjake any thoughts on how what type we should return or parse, and where we should draw the line?

@programmerjake
Copy link
Member

@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 input raises EOFError when hitting EOF, why not match that and return an UnexpectedEof error?

usage:

let foo: i32 = inputln()?.parse();

alternatively, have it return io::Result<Option<String>> returning None when it hits EOF.

usage:

loop {
    let Some(line) = inputln()? else {
        break;
    };
    let v: i32 = line.parse()?;
    println!("{v} * 2 == {}", v * 2);
}

@bazylhorsey
Copy link

bazylhorsey commented Nov 21, 2023

@programmerjake
On my notes above, is String really the best return? FromStr is implemented off of str. And the space is already allocated as a final for this functionality. What is the reasoning for returning String? Shouldn't there be an ownership handoff for better performance?

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 :)

@bazylhorsey
Copy link

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

@tmccombs
Copy link

Shouldn't there be an ownership handoff for better performance?

What would own the underlying data?

Or perhaps written another way, what would be the lifetime of the returned str?

The possibilities I see for that are:

  1. Accept a &'a mut String as an argument and return &'a str, so you can store it an object the caller allocated. This complicates the API thoguh, and makes it harder to use.
  2. Accept a &'a mut [u8], write to that and return &'a str. This has the same problem as 1, but is more flexible in where you store the data. But it also has the additional problem that the buffer might not be big enough to store the result.
  3. Create a separate wrapper object that you would call inputln on that has it's own internal buffer, and the return value has a lifetime that depends on that. Someting like fn inputln(&'a mut self, prompt: &str) -> &'a str.

@Victor-N-Suadicani
Copy link

Shouldn't there be an ownership handoff for better performance?

What would own the underlying data?

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 String when you're getting input anyway. And you always have the option of just "parsing" into a String if that is what you want regardless.

@bazylhorsey
Copy link

Wow now that's some real code hombre.
Could you fill us in on where this is at and why your RFC was not yet accepted by the libs team

@Victor-N-Suadicani
Copy link

Wow now that's some real code hombre. Could you fill us in on where this is at and why your RFC was not yet accepted by the libs team

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 Stdin.

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.

@bazylhorsey
Copy link

bazylhorsey commented Dec 31, 2023

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:

  • Problems are wide
  • Beginner devs don't want to have to understand 200-300 undergrad concepts to learn the first 30 minutes of the Rust tutorial.
  • I/O features don't need to handle 100% of advanced cases to be useful.
  • While the last point is true, there is a balance to achieve making this feature usable for beginner and experts alike.
  • MANY submitted their code. (Mine is NOT definitive), but after looking at many examples and cross-examining my own. My findings pointed me that it is better here to find the similarities and reduce, than find the union of features and culminate them.

Things this does:

  • make io casts to specific variables easier
  • leverage macros for straight-forward usage
  • remove worry of understanding the actual processes of OS I/O
  • Use common (but not overly complex) error precision
  • minimal interaction, this could be very simple in source.

Things this does NOT DO:

  • Handle concurrent or complex I/O patterns
  • Not tested with pipes and providing things like file as input, this can be discussed in the future.
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());
    }

}

@RustyYato
Copy link

The InputError should hold the actual error FromStr::Err. Stringly typed errors are a code smell.
Something like:

enum InputError<E> {
    Io(io::Error),
    Parse(E),
}

pub fn parse_input<T: FromStr>(prompt: &str) -> Result<T, InputError<T::Err>> { ... }

@tmccombs
Copy link

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.

@Phosphorus-M
Copy link
Contributor

Maybe it's better to separate the topics.
An input macro could be just an Option<String>?
Meanwhile, the scanf macro could be a Result<T, ParseError<E>> ?

I said that because in my opinion, we are talking about different cases.

However, this doesn't solve the first problem that Mara said.

What does this function do when it hits EOF or reads a non-newline-terminated line?

Python's input() throws an exception on a zero read() with no input, but running inputln()? in a loop will run forever when hitting the end of a file that's redirected to stdin

Besides we must start with just one easy case (Maybe) to replace the current

let mut input = String::new();
stdin().read_line(&mut input)

@abgros
Copy link

abgros commented Dec 14, 2024

@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.");
    }
}

input!() should probably panic if the input stream is closed as that is not really a recoverable error.

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 std::io just to get some user input.

@bazylhorsey
Copy link

bazylhorsey commented Dec 19, 2024

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

@abgros
Copy link

abgros commented Dec 19, 2024

@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.

@lebensterben
Copy link

@abgros

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:

  • "\n" translates to an empty string as the input
  • "C-D" translates to no input.

@bazylhorsey
Copy link

So the big question is, how do we get this moved forward and approved to PR to rust?

@abgros
Copy link

abgros commented Dec 21, 2024

@bazylhorsey having read the discussion I believe there's consensus that:

  • There should be a macro added to the prelude for accepting user input.
  • The macro should take a format string, similar to println!.
  • The function should never panic and instead return a Result.

So the only questions remaining are:

  • Whether the macro should return a String or a T: FromStr
  • Whether the macro should be called prompt! or input! or inputln!

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.

@Phosphorus-M
Copy link
Contributor

Phosphorus-M commented Dec 21, 2024

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

I sent you a PR to add formatting into the prompt

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.

Okay it could be logic to me, we can leave the task of trim it to the user.

So the big question is, how do we get this moved forward and approved to PR to rust?

We just make a new RFC with the example code of the library I guess

Edit:

  • Whether the macro should return a String or a T: FromStr

  • Whether the macro should be called prompt! or input! or inputln!

IMO T: FromStr and two options: input! and inputln!, in case of a simple message in the same line input! and in different lines inputln!

@bazylhorsey
Copy link

bazylhorsey commented Dec 22, 2024

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

  1. Should we unify the with/without-EOF variants (e.g., a single function that returns an enum)?
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. Is a single error type (covering both I/O and parse) ideal, or should we return io::Error/T::Err directly in some cases?

  2. Is the flush on prompt always desired, or should we let the user handle it manually? Personally, I think always flushing is a sane default as its almost any case I can think of. However, maybe we add an option or additional function to omit flush for people who want control of this manually. Once hitting this level of granularity, you might as well use the tutorial from the first chapter of the docs lol

@tmccombs
Copy link

1/2: I would just use a Result<T, InputErr<T>> where InputErr is defined something like:

enum InputErr<T: FromStr> {
  Eof,
  Parse(T::Err),
  Io(io::Error),
}

3: I think always flushing would be fine for this.

@bazylhorsey
Copy link

bazylhorsey commented Dec 23, 2024

Yet another release, consolodated EOF into a more generic error response the user can handle.
Also added @Phosphorus-M commit for adding arguments

@Phosphorus-M
Copy link
Contributor

Phosphorus-M commented Apr 20, 2025

@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 input! returns Result<T, _> by default, and try_input! returns Result<Option<T>, _>. The feedback I received was that Result<T, _> felt a bit more intuitive and easier to work with.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-libs-api Relevant to the library API team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.