Skip to content
Shell & TerminalDeep Dive Published Updated 8 min readViews unavailable

Bash select Menus: Validate Interactive Choices and Bound Input

Use Bash select for small interactive menus while handling REPLY, invalid choices, redirected input, repeated actions, and EOF explicitly.

Bash’s select compound command builds a small numbered menu from a list and repeatedly runs a block for the user’s choice. It is useful for a short interactive terminal workflow, such as choosing one of a few diagnostic actions. It is not a command-line argument parser, a graphical selector, or a safe protocol for unattended automation. A production script should make that boundary explicit and keep the operation behind the menu separate from the menu itself.

select supplies a choice number and the corresponding list value. It does not validate whether the chosen action is authorized, whether required files exist, or whether a destructive operation is appropriate. Treat the selection as input, map it to a controlled set of actions, and perform the real work in functions that can also be tested without an interactive prompt.

Understand the menu loop

The basic syntax is select name in words; do commands; done. Bash expands the words to make the displayed list. It writes numbered choices, prints the PS3 prompt, reads one line from standard input, assigns the selected word to name, and executes the body. The body repeats until it reaches break, returns from a function, or input reaches EOF. The raw line read is available in REPLY.

PS3='Choose a report: '

select report in 'Disk usage' 'Service state' 'Quit'; do
    case $REPLY in
        1) printf 'Selected: %s\n' "$report" ;;
        2) printf 'Selected: %s\n' "$report" ;;
        3) break ;;
        *) printf 'Choose a listed number, not %q.\n' "$REPLY" >&2 ;;
    esac
done

This handler branches on the numeric response rather than using the display text as a command name. The report variable is a presentation value; it should not be passed to eval or interpolated into a command string. Keep choices static and map them to explicit functions or case arms. If a list label changes for clarity, the action mapping should not accidentally change with it.

If the in words clause is omitted, Bash uses the positional parameters. When passing a dynamic list, write select choice in "$@" so each argument remains one entry, including entries with spaces or empty strings. Unquoted $@ or $* can split a single argument into multiple menu rows and apply pathname expansion. The same quoting discipline used for ordinary arguments applies to the menu source list.

Distinguish the selected value from the user’s response

On a valid numeric selection, Bash assigns the corresponding list word to the loop variable. The REPLY variable retains the raw input line. If the response is not a valid list index, the selected word is empty while REPLY still contains what the user entered. That lets the body distinguish an invalid selection from a valid choice whose label happens to be an empty string.

PS3='Action number: '
select action in status restart quit; do
    if [[ -z $action ]]; then
        printf 'No action for input %q.\n' "$REPLY" >&2
        continue
    fi

    case $action in
        status) show_status ;;
        restart) request_restart ;;
        quit) break ;;
    esac
done

In robust code, make list entries non-empty and unique if you use the selected word as the case key. If duplicate labels are a valid display requirement, branch on a separately designed identifier rather than assuming the visible text uniquely names an operation. Always put a default case in the case statement; it makes future list edits fail visibly rather than silently falling through.

select is line-oriented, not a single-keystroke menu. A user normally enters a number and presses Return. A blank line causes the choices to be displayed again. Invalid input does not automatically terminate the loop; the body decides whether to retry, display help, or leave. If input is redirected from a file or pipe, the same rules apply, but there may be no human available to correct a bad line. A menu is therefore a poor default interface for cron, CI, deployment pipelines, and scripts that must be composable.

Handle EOF and interruption as normal control flow

EOF is different from an invalid response. When input ends, the loop terminates rather than prompting forever. A script should decide what that means for the enclosing operation and return a meaningful status. Interactive cancellation is also not necessarily the same as selecting “Quit”: a terminal interrupt can trigger signal handling outside the loop. If the distinction matters, use the shell’s documented signal behavior and test it in a disposable terminal.

choose_action() {
    local action PS3='Action: '

    select action in inspect quit; do
        case $REPLY in
            1) inspect_target; return $? ;;
            2) return 0 ;;
            *) printf 'Enter 1 or 2.\n' >&2 ;;
        esac
    done

    # select left because standard input reached EOF.
    return 2
}

The function makes EOF distinguishable from the user’s explicit quit choice. Its calling code must still decide how to interpret status 2. A library function should generally return a status instead of calling exit, because exiting would terminate the entire shell that sourced the library. A top-level script can translate the function result into its own exit status.

Do not assume PS3 is a security boundary or a reliable logging channel. It is a prompt string; keep it static or quote any interpolated data. Avoid embedding terminal control sequences from untrusted input. If you need an audit trail, log the selected action through a separate, explicit logging path and avoid recording secrets typed at the prompt.

Separate presentation, policy, and side effects

The cleanest design keeps the menu block short. A function can gather the choice and dispatch to well-named operations. Each operation validates prerequisites and owns its side effects. That structure lets tests invoke show_status or request_restart directly, without simulating keyboard input or making the test harness depend on terminal behavior.

show_menu() {
    local choice PS3='Select: '

    select choice in status details quit; do
        case $REPLY in
            1) show_status ;;
            2) show_details ;;
            3) return 0 ;;
            *) printf 'Invalid menu index: %q\n' "$REPLY" >&2 ;;
        esac
    done

    return 2
}

The choice label is not trusted merely because it came from Bash’s own menu display. A caller can pipe arbitrary lines into standard input. If an action deletes, overwrites, restarts, or publishes something, validate the target and ask for confirmation at the point where the impact is clear. Consider a non-interactive mode with explicit flags and a dry-run option rather than asking a CI agent to answer a menu.

For one-time commands, select may be unnecessary overhead. A simple numbered prompt built with read gives control over timeouts, input descriptors, and retry count. Conversely, if the menu is a long-running prompt in a terminal application, a dedicated TUI library provides richer navigation and terminal-resize behavior. Pick select for the scope it actually covers: a few choices, a line of input, and a shell loop.

Test the actual input cases

Test a menu with every valid index, an out-of-range integer, nonnumeric text, a blank line, and EOF. Include list values with spaces if the choices are dynamic. Confirm that the choice variable and REPLY have the expected values, that an explicit quit returns the expected status, and that EOF cannot start an operation accidentally. Test the script both from a terminal and with redirected input, because terminal prompts can conceal assumptions that fail in batch execution.

Run syntax checks with the Bash versions you support and test behavior in a clean non-interactive process. Shell startup files can change options, functions, or PS3; reproducibility matters when a menu is used in a managed environment. Keep the menu path side-effect free until a valid action has been selected, and put its security checks in the action itself. This turns select from a shortcut into a small, auditable interface.

Input can also be redirected from a terminal, a file, or a pipeline. If a menu is fed by automation, every menu decision should be represented as a deliberate line in that input; do not let a default selection or an empty line start a privileged action. Detect whether the program is running with a terminal when the interaction is mandatory, and fail with an actionable message if it is not. For a non-interactive caller, accept a named option or positional argument and bypass the menu entirely.

The terminal presentation is also stateful. PS3 may be inherited from the environment or modified by startup files, and an interactive shell can have traps and options that a clean test shell does not. Set a prompt for the function’s own interaction and restore any state you temporarily change. Do not encode control characters in a prompt from untrusted input, and keep output from the selected action separate from the menu display so it can be piped or captured predictably.

Finally, limit retries when input is malformed. An interactive user can correct an entry, but a redirected stream may contain many invalid lines or never reach EOF if its producer remains open. A menu used in a long-running tool can track an attempt budget or provide an explicit cancel operation. This is a policy choice rather than a property supplied by select; a reliable interface defines how it behaves under both human and machine input.

Related:

Sources:

Comments