210

I'm creating a simple bash script and I want to create a select menu in it, like this:

$./script

echo "Choose your option:"

1) Option 1  
2) Option 2  
3) Option 3  
4) Quit  

And according to user's choice, I want different actions to be executed. I'm a bash shell scripting noob, I've searched the web for some answers, but got nothing really concrete.

dessert
  • 39,982
  • 5
    The question is old and protected, but I use fzf. Try seq 10 | fzf. The drawback is that fzf is not installed by default. You can find fzf here: https://github.com/junegunn/fzf – Lynch Nov 19 '18 at 15:33
  • Late addition I know - you may be able to use this menu program that I'm working on: Latest version: https://github.com/steveh250/Unix-Menu-Program – Steveh250 Apr 26 '21 at 03:25
  • This is a post based on the information found on this page: https://unix.stackexchange.com/questions/733425/creating-debug-of-a-bash-script-menu –  Jan 30 '23 at 13:12

11 Answers11

241
#!/bin/bash
# Bash Menu Script Example

PS3='Please enter your choice: '
options=("Option 1" "Option 2" "Option 3" "Quit")
select opt in "${options[@]}"
do
    case $opt in
        "Option 1")
            echo "you chose choice 1"
            ;;
        "Option 2")
            echo "you chose choice 2"
            ;;
        "Option 3")
            echo "you chose choice $REPLY which is $opt"
            ;;
        "Quit")
            break
            ;;
        *) echo "invalid option $REPLY";;
    esac
done

Add break statements wherever you need the select loop to exit. If a break is not performed, the select statement loops and the menu is re-displayed.

In the third option, I included variables that are set by the select statement to demonstrate that you have access to those values. If you choose it, it will output:

you chose choice 3 which is Option 3

You can see that $REPLY contains the string you entered at the prompt. It is used as an index into the array ${options[@]} as if the array were 1 based. The variable $opt contains the string from that index in the array.

Note that the choices could be a simple list directly in the select statement like this:

select opt in foo bar baz 'multi word choice'

but you can't put such a list in a scalar variable because of the spaces in one of the choices.

You can also use file globbing if you are choosing among files:

select file in *.tar.gz
  • @Abdull: That's the intended behavior. The "Quit" option executes a break which breaks out of the select loop. You can add break anywhere you need it. The Bash manual states "The commands are executed after each selection until a break command is executed, at which point the select command completes." – Dennis Williamson Dec 07 '15 at 17:31
  • FWIW, running this on 14.04 with GNU bash 4.3.11 produces errors on line 9: ./test.sh: line 9: syntax error near unexpected token"Option 1"' ./test.sh: line 9: "Option 1")' – Brian Brownton Dec 08 '16 at 14:59
  • @BrianMorton: I can't reproduce that. It's likely you're missing a quote or some other character earlier in the script (or have an extra). – Dennis Williamson Dec 08 '16 at 18:37
  • Should this work on Raspberry Pi A+, raspbian wheezy? I'm new to both. – Rod Jun 03 '17 at 05:39
  • 5
    What is that PS3 variable and whhy is not not referenced at all after being assigned? – dtmland Nov 30 '17 at 17:59
  • 9
    @dtmland: PS3 is the prompt for the select command. It is used automatically and doesn't need to be referenced explicitly. PS3 and select documentation. – Dennis Williamson Nov 30 '17 at 20:55
  • 1
    case $opt in should be case $REPLY in – Christian Jul 11 '19 at 08:28
  • 2
    @Christian: No, it shouldn't (but it could if I used the indices of $options in the case statement instead of the values). I think using the values better documents the functionality of the sections of the case statement. – Dennis Williamson Jul 11 '19 at 12:33
  • for long string options, it is not good to repeat the long string twice. – Jake Jan 11 '21 at 03:43
  • I found a way to automatically generate the list of options by first going through the script and picking the various Options. there is probably a cleaner way to achieve it but this seems to work:

    test=$(grep 'Option\|Quit' "$BASH_SOURCE" | grep -v test | sed 's/"//g' | sed 's/"//g' | sed "s/^[ \t]*//" | tr -d '\n' | sed 's/.$//') IFS=')' read -a options <<< $test

    – Batwam Apr 22 '22 at 14:22
  • this is not zsh compliant – zzzgoo Apr 25 '22 at 08:28
  • I note that when I enter a choice, the script with execute it, then repeate the PS3 lines. Is there any way to make it repeat the list of choices (Option 1, Option 2, ...) as well? I am running some lengthy actions with a long selection list so it would be good to have it being repeated each time. – Batwam Apr 28 '22 at 14:55
  • 1
    zzzgoo, the question is specifically tagged bash, so that's not a problem here – Ti Strga Jun 22 '22 at 18:26
  • 1
    @Batwam: Just put a while loop around the whole select block and add a choice that causes the while loop to exit. – Dennis Williamson Jun 23 '22 at 22:39
  • @Dennis that works, thanks! – Batwam Sep 26 '22 at 02:38
112

Using dialog, the command would look like this:

dialog --clear --backtitle "Backtitle here" --title "Title here" --menu "Choose one of the following options:" 15 40 4 \
1 "Option 1" \
2 "Option 2" \
3 "Option 3"

enter image description here

Putting it in a script:

#!/bin/bash

HEIGHT=15
WIDTH=40
CHOICE_HEIGHT=4
BACKTITLE="Backtitle here"
TITLE="Title here"
MENU="Choose one of the following options:"

OPTIONS=(1 "Option 1"
         2 "Option 2"
         3 "Option 3")

CHOICE=$(dialog --clear \
                --backtitle "$BACKTITLE" \
                --title "$TITLE" \
                --menu "$MENU" \
                $HEIGHT $WIDTH $CHOICE_HEIGHT \
                "${OPTIONS[@]}" \
                2>&1 >/dev/tty)

clear
case $CHOICE in
        1)
            echo "You chose Option 1"
            ;;
        2)
            echo "You chose Option 2"
            ;;
        3)
            echo "You chose Option 3"
            ;;
esac
Kulfy
  • 17,696
Alaa Ali
  • 31,535
  • I'd like to note that I put the line TERMINAL=$(tty) at the top of my script and then in the CHOICE variable definition I changed 2>&1 >/dev/tty to 2>&1 >$TERMINAL to avoid redirection issues if the script was run in a different terminal context. – Shrout1 Jun 12 '18 at 14:37
  • 1
    What does the --backtitle parameter do? – TRiG Aug 31 '18 at 23:22
  • 2
    It’s the title of the bluescreen in the background. You can see it in the top left corner of the screenshot that reads “Backtitle here”. – Alaa Ali Aug 31 '18 at 23:25
  • 2
    Note that dialog is not universally available on all Linux systems. Even though it looks kinda cool, your script might not be compatible across different systems/releases/distributions. – 88weighed Feb 11 '21 at 19:01
75

Not a new answer per se, but since there's no accepted answer yet, here are a few coding tips and tricks, for both select and zenity:

title="Select example"
prompt="Pick an option:"
options=("A" "B" "C")

echo "$title" PS3="$prompt " select opt in "${options[@]}" "Quit"; do case "$REPLY" in 1) echo "You picked $opt which is option 1";; 2) echo "You picked $opt which is option 2";; 3) echo "You picked $opt which is option 3";; $((${#options[@]}+1))) echo "Goodbye!"; break;; *) echo "Invalid option. Try another one.";continue;; esac done

while opt=$(zenity --title="$title" --text="$prompt" --list
--column="Options" "${options[@]}") do case "$opt" in "${options[0]}") zenity --info --text="You picked $opt, option 1";; "${options[1]}") zenity --info --text="You picked $opt, option 2";; "${options[2]}") zenity --info --text="You picked $opt, option 3";; *) zenity --error --text="Invalid option. Try another one.";; esac done

Worth mentioning:

  • Both will loop until the user explicitly chooses Quit (or Cancel for zenity). This is a good approach for interactive script menus: after a choice is selected and action performed, menu is presented again for another choice. If choice is meant to be one-time only, just use break after esac (the zenity approach could be further reduced also)

  • Both case are index-based, rather than value-based. I think this is easier to code and maintain

  • Array is also used for zenity approach.

  • "Quit" option is not among the initial, original options. It is "added" when needed, so your array stay clean. Afterall, "Quit" is not needed for zenity anyway, user can just click "Cancel" (or close the window) to exit. Notice how both uses the same, untouched array of options.

  • PS3 and REPLY vars can not be renamed. select is hardcoded to use those. All other variables in script (opt, options, prompt, title) can have any names you want, provided you do the adjustments

MestreLion
  • 20,086
  • Awesome explanation. Thank you. This question still ranks high on Google, so too bad it is closed. – MountainX Jun 30 '13 at 08:44
  • You could use the same case structure for the select version that you're using for the zenity version: case "$opt" in . . . "${options[0]}" ) . . . (instead of $REPLY and the indices 1, 2, and 3). – Dennis Williamson May 29 '18 at 20:40
  • @DennisWilliamson, yes I could, and in "real" code it would be preferable to use the same structure in both cases. I intentionally wanted to show the relation between $REPLY, indexes and values. – MestreLion May 31 '18 at 20:28
24

You can use this simple script for creating options

#!/bin/bash
echo "select the operation ************"
echo "  1)operation 1"
echo "  2)operation 2"
echo "  3)operation 3"
echo "  4)operation 4" 
read n case $n in 1) echo "You chose Option 1";; 2) echo "You chose Option 2";; 3) echo "You chose Option 3";; 4) echo "You chose Option 4";; *) echo "invalid option";; esac
jibin
  • 249
22

I have one more option that is a mixture of these answers but what makes it nice is that you only need to press one key and then the script continues thanks to the -n option of read. In this example, we are prompting to shutdown, reboot, or simply exit the script using ANS as our variable and the user only has to press E, R, or S. I also set the default to exit so if enter is pressed then the script will exit.

#!/bin/bash
read -n 1 -p "Would you like to exit, reboot, or shutdown? (E/r/s) " ans;

case $ans in r|R) sudo reboot;; s|S) sudo poweroff;; *) exit;; esac

Nav
  • 1,059
  • 1
    I like this, but if used with a number-based menu, it'd work only for 0 to 9, since it accepts only a single keypress. For anyone wanting to know how to support waiting for pressing enter key, you just have to remove the -n 1 from the read line. – Nav Jul 30 '21 at 01:46
11
#!/bin/sh
show_menu(){
    normal=`echo "\033[m"`
    menu=`echo "\033[36m"` #Blue
    number=`echo "\033[33m"` #yellow
    bgred=`echo "\033[41m"`
    fgred=`echo "\033[31m"`
    printf "\n${menu}*********************************************${normal}\n"
    printf "${menu}**${number} 1)${menu} Mount dropbox ${normal}\n"
    printf "${menu}**${number} 2)${menu} Mount USB 500 Gig Drive ${normal}\n"
    printf "${menu}**${number} 3)${menu} Restart Apache ${normal}\n"
    printf "${menu}**${number} 4)${menu} ssh Frost TomCat Server ${normal}\n"
    printf "${menu}**${number} 5)${menu} Some other commands${normal}\n"
    printf "${menu}*********************************************${normal}\n"
    printf "Please enter a menu option and enter or ${fgred}x to exit. ${normal}"
    read opt
}

option_picked(){
    msgcolor=`echo "\033[01;31m"` # bold red
    normal=`echo "\033[00;00m"` # normal white
    message=${@:-"${normal}Error: No message passed"}
    printf "${msgcolor}${message}${normal}\n"
}

clear
show_menu
while [ $opt != '' ]
    do
    if [ $opt = '' ]; then
      exit;
    else
      case $opt in
        1) clear;
            option_picked "Option 1 Picked";
            printf "sudo mount /dev/sdh1 /mnt/DropBox/; #The 3 terabyte";
            show_menu;
        ;;
        2) clear;
            option_picked "Option 2 Picked";
            printf "sudo mount /dev/sdi1 /mnt/usbDrive; #The 500 gig drive";
            show_menu;
        ;;
        3) clear;
            option_picked "Option 3 Picked";
            printf "sudo service apache2 restart";
            show_menu;
        ;;
        4) clear;
            option_picked "Option 4 Picked";
            printf "ssh lmesser@ -p 2010";
            show_menu;
        ;;
        x)exit;
        ;;
        \n)exit;
        ;;
        *)clear;
            option_picked "Pick an option from the menu";
            show_menu;
        ;;
      esac
    fi
done
  • 2
    I know this is old, but needs first line to read #!/bin/bash to compile. – JClar Dec 07 '14 at 12:15
  • 1
    Code review: The $ is missing from the $opt variable in the while statement. The if statement is redundant. Inconsistent indentation. Using menu in some places where it should be 'show_menu.show_menucould be put at the top of the loop instead of being repeated in eachcase. Inconsistent indentation. Mixing use of single square brackets and doubled ones. Using hard-coded ANSI sequences instead oftput. Use of all-caps var names is not recommended.FGREDshould be calledbgred. Use of backticks instead of$()`. Function definition should be consistent and don't use ... – Dennis Williamson May 29 '18 at 20:57
  • ...both forms together. Terminal semicolons aren't needed. Some colors, etc., defined twice. The case with \n will never be executed. Perhaps more. – Dennis Williamson May 29 '18 at 21:01
  • You shouldn't use printf "string with $variable in it\n" because that first parameter to printf is a format string, and characters such as % are special. Instead, use either printf '%s\n' "string with $variable in it" or interpolate the variable values into a fixed string printf 'string with a %s in it\n' "$variable" – Chris Davies Apr 06 '22 at 08:24
9

If you only want a very simple menu that shows "in place" and you can continue typing after that - without any fancy external dialog program, then you can use ANSI escape sequences and a simple loop to render the list and allow a cursor to be moved on top of it.

The answer here by user360154 already has everything you need, but it is also super fancy, does much more than needed and while the code is also formatted to look fancy - it isn't easy to read and understand.

Here's the same approach as user360154's but much simpler:

function choose_from_menu() {
    local prompt="$1" outvar="$2"
    shift
    shift
    local options=("$@") cur=0 count=${#options[@]} index=0
    local esc=$(echo -en "\e") # cache ESC as test doesn't allow esc codes
    printf "$prompt\n"
    while true
    do
        # list all options (option list is zero-based)
        index=0 
        for o in "${options[@]}"
        do
            if [ "$index" == "$cur" ]
            then echo -e " >\e[7m$o\e[0m" # mark & highlight the current option
            else echo "  $o"
            fi
            index=$(( $index + 1 ))
        done
        read -s -n3 key # wait for user to key in arrows or ENTER
        if [[ $key == $esc[A ]] # up arrow
        then cur=$(( $cur - 1 ))
            [ "$cur" -lt 0 ] && cur=0
        elif [[ $key == $esc[B ]] # down arrow
        then cur=$(( $cur + 1 ))
            [ "$cur" -ge $count ] && cur=$(( $count - 1 ))
        elif [[ $key == "" ]] # nothing, i.e the read delimiter - ENTER
        then break
        fi
        echo -en "\e[${count}A" # go up to the beginning to re-render
    done
    # export the selection to the requested output variable
    printf -v $outvar "${options[$cur]}"
}

Here is an example usage:


selections=(
"Selection A"
"Selection B"
"Selection C"
)

choose_from_menu "Please make a choice:" selected_choice "${selections[@]}" echo "Selected choice: $selected_choice"

Which should look like this:
demo

Guss
  • 3,535
  • Is it possible to make this menu scrollable to adapt to your screen size? I really like to look and feel of this but having something as less -F at the end would be really nice. – Steven Thiel Jul 14 '22 at 14:12
  • I like that solution, since it's simple to use and the code is also not too overwhelming. Unfortunately, it does not work from inside a zsh script (with zsh shebang like #!/usr/bin/env zsh). Thus, I modified it a little bit so it also runs under zsh: https://codeberg.org/lukeflo/shell-scripts/src/branch/main/zshmenu.sh – lukeflo Feb 27 '24 at 10:11
  • 1
    @lukeflo thank you. This script was written as an answer to a request for a Bash script, so it is what it is . But thank you for the zsh contribution - looks good . – Guss Feb 27 '24 at 13:18
9

Bash fancy menu

Try it out first, then visit my page for detailed description. No need for external libraries or programs like dialog or zenity.

#/bin/bash
# by oToGamez
# www.pro-toolz.net
  E='echo -e';e='echo -en';trap &quot;R;exit&quot; 2
ESC=$( $e &quot;\e&quot;)

TPUT(){ $e "\e[${1};${2}H";} CLEAR(){ $e "\ec";} CIVIS(){ $e "\e[?25l";} DRAW(){ $e "\e%@\e(0";} WRITE(){ $e "\e(B";} MARK(){ $e "\e[7m";} UNMARK(){ $e "\e[27m";} R(){ CLEAR ;stty sane;$e "\ec\e[37;44m\e[J";}; HEAD(){ DRAW for each in $(seq 1 13);do $E " x x" done WRITE;MARK;TPUT 1 5 $E "BASH SELECTION MENU ";UNMARK;} i=0; CLEAR; CIVIS;NULL=/dev/null FOOT(){ MARK;TPUT 13 5 printf "ENTER - SELECT,NEXT ";UNMARK;} ARROW(){ read -s -n3 key 2>/dev/null >&2 if [[ $key = $ESC[A ]];then echo up;fi if [[ $key = $ESC[B ]];then echo dn;fi;} M0(){ TPUT 4 20; $e "Login info";} M1(){ TPUT 5 20; $e "Network";} M2(){ TPUT 6 20; $e "Disk";} M3(){ TPUT 7 20; $e "Routing";} M4(){ TPUT 8 20; $e "Time";} M5(){ TPUT 9 20; $e "ABOUT ";} M6(){ TPUT 10 20; $e "EXIT ";} LM=6 MENU(){ for each in $(seq 0 $LM);do M${each};done;} POS(){ if [[ $cur == up ]];then ((i--));fi if [[ $cur == dn ]];then ((i++));fi if [[ $i -lt 0 ]];then i=$LM;fi if [[ $i -gt $LM ]];then i=0;fi;} REFRESH(){ after=$((i+1)); before=$((i-1)) if [[ $before -lt 0 ]];then before=$LM;fi if [[ $after -gt $LM ]];then after=0;fi if [[ $j -lt $i ]];then UNMARK;M$before;else UNMARK;M$after;fi if [[ $after -eq 0 ]] || [ $before -eq $LM ];then UNMARK; M$before; M$after;fi;j=$i;UNMARK;M$before;M$after;} INIT(){ R;HEAD;FOOT;MENU;} SC(){ REFRESH;MARK;$S;$b;cur=ARROW;} ES(){ MARK;$e "ENTER = main menu ";$b;read;INIT;};INIT while [[ "$O" != " " ]]; do case $i in 0) S=M0;SC;if [[ $cur == "" ]];then R;$e "\n$(w )\n";ES;fi;; 1) S=M1;SC;if [[ $cur == "" ]];then R;$e "\n$(ifconfig )\n";ES;fi;; 2) S=M2;SC;if [[ $cur == "" ]];then R;$e "\n$(df -h )\n";ES;fi;; 3) S=M3;SC;if [[ $cur == "" ]];then R;$e "\n$(route -n )\n";ES;fi;; 4) S=M4;SC;if [[ $cur == "" ]];then R;$e "\n$(date )\n";ES;fi;; 5) S=M5;SC;if [[ $cur == "" ]];then R;$e "\n$($e by oTo)\n";ES;fi;; 6) S=M6;SC;if [[ $cur == "" ]];then R;exit 0;fi;; esac;POS;done

muru
  • 197,895
  • 55
  • 485
  • 740
7

Since this is targeted at Ubuntu you should use whatever backend debconf is configured to use. You can find out the debconf backend with:

sudo -s "echo get debconf/frontend | debconf-communicate"

If it says "dialog" then it likely uses whiptail or dialog. On Lucid it's whiptail.

If that fails, use bash "select" as explained by Dennis Williamson.

Jorge Castro
  • 71,754
Li Lo
  • 15,894
  • 3
    This is probably overkill for that question, but +1 for mentioning whiptail and dialog! I wasnt aware of those commands... very sweet! – MestreLion Aug 05 '11 at 05:58
  • You don't need sudo for this. Instead echo get debconf/frontend | debconf-communicate 2>/dev/null (the discard of stderr is to avoid the warning about not being able to open a password database) – Chris Davies Apr 06 '22 at 08:26
7

I have used Zenity, which seems always there in Ubuntu, works very well and has many capabilities. This is a sketch of a possible menu:

#! /bin/bash

selection=$(zenity --list "Option 1" "Option 2" "Option 3" --column="" --text="Text above column(s)" --title="My menu")

case "$selection" in
"Option 1")zenity --info --text="Do something here for No1";;
"Option 2")zenity --info --text="Do something here for No2";;
"Option 3")zenity --info --text="Do something here for No3";;
esac
3

There is already the same question in serverfault answered. The solution there uses whiptail.

txwikinger
  • 28,462
  • Thanks, but as my script is for mainstream consumption, I don't want it to have any additional dependecies. But I'll bookmark that for use in the future, who knows. – Daniel Rodrigues Aug 08 '10 at 22:01