Bash Menu Function

2013-07-16

(Don't forget to check out my previous post today [which this has overshadowed] regarding IPMI @@ http://edgley.org/basic-ipmi-tools/; basic-ipmi-tools; over here; @@)

So, I was working on the new version of KSPLL (@@ https://github.com/edgleyUK/kspll; kspll; github page @@) and I was looking into the use of menus and what is required for them.

I'm not talking about the simple, Press 1 for this, D for that menu (which is what is currently avaliable in KSPLL by the way) -- I'm talking about "normal" menus -- onces where you, at the very least, navigate with the arrow keys.

So I came up with this:
~~~~
function __menu__ () {
	k=${#@}
	j=0
	l=(${@})
	[[ ! ${e} ]] && e=0
	clear
	while [[ $j -lt $k ]]; do
		[[ ${e} == ${j} ]] && echo -ne "\e[41m" || echo -ne "\e[0m"
		echo -n "["; echo -n "${l[$j]}"; echo -e "]\e[0m"
		j=$[${j}+1]
	done
	read -s -n 1 userin
	case ${userin} in
		"B") [[ ${e} -lt $((${k}-1)) ]] && e=$[${e}+1]; __menu__ ${l[@]} ;;
		"A") [[ ${e} -gt 0 ]] && e=$[${e}-1]; __menu__ ${l[@]} ;;
		"") s=${e} ;;
		*) __menu__ ${l[@]} ;;
	esac
	userin=""
}
~~~~

There is a bit of hard coded formating in that (which I don't like, but it will not doubt evolve over time), but this does exactly what I need. To use it, you need to do a bit of leg work (but much less so than repeating this over and over).

Something else to bear in mind -- "A" and "B" pick up the up and down arrow keys no problem, but they also pick up shift + a and shift + b.

Below is a *really* simple example of how to use this:

~~~~
#!/bin/bash

menu_main=("Second_Menu" "Third_Menu" "Fourth_Menu" "Exit")
menu_second=("Fifth_Menu" "Main_Menu")
menu_third=("Main_Menu")
menu_fourth=("Main_Menu")
menu_fifth=("Second_Menu")

function __menu__ () {
	k=${#@}
	j=0
	l=(${@})
	[[ ! ${e} ]] && e=0
	clear
	while [[ $j -lt $k ]]; do
		[[ ${e} == ${j} ]] && echo -ne "\e[41m" || echo -ne "\e[0m"
		echo -n "["; echo -n "${l[$j]}"; echo -e "]\e[0m"
		j=$[${j}+1]
	done
	read -s -n 1 userin
	case ${userin} in
		"B") [[ ${e} -lt $((${k}-1)) ]] && e=$[${e}+1]; __menu__ ${l[@]} ;;
		"A") [[ ${e} -gt 0 ]] && e=$[${e}-1]; __menu__ ${l[@]} ;;
		"") s=${e} ;;
		*) __menu__ ${l[@]} ;;
	esac
	userin=""
}

function __menu_second__ () {
	unset e s
	__menu__ ${menu_second[@]} 
	case ${s} in
		0) __menu_fifth__ ;;
		1) __menu_main__ ;;
	esac

}

function __menu_third__ () {
	unset e s
	__menu__ ${menu_third[@]}
	case ${s} in
		0) __menu_main__ ;;
	esac
}

function __menu_fourth__ () {
	unset e s
	__menu__ ${menu_fourth[@]}
	case ${s} in
		0) __menu_main__ ;;
	esac
}

function __menu_fifth__ () {
	unset e s
	__menu__ ${menu_fifth[@]}
	case ${s} in
		0) __menu_second__ ;;
	esac
}

function __menu_main__ () {
	unset e s
	__menu__ ${menu_main[@]}
	case ${s} in
		0) __menu_second__ ;;
		1) __menu_third__ ;;
		2) __menu_fourth__ ;;
		3) clear; exit 0 ;;
	esac

}

function __bootstrap__ () {
	clear
	__menu_main__
}

__bootstrap__
~~~~

As you can no doubt tell, you still need to run your case statements on the end results -- however, the menu itself is standardised so that the output is always 0-n depending on the menu titles you put in.

A big limitation at the moment is getting the menu title is based purely on arrays (which break spaces) so titles cannot contain spaces.

It's also a bit limiting with the "refresh" in that it will always clear the screen -- so you can't echo before a command. However, this will be addressed pretty soon, I just wanted to get the inital example out there.

This function has already been intergrated with my "bashlib" project which is to offer a more complete set of functions to include in lots of command line projects.

You can check it out from it's @@ https://github.com/edgleyUK/bashlib; bashlib; github page; @@

Update 2013-07-17

Changed the colouring and added padding to each menu item based on width -- odd charactered items will get an extra space to balance it out

Also page up and page down (as well as 5/6) will navigate now.

~~~~
function __menu__ () {
        k=${#@}
        j=0
        l=(${@})
        [[ ! ${e} ]] && e=0
        clear
        while [[ ${j} -lt ${k} ]]; do
                [[ ${e} == ${j} ]] && echo -ne "\e[0;30m\e[47m" || echo -ne "\e[0m"
                echo -n "[ "
                w=$(( 20 - ${#l[${j}]} ))
                [[ $(( ${w} % 2 )) -eq 1 ]] && echo -n " "
                w=$(( ${w} / 2 ))
                t=0
                while [[ ${t} -lt ${w} ]]; do
                        echo -n " "
                        t=$[${t}+1]
                done
                echo -n "${l[${j}]}"
                t=0
                while [[ ${t} -le ${w} ]]; do
                        echo -n " "
                        t=$[${t}+1]
                done
                echo -e " ]\e[0m"
                j=$[${j}+1]
        done
        read -s -n 1 userin
        case ${userin} in
                "B"|"6") [[ ${e} -lt $((${k}-1)) ]] && e=$[${e}+1]; __menu__ ${l[@]} ;;
                "A"|"5") [[ ${e} -gt 0 ]] && e=$[${e}-1]; __menu__ ${l[@]} ;;
                "") s=${e} ;;
                *) __menu__ ${l[@]} ;;
        esac
        userin=""
}
~~~~

I've also added a very similar function but for select options.

~~~~
function __menu_options__ () {
        k=${#@}
        j=0
        l=(${@})
        [[ ! ${e} ]] && e=0
        clear
        while [[ ${j} -lt ${k} ]]; do
                [[ ${e} == ${j} ]] && echo -ne "\e[0;30m\e[47m" || echo -ne "\e[0m"
                [[ ${o[${j}]} == 1 ]] && echo -n "[x][ " || echo -n "[ ][ "
                w=$(( 20 - ${#l[${j}]} ))
                [[ $(( ${w} % 2 )) -eq 1 ]] && echo -n " "
                w=$(( ${w} / 2 ))
                t=0
                while [[ ${t} -lt ${w} ]]; do
                        echo -n " "
                        t=$[${t}+1]
                done
                echo -n "${l[${j}]}"
                t=0
                while [[ ${t} -le ${w} ]]; do
                        echo -n " "
                        t=$[${t}+1]
                done
                echo -e " ]\e[0m"
                j=$[${j}+1]
        done
        read -s -n 1 userin
        case ${userin} in
                "B"|"6") [[ ${e} -lt $((${k}-1)) ]] && e=$[${e}+1]; __menu_options__ ${l[@]} ;;
                "A"|"5") [[ ${e} -gt 0 ]] && e=$[${e}-1]; __menu_options__ ${l[@]} ;;
                "x") s="x" ;;
                "") s=${e} ;;
                *) __menu_options__ ${l[@]} ;;
        esac
}
~~~~

You can use a simliar syntax to the normal menu, but you just have to add a quick check to determine what state the option is in:

~~~~
function __menu_user_options__ () {
        unset e s o
        __menu_options__ ${menu_options[@]}
        case ${s} in
                0) [[ ${o[0]} == 1 ]] && o[0]=0 || o[0]=1; __menu_user_options__ ;;
                1) [[ ${o[1]} == 1 ]] && o[1]=0 || o[1]=1; __menu_user_options__ ;;
                2) [[ ${o[2]} == 1 ]] && o[2]=0 || o[2]=1; __menu_user_options__ ;;
                3) __menu_main__ ;;
        esac

}
~~~~

This could be pretty easily intergrated with my config function -- you can insert each config value into the "o[]" array instead of using "val[]".