Multi-select menu in bash script

107,531

Solution 1

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

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
Share:
107,531

Related videos on Youtube

user38939
Author by

user38939

Updated on September 17, 2022

Comments

  • user38939
    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) 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.

  • Dennis Williamson
    Dennis Williamson over 12 years
    @am2605: See my edit. I added an example script.
  • Philip
    Philip over 12 years
    It only looks complex until you've used it once or twice, then you'll never use anything else...
  • Dennis Williamson
    Dennis Williamson over 9 years
    Also, a link to the origin of easybashgui.
  • Daniel
    Daniel over 8 years
    Good job! Good job!
  • Yokai
    Yokai about 6 years
    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.
  • dbf
    dbf almost 6 years
    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
    dbf almost 6 years
    Forgot to add; where -n2 SELECTION will accept two digits (e.g. 15), -n3 accepts three (e.g. 153), etc.
  • FuSsA
    FuSsA over 5 years
    Nice exemple! How to manage to run it in KSH ?
  • Dennis Williamson
    Dennis Williamson over 5 years
    @FuSsA: I edited my answer to show the changes needed to make it work in ksh.
  • peterh
    peterh over 5 years
    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.
  • FuSsA
    FuSsA over 5 years
    @Dennis: thank you , i'm trying to make "options" array dynamic( to list files from a directory )
  • Dennis Williamson
    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 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.
  • FuSsA
    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
    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
    Eli over 3 years
    how do you call it? how would the file look like?
  • TAAPSogeking
    TAAPSogeking over 3 years
    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
  • Manos Vajasan
    Manos Vajasan almost 3 years
    I wish I could upvote this more than once. :-)
  • Andrew
    Andrew over 1 year
    I 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
    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
    miu about 1 year
    Here's an improved version of this script: unix.stackexchange.com/a/673436/84968