8

How to create a menu in a shell script that will display 3 options that a user will use the arrows keys to move the highlight cursor and press enter to select one?

  • I think you are out of luck WRT to arrow key functionality and highlighting in a pure shell script (you might be able to do the latter with tput, but I think the former is not possible), but you can create simple menus in bash with select: tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_06.html – goldilocks Jul 25 '14 at 15:03
  • Do you mean a GUI menu (using something like [zenity]( Ben Browder) or a text-based one using something like ncurses? – terdon Jul 25 '14 at 15:37
  • I am trying to create a menu that is like the one you get to if you had to select the boot option for windows ("safe mode" "normal" etc) – Mrplow911 Jul 25 '14 at 17:49
  • 1
    There is the dialog package which creates basic faux-GUI terminal interfaces in scripts. – HalosGhost Jul 25 '14 at 18:31
  • @HalosGhost Do you know of any examples of this? – Mrplow911 Jul 25 '14 at 18:41
6

dialog is a great tool for what you are trying to achieve. Here's the example of a simple 3-choices menu:

dialog --menu "Choose one:" 10 30 3 \
    1 Red \
    2 Green \
    3 Blue

The syntax is the following:

dialog --menu <text> <height> <width> <menu-height> [<tag><item>]

The selection will be sent to stderr. Here's a sample script using 3 colors.

#!/bin/bash
TMPFILE=$(mktemp)

dialog --menu "Choose one:" 10 30 3 \
    1 Red \
    2 Green \
    3 Blue 2>$TMPFILE

RESULT=$(cat $TMPFILE)

case $RESULT in
    1) echo "Red";;
    2) echo "Green";;
    3) echo "Blue";;
    *) echo "Unknown color";;
esac

rm $TMPFILE

On Debian, you can install dialog through the package of the same name.

12

Here is a pure bash script solution in form of the select_option function, relying solely on ANSI escape sequences and the built-in read.

Works on Bash 4.2.45 on OSX. The funky parts that might not work equally well in all environments from all I know are the get_cursor_row(), key_input() (to detect up/down keys) and the cursor_to() functions.

#!/usr/bin/env bash

# Renders a text based list of options that can be selected by the
# user using up, down and enter keys and returns the chosen option.
#
#   Arguments   : list of options, maximum of 256
#                 "opt1" "opt2" ...
#   Return value: selected index (0 for opt1, 1 for opt2 ...)
function select_option {

    # 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_option()     { printf "   $1 "; }
    print_selected()   { printf "  $ESC[7m $1 $ESC[27m"; }
    get_cursor_row()   { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
    key_input()        { read -s -n3 key 2>/dev/null >&2
                         if [[ $key = $ESC[A ]]; then echo up;    fi
                         if [[ $key = $ESC[B ]]; then echo down;  fi
                         if [[ $key = ""     ]]; then echo enter; fi; }

    # initially print empty new lines (scroll down if at bottom of screen)
    for opt; do printf "\n"; done

    # determine current screen position for overwriting the options
    local lastrow=`get_cursor_row`
    local startrow=$(($lastrow - $#))

    # 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 selected=0
    while true; do
        # print options by overwriting the last lines
        local idx=0
        for opt; do
            cursor_to $(($startrow + $idx))
            if [ $idx -eq $selected ]; then
                print_selected "$opt"
            else
                print_option "$opt"
            fi
            ((idx++))
        done

        # user key control
        case `key_input` in
            enter) break;;
            up)    ((selected--));
                   if [ $selected -lt 0 ]; then selected=$(($# - 1)); fi;;
            down)  ((selected++));
                   if [ $selected -ge $# ]; then selected=0; fi;;
        esac
    done

    # cursor position back to normal
    cursor_to $lastrow
    printf "\n"
    cursor_blink_on

    return $selected
}

Here is an example usage:

echo "Select one option using up/down keys and enter to confirm:"
echo

options=("one" "two" "three")

select_option "${options[@]}"
choice=$?

echo "Choosen index = $choice"
echo "        value = ${options[$choice]}"

Output looks like below, with the currently selected option highlighted using inverse ansi coloring (hard to convey here in markdown). This can be adapted in the print_selected() function if desired.

Select one option using up/down keys and enter to confirm:

  [one] 
   two 
   three 

Update: Here is a little extension select_opt wrapping the above select_option function to make it easy to use in a case statement:

function select_opt {
    select_option "$@" 1>&2
    local result=$?
    echo $result
    return $result
}

Example usage with 3 literal options:

case `select_opt "Yes" "No" "Cancel"` in
    0) echo "selected Yes";;
    1) echo "selected No";;
    2) echo "selected Cancel";;
esac

You can also mix if there are some known entries (Yes and No in this case), and leverage the exit code $? for the wildcard case:

options=("Yes" "No" "${array[@]}") # join arrays to add some variable array
case `select_opt "${options[@]}"` in
    0) echo "selected Yes";;
    1) echo "selected No";;
    *) echo "selected ${options[$?]}";;
esac
  • 1
    This is beautiful and amazing; thank you very much for sharing! Is this your own originally? Is there a repo online to clone/fork? The only thing I could find that seemed to be in version control was on GitHub in stephenmm's Gist (with line editing added) which points back to here, lol. Working on my own modifications (in a Gist, but planning to make a repo) here though I need to update with the latest changes still. – l3l_aze Sep 14 '18 at 2:14
  • 1
    I used it in some non public code. Pulled it together from various bits and pieces found on the web :-) – Alexander Klimetschek Sep 14 '18 at 15:51
  • Wow; nice work. I started a repo with my modifications at https://github.com/l3laze/sind. So far the biggest differences are upgraded input handling and the addition of a title bar. I'm hoping to add single and multi-line editing, but haven't done anything towards those yet beyond looking at some code – l3l_aze Sep 15 '18 at 2:19

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.