Thursday, August 5, 2010

[Level 3] Menu Bash Script

One day, one friend of mine ask me how to create a menu with timeout limitation in bash, then I wrote the sample code for him.
I think this maybe a good idea if I can write a menu bash script with dynamic setting menu item and so on.
Therefore I write the script below, any feedback will be appreciate.

#!/bin/bash

##############################################
##
## for menu utility
##
## author: Stanley Huang
## licence: Creative Commons Attribution-Share Alike 3.0 Taiwan License
## last release reversion: 0.1
## last modify date: 2010/08/04 15:30
## change list:
##   01. 2010/08/04 15:30, Stanley Huang. Build.
##
##############################################

showUsage() {
  cat<<EOF
Usage:
  $0
     [-e] (expert mode)
     [-h] (help menu)
     [-t] timeout_sec (default timeout = $nTimeout sec)
     [-c] no. of columns to display (default no. of column = $nColumn)
     [-w] column wide (default column wide = $nWide)
     [-m] menu file
Ex.
  $0
  $0 -e
  $0 -h
  $0 -t 10
  $0 -c 3
  $0 -w 10
  $0 -m /tmp/t.menu
EOF
}

pak2c() {
  read -n 1 -p "Press any key to continue..." pak2c
}

checkItemExist() {
  local sItem=$1
  grep "^$sFilter$sItem$sMenuActSeparator" $sMenuFile > /dev/null 2>&1
  return $?
}

checkItemNotExist() {
  local sItem=$1
  checkItemExist $sItem
  if [ $? -eq 0 ]
  then
    return 1
  else
    return 0
  fi
}

getHeader() {
  local sHeader="`grep "^$sHeaderFilter" $sMenuFile | head -1 | cut -d' ' -f2-`"
  echo $sHeader
}

setHeader() {
  while true
  do
    read -p "Please enter your header: " sNewHeader
    [ ! -z "$sNewHeader" ] && break
  done
  sHeader=$sNewHeader
  if [ `grep -c "^$sHeaderFilter" $sMenuFile` -ne 0 ]
  then
    perl -pi -e "s/^$sHeaderFilter.*$/$sHeaderFilter$sNewHeader/" $sMenuFile
  else
    echo "$sHeaderFilter$sNewHeader" >> $sMenuFile
  fi
}

buildMenu() {
  clear
  declare -i nColumn=$1
  declare -i nWide=$2
  declare -i nCount=0
  declare -i nMod=0
  declare    sCount=""

  printf -- "$sOuterSeparatorLine\n" && \
  printf -- "        `getHeader`\n" && \
  printf -- "$sOuterSeparatorLine\n" && \

  while read fItem item
  do
    nCount=$nCount+1 && sCount=$nCount && [ $isExpert -eq 0 ] && sCount=.
    aMenu[$nCount]="`echo $item | awk -F$sMenuActSeparator '{print($1)}'`"
    aActs[$nCount]="`echo $item | awk -F$sMenuActSeparator '{print($2)}'`"
   
    fDisable=`echo $fItem | sed -e 's/[^*]//g'`
    printf "%${#nTotalCount}s) %${nWide}s%1s" $sCount ${aMenu[$nCount]} $fDisable
   
    nMod=$nCount%$nColumn
    if [ $nMod -eq 0 ]
    then
      printf "\n";
    else
      printf " ";
    fi
  done <<EOF
`grep "^$sFilter" $sMenuFile ## disable display with sorting`
EOF
  #### `grep "^$sFilter" $sMenuFile | sort -k 2 ## display with sorting`
  [ $nMod -ne 0 ] &&  printf "\n"

  printf -- "$sOuterSeparatorLine\n" && \
  if [ "$isExpert" -eq 0 ]
  then
    printf "%${#nTotalCount}s) %${nWide}s\n" "a" "(A)dd"
    printf "%${#nTotalCount}s) %${nWide}s\n" "c" "(C)hange name"
    printf "%${#nTotalCount}s) %${nWide}s%s\n" "d" "(D)isable" ", marked *"
    printf "%${#nTotalCount}s) %${nWide}s\n" "e" "(E)nable"
    printf "%${#nTotalCount}s) %${nWide}s\n" "h" "set (H)eader"
    printf "%${#nTotalCount}s) %${nWide}s\n" "m" "(M)odify"
    printf "%${#nTotalCount}s) %${nWide}s\n" "r" "(R)emove"
    printf "%${#nTotalCount}s) %${nWide}s\n" "s" "(S)ort data"
    printf "%${#nTotalCount}s) %${nWide}s\n" "v" "(V)iew cmd"
    printf -- "$sInnerSeparatorLine\n"
    printf "%${#nTotalCount}s) %${nWide}s\n" "n" "(N)ormal mode"
  else
    printf "%${#nTotalCount}s) %${nWide}s\n" "e" "(E)xpert mode"
  fi
  printf "%${#nTotalCount}s) %${nWide}s\n" "q" "(Q)uit"
  printf -- "$sOuterSeparatorLine\n"
}

## Usage:
##   repeat [character] [line size]
## Ex.
##   repeat x 100
repeat() {
  [ $# -lt 2 ] && return 1
  sChar=${1:0:1}
  nSize=$2
  printf -vch  "%${nSize}s" ""
  printf "%s\n" "${ch// /$sChar}"
}

############################################# main

clear
set -o noglob
declare -i nDefaultTimeout=10
declare -i nExpertTimeout=65535
declare -i nTimeout=$nDefaultTimeout
declare -i isExpert=1
declare -a aMenu
declare -a aActs
declare    sMenuActSeparator=":"
declare    sFilter=""
declare -i nTotalCount=0
declare -i nDefaultColumn=1
declare -i nExpertColumn=3
declare -i nColumn=$nDefaultColumn
declare -i nWide=16
declare -i nActionWide=20
declare    sMenuFile=$0 ## default menu file is program itself.

declare -i nSeparatorLength=30
declare    sOuterSeparatorLine=`repeat = $nSeparatorLength`
declare    sInnerSeparatorLine=`repeat - $nSeparatorLength`

while getopts ":ehc:t:w:m:" opt
do
  case $opt in
  e)
    isExpert=0
    ;;
  h)
    showUsage
    exit 0
    ;;
  c)
    nDefaultColumn=$OPTARG
    ;;
  m)
    sMenuFile=$OPTARG
    ;;
  t)
    nDefaultTimeout=$OPTARG
    ;;
  w)
    nWide=$OPTARG
    ;;
  :)
    echo "The option ($opt) without parameters, exit program..."
    showUsage
    exit 1
    ;;
  \?)
    echo "Not such option($OPTARG), exit program..."
    showUsage
    exit 1
    ;;
  esac
done

sEnableFilter="#@ "
sDisableFilter="#\* "
sNormalFilter=$sEnableFilter
sExpertFilter="#[@*][ ]"
sExpertFilter4Perl="(#[@*][ ])"
sHeaderFilter="#\^ "
if [ $isExpert -eq 0 ]
then
  sFilter=$sExpertFilter
  nTimeout=$nExpertTimeout
  nColumn=$nExpertColumn
else
  sFilter=$sEnableFilter
  nTimeout=$nDefaultTimeout
  nColumn=$nDefaultColumn
fi
nTotalCount=`grep -c "^$sFilter" $sMenuFile`

while true
do
  buildMenu $nColumn $nWide
  declare -i nChoice
  declare    sChoice
  declare -i fRead=0
  declare    sItem=""

  while true
  do
    echo -n "Please enter your choice ($nTimeout secs to timeout): "
    read -t $nTimeout sChoice || fRead=$?

    #### special events
    [ $fRead -eq 142 ] && echo "Time out..." && exit 0
    [ $isExpert -eq 1 ] && [ $sChoice == 'e' ] && $0 -e -t $nDefaultTimeout -m $sMenuFile && exit 0
    [ $isExpert -eq 0 ] && [ $sChoice == 'n' ] && $0 -t $nDefaultTimeout -m $sMenuFile && exit 0
    [ $sChoice == 'q' ] && exit 0

    #### choice reactions
    nChoice=$sChoice
    [ -z "$sChoice" ] && buildMenu $nColumn $nWide && continue
    [ $isExpert -eq 1 ] && ( [ $nChoice -le 0 ] || [ $nChoice -gt $nTotalCount ] ) && continue
    [ $isExpert -eq 0 ] && ( [ $sChoice == "0" ] || [ $nChoice -ne 0 ] ) && continue
    break
  done

  case "$sChoice" in
  a)
    while true
    do
      read -p "Please enter the item you want to add: " sItem
      [ -z $sItem ] && break
      checkItemExist "$sItem" || break
      echo "Item ($sItem) exist!!"
    done

    if [ ! -z $sItem ]
    then
      read -p "Please enter the action of the item you added: " sAct
      if [ ! -z $sAct ]
      then
        echo "$sEnableFilter$sItem$sMenuActSeparator$sAct" >> $sMenuFile
        echo "Add item ($sItem) done..."
      fi
    fi
    ;;
  c)
    while true
    do
      read -p "Please enter the item you want to rename: " sItem
      [ -z $sItem ] && break
      checkItemNotExist "$sItem" || break
      echo "Item ($sItem) doesnot exist!!"
    done

    if [ ! -z $sItem ]
    then
      read -p "Please enter the new name of the item: " sNewItemName
      perl -pi -e "s/^$sExpertFilter4Perl$sItem$sMenuActSeparator.*$/\1$sNewItemName$sMenuActSeparator$sAct/" $sMenuFile
      echo "change item name ($sItem -> $sNewItemName) done..."
    fi
    ;;
  d)
    while true
    do
      read -p "Please enter the item you want to disable: " sItem
      [ -z $sItem ] && break
      checkItemNotExist "$sItem" || break
      echo "Item ($sItem) doesnot exist!!"
    done

    if [ ! -z $sItem ]
    then
      perl -pi -e "s/^$sEnableFilter$sItem$sMenuActSeparator/$sDisableFilter$sItem$sMenuActSeparator/" $sMenuFile
      echo "disable item ($sItem) done..."
    fi
    ;;
  e)
    while true
    do
      read -p "Please enter the item you want to enable: " sItem
      [ -z $sItem ] && break
      checkItemNotExist "$sItem" || break
      echo "Item ($sItem) doesnot exist!!"
    done

    if [ ! -z $sItem ]
    then
      perl -pi -e "s/^$sDisableFilter$sItem$sMenuActSeparator/$sEnableFilter$sItem$sMenuActSeparator/" $sMenuFile
      echo "enable item ($sItem) done..."
    else
      echo "sItem is empty..."
    fi
    ;;
  h)
    setHeader
    ;;
  m)
    while true
    do
      read -p "Please enter the item you want to modify: " sItem
      [ -z $sItem ] && break
      checkItemNotExist "$sItem" || break
      echo "Item ($sItem) doesnot exist!!"
    done

    if [ ! -z $sItem ]
    then
      read -p "Please enter the action of the item: " sAct
      perl -pi -e "s/^$sExpertFilter4Perl$sItem$sMenuActSeparator.*$/\1$sItem$sMenuActSeparator$sAct/" $sMenuFile
      echo "modify item ($sItem) done..."
    fi
    ;;
  r)
    while true
    do
      read -p "Please enter the item you want to remove: " sItem
      [ -z $sItem ] && break
      checkItemNotExist "$sItem" || break
      echo "Item ($sItem) doesnot exist!!"
    done

    if [ ! -z $sItem ]
    then
      perl -pi -e "s/^$sExpertFilter$sItem$sMenuActSeparator.*\n$//" $sMenuFile
      echo "remove item ($sItem) done..."
    fi
    ;;
  s)
    cp $sMenuFile $sMenuFile.bak ## backup first
    declare -i nEOS=`grep -n '#### \*\*\* Data \*\*\*$' $sMenuFile | cut -d: -f1`
    declare -i nBOD=$nEOS+1 ## not work for Ubuntu
    declare -i nLOD=`wc -l $sMenuFile | awk '{print($1)}'`-$nEOS
    [ $nEOS -eq 0 ] && exit 0
    head -$nEOS $sMenuFile > /tmp/$$.main
    ##tail +$nBOD $sMenuFile | sed -e "s/^#[@*][ ]*$//" | sed -e "/^$/d" | sort -k 2 > /tmp/$$.data ## not work for Ubuntu
    tail -$nLOD $sMenuFile | sed -e "s/^$sExpertFilter*$//" | sed -e "/^$/d" | sort -k 2 > /tmp/$$.data
    cat /tmp/$$.main /tmp/$$.data > $sMenuFile
    rm /tmp/$$.main /tmp/$$.data
    echo "Sorting data done, please restart your program..."
    ;;
  v)
    while true
    do
      read -p "Please enter the item you want to view: " sItem
      [ -z $sItem ] && break
      checkItemNotExist "$sItem" || break
      echo "Item ($sItem) doesnot exist!!"
    done

    if [ ! -z $sItem ]
    then
      grep "^$sExpertFilter$sItem$sMenuActSeparator" $sMenuFile | awk -F"$sMenuActSeparator" '{print("Command:",$2)}'
    fi
    ;;
  "")
    ;;
  *)
    if [ $isExpert -eq 1 ] ## 1 for not expert mode!
    then   
      OLD_IFS=$IFS
      IFS=';'
      for sCmdOpts in ${aActs[$nChoice]}
      do
        echo "Execute Command: $sCmdOpts"
        eval $sCmdOpts
      done
      IFS=$OLD_IFS
    else
      continue
    fi     
    ;;
  esac
  pak2c
done

echo "End of process..."

exit
exit
exit

####
#### Menu item:
####   Use '#@ ', '#* ' at the begin of the line for setting menu item
####   #^ header(title)
####   #@ enable items
####   #* disable items
#### Ex.
####   #@ item
#### PS.
####   Do not modify the data below, use option "-e" (export mode) to modify.
####
#### *** Header ***
#^ My Menu
####
#### *** Data ***
#@ apple:echo "this an apple"
#@ banana:echo "this an banana"
#@ ls:ls
#@ id:id
#@ pwd:pwd
#@ stanley:id;pwd;ls -al;ls ./;echo 1 2 3;pwd;id

Wish this helps.
regards,
Stanley Huang