Simplify Dynamic Menu in Bash

Viewed 219

I'm working on a custom bash script (with which I have very little experience) and I'm looking for some help with a menu function. My script had several different menus accomplishing different functions so I decided to create one menu function that could be called and customized with set variables. It's unclean and ugly, but I've learned a lot making it. Can anyone shed some light on where I could have simplified or done things differently or more cleanly? Thanks in advance!

#!/bin/bash

# set colors
red='tput setaf 1'
blue='tput setaf 4'
green='tput setaf 2'
bold='tput bold'
normal='tput sgr0'

# define function to reset screen to title
function reset {
    clear
    $bold
    $blue
    echo -e "lineage build script\n\n"
    $normal
    }

function title {
    reset
    $bold
    echo -e "$1\n"
    $normal
    }

# function to create menu
# menu "<title(required)>" "<question(required)>" <all(required)> "<option1>"...
# <title> - Page title
# <question> - Question to be posed before the menu
# <all> - Whether or not to present a menu choice for "All"
# <option#> - List of options to present to the user
function menu {
    # set input at 255 (max error level for return function later)
    input=255
    # check input to see if it's acceptable (within presented range and a number)
    while [[ $input -ge $(($counter+1)) ]] || [[ $((input)) != $input ]]; do
        # a call to a previously defind function establishing consistent page feel.
        title "$1"
        echo -e "$2"
        # present warning if input was invalid
        if ! [[ $input =~ "255" ]]; then
            $red
            echo -e "Invalid entry. Please try again.\n"
            $normal
        else
            echo -e "\n"
        fi
        counter=0
        # present list items for each variable passed to function starting at position 4
        # in order to skip the first 3 (title, question, all)
        for i in "${@:4}"; do
            counter=$(($counter+1))
            echo -e "$counter) $i"
        done
        # present "All" option if variable 3 is any form of yes
        if [[ $3 =~ (y|yes|Y|YES) ]]; then $green; counter=$(($counter+1)); echo -e "$counter) All"; $normal; fi
        # present "Exit" option
        $red; counter=$(($counter+1)); echo -e "$counter) Exit\n"; $normal
        # gather input
        read -N 1 -p "[1-$counter]: " input
    done
    # bail if exit was chosen
    if [[ $input == $counter ]]; then $red; echo -e "\nExit"; exit; fi
    # pass choice back to script as return code
    return $input
    }

# call the menu function for testing
menu "Main Menu" "What would you like to do?" y "option 1" "option 2" "option 3" "option 4" "option 5"
#echo return code just to verify function
echo -e "\nYou chose $?"
1 Answers

Let's see if this helps in anyway (in retro). Not only shell scripting related.

  1. Naming

The reset function is a verb, where as title is not. But it could be, indicating what it does and that it's not a variable.

  1. Printing

You consistently used echo -e for printing. As Charles Duffy pointed out in a comment below, you are sacrificing POSIX compatibility by using -e.

It is not possible to use echo portably across all POSIX systems unless both -n (as the first argument) and escape sequences are omitted.
The printf utility can be used portably to emulate any of the traditional behaviors of the echo utility as follows (assuming that IFS has its standard value or is unset)
http://pubs.opengroup.org/onlinepubs/009696799/utilities/echo.html

Whatever actual way you will use for printing: how about making that a function? If you have to change, it's one place to go. Hopefully this won't lead you to a big list of overloaded functions (for the various printing options).

  1. Local

One reason I liked your program is that you kept variables where they belong, locally to the functions. Maybe you could step that up by actually denoting (where it makes sense) them local.

local [option] name[=value]

https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html

  1. Shift

To continue the previous point I usually prefer to store / move the ordered parameters to local variables. For example title_text=$1. In the menu function it would allow you to use shift. You would process "title", "menu name" and "all" options then you end up with only the options list in the $@ variable. A list of option names. Very handy if you later want to refactor some parts of the function (without breaking order) or you want to pass said list to another function to deal with the options.

shift [n]

Shift the positional parameters to the left by n. The positional parameters from n+1 … $# are renamed to $1 … $#-n. Parameters represented by the numbers $# to $#-n+1 are unset. n must be a non-negative number less than or equal to $#. If n is zero or greater than $#, the positional parameters are not changed. If n is not supplied, it is assumed to be 1. The return status is zero unless n is greater than $# or less than zero, non-zero otherwise.

https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html

  1. Exit codes

I have found your way of use for the return values (exit codes) nice. It's somewhat a pity that the only situation where it's not used is the actual case when you call exit. What I mean is that without going into the menu function, I was confused for a little while why I never see "You chose" if I "exit".

Related