The select loop construct in bash is not part of the posix standard. Though, it is widely used to easily generate interactive menus in a shell script. This post covers how to use a select loop with the select keyword in bash to create a simple selectable menu in a shell script.
What is a select loop in a shell script?
The select
construct is a bash shorthand to generate a numbered menu. It is not part of the POSIX shell standard. It is a useful command to easily ask a user to choose from a list of options.
Despite its name, the select loop is not part of the
bash loop constructs. Like the
bash if keyword, the select
keyword is part of the conditional constructs in the bash manual.
The select loop syntax is very close to the one of a for loop: select name [in words …]; do commands; done
.
The list of words after the in
keyword is expanded by the bash shell to generate a list of items (menu). The menu items are printed on the standard error output stream (STDERR), preceded by a number. Similar to the
bash for loop, if the [in words ...]
is omitted, the positional parameters are used as if using in "$@"
.
The select loop uses the PS3
variable to prompt the user. The user selection is read from the standard input. If the user selection is empty, the menu and the PS3 prompt is shown again. If a number is provided by the user, then the name
variable from the select loop is set to the corresponding word (menu item). Any other value read causes name
to be set to null.
The select command complete when an EOF
is read or the command is interrupted with a break
. The line read (number) is saved to the variable REPLY
.
Below is a classic example to prompt a user to choose a filename from the current directory, and displays the filename and index of the file selected.
select filename in *;
do
echo "You selected $filename ($REPLY)"
break;
done
👉 The select loop can be nested to create submenus, though the PS3 prompt variable is not changed when entering a nested loop. In such a case, make sure to set the PS3 variable accordingly.
How can I create a select menu in bash?
Below is an example using the bash select loop to generate a selectable menu from a bash array, using the PS3 variable to set the user prompt, and displaying the menu and index selected.
The menu display a list of animal and object and prompt the user to select one. The select loop continues if a non-animal item is selected from the menu and end when an animal is selected from the menu. This example does not test for invalid options which could be accomplished by using a bash case construct.
PS3="Choose an animal: "
options=(cat dog mouse chair cow bird apple)
select menu in "${options[@]}";
do
echo -e "\nyou picked $menu ($REPLY)"
if [[ $menu == "chair" || $menu == "apple" ]]; then
echo -e "$menu is not an animal\n"
else
echo "$menu is an animal"
break;
fi
done
When running this example, the output would look like below when first selecting the option 4
then 1
.
[me@linux ~]$ ./my-example-script
1) cat
2) dog
3) mouse
4) chair
5) cow
6) bird
7) apple
Choose an animal: 4
you picked chair (4)
chair is not an animal
Choose an animal: 1
you picked cat (1)
cat is an animal
How to pipe options to a select loop in bash?
As we mentioned earlier, the select command completes when EOF
is read and it reads the user selection from the standard input STDIN
which is the keyboard input in an interactive script. Though this is not the case when the command is pipped to your script, the standard output STDOUT
of the previous pipped command becomes the STDIN
.
#!/usr/bin/bash
# script: select-loop.sh
select opts;
do
echo "You selected $opts ($REPLY)"
break;
done
# Output of executing `select-loop.sh option1 option2` and selecting 1
1) option1
2) option2
#? 1
You selected option1 (1)
# Executing the script with a pipe result in no output / no select menu
[me@linux ~]$ echo "option1 option2" | ./select-loop.sh
[me@linux ~]$
To fix this behavior, we need to ensure the select menu will read from the /dev/tty
and that we are passing the option with proper word delimiter using the echo
or printf
commands.
#!/usr/bin/bash
# script: select-loop.sh
readarray -t opts
select item in "${opts[@]}";
do
echo "You selected $item ($REPLY)"
break;
done < /dev/tty
# Executing the script with a pipe and using the echo command
[me@linux ~]$ echo -e "option1\noption2" | ./select-loop.sh
1) option1
2) option2
#? 1
You selected option1 (1)
# Executing the script with a pipe and using the printf command
[me@linux ~]$ printf "%s\n" "option 1" "option 2" | ./select-loop.sh
1) option1
2) option2
#? 1
You selected option1 (1)