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]"
 }
 
-# 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()
 {
+       local ere
+
+       _gsu_get_command_regex
+       ere="$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()
 {(
-       local i count
+       local i count=0
        gsu_short_msg "Available commands:"
        for i in $gsu_cmds; do
                printf "$i"
@@ -43,6 +60,26 @@ _gsu_print_available_commands()
 ) 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.
 
@@ -55,7 +92,13 @@ _com_prefs()
 {
        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"
@@ -72,7 +115,7 @@ _com_prefs()
                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"
@@ -93,6 +136,11 @@ _com_prefs()
        done
 }
 
+complete_prefs()
+{
+       gsu_complete_options "e" "$@"
+}
+
 export gsu_man_txt="
 Print the manual.
 
@@ -138,13 +186,7 @@ _com_man()
 
 _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="
@@ -155,31 +197,57 @@ Usage: help [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()
 {
-       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 "--"
-                       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 -- '--' \
-                       | 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
@@ -200,31 +268,60 @@ _com_help()
                ret=$GSU_SUCCESS
                return
        fi
+       if test "$1" = "complete"; then
+               echo "$gsu_complete_txt"
+               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
+       _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
-       _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".
-# 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"
-#      [[ $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()
 {
@@ -255,21 +352,21 @@ gsu_getopts()
                        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}
-               result+=" o_$c1"
+               result+=" o_$c1="
                if [[ "$c2" = ":" ]]; then
                        let i++
                else
-                       result+="=false"
+                       result+="false"
                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}
@@ -296,6 +393,118 @@ gsu_getopts()
        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
@@ -310,7 +519,7 @@ gsu()
        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