]> git.tuebingen.mpg.de Git - gsu.git/blobdiff - misc/gsu/subcommand
gsu: Improve documentation of gsu_getopts().
[gsu.git] / misc / gsu / subcommand
index f09c66de613707c42794947e8b0b787c79f11f5e..3a1bc6db6697c7a0676a45ae940b930d18ee8901 100644 (file)
@@ -11,21 +11,38 @@ _gsu_usage()
        gsu_short_msg "# Usage: $_gsu_self command [options]"
 }
 
        gsu_short_msg "# Usage: $_gsu_self command [options]"
 }
 
-# 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]\+\)()'
+# Each line matching this is recognized as a subcommand. The name of the may be
+# given as $1. In any case the subcommand is the first subexpression.
+_gsu_get_command_regex()
+{
+       local cmd="${1:-[-a-zA-Z_0-9]+}"
+       result="^com_($cmd)\(\)"
+}
 
 _gsu_available_commands()
 {
 
 _gsu_available_commands()
 {
+       local ere
+
+       _gsu_get_command_regex
+       ere="$result"
        result="$({
        result="$({
-               printf "help\nman\nprefs\n"
-               sed -ne "s/$gsu_command_regex/\1/g;T;p" $0
-               } | sort | tr '\n' ' ')"
+               printf "help\nman\nprefs\ncomplete\n"
+               sed -Ee '
+                       # if line matches, isolate command name
+                       s/'"$ere"'/\1/g
+
+                       # if there is a match, (print it and) start next cycle
+                       t
+
+                       # otherwise delete it
+                       d
+               ' $0
+       } | sort | tr '\n' ' ')"
 }
 
 _gsu_print_available_commands()
 {(
 }
 
 _gsu_print_available_commands()
 {(
-       local i count
+       local i count=0
        gsu_short_msg "Available commands:"
        for i in $gsu_cmds; do
                printf "$i"
        gsu_short_msg "Available commands:"
        for i in $gsu_cmds; do
                printf "$i"
@@ -43,6 +60,26 @@ _gsu_print_available_commands()
 ) 2>&1
 }
 
 ) 2>&1
 }
 
+gsu_complete_options()
+{
+       local opts="$1" cword="$2" cur opt
+       local -a words
+
+       shift 2
+       words=("$@")
+       cur="${words[$cword]}"
+       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
+}
+
 export gsu_prefs_txt="
 Print the current preferences.
 
 export gsu_prefs_txt="
 Print the current preferences.
 
@@ -55,7 +92,13 @@ _com_prefs()
 {
        local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}"
 
 {
        local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}"
 
-       if [[ "$1" = "-e" ]]; then
+       gsu_getopts "e"
+       eval "$result"
+       (($ret < 0)) && return
+       gsu_check_arg_count $# 0 0
+       (($ret < 0)) && return
+
+       if [[ "$o_e" == "true" ]]; then
                ret=-$E_GSU_MKDIR
                result="${conf%/*}"
                mkdir -p "$result"
                ret=-$E_GSU_MKDIR
                result="${conf%/*}"
                mkdir -p "$result"
@@ -72,7 +115,7 @@ _com_prefs()
                local name= option_type= default_value= required=
                local description= help_text=
                eval "${gsu_options[$i]}"
                local name= option_type= default_value= required=
                local description= help_text=
                eval "${gsu_options[$i]}"
-               eval val='"$'${gsu_config_var_prefix}_$name'"'
+               eval val='"${'${gsu_config_var_prefix}_$name:-'}"'
                case "$required" in
                true|yes)
                        printf "# required"
                case "$required" in
                true|yes)
                        printf "# required"
@@ -93,6 +136,11 @@ _com_prefs()
        done
 }
 
        done
 }
 
+complete_prefs()
+{
+       gsu_complete_options "e" "$@"
+}
+
 export gsu_man_txt="
 Print the manual.
 
 export gsu_man_txt="
 Print the manual.
 
@@ -138,13 +186,7 @@ _com_man()
 
 _gsu_banner_msg()
 {
 
 _gsu_banner_msg()
 {
-       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
-       gsu_short_msg "$txt ###"
+       gsu_short_msg "### $_gsu_self -- ###"
 }
 
 export gsu_help_txt="
 }
 
 export gsu_help_txt="
@@ -155,31 +197,57 @@ 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."
 
+export gsu_complete_txt="
+Command line completion.
+
+Usage: complete [<cword> <word>...]
+
+When executed without argument the command writes bash code to
+stdout. This code is suitable to be evaled from .bashrc to enable
+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()
 {
-       local a b
-       if test -z "$1"; then
+       local a b ere tab='     '
+
+       _gsu_get_command_regex
+       ere="$result"
+
+       if (($# == 0)); then
                _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 "--"
                _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
+                       printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
+                       grep -EA 2 "$ere" $0
                } | grep -v -- '--' \
                } | 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;}' \
-                       | sed -e "s/${gsu_command_regex}#*/\1\t/" \
-                       | sort \
-                       | while read a b; do
-                               printf "$a\t"
-                               if test ${#a} -lt 8; then
-                                       printf "\t"
-                               fi
-                               echo "$b"
-                        done
+                       | sed -En "/$ere/"'!d
+                               # remove everything but the command name
+                               s/^com_(.*)\(\).*/\1/
+
+                               # append tab after short commands (less than 8 chars)
+                               s/^(.{1,7})$/\1'"$tab"'/g
+
+                               # remove next line (should contain only ## anyway)
+                               N
+                               s/#.*//
+
+                               # append next line, removing leading ##
+                               N
+                               s/#+ *//g
+
+                               # replace newline by tab
+                               y/\n/'"$tab"'/
+
+                               # and print the sucker
+                               p'
                echo
                echo "# Try $_gsu_self help <command> for info on <command>."
                ret=$GSU_SUCCESS
                echo
                echo "# Try $_gsu_self help <command> for info on <command>."
                ret=$GSU_SUCCESS
@@ -200,31 +268,60 @@ _com_help()
                ret=$GSU_SUCCESS
                return
        fi
                ret=$GSU_SUCCESS
                return
        fi
+       if test "$1" = "complete"; then
+               echo "$gsu_complete_txt"
+               ret=$GSU_SUCCESS
+               return
+       fi
        ret=$GSU_SUCCESS
        ret=$GSU_SUCCESS
-       if grep -q "^com_$1()" $0; then
-               sed -e "1,/^com_$1()$/d" -e '/^{/,$d' -e 's/^## *//' $0
+       _gsu_get_command_regex "$1"
+       ere="$result"
+       if ! grep -Eq "$ere" $0; then
+               _gsu_print_available_commands
+               result="$1"
+               ret=-$E_GSU_BAD_COMMAND
                return
        fi
                return
        fi
-       _gsu_print_available_commands
-       result="$1"
-       ret=-$E_GSU_BAD_COMMAND
+       sed -nEe '
+               # only consider lines in the comment of the function
+               /'"$ere"'/,/^[^#]/ {
+
+                       # remove leading ##
+                       s/^## *//
+
+                       # if it did start with ##, jump to label p and print it
+                       tp
+
+                       # otherwise, move on to next line
+                       d
+
+                       # print it
+                       :p
+                       p
+               }
+       ' $0
+}
+
+complete_help()
+{
+       _gsu_available_commands
+       echo "$result"
 }
 
 }
 
-# Wrapper for bash's getopts.
+# 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".
 #
 # 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.
+# 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"
 #
 # Example:
 #      gsu_getopts abc:x:y
 #      eval "$result"
-#      [[ $ret -lt 0 ]] && return
+#      (($ret < 0)) && return
 #
 #
-#      [[ "$o_a" = "true ]] && echo "The -a flag was given"
+#      [[ "$o_a" = 'true' ]] && echo 'The -a flag was given'
 #      [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
 gsu_getopts()
 {
 #      [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
 gsu_getopts()
 {
@@ -255,21 +352,21 @@ gsu_getopts()
                        exit 1
                esac
        done
                        exit 1
                esac
        done
-       result="local opt"
+       result="local _gsu_getopts_opt"
        for ((i=0; i < ${#1}; i++)); do
                c1=${1:$i:1}
                c2=${1:$(($i + 1)):1}
        for ((i=0; i < ${#1}; i++)); do
                c1=${1:$i:1}
                c2=${1:$(($i + 1)):1}
-               result+=" o_$c1"
+               result+=" o_$c1="
                if [[ "$c2" = ":" ]]; then
                        let i++
                else
                if [[ "$c2" = ":" ]]; then
                        let i++
                else
-                       result+="=false"
+                       result+="false"
                fi
        done
        result+="
        OPTIND=1
                fi
        done
        result+="
        OPTIND=1
-       while getopts $1 opt \"\$@\"; do
-               case \"\$opt\" in
+       while getopts $1 _gsu_getopts_opt \"\$@\"; do
+               case \"\$_gsu_getopts_opt\" in
 "
        for ((i=0; i < ${#1}; i++)); do
                c1=${1:$i:1}
 "
        for ((i=0; i < ${#1}; i++)); do
                c1=${1:$i:1}
@@ -296,6 +393,118 @@ gsu_getopts()
        ret=$GSU_SUCCESS
 }
 
        ret=$GSU_SUCCESS
 }
 
+_com_complete()
+{
+       local cmd n cword
+       local -a words
+
+       if (($# == 0)); then
+               cat <<EOF
+               local cur="\${COMP_WORDS[\$COMP_CWORD]}";
+               local -a candidates;
+
+               candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
+               COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
+EOF
+               ret=$GSU_SUCCESS
+               return
+       fi
+
+       cword="$1"
+       gsu_is_a_number "$cword"
+       (($ret < 0)) && return
+       if (($cword <= 1)); then
+               _gsu_available_commands
+               echo "${result}"
+               ret=$GSU_SUCCESS
+               return
+       fi
+       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[@]}"
+       # ignore errors, they would only clutter the completion output
+       ret=$GSU_SUCCESS
+}
+
+# Find out if the current word is a parameter for an option.
+#
+# $1:   usual getopts option string.
+# $2:   The current word number.
+# $3..: All words of the current command line.
+#
+# return: If yes, $result contains the letter of the option for which the
+# current word is a parameter. Otherwise, $result is empty.
+#
+gsu_cword_is_option_parameter()
+{
+       local opts="$1" cword="$2" prev i n
+       local -a words
+
+       result=
+       (($cword == 0)) && return
+       ((${#opts} < 2)) && return
+
+       shift 2
+       words=("$@")
+       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
+               let i++
+               [[ "$prev" != "-$opt" ]] && continue
+               result="$opt"
+               return
+       done
+       ret=0
+}
+
+# Get the word number on which the cursor is, not counting options.
+#
+# This is useful for completing commands whose possible completions depend
+# on the word number, for example mount.
+#
+# $1:   Getopt option string.
+# $2:   The current word number.
+# $3..: All words of the current command line.
+#
+# return: If the current word is an option, or a parameter to an option,
+# this function sets $result to -1. Otherwise, the number of the non-option
+# is returned in $result.
+#
+gsu_get_unnamed_arg_num()
+{
+       local opts="$1" cword="$2" prev cur
+       local -i i n=0
+       local -a words
+
+       shift 2
+       words=("$@")
+       cur="${words[$cword]}"
+       prev="${words[$(($cword - 1))]}"
+       result=-1
+       [[ "$cur" == -* ]] && return
+       [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
+
+       for ((i=1; i <= $cword; i++)); do
+               prev="${words[$(($i - 1))]}"
+               cur="${words[$i]}"
+               [[ "$cur" == -* ]] && continue
+               if [[ "$prev" == -* ]]; then
+                       opt=${prev#-}
+                       [[ "$opts" != *$opt:* ]] && let n++
+                       continue
+               fi
+               let n++
+       done
+       result="$(($n - 1))"
+}
+
 gsu()
 {
        local i
 gsu()
 {
        local i
@@ -310,7 +519,7 @@ gsu()
        arg="$1"
        shift
        # check internal commands
        arg="$1"
        shift
        # check internal commands
-       if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" ]]; then
+       if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
                _com_$arg "$@"
                if [[ "$ret" -lt 0 ]]; then
                        gsu_err_msg
                _com_$arg "$@"
                if [[ "$ret" -lt 0 ]]; then
                        gsu_err_msg