Multi-select menu in bash script
Solution 1
I think you should take a look at dialog or whiptail.
Edit:
Here's an example script using the options from your question:
#!/bin/bash
cmd=(dialog --separate-output --checklist "Select options:" 22 76 16)
options=(1 "Option 1" off # any option can be set to default to "on"
2 "Option 2" off
3 "Option 3" off
4 "Option 4" off)
choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
clear
for choice in $choices
do
case $choice in
1)
echo "First Option"
;;
2)
echo "Second Option"
;;
3)
echo "Third Option"
;;
4)
echo "Fourth Option"
;;
esac
done
Solution 2
If you think whiptail
is complex, here it goes a bash-only code that does exactly what you want. It's short (~20 lines), but a bit cryptic for a begginner. Besides showing "+" for checked options, it also provides feedback for each user action ("invalid option", "option X was checked"/unchecked, etc).
That said, there you go!
Hope you enjoy... its was quite a fun challenge to make it :)
#!/bin/bash
# customize with your own.
options=("AAA" "BBB" "CCC" "DDD")
menu() {
echo "Avaliable options:"
for i in ${!options[@]}; do
printf "%3d%s) %s\n" $((i+1)) "${choices[i]:- }" "${options[i]}"
done
if [[ "$msg" ]]; then echo "$msg"; fi
}
prompt="Check an option (again to uncheck, ENTER when done): "
while menu && read -rp "$prompt" num && [[ "$num" ]]; do
[[ "$num" != *[![:digit:]]* ]] &&
(( num > 0 && num <= ${#options[@]} )) ||
{ msg="Invalid option: $num"; continue; }
((num--)); msg="${options[num]} was ${choices[num]:+un}checked"
[[ "${choices[num]}" ]] && choices[num]="" || choices[num]="+"
done
printf "You selected"; msg=" nothing"
for i in ${!options[@]}; do
[[ "${choices[i]}" ]] && { printf " %s" "${options[i]}"; msg=""; }
done
echo "$msg"
Solution 3
Here's a way to do exactly what you want using only Bash features with no external dependencies. It marks the current selections and allows you to toggle them.
#!/bin/bash
# Purpose: Demonstrate usage of select and case with toggleable flags to indicate choices
# 2013-05-10 - Dennis Williamson
choice () {
local choice=$1
if [[ ${opts[choice]} ]] # toggle
then
opts[choice]=
else
opts[choice]=+
fi
}
PS3='Please enter your choice: '
while :
do
clear
options=("Option 1 ${opts[1]}" "Option 2 ${opts[2]}" "Option 3 ${opts[3]}" "Done")
select opt in "${options[@]}"
do
case $opt in
"Option 1 ${opts[1]}")
choice 1
break
;;
"Option 2 ${opts[2]}")
choice 2
break
;;
"Option 3 ${opts[3]}")
choice 3
break
;;
"Option 4 ${opts[4]}")
choice 4
break
;;
"Done")
break 2
;;
*) printf '%s\n' 'invalid option';;
esac
done
done
printf '%s\n' 'Options chosen:'
for opt in "${!opts[@]}"
do
if [[ ${opts[opt]} ]]
then
printf '%s\n' "Option $opt"
fi
done
For ksh, change the first two lines of the function:
function choice {
typeset choice=$1
and the shebang to #!/bin/ksh
.
Solution 4
Here's a bash function that allows user to select multiple options with arrow keys and Space, and confirm with Enter. It has a nice menu-like feel. I wrote it with the help of https://unix.stackexchange.com/a/415155. It can be called like this:
multiselect result "Option 1;Option 2;Option 3" "true;;true"
The result is stored as an array in a variable with the name supplied as the first argument. Last argument is optional and is used for making some options selected by default. It looks like this.
function prompt_for_multiselect {
# little helpers for terminal print control and key input
ESC=$( printf "\033")
cursor_blink_on() { printf "$ESC[?25h"; }
cursor_blink_off() { printf "$ESC[?25l"; }
cursor_to() { printf "$ESC[$1;${2:-1}H"; }
print_inactive() { printf "$2 $1 "; }
print_active() { printf "$2 $ESC[7m $1 $ESC[27m"; }
get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
key_input() {
local key
IFS= read -rsn1 key 2>/dev/null >&2
if [[ $key = "" ]]; then echo enter; fi;
if [[ $key = $'\x20' ]]; then echo space; fi;
if [[ $key = $'\x1b' ]]; then
read -rsn2 key
if [[ $key = [A ]]; then echo up; fi;
if [[ $key = [B ]]; then echo down; fi;
fi
}
toggle_option() {
local arr_name=$1
eval "local arr=(\"\${${arr_name}[@]}\")"
local option=$2
if [[ ${arr[option]} == true ]]; then
arr[option]=
else
arr[option]=true
fi
eval $arr_name='("${arr[@]}")'
}
local retval=$1
local options
local defaults
IFS=';' read -r -a options <<< "$2"
if [[ -z $3 ]]; then
defaults=()
else
IFS=';' read -r -a defaults <<< "$3"
fi
local selected=()
for ((i=0; i<${#options[@]}; i++)); do
selected+=("${defaults[i]}")
printf "\n"
done
# determine current screen position for overwriting the options
local lastrow=`get_cursor_row`
local startrow=$(($lastrow - ${#options[@]}))
# ensure cursor and input echoing back on upon a ctrl+c during read -s
trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
cursor_blink_off
local active=0
while true; do
# print options by overwriting the last lines
local idx=0
for option in "${options[@]}"; do
local prefix="[ ]"
if [[ ${selected[idx]} == true ]]; then
prefix="[x]"
fi
cursor_to $(($startrow + $idx))
if [ $idx -eq $active ]; then
print_active "$option" "$prefix"
else
print_inactive "$option" "$prefix"
fi
((idx++))
done
# user key control
case `key_input` in
space) toggle_option selected $active;;
enter) break;;
up) ((active--));
if [ $active -lt 0 ]; then active=$((${#options[@]} - 1)); fi;;
down) ((active++));
if [ $active -ge ${#options[@]} ]; then active=0; fi;;
esac
done
# cursor position back to normal
cursor_to $lastrow
printf "\n"
cursor_blink_on
eval $retval='("${selected[@]}")'
}
Solution 5
I wrote a library called questionnaire, which is a mini-DSL for creating command line questionnaires. It prompts the user to answer a series of questions and prints the answers to stdout.
It makes your task really easy. Install it with pip install questionnaire
and create a script, e.g. questions.py
, like this:
from questionnaire import Questionnaire
q = Questionnaire(out_type='plain')
q.add_question('options', prompt='Choose some options', prompter='multiple',
options=['Option 1', 'Option 2', 'Option 3', 'Option 4'], all=None)
q.run()
Then run python questions.py
. When you're done answering the questions they're printed to stdout. It works with Python 2 and 3, one of which is almost certainly installed on your system.
It can handle much more complicated questionnaires as well, in case anyone wants to do this. Here are some features:
- Prints answers as JSON (or as plain text) to stdout
- Allows users to go back and reanswer questions
- Supports conditional questions (questions can depend on previous answers)
- Supports the following types of questions: raw input, choose one, choose many
- No mandatory coupling between question presentation and answer values
Related videos on Youtube

user38939
Updated on September 17, 2022Comments
-
user38939 3 months
I'm a bash newbie but I would like to create a script in which I'd like to allow the user to select multiple options from a list of options.
Essentially what I would like is something similar to the example below:
#!/bin/bash OPTIONS="Hello Quit" select opt in $OPTIONS; do if [ "$opt" = "Quit" ]; then echo done exit elif [ "$opt" = "Hello" ]; then echo Hello World else clear echo bad option fi done
(Sourced from http://www.faqs.org/docs/Linux-HOWTO/Bash-Prog-Intro-HOWTO.html#ss9.1)
However my script would have more options, and I'd like to allow multiples to be selected. So something like this:
1) Option 1
2) Option 2
3) Option 3
4) Option 4
5) DoneHaving feedback on the ones they have selected would also be great, eg plus signs next to ones they have already selected. Eg if you select "1" I'd like to page to clear and reprint:
1) Option 1 + 2) Option 2 3) Option 3 4) Option 4 5) Done
Then if you select "3":
1) Option 1 + 2) Option 2 3) Option 3 + 4) Option 4 5) Done
Also, if they again selected (1) I'd like it to "deselect" the option:
1) Option 1 2) Option 2 3) Option 3 + 4) Option 4 5) Done
And finally when Done is pressed I'd like a list of the ones that were selected to be displayed before the program exits, eg if the current state is:
1) Option 1 2) Option 2 + 3) Option 3 + 4) Option 4 + 5) Done
Pressing 5 should print:
Option 2, Option 3, Option 4
...and the script terminate.
So my question - is this possible in bash, and if so is anyone able to provide a code sample?
Any advice would be much appreciated.
-
Dennis Williamson over 12 years@am2605: See my edit. I added an example script.
-
Philip over 12 yearsIt only looks complex until you've used it once or twice, then you'll never use anything else...
-
Dennis Williamson over 9 yearsAlso, a link to the origin of
easybashgui
. -
Daniel over 8 yearsGood job! Good job!
-
Yokai about 6 yearsThis one is a bit cryptic but I love your usage of complex brace expansions and dynamic arrays. It took me a bit of time to be able to read everything as it happens but I love it. I also love the fact that you used the printf() function built-in. I don't find many that know about it existing in bash. Very handy if one is used to coding in C.
-
dbf almost 6 yearsExcellent answer. Also add a note for increasing the number, e..g Option 15; where
n1 SELECTION
is the crucial part to increase the number of digits .. -
dbf almost 6 yearsForgot to add; where
-n2 SELECTION
will accept two digits (e.g. 15),-n3
accepts three (e.g. 153), etc. -
FuSsA over 5 yearsNice exemple! How to manage to run it in KSH ?
-
Dennis Williamson over 5 years@FuSsA: I edited my answer to show the changes needed to make it work in ksh.
-
peterh over 5 yearsThe array handling in bash is very hardcore. You are not only the first, you are the only one above 40k on the whole trinity.
-
FuSsA over 5 years@Dennis: thank you , i'm trying to make "options" array dynamic( to list files from a directory )
-
Dennis Williamson over 5 years@FuSsA:
options=(*)
(or other globbing patterns) will get you a list of files in the array. The challenge then would be getting the selection marks array (${opts[@]}
) zipped together with it. It can be done with afor
loop, but it would have to be run for each pass through the outerwhile
loop. You might want to consider usingdialog
orwhiptail
as I mentioned in my other answer - though these are external dependencies. -
FuSsA over 5 years@Dennis: What about if i want to printf the string instead of the number of choice in the options chosen section ? :D
-
Dennis Williamson over 5 years@FuSsA: Then you could save the string in another array (or use
${opts[@]}
and save the string, passed as an additional argument to the function, instead of+
). -
Eli over 3 yearshow do you call it? how would the file look like?
-
TAAPSogeking over 3 yearsIf anyone wanted to be able to select multiple options (space separated) at once:
while menu && read -rp "$prompt" nums && [[ "$nums" ]]; do while read num; do ... done < <(echo $nums |sed "s/ /\n/g") done
-
Manos Vajasan almost 3 yearsI wish I could upvote this more than once. :-)
-
Andrew over 1 yearI can't ever imagine actually wanting to code something like this in bash... What a miserable approach to take. Something like this should be coded in a "real" language.
-
MestreLion over 1 year@Andrew: I totally agree, but if you're already using a bash script such as the OP, then having a "pure-bash" solution is quite handy, and might be better than resorting to another language or external program.
-
miu about 1 yearHere's an improved version of this script: unix.stackexchange.com/a/673436/84968