Capturing user inputs in the terminal has some little nuances to it that can give you a hard time if you don’t know about them.
It’s been quite some time since I coded in a lower level language than JavaScript. Distributed systems recently piqued my interest thus I decided to pick-up a lower-level compiled language. I chose Rust which I knew nothing about. Inspired by @swyx’s “learn in public” mantra I decided to document my journey learning Rust.
I faced three issues while developing a CLI where I needed to repeatedly ask for the user input until it matches specific keywords. Here is the initial code I wrote but it doesn’t work as expected:
use std::io;
fn main() {
let mut choice = String::new();
while choice.as_str() != "q" {
println!("What do you want to do?");
println!("(p)rint hello");
println!("(q)uit the program");
print!("Your choice: ");
io::stdin()
.read_line(&mut choice)
.expect("Cannot read user input");
println!();
if choice.as_str() == "p" {
println!("Hello!");
println!();
};
}
}
Notes: if you don’t understand why we need to do “choice.as_str()” to compare String with string slices, I encourage you to read another of my articles.
Did you find all three problems with the above code? 🤓
If yes, then wonderful I don’t have anything else to teach you here otherwise you can keep reading. Of course you can still keep reading to compare your foundings with mine.
Flush it
If you ran the above code you might have noticed that the print!("Your choice: ")
call is not displayed when you’d expect it to be:
It should be displayed right after the line: (q)uit the program
however it is displayed after the user answer. This is due to the fact that stdout
is line-buffered by default so the content of the current line won’t be displayed until a line return character is encountered. To bypass this behavior we need to call std::io::stdout().flush()
. I learned it thanks to this StackOverflow answer. The code is now:
use std::io;
use std::io::Write;
fn main() {
let mut choice = String::new();
while choice.as_str() != "q" {
println!("What do you want to do?");
println!("(p)rint hello");
println!("(q)uit the program");
print!("Your choice: ");
io::stdout().flush().expect("Cannot flush stdout");
io::stdin()
.read_line(&mut choice)
.expect("Cannot read user input");
println!();
if choice.as_str() == "p" {
println!("Hello!");
println!();
};
}
}
Two things changed here:
- We called
flush
after theprint!
call that way the user answer is displayed right after it. I usedexpect
here becauseflush
returns aResult
enum value. We need to handle it in case we got an error.expect
was just the convenient way of handling errors in this small example. - We also needed to bring into scope the trait that
flush
relies upon to do its job:Write
.
Great! Now it displays properly but it ain’t over yet 🥵.
Make a clean slate
If you played with the program already, you might have noticed that no matter what choice you make the program doesn’t take it into account. See for yourself:
From here it’s a bit hard to see what’s going on so let’s display choice
content. You need to add this:
// --snip--
println!("You selected: {}", choice);
if choice.as_str() == "p" {
println!("Hello!");
println!();
};
// --snip--
Here is what we can see:
choice
contains every input we provided to the program… I wasn’t expecting that either but seems logical. read_line()
actually appends every choice you type to choice
whereas I was expecting it to replace its content every time. Do I hear RTFM! in the back? Well it’s definitely written in the function documentation. 😅
Nevermind friend! It happens that there is a function on the String
type that will be quite handy in our case: clear
. This function truncates the String
, removing all contents. Let’s use it right after we enter the while
loop:
while choice.as_str() != "q" {
choice.clear();
// --snip--
}
Let’s give it a try:
Hooray! choice
now has only the content we’re interested in… not so quite yet. Actually there is still an intruder 🐱👤. Did you find it yet?
Trim the edges off!
The read_line
function does exactly what it’s intended for: reading the user inputs until the Enter key is pressed. The thing is that it also captures the actual line return character and saves it inside choice
. The while
condition always evaluates to true
: "q\n" != "q"
. On the other hand the if
condition always evaluates to false
: "p\n" == "p"
.
The fix is simple. Use the String.trim()
function instead of as_str()
in the conditions. We don’t need to call again as_str()
because trim()
returns a &str
.
Let’s give it a try:
We’re done! Our little program works properly now. 🤝
The final program is:
use std::io;
use std::io::Write;
fn main() {
let mut choice = String::new();
while choice.trim() != "q" {
choice.clear();
println!("What do you want to do?");
println!("(p)rint hello");
println!("(q)uit the program");
print!("Your choice: ");
io::stdout().flush().expect("Cannot flush stdout");
io::stdin()
.read_line(&mut choice)
.expect("Cannot read user input");
println!();
println!("You selected: {}", choice);
if choice.trim() == "p" {
println!("Hello!");
println!();
};
}
}
Conclusion
We learned that flush
is needed when you want to display the content of the current line without waiting for an actual line ending character. We saw that read_line
appends the user inputs into the variable it’s passed rather than replacing its content. Finally we discovered that we need to clear any whitespace and line ending character from the user inputs in order to properly compare it with string slices.
That’s it for today. I hope it helped!