To complement Aserre's helpful answer, which explains the problem with your code and offers an effective workaround, with background information and a generic, reusable custom select
implementation that allows empty input:
Background information
To spell it out explicitly: select
itself ignores empty input (just pressing Enter) and simply re-prompts - user code doesn't even get to run in response.
In fact, select
uses the empty string to signal to user code that an invalid choice was typed.
That is, if the output variable - $opt
, int this case - is empty inside the select
statement, the implication is that an invalid choice index was typed by the user.
The output variable receives the chosen option's text - either 'Yep'
or 'Nope'
in this case - not the index typed by the user.
(By contrast, your code examines $REPLY
instead of the output variable, which contains exactly what the user typed, which is the index in case of a valid choice, but may contain extra leading and trailing whitespace).
Note that in the event that you didn't want to allow empty input, you could
simply indicate to the user in the prompt text that ^C
(Ctrl+C) can be used to abort the prompt.
Generic custom select
function that also accepts empty input
The following function closely emulates what select
does while also allowing empty input (just pressing Enter). Note that the function intercepts invalid input, prints a warning, and re-prompts:
# Custom `select` implementation that allows *empty* input.
# Pass the choices as individual arguments.
# Output is the chosen item, or "", if the user just pressed ENTER.
# Example:
# choice=$(selectWithDefault 'one' 'two' 'three')
selectWithDefault() {
local item i=0 numItems=$#
# Print numbered menu items, based on the arguments passed.
for item; do # Short for: for item in "$@"; do
printf '%s\n' "$((++i))) $item"
done >&2 # Print to stderr, as `select` does.
# Prompt the user for the index of the desired item.
while :; do
printf %s "${PS3-#? }" >&2 # Print the prompt string to stderr, as `select` does.
read -r index
# Make sure that the input is either empty or that a valid index was entered.
[[ -z $index ]] && break # empty input
(( index >= 1 && index <= numItems )) 2>/dev/null || { echo "Invalid selection. Please try again." >&2; continue; }
break
done
# Output the selected item, if any.
[[ -n $index ]] && printf %s "${@: index:1}"
}
You could call it as follows:
# Print the prompt message and call the custom select function.
echo "Include audits (default is 'Nope')?"
optionsAudits=('Yep' 'Nope')
opt=$(selectWithDefault "${optionsAudits[@]}")
# Process the selected item.
case $opt in
'Yep') includeAudits=true; ;;
''|'Nope') includeAudits=false; ;; # $opt is '' if the user just pressed ENTER
esac
Alternative implementation that lets the function itself handle the default logic:Thanks, RL-S
This implementation differs from the above in two respects:
It allows you to designate a default among the choices, by prefixing it with !
, with the first choice becoming the default otherwise. The default choice is printed with a trailing [default]
(and without the leading !
). The function then translates empty input into the default choice.
The selected choice is returned as a 1
-based index rather than the text. In other words: you can assume that a valid choice was made when the function returns, and that choice is indicated by its position among the choices given.
# Custom `select` implementation with support for a default choice
# that the user can make by pressing just ENTER.
# Pass the choices as individual arguments; e.g. `selectWithDefault Yes No``
# The first choice is the default choice, unless you designate
# one of the choices as the default with a leading '!', e.g.
# `selectWithDefault Yes !No`
# The default choice is printed with a trailing ' [default]'
# Output is the 1-based *index* of the selected choice, as shown
# in the UI.
# Example:
# choice=$(selectWithDefault 'Yes|No|!Abort' )
selectWithDefault() {
local item i=0 numItems=$# defaultIndex=0
# Print numbered menu items, based on the arguments passed.
for item; do # Short for: for item in "$@"; do
[[ "$item" == !* ]] && defaultIndex=$(( $i + 1)) && item="${item:1} [default]"
printf '%s\n' "$((++i))) $item"
done >&2 # Print to stderr, as `select` does.
# Prompt the user for the index of the desired item.
while :; do
printf %s "${PS3-#? }" >&2 # Print the prompt string to stderr, as `select` does.
read -r index
# Make sure that the input is either empty or that a valid index was entered.
[[ -z $index ]] && index=$defaultIndex && break # empty input == default choice
(( index >= 1 && index <= numItems )) 2>/dev/null || { echo "Invalid selection. Please try again." >&2; continue; }
break
done
# Output the selected *index* (1-based).
printf $index
}
Sample call:
# Print the prompt message and call the custom select function,
# designating 'Abort' as the default choice.
echo "Include audits?"
ndx=$(selectWithDefault 'Yes' 'No', '!Abort')
case $ndx in
1) echo "include";;
2) echo "don't include";;
3) echo "abort";;
esac
Optional reading: A more idiomatic version of your original code
Note: This code doesn't solve the problem, but shows more idiomatic use of the select
statement; unlike the original code, this code re-displays the prompt if an invalid choice was made:
optionsAudits=("Yep" "Nope")
echo "Include audits (^C to abort)?"
select opt in "${optionsAudits[@]}"; do
# $opt being empty signals invalid input.
[[ -n $opt ]] || { echo "What's that? Please try again." >&2; continue; }
break # a valid choice was made, exit the prompt.
done
case $opt in # $opt now contains the *text* of the chosen option
'Yep')
includeAudits=true
;;
'Nope') # could be just `*` in this case.
includeAudits=false
;;
esac
Note:
The case
statement was moved out of the select
statement, because the latter now guarantees that only valid inputs can be made.
The case
statement tests the output variable ($opt
) rather than the raw user input ($REPLY
), and that variable contains the choice text, not its index.
select
statement is unusual, because it examines the raw user input -$REPLY
- rather than theselect
statement's output variable,$opt
(which contains the chosen option's text, not the index). – Bereave