Reading from stdin
If you want to create an interactive application, it's easy to prototype your functionality with the command line. For CLI programs, this will be all the interaction you need.
How to do it...
In the
src/bin
folder, create a file calledstdin.rs
Add the following code and run it with
cargo run --bin stdin
:
1 use std::io; 2 use std::io::prelude::*; 3 4 fn main() { 5 print_single_line("Please enter your forename: "); 6 let forename = read_line_iter(); 7 8 print_single_line("Please enter your surname: "); 9 let surname = read_line_buffer(); 10 11 print_single_line("Please enter your age: "); 12 let age = read_number(); 13 14 println!( 15 "Hello, {} year old human named {} {}!", 16 age, forename, surname 17 ); 18 } 19 20 fn print_single_line(text: &str) { 21 // We can print lines without adding a newline 22 print!("{}", text); 23 // However, we need to flush stdout afterwards 24 // in order to guarantee that the data actually displays 25 io::stdout().flush().expect("Failed to flush stdout"); 26 } 27 28 fn read_line_iter() -> String { 29 let stdin = io::stdin(); 30 // Read one line of input iterator-style 31 let input = stdin.lock().lines().next(); 32 input 33 .expect("No lines in buffer") 34 .expect("Failed to read line") 35 .trim() 36 .to_string() 37 } 38 39 fn read_line_buffer() -> String { 40 // Read one line of input buffer-style 41 let mut input = String::new(); 42 io::stdin() 43 .read_line(&mut input) 44 .expect("Failed to read line"); 45 input.trim().to_string() 46 } 47 48 fn read_number() -> i32 { 49 let stdin = io::stdin(); 50 loop { 51 // Iterate over all lines that will be inputted 52 for line in stdin.lock().lines() { 53 let input = line.expect("Failed to read line"); 54 // Try to convert a string into a number 55 match input.trim().parse::<i32>() { 56 Ok(num) => return num, 57 Err(e) => println!("Failed to read number: {}", e), 58 } 59 } 60 } 61 }
How it works...
In order to read from the standard console input, stdin
, we first need to obtain a handle to it. We do this by calling io::stdin()
[29]. Imagine the returned object as a reference to a global stdin
object. This global buffer is managed by a Mutex
, which means that only one thread can access it at a time (more on that later in the book, in the Parallelly accessing resources with Mutexes section inChapter 7,Parallelism and Rayon). We get this access by locking (usinglock()
) the buffer, which returns a new handle [31]. After we have done this, we can call thelines
method on it, which returns an iterator over the lines the user will write [31 and 52]. More on iterators in the Accessing collections as Iterators section inChapter 2,Working with Collections.
Finally, we can iterate over as many submitted lines as we want until some kind of break condition is reached, otherwise the iteration would go on forever. In our example, we break the number-checking loop as soon as a valid number has been entered [56].
If we're not particularly picky about our input and just want the next line, we have two options:
We can continue using the infinite iterator provided by
lines()
, but simply call next on it in order to just take the first one. This comes with an additional error check as, generally speaking, we cannot guarantee that there is a next element.We can use
read_line
in order to populate an existing buffer [43]. This doesn't require that welock
the handler first, as it is done implicitly.
Although they both result in the same end effect, you should choose the first option. It is more idiomatic as it uses iterators instead of a mutable state, which makes it more maintainable and readable.
On a side note, we are using print!
instead of println!
in some places in this recipe for aesthetic reasons [22]. If you prefer the look of newlines before user input, you can refrain from using them.
There's more...
This recipe is written with the assumption that you want to use stdin for live interaction over the cli
. If you plan on instead piping some data into it (for example, cat foo.txt | stdin.rs
on *nix), you can stop treating the iterator returned by lines()
as infinite and retrieve the individual lines, not unlike how you retrieved the individual parameters in the last recipe.
There are various calls to trim()
in our recipe [35, 45 and 55]. This method removes leading and trailing whitespace in order to enhance the user-friendliness of our program. We are going to look at it in detail in the Using a string section in Chapter 2, Working with Collections.
See also
- Interacting with environment variables recipe inChapter 1, Learning the Basics
- Using a string and Accessing collections as iterators recipe inChapter 2, Working with Collections
- Parallelly accessing resources with Mutexes recipe inChapter 7, Parallelism and Rayon