28

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.

35

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

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"
  • Good job! Good job! – Daniel Jun 3 '14 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 '16 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 – TAAPSogeking Jun 3 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 at 9:50
5

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.

  • Nice exemple! How to manage to run it in KSH ? – FuSsA Mar 27 '17 at 19:55
  • 1
    @FuSsA: I edited my answer to show the changes needed to make it work in ksh. – Paused until further notice. Mar 27 '17 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 says reinstate Monica Mar 27 '17 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. – Paused until further notice. Mar 28 '17 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 +). – Paused until further notice. Mar 28 '17 at 20:16
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

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
  • 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 '16 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 '16 at 21:50
1

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[@]}")'
}
  • how do you call it? how would the file look like? – Eli Mar 4 at 1:14
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
export supermode=none

source easybashgui

list "Option 1" "Option 2" "Option 3" "Option 4"
  • 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 '13 at 8:16
  • Also, a link to the origin of easybashgui. – Paused until further notice. May 10 '13 at 13:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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