37

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) Done

Having 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.

9 Answers 9

41

I think you should take a look at dialog or whiptail.

dialog box

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
3
  • Thanks for that. Looks more complex than I had hoped, but I will check it out :-)
    – user38939
    May 28, 2010 at 11:45
  • @am2605: See my edit. I added an example script. May 28, 2010 at 15:14
  • 4
    It only looks complex until you've used it once or twice, then you'll never use anything else...
    – Chris S
    May 28, 2010 at 15:19
33

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"
7
  • 1
    Good job! Good job!
    – Daniel
    Jun 3, 2014 at 21:03
  • 4
    This 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.
    – Yokai
    Sep 19, 2016 at 10:21
  • 1
    If 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 Jun 3, 2019 at 4:14
  • 1
    This was really useful in developing a script which is used by multiple other people who do not have access to whiptail or other packages because they are using git bash on windows!
    – Dr Ivol
    Jun 13, 2019 at 9:50
  • 2
    I wish I could upvote this more than once. :-)
    – MountainX
    Dec 9, 2019 at 1:45
7

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.

7
  • Nice exemple! How to manage to run it in KSH ?
    – FuSsA
    Mar 27, 2017 at 19:55
  • 1
    @FuSsA: I edited my answer to show the changes needed to make it work in ksh. Mar 27, 2017 at 20:19
  • 1
    The array handling in bash is very hardcore. You are not only the first, you are the only one above 40k on the whole trinity.
    – peterh
    Mar 27, 2017 at 22:00
  • 1
    @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 a for loop, but it would have to be run for each pass through the outer while loop. You might want to consider using dialog or whiptail as I mentioned in my other answer - though these are external dependencies. Mar 28, 2017 at 16:55
  • 1
    @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 +). Mar 28, 2017 at 20:16
5

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[@]}")'
}
2
2

I used the example from MestreLion and drafted the code below. All you need to do is update the options and actions in the first two sections.

#!/bin/bash
#title:         menu.sh
#description:   Menu which allows multiple items to be selected
#author:        Nathan Davieau
#               Based on script from MestreLion
#created:       May 19 2016
#updated:       N/A
#version:       1.0
#usage:         ./menu.sh
#==============================================================================

#Menu options
options[0]="AAA"
options[1]="BBB"
options[2]="CCC"
options[3]="DDD"
options[4]="EEE"

#Actions to take based on selection
function ACTIONS {
    if [[ ${choices[0]} ]]; then
        #Option 1 selected
        echo "Option 1 selected"
    fi
    if [[ ${choices[1]} ]]; then
        #Option 2 selected
        echo "Option 2 selected"
    fi
    if [[ ${choices[2]} ]]; then
        #Option 3 selected
        echo "Option 3 selected"
    fi
    if [[ ${choices[3]} ]]; then
        #Option 4 selected
        echo "Option 4 selected"
    fi
    if [[ ${choices[4]} ]]; then
        #Option 5 selected
        echo "Option 5 selected"
    fi
}

#Variables
ERROR=" "

#Clear screen for menu
clear

#Menu function
function MENU {
    echo "Menu Options"
    for NUM in ${!options[@]}; do
        echo "[""${choices[NUM]:- }""]" $(( NUM+1 ))") ${options[NUM]}"
    done
    echo "$ERROR"
}

#Menu loop
while MENU && read -e -p "Select the desired options using their number (again to uncheck, ENTER when done): " -n1 SELECTION && [[ -n "$SELECTION" ]]; do
    clear
    if [[ "$SELECTION" == *[[:digit:]]* && $SELECTION -ge 1 && $SELECTION -le ${#options[@]} ]]; then
        (( SELECTION-- ))
        if [[ "${choices[SELECTION]}" == "+" ]]; then
            choices[SELECTION]=""
        else
            choices[SELECTION]="+"
        fi
            ERROR=" "
    else
        ERROR="Invalid option: $SELECTION"
    fi
done

ACTIONS
2
  • Excellent 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
    Dec 2, 2016 at 21:39
  • Forgot to add; where -n2 SELECTION will accept two digits (e.g. 15), -n3 accepts three (e.g. 153), etc.
    – dbf
    Dec 2, 2016 at 21:50
2

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
1

Since I did not find any suitable BASH alternativ for prompt-toolkit (python), dialoguer (rust) or inquirer (node) I gave it a try on my own:

https://i.stack.imgur.com/6AyAI.png

https://asciinema.org/a/Y4hLxnN20JtAlrn3hsC6dCRn8 https://gist.github.com/blurayne/f63c5a8521c0eeab8e9afd8baa45c65e

1

Thank you for your example. This is my first proposition.

When one of the choices is selected, another choice must be deselected. For example, if I choose to backup, I will not choose to restore at the same time. Therefore, the choice to save must remove the choice to restore.

The same, if i select Quit, all choice need reset.

Otherwise, that's basically what I wanted to do. Thank you for sharing.

#!/bin/bash

# Le choix des options.
options=("Quitter et ne rien sauvegarder (Action prioritaire.)" "Démarrer une sauvegarde" "Démarrer une restauration" "Option 1" "Option 2" "Option 3" "Option 4" )

# Fonction pour préparer l'affichage du menu, ou, d'un message d'erreur.
menu() {
    echo "Le choix des options :"
    for i in ${!options[@]}; do 
        printf "%3d%s) %s\r\n" $((i+1)) "${choices[i]:- }" "${options[i]}"
    done
    if [[ "$msg" ]]; then echo "$msg"; fi
}

# Permet d'effacer l'écran pour le premier affichage du menu.
clear
echo "############################################################"
echo "# Sauvegarder les fichiers de configuration de son serveur #"
echo "############################################################"
echo ""

# Faire une sélection.
# Le retour à la ligne est voulu dans la variable prompt qui est un message
prompt="
Sélectionner / Déselectionner une option puis valider avec la touche entrée."
while menu && read -rp "$prompt" num && [[ "$num" ]]; do

# Permet d'effacer les menus précédents qui sont réaffiché lors de chaque sélection.
# Ne conserve que le dernier affichage du menu.
clear
echo "############################################################"
echo "# Sauvegarder les fichiers de configuration de son serveur #"
echo "############################################################"
echo ""

    [[ "$num" != *[![:digit:]]* ]] &&
    (( num > 0 && num <= ${#options[@]} )) ||
    { msg="# Cette option n'existe pas : '$num'."; continue; }
    ((num--)); msg="
Dernière action choisie :
- ${options[num]} a été ${choices[num]:+dé}sélectionné."
    [[ "${choices[num]}" ]] && choices[num]="" || choices[num]="+"
done

# Récapitulatif de toutes les actions choisies.
clear
echo "############################################################"
echo "# Sauvegarder les fichiers de configuration de son serveur #"
echo "############################################################"

printf "\r\nToutes les actions choisies :\r\n"; msg=" - Aucun choix n'a été effectué."
for i in ${!options[@]}; do 
    [[ "${choices[i]}" ]] && { printf " - %s\r\n" "${options[i]}"; msg=""; }
done

# Afficher un message d'erreur.
echo "$msg"

# Si aucun choix n'est effectué, relancer le programme :
if [[ $msg = " - Aucun choix n'a été effectué." ]]
then
 printf " \r\n"
 echo "Fin du programme. Aucune sauvegarde / restauration n'a été effectuée.";
else
 echo "Démarrage de la sauvegarde / restauration."
 echo ""
fi


###############################################################


# Commencer la sauvegarde si les options ont été sélectionnées :
if [[ ${choices[1]} != "" ]]
then
echo "L'action 1 doit maintenant être effectuée"
fi

if [[ ${choices[2]} != "" ]]
then
echo "L'action 2 doit maintenant être effectuée"
fi
-1

EasyBashGui is a bash library that implements an abstraction of dialog UIs using various existing dialog program. It supports yad, gtkdialog, kdialog, zenity, Xdialog, dialog, and whiptail and uses whatever you have installed (with preference to graphical dialog programs if available).

Here is an example of using the list command from easybashgui to select one option out of a list:

export supermode=none

source easybashgui

list "Option 1" "Option 2" "Option 3" "Option 4"
2
  • 2
    Maybe you could add a little description of what this is doing? For future visitors, not so much for the OP.
    – slm
    May 10, 2013 at 8:16
  • Also, a link to the origin of easybashgui. May 10, 2013 at 13:18

Not the answer you're looking for? Browse other questions tagged or ask your own question.