| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user |
| Scope | Bash select syntax, numbered menus on standard error, PS3 prompts, the selected variable and REPLY, invalid-choice validation, case branches, menus from arrays and globs, function arguments as menu items, quit with break, and blank-line or EOF behavior. Does not cover full case pattern reference, array CRUD, or interactive read option lists. |
| Related guides | Bash shell script tutorial Shell scripting course hub |
Bash select creates a numbered terminal menu without manually printing and parsing every option. PS3 controls the prompt, the loop variable holds the selected item, and REPLY holds the input line the user entered.
Bash select is not part of POSIX sh. Run scripts with a Bash shebang and invoke Bash directly — see Bash vs sh before pointing /bin/sh at a menu script. Bash prints the numbered list once, then waits at the PS3 prompt; the list is printed again only when the user submits a blank line. The loop keeps going until you break, return, or hit EOF.
| Need | Use |
|---|---|
| Create menu | select item in ...; do ...; done |
| Change prompt | PS3='Choose: ' |
| Selected text | $item |
| Raw input / menu number | $REPLY |
| Invalid choice | [[ -z $item ]] |
| Exit menu | break |
Bash select Syntax
A select loop has the same shape as for, but Bash drives the menu and input for you:
select NAME [in WORDS]; do
COMMANDS
doneWhen you name words after in, Bash numbers each word and waits for the user to type a number. Omit in WORDS and Bash uses the script's positional parameters ("$@") instead. The numbered menu goes to standard error so you can redirect normal script output without hiding the choices.
Build Your First Numbered Menu
Start with a deployment picker backed by an array:
PS3='Choose deployment target (1-3): '
environments=(production staging development)
select env in "${environments[@]}"; do
echo "Deploying to $env"
break
donePipe 2 as the menu choice to run the same block non-interactively:
printf '2\n' | bash -c 'PS3="Choose deployment target (1-3): "; environments=(production staging development); select env in "${environments[@]}"; do echo "Deploying to $env"; break; done'1) production
2) staging
3) development
Choose deployment target (1-3): Deploying to stagingThe menu lines and PS3 prompt appear on stderr; Deploying to staging is ordinary stdout from echo. Option 2 maps to staging, which lands in env.
Customize the Prompt with PS3
PS3 holds the prompt string Bash prints after the numbered list. The default is #? , which is easy to miss in a real script. Set it before select:
PS3='Region code [1-3]: '
regions=(us-east eu-west ap-south)
select region in "${regions[@]}"; do
echo "Selected region: $region"
break
doneRun with choice 1 to confirm the custom prompt:
printf '1\n' | bash -c 'PS3="Region code [1-3]: "; regions=(us-east eu-west ap-south); select region in "${regions[@]}"; do echo "Selected region: $region"; break; done'1) us-east
2) eu-west
3) ap-south
Region code [1-3]: Selected region: us-eastKeep the prompt short and mention the valid range when the list is longer than a few items. Do not put spaces around = in the assignment: use PS3='Choose: ', not PS3= 'Choose: '.
Get the Selected Value and Menu Number with REPLY
After Enter, the loop variable gets the word that matches the number and REPLY gets the raw input line. If you need the numeric menu choice rather than the selected text, read $REPLY. Inspect both:
printf '2\n' | bash -c 'PS3="Pick: "; select x in alpha beta gamma; do echo "x=$x REPLY=$REPLY"; break; done'1) alpha
2) beta
3) gamma
Pick: x=beta REPLY=2REPLY is the same variable Bash read uses when you omit a variable name. In select, it always holds what was typed, whether or not that number matched a menu item.
Validate Invalid Menu Choices
An out-of-range number or non-numeric text leaves the selected variable empty while REPLY still records the input. Bash prints no error. Wrap the check in Bash if else before you act on the selected word:
PS3='Pick an environment: '
select env in production staging development; do
if [[ -z $env ]]; then
echo "Invalid choice: $REPLY (enter 1-3)" >&2
continue
fi
echo "Using $env"
break
doneFeed an out-of-range number followed by a valid one:
printf '9\n2\n' | bash -c 'PS3="Pick an environment: "; select env in production staging development; do if [[ -z $env ]]; then echo "Invalid choice: $REPLY (enter 1-3)" >&2; continue; fi; echo "Using $env"; break; done'1) production
2) staging
3) development
Pick an environment: Invalid choice: 9 (enter 1-3)
Using stagingcontinue skips the rest of the body and returns to the next PS3 prompt without exiting the script. That pattern is safer than assuming a non-empty variable after every select pass.
Use select with case
Branch on the selected word with Bash case:
PS3='Action: '
select action in deploy status logs Quit; do
case $action in
deploy) echo "Running deploy..." ;;
status) echo "Checking status..." ;;
logs) echo "Tailing logs..." ;;
Quit) echo "Bye."; break ;;
*) echo "Invalid"; continue ;;
esac
break
doneChoose status to see the matching branch run:
printf '2\n' | bash -c 'PS3="Action: "; select action in deploy status logs Quit; do case $action in deploy) echo "Running deploy...";; status) echo "Checking status...";; logs) echo "Tailing logs...";; Quit) echo "Bye."; break;; *) echo "Invalid"; continue;; esac; break; done'1) deploy
2) status
3) logs
4) Quit
Action: Checking status...The * branch catches an empty $action when validation was skipped.
Build a Menu from an Array
"${array[@]}" expands to separate words even when items contain spaces:
regions=("us east" "eu west" "ap south")
PS3='Choose region: '
select region in "${regions[@]}"; do
echo "Region is: $region"
break
donePick option 2 to confirm multi-word items stay intact:
printf '2\n' | bash -c 'regions=("us east" "eu west" "ap south"); PS3="Choose region: "; select region in "${regions[@]}"; do echo "Region is: $region"; break; done'1) us east
2) eu west
3) ap south
Choose region: Region is: eu westQuote "${regions[@]}" every time; "${regions[*]}" can collapse entries. See split string into array to build the list from text.
Use Function Arguments as Menu Items
Without in WORDS, select reads from "$@" inside a Bash function:
pick_one() {
PS3='Choose: '
select item; do
echo "You picked: $item"
break
done
}
pick_one alpha beta gammaCall it with a piped choice of 1:
printf '1\n' | bash -c 'pick_one() { PS3="Choose: "; select item; do echo "You picked: $item"; break; done; }; pick_one alpha beta gamma'1) alpha
2) beta
3) gamma
Choose: You picked: alphaMenu items come from the call-site arguments instead of a hard-coded array inside the function.
Add a Quit Option with break
select does not exit after one successful pick. Add a quit item and use break when the user chooses it:
PS3='Select (or choose Quit): '
select choice in start stop restart Quit; do
case $choice in
Quit)
echo "Exiting."
break
;;
'')
echo "Invalid selection." >&2
continue
;;
*)
echo "Would run: $choice"
break
;;
esac
doneSend 4 to select Quit:
printf '4\n' | bash -c 'PS3="Select (or choose Quit): "; select choice in start stop restart Quit; do case $choice in Quit) echo "Exiting."; break;; "") echo "Invalid selection." >&2; continue;; *) echo "Would run: $choice"; break;; esac; done'1) start
2) stop
3) restart
4) Quit
Select (or choose Quit): Exiting.Without break, select prompts for another choice after the loop body finishes. The numbered list is redisplayed when the user enters a blank line, not after every normal pick.
Select Files Safely
Build the file list with a Bash globbing pattern into an array instead of parsing ls output. Filenames with spaces stay one menu entry per file:
(
shopt -s nullglob
config_dir=./configs
configs=("$config_dir"/*.conf)
((${#configs[@]})) || {
echo 'No .conf files found.' >&2
exit 1
}
PS3='Config file: '
select cfg in "${configs[@]}"; do
[[ -n $cfg ]] || { echo 'Invalid.' >&2; continue; }
echo "Editing $cfg"
break
done
)Create a configs directory with two sample files:
mkdir -p configs && printf 'port=8080\n' > configs/app.conf && printf 'debug=1\n' > configs/dev.confPipe 1 to pick the first .conf file from the glob:
printf '1\n' | bash -c '( shopt -s nullglob; config_dir=./configs; configs=("$config_dir"/*.conf); ((${#configs[@]})) || { echo "No .conf files found." >&2; exit 1; }; PS3="Config file: "; select cfg in "${configs[@]}"; do [[ -n $cfg ]] || { echo "Invalid." >&2; continue; }; echo "Editing $cfg"; break; done )'1) ./configs/app.conf
2) ./configs/dev.conf
Config file: Editing ./configs/app.confThe subshell enables nullglob only for this block, so a caller's existing shell options stay unchanged. With nullglob, an unmatched glob yields an empty array and the script exits before showing an empty picker. Because the menu runs in a subshell, variables set inside it do not persist after the closing ). Perform the action that uses $cfg inside the block, or preserve and restore the nullglob setting instead if you need the selected value afterward.
What Happens on Blank Input and EOF?
An empty line reprints the menu. The selected variable stays empty and REPLY is blank:
printf '\n1\n' | bash -c 'PS3="Pick: "; select x in a b; do echo "x=${x:-empty}"; break; done' 2>&11) a
2) b
Pick: 1) a
2) b
Pick: x=aThe menu block appears twice because the blank line triggered a redraw. EOF (Ctrl+D or closed input) ends the loop with exit status 1 — Bash exit codes explains how to read $? when input ends before a valid choice:
printf '' | bash -c 'PS3="Pick: "; select x in a b; do echo "x=$x"; done; echo "exit=$?"'1) a
2) b
Pick: exit=1Check $? after the loop if you must distinguish a deliberate quit from unexpected EOF on a pipe.
When select Is a Good Fit—and When It Is Not
select fits terminal-only admin scripts and quick wrappers where a numbered list is enough.
| Good fit | Poor fit |
|---|---|
| Pick one item from a short list you control | Arrow-key TUIs or GUI dialogs |
| Operator runs the script in an interactive terminal | Non-interactive CI or cron jobs |
| You want stderr menus separate from stdout data | Remote APIs or web front ends |
For cron or CI, pass the choice through Bash getopts flags or environment variables instead of a menu.
Summary
Bash select prints a numbered menu to stderr, reads a line into REPLY, and sets the loop variable to the matching word. Set PS3 for a clear prompt, validate with [[ -z $var ]] when the number is wrong, and branch with case once you have a real selection.
The loop runs again after every pick unless you break or return; the next pass returns to the PS3 prompt without reprinting the numbered list unless the user enters a blank line. Add a Quit item, build lists from "${array[@]}" or function "$@", and load filenames from globs instead of ls. EOF exits with status 1. Reserve select for interactive terminals and pass arguments directly when no human is at the keyboard.
References
Frequently Asked Questions
What is REPLY in Bash select?
REPLY is a Bash variable that stores the raw input line entered at the PS3 prompt. It holds the text whether or not that input matched a menu number. The loop variable gets the selected word only when the number is valid.
Why does select keep looping?
select is designed as a loop, not a one-shot question. After each selection, Bash runs the do block and returns to the PS3 prompt. The numbered list is printed again only when the user submits a blank line. Use break or return inside the body when you are done, or add a Quit option that breaks out.
How do I handle invalid input?
Test whether the loop variable is empty after each selection. An out-of-range number or non-numeric text leaves the variable unset while REPLY still records what was typed. Print an error, then continue to skip the rest of the body and return to the PS3 prompt.
Why do I get select: not found?
select is not POSIX sh syntax. Use a Bash shebang such as #!/usr/bin/env bash and run the script directly or with bash script.sh. Running sh script.sh can invoke a different shell that does not support select.
How do I change the prompt?
Assign the PS3 variable before the select statement. Bash prints the value of PS3 after the numbered list. You can include hints such as the valid numeric range in that string.
Can menu items contain spaces?
Yes, when each item is a separate element in an array and you expand with "${array[@]}". Bash treats one array element as one menu line even if the text contains spaces. Use "${array[@]}" so each element stays a separate menu item. Avoid "${array[*]}" here because it expands the array as a single word.

