Merge topic branch t/gui into pu
[gsu.git] / subcommand
index bab8ded4ea1d9f0b99562eca7dc8d257244062a0..d37481cdbdccb5b62268d3b788de84b61a2ab347 100644 (file)
@@ -1,15 +1,16 @@
 #!/bin/bash
 #!/bin/bash
-# (C) 2006-2011 Andre Noll
+# Copyright (C) 2006 Andre Noll
+# Licensed under the LGPL, version 3. See COPYING and COPYING.LESSER.
 
 if [[ "$(type -t _gsu_setup)" != "function" ]]; then
        gsu_dir=${gsu_dir:-${BASH_SOURCE[0]%/*}}
 
 if [[ "$(type -t _gsu_setup)" != "function" ]]; then
        gsu_dir=${gsu_dir:-${BASH_SOURCE[0]%/*}}
-       . $gsu_dir/common || exit 1
+       . "$gsu_dir/common" || exit 1
        _gsu_setup
 fi
 
 _gsu_usage()
 {
        _gsu_setup
 fi
 
 _gsu_usage()
 {
-       gsu_short_msg "# Usage: $_gsu_self command [options]"
+       gsu_short_msg "# Usage: $gsu_name command [options]"
 }
 
 # Return an extended regular expression to match against $0.
 }
 
 # Return an extended regular expression to match against $0.
@@ -24,7 +25,7 @@ _gsu_usage()
 # exactly one parenthesized subexpression for matching the command name.
 _gsu_get_command_regex()
 {
 # exactly one parenthesized subexpression for matching the command name.
 _gsu_get_command_regex()
 {
-       local cmd="${1:-[-a-zA-Z_0-9]+}"
+       local cmd="${1:-[-a-zA-Z_0-9]{1,\}}"
        result="^com_($cmd)\(\)"
 }
 
        result="^com_($cmd)\(\)"
 }
 
@@ -45,30 +46,155 @@ _gsu_available_commands()
 
                        # otherwise delete it
                        d
 
                        # otherwise delete it
                        d
-               ' $0
+               ' "$0"
        } | sort | tr '\n' ' ')"
 }
 
        } | sort | tr '\n' ' ')"
 }
 
-_gsu_print_available_commands()
-{(
-       local i count=0
-       gsu_short_msg "Available commands:"
-       for i in $gsu_cmds; do
-               printf "$i"
-               count=$(($count + 1))
-               if test $(($count % 4)) -eq 0; then
-                       echo
+# 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 required
+gsu_check_arg_count()
+{
+       ret=-$E_GSU_BAD_ARG_COUNT
+       if (($# == 2)); then # only num1 is given
+               result="at least $2 args required, $1 given"
+               (($1 < $2)) && return
+               ret=$GSU_SUCCESS
+               return
+       fi
+       # num1 and num2 given
+       result="need at least $2 args, $1 given"
+       (($1 < $2)) && return
+       result="need at most $3 args, $1 given"
+       (($1 > $3)) && return
+       ret=$GSU_SUCCESS
+}
+
+# Wrapper for the bash getopts builtin.
+#
+# 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 argument and either the empty
+# string or the given argument for options that take an argument.
+#
+# Example:
+#      gsu_getopts abc:x:y
+#      eval "$result"
+#      (($ret < 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 i
+       local c c1 c2 tab='     ' cr='
+'
+
+       gsu_check_arg_count $# 1 1
+       if ((ret < 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 _gsu_getopts_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 _gsu_getopts_opt \"\$@\"; do
+               case \"\$_gsu_getopts_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
+}
+
+_gsu_print_available_commands()
+{
+       local cmd cmds
+       local -i maxlen=0 cols width=80 count=0
+
+       result=$(stty size 2>/dev/null)
+       if (($? == 0)); then
+               gsu_is_a_number "${result#* }"
+               ((ret >= 0)) && ((result > 0)) && width=$result
+       fi
+       _gsu_available_commands
+       cmds=$result
+       for cmd in $cmds; do
+               ((${#cmd} > maxlen)) && maxlen=${#cmd}
+       done
+       let maxlen++
+       ((width < maxlen)) && cols=1 || cols=$((width / maxlen))
+       printf 'Available commands:'
+       for cmd in $cmds; do
+               ((count % cols == 0)) && printf '\n'
+               printf '%-*s' $maxlen $cmd
+               let ++count
+       done
+       printf '\n'
 }
 
 }
 
+# Print all options of the given optstring to stdout if the word in the current
+# command line begins with a hyphen character.
+#
+# Returns 0 if the current word does not start with a hyphen, one otherwise.
 gsu_complete_options()
 {
        local opts="$1" cword="$2" cur opt
 gsu_complete_options()
 {
        local opts="$1" cword="$2" cur opt
@@ -80,42 +206,46 @@ gsu_complete_options()
        ret=0
        [[ ! "$cur" == -* ]] && return
 
        ret=0
        [[ ! "$cur" == -* ]] && return
 
-       ret=0
        for ((i=0; i < ${#opts}; i++)); do
                opt="${opts:$i:1}"
                [[ "$opt" == ":" ]] && continue
                printf "%s" "-$opt "
        for ((i=0; i < ${#opts}; i++)); do
                opt="${opts:$i:1}"
                [[ "$opt" == ":" ]] && continue
                printf "%s" "-$opt "
-               let ret++
        done
        done
+       ret=1
 }
 
 }
 
-export gsu_prefs_txt="
+declare -A _gsu_help_text=() # indexed by autocmd
+com_prefs_options='e'
+
+_gsu_help_text['prefs']='
 Print the current preferences.
 
 Usage: prefs [-e]
 
 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()
+If -e is given, the config file is opened with the default editor.
+Without options, the command prints out a list of all config variables,
+together with their current value and the default value.
+'
+
+com_prefs()
 {
        local i conf="${gsu_config_file:=${HOME:-}/.$gsu_name.rc}"
 
 {
        local i conf="${gsu_config_file:=${HOME:-}/.$gsu_name.rc}"
 
-       gsu_getopts "e"
+       gsu_getopts "$com_prefs_options"
        eval "$result"
        eval "$result"
-       (($ret < 0)) && return
+       ((ret < 0)) && return
        gsu_check_arg_count $# 0 0
        gsu_check_arg_count $# 0 0
-       (($ret < 0)) && return
+       ((ret < 0)) && return
 
        if [[ "$o_e" == "true" ]]; then
                ret=-$E_GSU_MKDIR
                result="${conf%/*}"
                mkdir -p "$result"
 
        if [[ "$o_e" == "true" ]]; then
                ret=-$E_GSU_MKDIR
                result="${conf%/*}"
                mkdir -p "$result"
-               [[ $? -ne 0 ]] && return
+               (($? != 0)) && return
                ret=-$E_GSU_EDITOR
                result="${EDITOR:-vi}"
                ret=-$E_GSU_EDITOR
                result="${EDITOR:-vi}"
-               "$result" "$conf"
-               [[ $? -ne 0 ]] && return
+               "$result" $conf
+               (($? != 0)) && return
                ret=$GSU_SUCCESS
                return
        fi
                ret=$GSU_SUCCESS
                return
        fi
@@ -133,43 +263,334 @@ _com_prefs()
                        printf "# optional"
                        ;;
                esac
                        printf "# optional"
                        ;;
                esac
-               printf " $option_type: $description"
+               printf " %s: %s" "$option_type" "$description"
                if [[ "$required" != "yes" && "$required" != "true" ]]; then
                if [[ "$required" != "yes" && "$required" != "true" ]]; then
-                       printf " [$default_value]"
+                       printf " [%s]" "$default_value"
                fi
                echo
                [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
                fi
                echo
                [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
-               printf "$name=$val"
+               printf "%s=%s" "$name" "$val"
                [[ "$val" == "$default_value" ]] && printf " # default"
                echo
        done
 }
 
                [[ "$val" == "$default_value" ]] && printf " # default"
                echo
        done
 }
 
+_gsu_isatty()
+{(
+       exec 3<&1
+       stty 0<&3 &> /dev/null
+)}
+
 complete_prefs()
 {
 complete_prefs()
 {
-       gsu_complete_options "e" "$@"
+       gsu_complete_options "$com_prefs_options" "$@"
+}
+
+_gsu_man_options='m:b:'
+
+complete_man()
+{
+       gsu_complete_options "$_gsu_man_options" "$@"
+       ((ret > 0)) && return
+       gsu_cword_is_option_parameter "$_gsu_man_options" "$@"
+       [[ "$result" == 'm' ]] && printf 'roff\ntext\nhtml\n'
 }
 
 }
 
-export gsu_man_txt="
+_gsu_help_text['man']='
 Print the manual.
 
 Print the manual.
 
-Usage: man"
+Usage: man [-m <mode>] [-b <browser>]
+
+-m: Set output format (text, roff or html). Default: roff.
+-b: Use the specified browser. Implies html mode.
+
+If stdout is not associated with a terminal device, the command
+dumps the man page to stdout and exits.
+
+Otherwise, it tries to display the manual page as follows. In text
+mode, plain text is piped to $PAGER. In roff mode, the roff output
+is filtered through nroff, then piped to $PAGER. For both formats,
+if $PAGER is unset, less(1) is assumed.
+
+In html mode, html output is written to a temporary file, and this
+file is displayed as a page in the web browser. If -b is not given,
+the command stored in the $BROWSER environment variable is executed
+with the path to the temporary file as an argument. If $BROWSER is
+unset, elinks(1) is assumed.
+'
+
+_gsu_read_line()
+{
+       local -n p="$1"
+       local l OIFS="$IFS"
+
+       IFS=
+       read -r l || return
+       IFS="$OIFS"
+       p="$l"
+}
+
+_gsu_change_roffify_state()
+{
+       local -n statep="$1"
+       local new_state="$2"
+       local old_state="$statep"
+
+       [[ "$old_state" == "$new_state" ]] && return 0
+
+       case "$old_state" in
+       text);;
+       example) printf '.EE\n';;
+       enum) printf '.RE\n';;
+       esac
+       case "$new_state" in
+       text);;
+       example) printf '.EX\n';;
+       enum) printf '.RS 2\n';;
+       esac
+
+       statep="$new_state"
+       return 1
+}
+
+_gsu_print_protected_roff_line()
+{
+       local line="$1"
+       local -i n=0
+
+       while [[ "${line:$n:1}" == ' ' ]]; do
+               let n++
+       done
+       line="${line:$n}"
+       printf '\\&%s\n' "${line//\\/\\\\}"
+}
+
+_gsu_roffify_maindoc()
+{
+       local state='text' TAB='        '
+       local line next_line
+       local -i n
+
+       _gsu_read_line 'line' || return
+       while _gsu_read_line next_line; do
+               if [[ "$next_line" =~ ^(----|====|~~~~) ]]; then # heading
+                       printf '.SS %s\n' "$line"
+                       _gsu_read_line line || return
+                       _gsu_change_roffify_state 'state' 'text'
+                       continue
+               fi
+               if [[ "${line:0:1}" == "$TAB" ]]; then # example
+                       _gsu_change_roffify_state 'state' 'example'
+                       _gsu_print_protected_roff_line "$line"
+                       line="$next_line"
+                       continue
+               fi
+               n=0
+               while [[ "${line:$n:1}" == ' ' ]]; do
+                       let n++
+               done
+               line=${line:$n};
+               if [[ "${line:0:1}" == '*' ]]; then # enum
+                       line=${line#\*}
+                       _gsu_change_roffify_state 'state' 'enum'
+                       printf '\n\(bu %s\n' "$line"
+                       line="$next_line"
+                       continue
+               fi
+               if [[ "$line" =~ ^$ ]]; then # new paragraph
+                       _gsu_change_roffify_state 'state' 'text'
+                       printf '.PP\n'
+               else
+                       _gsu_print_protected_roff_line "$line"
+               fi
+               line="$next_line"
+       done
+       _gsu_print_protected_roff_line "$line"
+}
+
+_gsu_extract_maindoc()
+{
+       sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' -e 's/^# *//' -e 's/^#//g' "$0"
+}
+
+_gsu_roffify_cmds()
+{
+       local line cmd= desc= state='text' TAB='        '
+
+       while _gsu_read_line line; do
+               if [[ "${line:0:1}" != '#' ]]; then # com_foo()
+                       line="${line#com_}"
+                       cmd="${line%()}"
+                       continue
+               fi
+               line="${line####}"
+               if [[ "$line" =~ ^[[:space:]]*$ ]]; then
+                       printf '.PP\n'
+                       _gsu_change_roffify_state 'state' 'text'
+                       continue
+               fi
+               if [[ -n "$cmd" ]]; then # desc or usage
+                       if [[ -z "$desc" ]]; then # desc
+                               desc="$line"
+                               continue
+                       fi
+                       # usage
+                       _gsu_change_roffify_state 'state' 'text'
+                       printf '\n.SS %s \\- %s\n' "$cmd" "$desc"
+                       printf '\n.I %s\n'  "$line"
+                       cmd=
+                       desc=
+                       continue
+               fi
+               line="${line# }"
+               if [[ "${line:0:1}" == "$TAB" ]]; then
+                       _gsu_change_roffify_state 'state' 'example'
+                       _gsu_print_protected_roff_line "$line"
+                       continue
+               fi
+               if [[ "$line" == -*:* ]]; then
+                       _gsu_change_roffify_state 'state' 'enum'
+                       printf '.PP\n.B %s:\n' "${line%%:*}"
+                       _gsu_print_protected_roff_line "${line#*:}"
+                       continue
+               fi
+               _gsu_print_protected_roff_line "$line"
+       done
+}
+
+_gsu_roffify_autocmds()
+{
+       local cmd help_txt
+
+       for cmd in "${!_gsu_help_text[@]}"; do
+               help_txt="${_gsu_help_text["$cmd"]}"
+               {
+                       printf 'com_%s()\n' "$cmd"
+                       sed -e 's/^/## /g' <<< "$help_txt"
+               } | _gsu_roffify_cmds
+       done
+}
+
+_gsu_roff_man()
+{
+       local name="$1" sect_num="$2" month_year="$3" pkg="$4" sect_name="$5"
+       local purpose="$6"
+       local ere
+
+       cat << EOF
+.TH "${name^^}" "$sect_num" "$month_year" "$pkg" "$sect_name"
+.SH NAME
+$name \- $purpose
+.SH SYNOPSIS
+.B $name
+\fI\,<subcommand>\/\fR [\fI\,<options>\/\fR] [\fI\,<arguments>\/\fR]
+.SH DESCRIPTION
+EOF
+       _gsu_extract_maindoc | _gsu_roffify_maindoc
+
+       printf '\n.SH "GENERIC SUBCOMMANDS"\n'
+       printf 'The following commands are automatically created by gsu\n'
+       _gsu_roffify_autocmds
+
+       printf '\n.SH "LIST OF SUBCOMMANDS"\n'
+       printf 'Each command has its own set of options as described below.\n'
+
+       _gsu_get_command_regex
+       ere="$result"
+       # only consider lines in the comment of the function
+       sed -nEe '/'"$ere"'/,/^[^#]/p' "$0" | _gsu_roffify_cmds
+}
+
+_gsu_file_mtime()
+{
+       local file="$1"
+       result="$(find "$file" -printf '%TB %TY' 2>/dev/null)" # GNU
+       (($? == 0)) && [[ -n "$result" ]] && return
+       result="$(stat -f %Sm -t '%B %Y' "$file" 2>/dev/null)" # BSD
+       (($? == 0)) && [[ -n "$result" ]] && return
+       result="$(date '+%B %Y' 2>/dev/null)" # POSIX
+       (($? == 0)) && [[ -n "$result" ]] && return
+       result='[unknown date]'
+}
 
 
-_com_man()
+com_man()
 {
        local equal_signs="=================================================="
        local minus_signs="--------------------------------------------------"
 {
        local equal_signs="=================================================="
        local minus_signs="--------------------------------------------------"
-        local com num
+       local filter='cat' pager='cat' browser=${BROWSER:-elinks} tmpfile=
+       local com num isatty pipeline
 
 
-       echo "$_gsu_self (_${gsu_banner_txt}_) manual"
-       echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
-       echo
+       gsu_getopts "$_gsu_man_options"
+       eval "$result"
+       ((ret < 0)) && return
+       if [[ -n "$o_b" ]]; then
+               o_m='html'
+               browser="$o_b"
+       elif [[ -z "$o_m" ]]; then
+               o_m='roff'
+       fi
 
 
-        sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
+       _gsu_isatty && isatty='true' || isatty='false'
+       if [[ "$o_m" == 'roff' ]]; then
+               if [[ "$isatty" == 'true' ]]; then
+                       filter='nroff -Tutf8 -mandoc'
+                       pager="${PAGER:-less}"
+               fi
+       elif [[ "$o_m" == 'text' ]]; then
+               if [[ "$isatty" == 'true' ]]; then
+                       pager="${PAGER:-less}"
+               fi
+       elif [[ "$o_m" == 'html' ]]; then
+               filter='groff -T html -m man'
+               if [[ "$isatty" == 'true' ]]; then
+                       gsu_make_tempfile "gsu_html_man.XXXXXX.html"
+                       ((ret < 0)) && return || tmpfile="$result"
+                       trap "rm -f $tmpfile" RETURN EXIT
+               fi
+       fi
+       [[ "$pager" == 'less' ]] && export LESS=${LESS-RI}
+       case "$o_m" in
+       roff|html)
+               _gsu_file_mtime "$0"
+               _gsu_roff_man "$gsu_name" '1' "$result" \
+                       "${gsu_package-${gsu_name^^}(1)}" \
+                       "User Commands" "${gsu_banner_txt}" \
+               | $filter | {
+                       if [[ -n "$tmpfile" ]]; then
+                               cat > "$tmpfile"
+                       else
+                               $pager
+                       fi
+               }
+               if (($? != 0)); then
+                       ret=-$E_GSU_XCMD
+                       result="filter: $filter"
+                       return
+               fi
+               if [[ -n "$tmpfile" ]]; then
+                       ret=-$E_GSU_XCMD
+                       result="$browser"
+                       "$browser" "$tmpfile" || return
+               fi
+               ret=$GSU_SUCCESS
+               return
+               ;;
+       text) ;;
+       "") ;;
+       *)
+               ret=-$E_GSU_INVAL
+               result="$o_m"
+               return
+       esac
+       {
+       echo "$gsu_name (_${gsu_banner_txt}_) manual"
+       echo "${equal_signs:0:${#gsu_name} + ${#gsu_banner_txt} + 16}"
+       echo
+       _gsu_extract_maindoc
        echo "----"
        echo "----"
-        echo
-       echo "$_gsu_self usage"
-       echo "${minus_signs:0:${#_gsu_self} + 6}"
+       echo
+       echo "$gsu_name usage"
+       echo "${minus_signs:0:${#gsu_name} + 6}"
        printf "\t"
        _gsu_usage 2>&1
        echo "Each command has its own set of options as described below."
        printf "\t"
        _gsu_usage 2>&1
        echo "Each command has its own set of options as described below."
@@ -179,34 +600,31 @@ _com_man()
        echo "Available commands:"
 
        _gsu_available_commands
        echo "Available commands:"
 
        _gsu_available_commands
-        for com in $result; do
+       for com in $result; do
                num=${#com}
                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
+               ((num < 4)) && num=4
+               echo "${minus_signs:0:$num}"
+               echo "$com"
+               echo "${minus_signs:0:$num}"
+               "$0" help "$com"
                echo
                echo
-        done
-        ret=$GSU_SUCCESS
-}
-
-_gsu_banner_msg()
-{
-       gsu_short_msg "### $_gsu_self -- ###"
+       done
+       } | $pager
+       ret=$GSU_SUCCESS
 }
 
 }
 
-export gsu_help_txt="
+_gsu_help_text['help']='
 Print online help.
 
 Print online help.
 
-Usage: help [command]
+Usage: help [-a] [command]
 
 Without arguments, print the list of available commands. Otherwise,
 
 Without arguments, print the list of available commands. Otherwise,
-print the help text for the given command."
+print the help text for the given command.
+
+-a: Also show the help of automatic commands. Ignored if a command
+is given.'
 
 
-export gsu_complete_txt="
+_gsu_help_text['complete']='
 Command line completion.
 
 Usage: complete [<cword> <word>...]
 Command line completion.
 
 Usage: complete [<cword> <word>...]
@@ -218,24 +636,34 @@ completion.
 If at least one argument is given, all possible completions are
 written to stdout. This can be used from the completion function of
 the subcommand.
 If at least one argument is given, all possible completions are
 written to stdout. This can be used from the completion function of
 the subcommand.
-"
+'
 
 
-_com_help()
+com_help_options='a'
+com_help()
 {
 {
-       local a b ere tab='     '
+       local ere tab=' ' txt
+
+       gsu_getopts "$com_help_options"
+       eval "$result"
+       ((ret < 0)) && return
 
        _gsu_get_command_regex
        ere="$result"
 
        if (($# == 0)); then
 
        _gsu_get_command_regex
        ere="$result"
 
        if (($# == 0)); then
-               _gsu_banner_msg 2>&1
+               gsu_short_msg "### $gsu_name -- $gsu_banner_txt ###"
                _gsu_usage 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 "--"
-                       printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
-                       grep -EA 2 "$ere" $0
+                       if [[ "$o_a" == 'true' ]]; then
+                               _gsu_mfcb() { printf '%s\n' "$2"; }
+                               for cmd in "${!_gsu_help_text[@]}"; do
+                                       printf "com_%s()" "$cmd"
+                                       txt="${_gsu_help_text["$cmd"]}"
+                                       mapfile -n 3 -c 1 -C _gsu_mfcb <<< "$txt"
+                               printf -- '--\n'
+                               done
+                       fi
+                       grep -EA 2 "$ere" "$0"
                } | grep -v -- '--' \
                        | sed -En "/$ere/"'!d
                                # remove everything but the command name
                } | grep -v -- '--' \
                        | sed -En "/$ere/"'!d
                                # remove everything but the command name
@@ -256,36 +684,36 @@ _com_help()
                                y/\n/'"$tab"'/
 
                                # and print the sucker
                                y/\n/'"$tab"'/
 
                                # and print the sucker
-                               p'
-               echo
-               echo "# Try $_gsu_self help <command> for info on <command>."
-               ret=$GSU_SUCCESS
-               return
-       fi
-       if test "$1" = "help"; then
-               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"
+                               p
+                       ' | {
+                               local -a cmds=() descs=()
+                               local -i i maxlen=1
+                               local cmd desc
+                               while read cmd desc; do
+                                       ((maxlen < ${#cmd})) && maxlen=${#cmd}
+                                       cmds[${#cmds[@]}]=$cmd
+                                       descs[${#descs[@]}]=$desc
+                               done
+                               for ((i = 0; i < ${#cmds[@]}; i++)); do
+                                       printf '%-*s %s\n' $maxlen ${cmds[$i]} \
+                                               "${descs[$i]}"
+                               done
+                       }
+               printf "\n# Try %s help <command> for info on <command>, or %s help -a to see\n" \
+                       "$gsu_name" "$gsu_name"
+               printf '# also the subcommands which are automatically generated by gsu.\n'
                ret=$GSU_SUCCESS
                return
        fi
                ret=$GSU_SUCCESS
                return
        fi
-       if test "$1" = "complete"; then
-               echo "$gsu_complete_txt"
+       for cmd in "${!_gsu_help_text[@]}"; do
+               [[ "$1" != "$cmd" ]] && continue
+               printf '%s\n' "${_gsu_help_text["$cmd"]}"
                ret=$GSU_SUCCESS
                return
                ret=$GSU_SUCCESS
                return
-       fi
-       ret=$GSU_SUCCESS
+       done
        _gsu_get_command_regex "$1"
        ere="$result"
        _gsu_get_command_regex "$1"
        ere="$result"
-       if ! grep -Eq "$ere" $0; then
+       if ! grep -Eq "$ere" "$0"; then
                _gsu_print_available_commands
                result="$1"
                ret=-$E_GSU_BAD_COMMAND
                _gsu_print_available_commands
                result="$1"
                ret=-$E_GSU_BAD_COMMAND
@@ -308,7 +736,8 @@ _com_help()
                        :p
                        p
                }
                        :p
                        p
                }
-       ' $0
+       ' "$0"
+       ret=$GSU_SUCCESS
 }
 
 complete_help()
 }
 
 complete_help()
@@ -317,92 +746,7 @@ complete_help()
        echo "$result"
 }
 
        echo "$result"
 }
 
-# Wrapper for the bash getopts builtin.
-#
-# 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 argument and either the emtpy
-# string or the given argument for options that take an argument.
-#
-# Example:
-#      gsu_getopts abc:x:y
-#      eval "$result"
-#      (($ret < 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 _gsu_getopts_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
-                       result+="false"
-               fi
-       done
-       result+="
-       OPTIND=1
-       while getopts $1 _gsu_getopts_opt \"\$@\"; do
-               case \"\$_gsu_getopts_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
-}
-
-_com_complete()
+com_complete()
 {
        local cmd n cword
        local -a words
 {
        local cmd n cword
        local -a words
@@ -413,7 +757,12 @@ _com_complete()
                local -a candidates;
 
                candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
                local -a candidates;
 
                candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
-               COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
+               if ((\$? == 0)); then
+                       COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
+               else
+                       compopt -o filenames;
+                       COMPREPLY=(\$(compgen -fd -- "\$cur"));
+               fi
 EOF
                ret=$GSU_SUCCESS
                return
 EOF
                ret=$GSU_SUCCESS
                return
@@ -421,8 +770,8 @@ EOF
 
        cword="$1"
        gsu_is_a_number "$cword"
 
        cword="$1"
        gsu_is_a_number "$cword"
-       (($ret < 0)) && return
-       if (($cword <= 1)); then
+       ((ret < 0)) && return
+       if ((cword <= 1)); then
                _gsu_available_commands
                echo "${result}"
                ret=$GSU_SUCCESS
                _gsu_available_commands
                echo "${result}"
                ret=$GSU_SUCCESS
@@ -431,9 +780,10 @@ EOF
        shift
        words=("$@")
        cmd="${words[1]}"
        shift
        words=("$@")
        cmd="${words[1]}"
-       ret=$GSU_SUCCESS # It's not an error if no completer was defined
-       [[ "$(type -t complete_$cmd)" != "function" ]] && return
-       complete_$cmd "$cword" "${words[@]}"
+       # if no completer is defined for this subcommand we exit unsuccessfully
+       # to let the generic completer above fall back to file name completion.
+       [[ "$(type -t "complete_$cmd")" != "function" ]] && exit 1
+       "complete_$cmd" "$cword" "${words[@]}"
        # ignore errors, they would only clutter the completion output
        ret=$GSU_SUCCESS
 }
        # ignore errors, they would only clutter the completion output
        ret=$GSU_SUCCESS
 }
@@ -449,24 +799,25 @@ EOF
 #
 gsu_cword_is_option_parameter()
 {
 #
 gsu_cword_is_option_parameter()
 {
-       local opts="$1" cword="$2" prev i n
+       local opts="$1" cword="$2"
+       local opt prev i n
        local -a words
 
        result=
        local -a words
 
        result=
-       (($cword == 0)) && return
+       ((cword == 0)) && return
        ((${#opts} < 2)) && return
 
        shift 2
        words=("$@")
        ((${#opts} < 2)) && return
 
        shift 2
        words=("$@")
-       prev="${words[$(($cword - 1))]}"
+       prev="${words[$((cword - 1))]}"
        [[ ! "$prev" == -* ]] && return
 
        n=$((${#opts} - 1))
        for ((i=0; i <= $n; i++)); do
                opt="${opts:$i:1}"
        [[ ! "$prev" == -* ]] && return
 
        n=$((${#opts} - 1))
        for ((i=0; i <= $n; i++)); do
                opt="${opts:$i:1}"
-               [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
+               [[ "${opts:$((i + 1)):1}" != ":" ]] && continue
                let i++
                let i++
-               [[ "$prev" != "-$opt" ]] && continue
+               [[ ! "$prev" =~ ^-.*$opt$ ]] && continue
                result="$opt"
                return
        done
                result="$opt"
                return
        done
@@ -495,13 +846,13 @@ gsu_get_unnamed_arg_num()
        shift 2
        words=("$@")
        cur="${words[$cword]}"
        shift 2
        words=("$@")
        cur="${words[$cword]}"
-       prev="${words[$(($cword - 1))]}"
+       prev="${words[$((cword - 1))]}"
        result=-1
        [[ "$cur" == -* ]] && return
        [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
 
        for ((i=1; i <= $cword; i++)); do
        result=-1
        [[ "$cur" == -* ]] && return
        [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
 
        for ((i=1; i <= $cword; i++)); do
-               prev="${words[$(($i - 1))]}"
+               prev="${words[$((i - 1))]}"
                cur="${words[$i]}"
                [[ "$cur" == -* ]] && continue
                if [[ "$prev" == -* ]]; then
                cur="${words[$i]}"
                [[ "$cur" == -* ]] && continue
                if [[ "$prev" == -* ]]; then
@@ -511,7 +862,7 @@ gsu_get_unnamed_arg_num()
                fi
                let n++
        done
                fi
                let n++
        done
-       result="$(($n - 1))"
+       result="$((n - 1))"
 }
 
 # Entry point for all gsu-based scripts.
 }
 
 # Entry point for all gsu-based scripts.
@@ -532,70 +883,25 @@ gsu_get_unnamed_arg_num()
 gsu()
 {
        local i
 gsu()
 {
        local i
-       _gsu_available_commands
-       gsu_cmds="$result"
-       if test $# -eq 0; then
+
+       if (($# == 0)); then
                _gsu_usage
                _gsu_print_available_commands
                exit 1
        fi
        arg="$1"
        shift
                _gsu_usage
                _gsu_print_available_commands
                exit 1
        fi
        arg="$1"
        shift
-       # check internal commands
-       if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
-               _com_$arg "$@"
-               if [[ "$ret" -lt 0 ]]; then
+       if [[ "$(type -t "com_$arg")" == 'function' ]]; then
+               "com_$arg" "$@"
+               if ((ret < 0)); then
                        gsu_err_msg
                        exit 1
                fi
                exit 0
        fi
                        gsu_err_msg
                        exit 1
                fi
                exit 0
        fi
-
-       # external commands
-       for i in $gsu_cmds; do
-               if test "$arg" = "$i"; then
-                       com_$arg "$@"
-                       if [[ "$ret" -lt 0 ]]; then
-                               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 1>&2
        exit 1
 }
        exit 1
 }
-
-# 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
-}