Merge branch 'refs/heads/t/help-a' into pu
[gsu.git] / subcommand
index 8bb755f..1b204e0 100644 (file)
@@ -1,9 +1,11 @@
 #!/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_is_a_number) != "function" ]]; then
-       GSU_DIR=${GSU_DIR:=${HOME-}/.gsu}
-       . $GSU_DIR/common || exit 1
+if [[ "$(type -t _gsu_setup)" != "function" ]]; then
+       gsu_dir=${gsu_dir:-${BASH_SOURCE[0]%/*}}
+       . "$gsu_dir/common" || exit 1
+       _gsu_setup
 fi
 
 _gsu_usage()
@@ -23,7 +25,7 @@ _gsu_usage()
 # 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)\(\)"
 }
 
@@ -44,30 +46,149 @@ _gsu_available_commands()
 
                        # otherwise delete it
                        d
-               ' $0
+               ' "$0"
        } | sort | tr '\n' ' ')"
 }
 
+# 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 c 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
+                       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
+}
+
 _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
+{
+       local cmd cmds
+       local -i count=0
+
+       _gsu_available_commands
+       cmds="$result"
+       printf 'Available commands:\n'
+       for cmd in $cmds; do
+               printf '%s' "$cmd"
+               let ++count
+               if ((count % 4)); then
+                       printf '\t'
+                       ((${#cmd} < 8)) && printf '\t'
                else
-                       printf "\t"
-                       if test ${#i} -lt 8; then
-                               printf "\t"
-                       fi
+                       printf '\n'
                fi
        done
-       echo
-) 2>&1
+       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
@@ -79,42 +200,46 @@ gsu_complete_options()
        ret=0
        [[ ! "$cur" == -* ]] && return
 
-       ret=0
        for ((i=0; i < ${#opts}; i++)); do
                opt="${opts:$i:1}"
                [[ "$opt" == ":" ]] && continue
                printf "%s" "-$opt "
-               let ret++
        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]
 
-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}"
 
-       gsu_getopts "e"
+       gsu_getopts "$com_prefs_options"
        eval "$result"
-       (($ret < 0)) && return
+       ((ret < 0)) && return
        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"
-               [[ $? -ne 0 ]] && return
+               (($? != 0)) && return
                ret=-$E_GSU_EDITOR
                result="${EDITOR:-vi}"
                "$result" "$conf"
-               [[ $? -ne 0 ]] && return
+               (($? != 0)) && return
                ret=$GSU_SUCCESS
                return
        fi
@@ -132,41 +257,332 @@ _com_prefs()
                        printf "# optional"
                        ;;
                esac
-               printf " $option_type: $description"
+               printf " %s: %s" "$option_type" "$description"
                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"
-               printf "$name=$val"
+               printf "%s=%s" "$name" "$val"
                [[ "$val" == "$default_value" ]] && printf " # default"
                echo
        done
 }
 
+_gsu_isatty()
+{(
+       exec 3<&1
+       stty 0<&3 &> /dev/null
+)}
+
 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.
 
-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'
+                       printf '%s\n' "$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
+}
 
-_com_man()
+_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()
 {
        local equal_signs="=================================================="
        local minus_signs="--------------------------------------------------"
-        local com num
+       local filter='cat' pager='cat' browser=${BROWSER:-elinks} tmpfile=
+       local com num isatty pipeline
 
+       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
+
+       _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
-
-        sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
+       _gsu_extract_maindoc
        echo "----"
-        echo
+       echo
        echo "$gsu_name usage"
        echo "${minus_signs:0:${#gsu_name} + 6}"
        printf "\t"
@@ -178,34 +594,31 @@ _com_man()
        echo "Available commands:"
 
        _gsu_available_commands
-        for com in $result; do
+       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
+               ((num < 4)) && num=4
+               echo "${minus_signs:0:$num}"
+               echo "$com"
+               echo "${minus_signs:0:$num}"
+               "$0" help "$com"
                echo
-        done
-        ret=$GSU_SUCCESS
-}
-
-_gsu_banner_msg()
-{
-       gsu_short_msg "### $gsu_name -- ###"
+       done
+       } | $pager
+       ret=$GSU_SUCCESS
 }
 
-export gsu_help_txt="
+_gsu_help_text['help']='
 Print online help.
 
-Usage: help [command]
+Usage: help [-a] [command]
 
 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.
 
-export gsu_complete_txt="
+-a: Also show the help of automatic commands. Ignored if a command
+is given.'
+
+_gsu_help_text['complete']='
 Command line completion.
 
 Usage: complete [<cword> <word>...]
@@ -217,24 +630,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.
-"
+'
 
-_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_banner_msg 2>&1
+               gsu_short_msg "### $gsu_name -- $gsu_banner_txt ###"
                _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
@@ -261,30 +684,15 @@ _com_help()
                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"
+       for cmd in "${!_gsu_help_text[@]}"; do
+               [[ "$1" != "$cmd" ]] && continue
+               printf '%s\n' "${_gsu_help_text["$cmd"]}"
                ret=$GSU_SUCCESS
                return
-       fi
-       if test "$1" = "prefs"; then
-               echo "$gsu_prefs_txt"
-               ret=$GSU_SUCCESS
-               return
-       fi
-       if test "$1" = "complete"; then
-               echo "$gsu_complete_txt"
-               ret=$GSU_SUCCESS
-               return
-       fi
-       ret=$GSU_SUCCESS
+       done
        _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
@@ -307,7 +715,8 @@ _com_help()
                        :p
                        p
                }
-       ' $0
+       ' "$0"
+       ret=$GSU_SUCCESS
 }
 
 complete_help()
@@ -316,92 +725,7 @@ complete_help()
        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
@@ -412,7 +736,12 @@ _com_complete()
                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
@@ -420,8 +749,8 @@ EOF
 
        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
@@ -430,9 +759,10 @@ EOF
        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
 }
@@ -448,24 +778,25 @@ EOF
 #
 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=
-       (($cword == 0)) && return
+       ((cword == 0)) && return
        ((${#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}"
-               [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
+               [[ "${opts:$((i + 1)):1}" != ":" ]] && continue
                let i++
-               [[ "$prev" != "-$opt" ]] && continue
+               [[ ! "$prev" =~ ^-.*$opt$ ]] && continue
                result="$opt"
                return
        done
@@ -494,13 +825,13 @@ gsu_get_unnamed_arg_num()
        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
-               prev="${words[$(($i - 1))]}"
+               prev="${words[$((i - 1))]}"
                cur="${words[$i]}"
                [[ "$cur" == -* ]] && continue
                if [[ "$prev" == -* ]]; then
@@ -510,7 +841,7 @@ gsu_get_unnamed_arg_num()
                fi
                let n++
        done
-       result="$(($n - 1))"
+       result="$((n - 1))"
 }
 
 # Entry point for all gsu-based scripts.
@@ -531,71 +862,25 @@ gsu_get_unnamed_arg_num()
 gsu()
 {
        local i
-       _gsu_setup
-       _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
-       # 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
-
-       # 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
-       _gsu_print_available_commands
+       _gsu_print_available_commands 1>&2
        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
-}