gsu: Avoid duplication of command regex.
[gsu.git] / funcs / gsu
index 0403039f90661a3ac36ce534c5e8befff37e5a4b..63e0acf7dfb705047efb1f4809c9af3282fbe9a3 100644 (file)
--- a/funcs/gsu
+++ b/funcs/gsu
@@ -1,17 +1,24 @@
 #!/bin/bash
 # gsu -- the global subcommand utility
 #!/bin/bash
 # gsu -- the global subcommand utility
-# (C) 2006-2007 Andre Noll
+# (C) 2006-2011 Andre Noll
 
 
-gsu_init_errors()
+_gsu_init_errors()
 {
        gsu_errors="
 GSU_SUCCESS                    success
 E_GSU_BAD_COMMAND              invalid command
 E_GSU_NOT_A_NUMBER             not a number
 {
        gsu_errors="
 GSU_SUCCESS                    success
 E_GSU_BAD_COMMAND              invalid command
 E_GSU_NOT_A_NUMBER             not a number
+E_GSU_BAD_CONFIG_VAR           invalid config variable
+E_GSU_NEED_VALUE               value required but not given
+E_GSU_BAD_BOOL                 bad value for boolian option
+E_GSU_BAD_OPTION_TYPE          invalid option type
+E_GSU_BAD_ARG_COUNT            invalid number of arguments
+E_GSU_EDITOR                   failed to execute editor
+E_GSU_MKDIR                    failed to create directory
+E_GSU_GETOPTS                  getopts error
 $gsu_errors
 "
 $gsu_errors
 "
-       local a b i
-       local i=0
+       local a b i=0
        while read a b; do
                if test -z "$a"; then
                        continue
        while read a b; do
                if test -z "$a"; then
                        continue
@@ -19,12 +26,12 @@ $gsu_errors
                #echo "a:$a,  b: $b"
                gsu_error_txt[i]="$b"
                eval $a=$i
                #echo "a:$a,  b: $b"
                gsu_error_txt[i]="$b"
                eval $a=$i
-               i=$((i + 1))
+               i=$(($i + 1))
        done << EOF
        $gsu_errors
 EOF
 }
        done << EOF
        $gsu_errors
 EOF
 }
-export -f gsu_init_errors
+export -f _gsu_init_errors
 
 # check if $1 is a number
 gsu_is_a_number()
 
 # check if $1 is a number
 gsu_is_a_number()
@@ -38,6 +45,37 @@ gsu_is_a_number()
 }
 export -f gsu_is_a_number
 
 }
 export -f gsu_is_a_number
 
+# Check number of arguments.
+#
+# Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
+#
+# Check that <num_given> is between <num1> and <num2> inclusively.
+# If only <num1> ist given, num2 is assumed to be infinity.
+#
+# Examples:
+#      0 0 no argument allowed
+#      1 1 exactly one argument required
+#      0 2 at most two arguments admissible
+#      2   at least two arguments reqired
+#
+gsu_check_arg_count()
+{
+       ret=-$E_GSU_BAD_ARG_COUNT
+       if [[ $# -eq 2 ]]; then # only num1 is given
+               result="at least $2 args required, $1 given"
+               [[ $1 -lt $2 ]] && return
+               ret=$GSU_SUCCESS
+               return
+       fi
+       # num1 and num2 given
+       result="need at least $2 args, $1 given"
+       [[ $1 -lt $2 ]] && return
+       result="need at most $3 args, $1 given"
+       [[ $1 -gt $3 ]] && return
+       ret=$GSU_SUCCESS
+}
+export -f gsu_check_arg_count
+
 gsu_short_msg()
 {
        echo "$1" 1>&2
 gsu_short_msg()
 {
        echo "$1" 1>&2
@@ -46,27 +84,29 @@ export -f gsu_short_msg
 
 gsu_msg()
 {
 
 gsu_msg()
 {
-       gsu_short_msg "$gsu_self: $1"
+       gsu_short_msg "$_gsu_self: $1"
 }
 export -f gsu_msg
 
 gsu_date_msg()
 {
 }
 export -f gsu_msg
 
 gsu_date_msg()
 {
-       gsu_short_msg "$gsu_self $(date): $1"
+       gsu_short_msg "$_gsu_self $(date): $1"
 }
 export -f gsu_date_msg
 
 }
 export -f gsu_date_msg
 
-gsu_banner_msg()
+
+
+_gsu_banner_msg()
 {
 {
-       local txt="*** $gsu_self --"
+       local txt="### $_gsu_self --"
        if test -z "$gsu_banner_txt"; then
                txt="$txt set \$gsu_banner_txt to customize this message"
        else
                txt="$txt $gsu_banner_txt"
        fi
        if test -z "$gsu_banner_txt"; then
                txt="$txt set \$gsu_banner_txt to customize this message"
        else
                txt="$txt $gsu_banner_txt"
        fi
-       gsu_short_msg "$txt ***"
+       gsu_short_msg "$txt ###"
 }
 }
-export -f gsu_banner_msg
+export -f _gsu_banner_msg
 
 gsu_err_msg()
 {
 
 gsu_err_msg()
 {
@@ -87,15 +127,144 @@ gsu_err_msg()
        else
                txt="${gsu_error_txt[$err]}"
        fi
        else
                txt="${gsu_error_txt[$err]}"
        fi
-        echo "$gsu_self: $txt" 1>&2
+       gsu_msg "$txt"
 }
 export -f gsu_err_msg
 
 }
 export -f gsu_err_msg
 
-gsu_usage()
+_gsu_usage()
 {
 {
-       gsu_short_msg "Usage: $gsu_self command [options]"
+       gsu_short_msg "# Usage: $_gsu_self command [options]"
+}
+export -f _gsu_usage
+
+# Each line matching this is recognized as a subcommand. The name
+# of the subcommand is the first subexpression.
+export gsu_command_regex='^com_\([a-zA-Z_0-9]\+\)()'
+
+_gsu_available_commands()
+{
+       result="$({
+               printf "help\nman\nprefs\n"
+               sed -ne "s/$gsu_command_regex/\1/g;T;p" $0
+               } | sort | tr '\n' ' ')"
+}
+export -f _gsu_available_commands
+
+_gsu_print_available_commands()
+{(
+       local i count
+       gsu_short_msg "Available commands:"
+       for i in $gsu_cmds; do
+               printf "$i"
+               count=$(($count + 1))
+               if test $(($count % 4)) -eq 0; then
+                       echo
+               else
+                       printf "\t"
+                       if test ${#i} -lt 8; then
+                               printf "\t"
+                       fi
+               fi
+       done
+       echo
+) 2>&1
+}
+export -f _gsu_print_available_commands
+
+export gsu_prefs_txt="
+Print the current preferences.
+
+Usage: prefs [-e]
+
+If -e is given, the config file is opened with the default editor.  Without
+options, the command prints out a list of all cmt config variables, together
+with their current value and the default value."
+_com_prefs()
+{
+       local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}"
+
+       if [[ "$1" = "-e" ]]; then
+               ret=-$E_GSU_MKDIR
+               result="${conf%/*}"
+               mkdir -p "$result"
+               [[ $? -ne 0 ]] && return
+               ret=-$E_GSU_EDITOR
+               result="${EDITOR:-vi}"
+               "$result" "$conf"
+               [[ $? -ne 0 ]] && return
+               ret=$GSU_SUCCESS
+               return
+       fi
+
+       for ((i=0; i < ${#gsu_options[@]}; i++)); do
+               local name= option_type= default_value= required=
+               local description= help_text=
+               eval "${gsu_options[$i]}"
+               eval val='"$'${gsu_config_var_prefix}_$name'"'
+               case "$required" in
+               true|yes)
+                       printf "# required"
+                       ;;
+               *)
+                       printf "# optional"
+                       ;;
+               esac
+               printf " $option_type: $description"
+               if [[ "$required" != "yes" && "$required" != "true" ]]; then
+                       printf " [$default_value]"
+               fi
+               echo
+               [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
+               printf "$name=$val"
+               [[ "$val" == "$default_value" ]] && printf " # default"
+               echo
+       done
+}
+export -f _com_prefs
+
+export gsu_man_txt="
+Print the manual.
+
+Usage: man"
+
+_com_man()
+{
+       local equal_signs="=================================================="
+       local minus_signs="--------------------------------------------------"
+        local com num
+
+       echo "$_gsu_self (_${gsu_banner_txt}_) manual"
+       echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
+       echo
+
+        sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
+       echo "----"
+        echo
+       echo "$_gsu_self usage"
+       echo "${minus_signs:0:${#_gsu_self} + 6}"
+       printf "\t"
+       _gsu_usage 2>&1
+       echo "Each command has its own set of options as described below."
+       echo
+       echo "----"
+       echo
+       echo "Available commands:"
+
+       _gsu_available_commands
+        for com in $result; do
+               num=${#com}
+               if test $num -lt 4; then
+                       num=4
+               fi
+                echo "${minus_signs:0:$num}"
+                echo "$com"
+                echo "${minus_signs:0:$num}"
+                $0 help $com
+               echo
+        done
+        ret=$GSU_SUCCESS
 }
 }
-export -f gsu_usage
+export -f _com_man
 
 export gsu_help_txt="
 Print online help.
 
 export gsu_help_txt="
 Print online help.
@@ -104,107 +273,272 @@ Usage: help [command]
 
 Without arguments, print the list of available commands. Otherwise,
 print the help text for the given command."
 
 Without arguments, print the list of available commands. Otherwise,
 print the help text for the given command."
-com_help()
+
+_com_help()
 {
        local a b
        if test -z "$1"; then
 {
        local a b
        if test -z "$1"; then
-               gsu_banner_msg
-               gsu_usage
-               # sed is magic, baby
-               grep -A 2 "^com_\([a-zA-Z_0-9]\+\)()" $0 \
-                       | grep -v -- '--' \
-                       | sed -e '/^com_\([a-zA-Z_0-9]\+\)()/bs' \
+               _gsu_banner_msg 2>&1
+               _gsu_usage 2>&1
+               {
+                       printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
+                       printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
+                       printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
+                       grep -A 2 "$gsu_command_regex" $0
+               } | grep -v -- '--' \
+                       | sed -e "/$gsu_command_regex/bs" \
                                -e 'H;$!d;x;s/\n//g;b' \
                                -e :s \
                                -e 'x;s/\n//g;${p;x;}' \
                                -e 'H;$!d;x;s/\n//g;b' \
                                -e :s \
                                -e 'x;s/\n//g;${p;x;}' \
-                       | sed -e 's/^com_\([a-zA-Z_0-9]\+\)()#*/\1\t/' \
+                       | sed -e "s/${gsu_command_regex}#*/\1\t/" \
                        | sort \
                        | while read a b; do
                        | sort \
                        | while read a b; do
-                                echo -en "$a\t"
-                                if test ${#a} -lt 8; then
-                                        echo -en "\t"
-                                fi
-                                echo "$b"
+                               printf "$a\t"
+                               if test ${#a} -lt 8; then
+                                       printf "\t"
+                               fi
+                               echo "$b"
                         done
                echo
                         done
                echo
-               gsu_msg "Try $0 help <command> for info on <command>."
+               echo "# Try $_gsu_self help <command> for info on <command>."
                ret=$GSU_SUCCESS
                return
        fi
        if test "$1" = "help"; then
                ret=$GSU_SUCCESS
                return
        fi
        if test "$1" = "help"; then
-               gsu_short_msg "$gsu_help_txt"
+               echo "$gsu_help_txt"
+               ret=$GSU_SUCCESS
+               return
+       fi
+       if test "$1" = "man"; then
+               echo "$gsu_man_txt"
+               ret=$GSU_SUCCESS
+               return
+       fi
+       if test "$1" = "prefs"; then
+               echo "$gsu_prefs_txt"
                ret=$GSU_SUCCESS
                return
        fi
        ret=$GSU_SUCCESS
        if grep -q "^com_$1()" $0; then
                ret=$GSU_SUCCESS
                return
        fi
        ret=$GSU_SUCCESS
        if grep -q "^com_$1()" $0; then
-               sed -e "1,/com_$1()/d" -e '/^{/,$d' -e 's/^## *//' $0
+               sed -e "1,/^com_$1()$/d" -e '/^{/,$d' -e 's/^## *//' $0
                return
        fi
                return
        fi
-       gsu_print_available_commands
+       _gsu_print_available_commands
        result="$1"
        ret=-$E_GSU_BAD_COMMAND
 }
        result="$1"
        ret=-$E_GSU_BAD_COMMAND
 }
-export -f com_help
+export -f _com_help
 
 
-gsu_available_commands()
+# internal gsu function that syntactically checks the gsu_options array
+# for errors and parses the config file.
+_gsu_check_options()
 {
 {
-       result="$( (echo help; grep "^com_[a-z_]\+()" $0) \
-               | sed -e 's/^com_//' -e 's/()//' \
-               | sort \
-               | tr '\n' ' ')"
-       ret=$SUCCESS
+       local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}" val
+
+       for ((i=0; i < ${#gsu_options[@]}; i++)); do
+               eval "${gsu_options[$i]}"
+               eval val='"'\$$name'"'
+               eval orig_${gsu_config_var_prefix}_$name='"'${val}'"'
+       done
+
+       [[ -r "$conf" ]] && source "$conf"
+
+       for ((i=0; i < ${#gsu_options[@]}; i++)); do
+               local name= option_type= default_value= required=
+               local description= help_text=
+               local val orig_val
+
+               eval "${gsu_options[$i]}"
+
+
+               # Check name. It must be non-empty and consist of [a-zA-Z_0-9]
+               # only.  Moreover it must not start with [a-zA-Z].
+
+               ret=-$E_GSU_BAD_CONFIG_VAR
+               result="name: '$name'"
+               # bash's =~ works only for 3.2 and newer, so use grep
+               echo "$name" | grep '^[a-zA-Z][a-zA-Z_0123456789]*$' &> /dev/null;
+               [[ $? -ne 0 ]] && return
+
+               eval orig_val='"'\$orig_${gsu_config_var_prefix}_$name'"'
+               if [[ -z "$orig_val" ]]; then
+                       eval val='"'\$$name'"'
+               else
+                       val="$orig_val"
+               fi
+               case "$required" in
+               true|yes)
+                       ret=-$E_GSU_NEED_VALUE
+                       result="$name"
+                       [[ -z "$val" ]] && return
+                       ;;
+               false|no)
+                       ;;
+               *)
+                       ret=-$E_GSU_BAD_BOOL
+                       result="required: $required, name: $name, val: $val"
+                       return
+               esac
+
+               eval ${gsu_config_var_prefix}_$name='"'\${val:=$default_value}'"'
+               # Check option type. ATM, only num and string are supported
+               # Other types may be added without breaking compatibility
+               case "$option_type" in
+               string)
+                       ;;
+               num)
+                       gsu_is_a_number "$val"
+                       [[ $ret -lt 0 ]] && return
+                       ;;
+               *)
+                       ret=-$E_GSU_BAD_OPTION_TYPE
+                       result="$name/$option_type"
+                       return
+               esac
+       done
+       ret=$GSU_SUCCESS
 }
 }
-export -f gsu_available_commands
+export -f _gsu_check_options
 
 
-gsu_print_available_commands()
-{(
-       local i count
-       gsu_short_msg "Available commands:"
-       for i in $gsu_cmds; do
-               printf "$i"
-               count=$((count + 1))
-               if test $((count % 4)) -eq 0; then
-                       echo
+# Wrapper for bash's getopts.
+#
+# Aborts on programming errors such as missing or invalid option string.  On
+# success $result contains shell code that can be eval'ed. For each defined
+# option x, the local variable o_x will be created when calling eval "$result".
+# o_x contains true/false for options without an argument or the emtpy string/the
+# given argument, depending on whether this option was contained in the "$@"
+# array.
+#
+# Example:
+#      gsu_getopts abc:x:y
+#      eval "$result"
+#      [[ $ret -lt 0 ]] && return
+#
+#      [[ "$o_a" = "true ]] && echo "The -a flag was given"
+#      [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
+gsu_getopts()
+{
+       local i c tab=' ' cr='
+'
+
+       gsu_check_arg_count $# 1 1
+       if [[ $ret -lt 0 ]]; then
+               gsu_err_msg
+               exit 1
+       fi
+
+       ret=-$E_GSU_GETOPTS
+       result="invalid optstring $1"
+       if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
+               gsu_err_msg
+               exit 1
+       fi
+
+       for ((i=0; i < ${#1}; i++)); do
+               c=${1:$i:1}
+               case "$c" in
+               [a-zA-Z:]);;
+               *)
+                       ret=-$E_GSU_GETOPTS
+                       result="invalid character $c in optstring"
+                       gsu_err_msg
+                       exit 1
+               esac
+       done
+       result="local opt"
+       for ((i=0; i < ${#1}; i++)); do
+               c1=${1:$i:1}
+               c2=${1:$(($i + 1)):1}
+               result+=" o_$c1"
+               if [[ "$c2" = ":" ]]; then
+                       let i++
                else
                else
-                       printf "\t"
-                       if test ${#i} -lt 8; then
-                               printf "\t"
-                       fi
+                       result+="=false"
                fi
        done
                fi
        done
-       echo
-) 2>&1
+       result+="
+       OPTIND=1
+       while getopts $1 opt \"\$@\"; do
+               case \"\$opt\" in
+"
+       for ((i=0; i < ${#1}; i++)); do
+               c1=${1:$i:1}
+               c2=${1:$(($i + 1)):1}
+               result+="$tab$tab$c1) o_$c1="
+               if [[ "$c2" = ":" ]]; then
+                       result+="\"\$OPTARG\""
+                       let i++
+               else
+                       result+="true"
+               fi
+               result+=";;$cr"
+       done
+       result+="
+               *)
+                       ret=-\$E_GSU_GETOPTS
+                       result=\"invalid option given\"
+                       return
+                       ;;
+               esac
+       done
+       shift \$((\$OPTIND - 1))
+"
+       ret=$GSU_SUCCESS
 }
 }
-export -f gsu_print_available_commands
+export -f gsu_getopts
 
 gsu()
 {
 
 gsu()
 {
-       gsu_self="$(basename $0)"
-       gsu_init_errors
-       gsu_available_commands
+       local i
+       _gsu_self="$(basename $0)"
+       gsu_name="${gsu_name:=$_gsu_self}"
+       gsu_config_var_prefix="${gsu_config_var_prefix:=$gsu_name}"
+       _gsu_init_errors
+       _gsu_check_options
+       if [[ "$ret" -lt 0 ]]; then
+               if [[ "$1" != "help" && "$1" != "man" ]]; then
+                       gsu_err_msg
+                       exit 1
+               fi
+       fi
+       _gsu_available_commands
        gsu_cmds="$result"
        if test $# -eq 0; then
        gsu_cmds="$result"
        if test $# -eq 0; then
-               gsu_usage
-               gsu_print_available_commands
+               _gsu_usage
+               _gsu_print_available_commands
                exit 1
        fi
        arg="$1"
        shift
                exit 1
        fi
        arg="$1"
        shift
+       # check internal commands
+       if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" ]]; then
+               _com_$arg "$@"
+               if [[ "$ret" -lt 0 ]]; then
+                       gsu_err_msg
+                       exit 1
+               fi
+               exit 0
+       fi
+
+       # external commands
        for i in $gsu_cmds; do
                if test "$arg" = "$i"; then
        for i in $gsu_cmds; do
                if test "$arg" = "$i"; then
-                       com_$arg $*
-                       if test $ret -lt 0; then
+                       com_$arg "$@"
+                       if [[ "$ret" -lt 0 ]]; then
                                gsu_err_msg
                                exit 1
                        fi
                        exit 0
                fi
        done
                                gsu_err_msg
                                exit 1
                        fi
                        exit 0
                fi
        done
+
        ret=-$E_GSU_BAD_COMMAND
        result="$arg"
        gsu_err_msg
        ret=-$E_GSU_BAD_COMMAND
        result="$arg"
        gsu_err_msg
-       gsu_print_available_commands
+       _gsu_print_available_commands
        exit 1
 }
        exit 1
 }
-# no need to export this
+export -f gsu
+
+# TODO: gsu_strerror: get error string