Bash can build interactive menus that feel almost like a GUI, right from your terminal.

Let’s say you want a simple script to manage a few services: start, stop, or check their status.

#!/bin/bash

# Function to display the menu
show_menu() {
    clear
    echo "============================"
    echo "  Service Management Menu"
    echo "============================"
    echo "1. Start Web Server"
    echo "2. Stop Web Server"
    echo "3. Check Web Server Status"
    echo "4. Start Database"
    echo "5. Stop Database"
    echo "6. Check Database Status"
    echo "7. Exit"
    echo "============================"
    read -p "Enter your choice [1-7]: " choice
}

# Function to handle web server actions
handle_web_server() {
    case $1 in
        "start")
            echo "Starting web server..."
            # Simulate starting the server
            sleep 1
            echo "Web server started."
            ;;
        "stop")
            echo "Stopping web server..."
            # Simulate stopping the server
            sleep 1
            echo "Web server stopped."
            ;;
        "status")
            echo "Checking web server status..."
            # Simulate checking status
            sleep 1
            echo "Web server is running."
            ;;
    esac
    read -p "Press Enter to continue..."
}

# Function to handle database actions
handle_database() {
    case $1 in
        "start")
            echo "Starting database..."
            # Simulate starting the database
            sleep 1
            echo "Database started."
            ;;
        "stop")
            echo "Stopping database..."
            # Simulate stopping the database
            sleep 1
            echo "Database stopped."
            ;;
        "status")
            echo "Checking database status..."
            # Simulate checking status
            sleep 1
            echo "Database is running."
            ;;
    esac
    read -p "Press Enter to continue..."
}

# Main loop
while true; do
    show_menu
    case $choice in
        1) handle_web_server "start" ;;
        2) handle_web_server "stop" ;;
        3) handle_web_server "status" ;;
        4) handle_database "start" ;;
        5) handle_database "stop" ;;
        6) handle_database "status" ;;
        7) echo "Exiting."; exit 0 ;;
        *) echo "Invalid choice. Please enter a number between 1 and 7."; sleep 2 ;;
    esac
done

When you run this script, you see the show_menu function clear the screen and print the options. The read -p command then pauses the script, waiting for user input, and stores it in the choice variable. The case statement then directs execution to the appropriate handler function based on the user’s input. Each handler performs its simulated task and then waits for another Enter press to return to the menu. This loop continues until the user selects option 7 to exit.

The core of this interactivity comes from read. It’s a built-in Bash command that reads a line from standard input and assigns it to one or more variables. The -p option is crucial for user experience; it displays a prompt string before reading input, making it clear to the user what is expected. clear is another key command for menu-like interfaces, ensuring a clean slate for each menu display.

To manage different sections of the menu, we use functions. This keeps the code organized and reusable. Each function (handle_web_server, handle_database) takes an argument (like "start", "stop", "status") and uses its own case statement to perform specific actions. This modularity is key to building more complex interactive scripts. The while true loop is the engine that keeps the menu alive, constantly redisplaying the menu after an action is completed, until an explicit exit command is encountered.

The select command in Bash offers an even more streamlined way to create menus, especially when you have a list of items to choose from. Instead of manually creating numbered options and handling read and case, select automates much of that.

Consider this alternative for the menu display and choice handling:

#!/bin/bash

# Array of options
options=("Start Web Server" "Stop Web Server" "Check Web Server Status" "Start Database" "Stop Database" "Check Database Status" "Exit")

# Function to display the menu using select
show_menu_select() {
    PS3="Enter your choice: " # Prompt for select
    select opt in "${options[@]}"
    do
        case $opt in
            "Start Web Server")
                echo "Starting web server..."
                sleep 1
                echo "Web server started."
                ;;
            "Stop Web Server")
                echo "Stopping web server..."
                sleep 1
                echo "Web server stopped."
                ;;
            "Check Web Server Status")
                echo "Checking web server status..."
                sleep 1
                echo "Web server is running."
                ;;
            "Start Database")
                echo "Starting database..."
                sleep 1
                echo "Database started."
                ;;
            "Stop Database")
                echo "Stopping database..."
                sleep 1
                echo "Database stopped."
                ;;
            "Check Database Status")
                echo "Checking database status..."
                sleep 1
                echo "Database is running."
                ;;
            "Exit")
                echo "Exiting."
                break # Exit the select loop
                ;;
            *) echo "Invalid option $REPLY";;
        esac
        # After an action, we want to show the menu again.
        # The 'select' command automatically re-displays the menu after each valid choice.
        # For invalid choices, we break and then need to re-enter the select loop.
        # However, a simpler approach is to just let select handle it, and for exiting, we use 'break'.
        # If we wanted to return to the menu after *every* action, we'd need a loop around 'select'.
        # For this example, 'select' will re-prompt after each valid action until 'Exit' is chosen.
        # If we wanted to go back to the menu *after* each action (like the first example),
        # we'd need a while loop around the select command.
        # The 'break' statement here only exits the 'select' loop.
        # To exit the script, we need to handle the 'Exit' case.
        if [[ "$opt" == "Exit" ]]; then
            exit 0
        fi
    done
}

# Call the function
show_menu_select

The select command iterates over the items in the array options. It automatically displays them with numbers, sets the prompt to PS3, and waits for input. When a valid number is entered, select assigns the corresponding item to the variable opt and executes the case statement. Crucially, after the case statement finishes for a valid selection, select automatically re-displays the menu and prompts again, creating a continuous menu loop without an explicit while true in the main flow. The REPLY variable holds the raw user input. Using break within the select loop exits that loop, not necessarily the script. The "Exit" option explicitly calls exit 0 to terminate the script.

The most surprising aspect of Bash menus is how select handles the loop. It doesn’t just read input; it actively re-displays the numbered menu and re-prompts after each valid selection, making it inherently cyclical until explicitly broken out of.

After you’ve mastered interactive menus, your next step is handling user input with more sophisticated validation and error recovery, perhaps by parsing more complex command-line arguments or using read with timeouts.

Want structured learning?

Take the full Bash course →