Simple CLI menu in the terminal (color selector)

stdin match Option Some None Result Ok Err

A simple assignment: Given a list of colors, (e.g. blue green yellow white) present the user a menu on the command line:

  1. blue
  2. green
  3. yellow
  4. white

Allow the user to type in one of the numbers and show the name of the corresponding color. e.g. if the users types in 3, print out yellow. Make it flexible so changing the list will change the menu without the need for any further changes in the code.

Then move the colors out to a text file called colors.txt where each row is the name of a color and display the menu that way.

Solutions

There are a number of solutions, but they also have some common parts.

Showing the menu

We iterate over the values and we use enumerate to get a tuple of the index and the the value. However the index will start at 0 and we would like to show the menu starting from 1 so we display ix + 1

for (ix, value) in values.iter().enumerate() {
    println!("{}) {}", ix + 1, value);
}

Reading the input from STDIN

In this version we call expect on the read_line method. This will panic! in case we can't read from Standard Input.

let mut response = String::new();
std::io::stdin()
    .read_line(&mut response)
    .expect("Failed to get input");

I am not sure how could that happen in such an interactive application, but let's see an alternative in case you'd like to handle gracefully the case when reading from STDIN fails and you'd like to repeat the action.

match std::io::stdin().read_line(&mut response) {
    Ok(_) => {},
    Err(err) => {
        eprintln!("Error: {}", err);
        continue;
    }
}

Hard-coded colors - keep asking till we get a valid answer

examples/infinite-cli-menu-with-hard-coded-values/src/main.rs

fn main() {
    let colors = vec!["blue", "red", "green", "yellow"];

    let color = infinite_menu(colors);
    println!("The selected color is {}", color);
}

fn infinite_menu(values: Vec<&str>) -> &str {
    loop {
        for (ix, value) in values.iter().enumerate() {
            println!("{}) {}", ix + 1, value);
        }

        let mut response = String::new();
        std::io::stdin()
            .read_line(&mut response)
            .expect("Failed to get input");

        // match std::io::stdin().read_line(&mut response) {
        //     Ok(_) => {},
        //     Err(err) => {
        //         eprintln!("Error: {}", err);
        //         continue;
        //     }
        // }

        match response.trim_end().parse::<usize>() {
            Ok(idx) => {
                if 1 <= idx && idx <= values.len() {
                    return values[idx - 1];
                }
            }
            Err(_err) => {}
        }
        println!("Input must be a number between 1-{}", values.len());
    }
}

Read colors from file - keep asking till we get a valid answer

In order to make this work we change the vector we pass to the infinite_menu function to be a vector of String elements and we also set the returned value to be a String.

We also had to clone the string that we return to disconnect it from the vector that was passed to us.

examples/infinite-cli-menu/src/main.rs

fn main() {
    let path = "colors.txt";

    let colors = match std::fs::read_to_string(path) {
        Ok(val) => {
            val.split_terminator('\n').map(|row| row.to_owned()).collect::<Vec<String>>()
        },
        Err(err) => {
            eprintln!("Could no read '{}' Error: {}", path, err);
            std::process::exit(1);
        }
    };

    let color = infinite_menu(colors);
    println!("The selected color is {}", color);
}

fn infinite_menu(values: Vec<String>) -> String {
    loop {
        for (ix, value) in values.iter().enumerate() {
            println!("{}) {}", ix + 1, value);
        }
        let mut response = String::new();
        std::io::stdin()
            .read_line(&mut response)
            .expect("Failed to get input");

        match response.trim_end().parse::<usize>() {
            Ok(idx) => {
                if 1 <= idx && idx <= values.len() {
                    return values[idx - 1].clone();
                }
            }
            Err(err) => eprintln!("{}", err),
        }
        println!("Input must be a number between 1-{}", values.len());
    }
}

The list of colors:

examples/infinite-cli-menu/colors.txt

blue
red
green
yellow
black

Hard-coded colors - return an Option

In this example, instead of having a loop in which we keep asking the user till we get an acceptable answer, we only ask once and if it was not a whole number or was not in the expected range then we return None.

In this version we already pass a vector of String value, but it is built up from a vector of hard-coded &str values. This is "being prepared for the case when we are going to read from a file, but not actually doing it.

examples/cli-menu-returning-option/src/main.rs

fn main() {
    let colors = vec!["blue", "red", "green", "yellow"]
        .into_iter()
        .map(|str| str.to_string())
        .collect::<Vec<String>>();

    let color = match menu(&colors) {
        Some(val) => val,
        None => {
            eprintln!("No valid value was provided");
            std::process::exit(1);
        }
    };
    println!("The selected color is {}", color);
}

fn menu(values: &Vec<String>) -> Option<String> {
        for (ix, value) in values.iter().enumerate() {
            println!("{}) {}", ix + 1, value);
        }
        let mut response = String::new();
        std::io::stdin()
            .read_line(&mut response)
            .expect("Failed to get input");

        match response.trim_end().parse::<usize>() {
            Ok(idx) => {
                if 1 <= idx && idx <= values.len() {
                    return Some(values[idx - 1].clone());
                }
            }
            Err(_err) => {}
        }
        None
}

Conclusion

I think this is a nice demonstration of using match to find out if an operation succeeded. How to handle a Result that can be either Ok or Err?

It is also a nice way to see how to return an Option that can be either a real value (a String in our case) wrapped in Some or None and then how to handle that with match.

Author

Gabor Szabo (szabgab)

Gabor Szabo, the author of the Rust Maven web site maintains several Open source projects in Rust and while he still feels he has tons of new things to learn about Rust he already offers training courses in Rust and still teaches Python, Perl, git, GitHub, GitLab, CI, and testing.

Gabor Szabo