]> git.tuebingen.mpg.de Git - gsu.git/blob - subcommand
subcommand: Improve formatting of command list.
[gsu.git] / subcommand
1 #!/bin/bash
2 # Copyright (C) 2006 Andre Noll
3 # Licensed under the LGPL, version 3. See COPYING and COPYING.LESSER.
4
5 if [[ "$(type -t _gsu_setup)" != "function" ]]; then
6         gsu_dir=${gsu_dir:-${BASH_SOURCE[0]%/*}}
7         . "$gsu_dir/common" || exit 1
8         _gsu_setup
9 fi
10
11 _gsu_usage()
12 {
13         gsu_short_msg "# Usage: $gsu_name command [options]"
14 }
15
16 # Return an extended regular expression to match against $0.
17 #
18 # When called without argument, the expression matches all lines which define a
19 # subcommand.
20 #
21 # If an argument is given, the returned expression matches only the subcommand
22 # passed as $1. This is useful to tell if a string is a valid subcommand.
23 #
24 # Regardless of whether an argument is given, the returned expression contains
25 # exactly one parenthesized subexpression for matching the command name.
26 _gsu_get_command_regex()
27 {
28         local cmd="${1:-[-a-zA-Z_0-9]{1,\}}"
29         result="^com_($cmd)\(\)"
30 }
31
32 _gsu_available_commands()
33 {
34         local ere
35
36         _gsu_get_command_regex
37         ere="$result"
38         result="$({
39                 printf "help\nman\nprefs\ncomplete\n"
40                 sed -Ee '
41                         # if line matches, isolate command name
42                         s/'"$ere"'/\1/g
43
44                         # if there is a match, (print it and) start next cycle
45                         t
46
47                         # otherwise delete it
48                         d
49                 ' "$0"
50         } | sort | tr '\n' ' ')"
51 }
52
53 # Check number of arguments.
54 #
55 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
56 #
57 # Check that <num_given> is between <num1> and <num2> inclusively.
58 # If only <num1> ist given, num2 is assumed to be infinity.
59 #
60 # Examples:
61 #       0 0 no argument allowed
62 #       1 1 exactly one argument required
63 #       0 2 at most two arguments admissible
64 #       2   at least two arguments required
65 gsu_check_arg_count()
66 {
67         ret=-$E_GSU_BAD_ARG_COUNT
68         if (($# == 2)); then # only num1 is given
69                 result="at least $2 args required, $1 given"
70                 (($1 < $2)) && return
71                 ret=$GSU_SUCCESS
72                 return
73         fi
74         # num1 and num2 given
75         result="need at least $2 args, $1 given"
76         (($1 < $2)) && return
77         result="need at most $3 args, $1 given"
78         (($1 > $3)) && return
79         ret=$GSU_SUCCESS
80 }
81
82 # Wrapper for the bash getopts builtin.
83 #
84 # Aborts on programming errors such as missing or invalid option string.  On
85 # success $result contains shell code that can be eval'ed. For each defined
86 # option x, the local variable o_x will be created when calling eval "$result".
87 # o_x contains true/false for options without argument and either the empty
88 # string or the given argument for options that take an argument.
89 #
90 # Example:
91 #       gsu_getopts abc:x:y
92 #       eval "$result"
93 #       (($ret < 0)) && return
94 #
95 #       [[ "$o_a" = 'true' ]] && echo 'The -a flag was given'
96 #       [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
97 gsu_getopts()
98 {
99         local -i i
100         local c c1 c2 tab='     ' cr='
101 '
102
103         gsu_check_arg_count $# 1 1
104         if ((ret < 0)); then
105                 gsu_err_msg
106                 exit 1
107         fi
108
109         ret=-$E_GSU_GETOPTS
110         result="invalid optstring $1"
111         if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
112                 gsu_err_msg
113                 exit 1
114         fi
115
116         for ((i=0; i < ${#1}; i++)); do
117                 c=${1:$i:1}
118                 case "$c" in
119                 [a-zA-Z:]);;
120                 *)
121                         ret=-$E_GSU_GETOPTS
122                         result="invalid character $c in optstring"
123                         gsu_err_msg
124                         exit 1
125                 esac
126         done
127         result="local _gsu_getopts_opt"
128         for ((i=0; i < ${#1}; i++)); do
129                 c1=${1:$i:1}
130                 c2=${1:$((i + 1)):1}
131                 result+=" o_$c1="
132                 if [[ "$c2" = ":" ]]; then
133                         let i++
134                 else
135                         result+="false"
136                 fi
137         done
138         result+="
139         OPTIND=1
140         while getopts $1 _gsu_getopts_opt \"\$@\"; do
141                 case \"\$_gsu_getopts_opt\" in
142 "
143         for ((i=0; i < ${#1}; i++)); do
144                 c1=${1:$i:1}
145                 c2=${1:$((i + 1)):1}
146                 result+="$tab$tab$c1) o_$c1="
147                 if [[ "$c2" = ":" ]]; then
148                         result+="\"\$OPTARG\""
149                         let i++
150                 else
151                         result+="true"
152                 fi
153                 result+=";;$cr"
154         done
155         result+="
156                 *)
157                         ret=-\$E_GSU_GETOPTS
158                         result=\"invalid option given\"
159                         return
160                         ;;
161                 esac
162         done
163         shift \$((\$OPTIND - 1))
164 "
165         ret=$GSU_SUCCESS
166 }
167
168 _gsu_print_available_commands()
169 {
170         local cmd cmds
171         local -i maxlen=0 cols width=80 count=0
172
173         result=$(stty size 2>/dev/null)
174         if (($? == 0)); then
175                 gsu_is_a_number "${result#* }"
176                 ((ret >= 0)) && ((result > 0)) && width=$result
177         fi
178         _gsu_available_commands
179         cmds=$result
180         for cmd in $cmds; do
181                 ((${#cmd} > maxlen)) && maxlen=${#cmd}
182         done
183         let maxlen++
184         ((width < maxlen)) && cols=1 || cols=$((width / maxlen))
185         printf 'Available commands:'
186         for cmd in $cmds; do
187                 ((count % cols == 0)) && printf '\n'
188                 printf '%-*s' $maxlen $cmd
189                 let ++count
190         done
191         printf '\n'
192 }
193
194 # Print all options of the given optstring to stdout if the word in the current
195 # command line begins with a hyphen character.
196 #
197 # Returns 0 if the current word does not start with a hyphen, one otherwise.
198 gsu_complete_options()
199 {
200         local opts="$1" cword="$2" cur opt
201         local -a words
202
203         shift 2
204         words=("$@")
205         cur="${words[$cword]}"
206         ret=0
207         [[ ! "$cur" == -* ]] && return
208
209         for ((i=0; i < ${#opts}; i++)); do
210                 opt="${opts:$i:1}"
211                 [[ "$opt" == ":" ]] && continue
212                 printf "%s" "-$opt "
213         done
214         ret=1
215 }
216
217 declare -A _gsu_help_text=() # indexed by autocmd
218 com_prefs_options='e'
219
220 _gsu_help_text['prefs']='
221 Print the current preferences.
222
223 Usage: prefs [-e]
224
225 If -e is given, the config file is opened with the default editor.
226 Without options, the command prints out a list of all config variables,
227 together with their current value and the default value.
228 '
229
230 com_prefs()
231 {
232         local i conf="${gsu_config_file:=${HOME:-}/.$gsu_name.rc}"
233
234         gsu_getopts "$com_prefs_options"
235         eval "$result"
236         ((ret < 0)) && return
237         gsu_check_arg_count $# 0 0
238         ((ret < 0)) && return
239
240         if [[ "$o_e" == "true" ]]; then
241                 ret=-$E_GSU_MKDIR
242                 result="${conf%/*}"
243                 mkdir -p "$result"
244                 (($? != 0)) && return
245                 ret=-$E_GSU_EDITOR
246                 result="${EDITOR:-vi}"
247                 "$result" $conf
248                 (($? != 0)) && return
249                 ret=$GSU_SUCCESS
250                 return
251         fi
252
253         for ((i=0; i < ${#gsu_options[@]}; i++)); do
254                 local name= option_type= default_value= required=
255                 local description= help_text=
256                 eval "${gsu_options[$i]}"
257                 eval val='"${'${gsu_config_var_prefix}_$name:-'}"'
258                 case "$required" in
259                 true|yes)
260                         printf "# required"
261                         ;;
262                 *)
263                         printf "# optional"
264                         ;;
265                 esac
266                 printf " %s: %s" "$option_type" "$description"
267                 if [[ "$required" != "yes" && "$required" != "true" ]]; then
268                         printf " [%s]" "$default_value"
269                 fi
270                 echo
271                 [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
272                 printf "%s=%s" "$name" "$val"
273                 [[ "$val" == "$default_value" ]] && printf " # default"
274                 echo
275         done
276 }
277
278 _gsu_isatty()
279 {(
280         exec 3<&1
281         stty 0<&3 &> /dev/null
282 )}
283
284 complete_prefs()
285 {
286         gsu_complete_options "$com_prefs_options" "$@"
287 }
288
289 _gsu_man_options='m:b:'
290
291 complete_man()
292 {
293         gsu_complete_options "$_gsu_man_options" "$@"
294         ((ret > 0)) && return
295         gsu_cword_is_option_parameter "$_gsu_man_options" "$@"
296         [[ "$result" == 'm' ]] && printf 'roff\ntext\nhtml\n'
297 }
298
299 _gsu_help_text['man']='
300 Print the manual.
301
302 Usage: man [-m <mode>] [-b <browser>]
303
304 -m: Set output format (text, roff or html). Default: roff.
305 -b: Use the specified browser. Implies html mode.
306
307 If stdout is not associated with a terminal device, the command
308 dumps the man page to stdout and exits.
309
310 Otherwise, it tries to display the manual page as follows. In text
311 mode, plain text is piped to $PAGER. In roff mode, the roff output
312 is filtered through nroff, then piped to $PAGER. For both formats,
313 if $PAGER is unset, less(1) is assumed.
314
315 In html mode, html output is written to a temporary file, and this
316 file is displayed as a page in the web browser. If -b is not given,
317 the command stored in the $BROWSER environment variable is executed
318 with the path to the temporary file as an argument. If $BROWSER is
319 unset, elinks(1) is assumed.
320 '
321
322 _gsu_read_line()
323 {
324         local -n p="$1"
325         local l OIFS="$IFS"
326
327         IFS=
328         read -r l || return
329         IFS="$OIFS"
330         p="$l"
331 }
332
333 _gsu_change_roffify_state()
334 {
335         local -n statep="$1"
336         local new_state="$2"
337         local old_state="$statep"
338
339         [[ "$old_state" == "$new_state" ]] && return 0
340
341         case "$old_state" in
342         text);;
343         example) printf '.EE\n';;
344         enum) printf '.RE\n';;
345         esac
346         case "$new_state" in
347         text);;
348         example) printf '.EX\n';;
349         enum) printf '.RS 2\n';;
350         esac
351
352         statep="$new_state"
353         return 1
354 }
355
356 _gsu_print_protected_roff_line()
357 {
358         local line="$1"
359         local -i n=0
360
361         while [[ "${line:$n:1}" == ' ' ]]; do
362                 let n++
363         done
364         line="${line:$n}"
365         printf '\\&%s\n' "${line//\\/\\\\}"
366 }
367
368 _gsu_roffify_maindoc()
369 {
370         local state='text' TAB='        '
371         local line next_line
372         local -i n
373
374         _gsu_read_line 'line' || return
375         while _gsu_read_line next_line; do
376                 if [[ "$next_line" =~ ^(----|====|~~~~) ]]; then # heading
377                         printf '.SS %s\n' "$line"
378                         _gsu_read_line line || return
379                         _gsu_change_roffify_state 'state' 'text'
380                         continue
381                 fi
382                 if [[ "${line:0:1}" == "$TAB" ]]; then # example
383                         _gsu_change_roffify_state 'state' 'example'
384                         _gsu_print_protected_roff_line "$line"
385                         line="$next_line"
386                         continue
387                 fi
388                 n=0
389                 while [[ "${line:$n:1}" == ' ' ]]; do
390                         let n++
391                 done
392                 line=${line:$n};
393                 if [[ "${line:0:1}" == '*' ]]; then # enum
394                         line=${line#\*}
395                         _gsu_change_roffify_state 'state' 'enum'
396                         printf '\n\(bu %s\n' "$line"
397                         line="$next_line"
398                         continue
399                 fi
400                 if [[ "$line" =~ ^$ ]]; then # new paragraph
401                         _gsu_change_roffify_state 'state' 'text'
402                         printf '.PP\n'
403                 else
404                         _gsu_print_protected_roff_line "$line"
405                 fi
406                 line="$next_line"
407         done
408         _gsu_print_protected_roff_line "$line"
409 }
410
411 _gsu_extract_maindoc()
412 {
413         sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' -e 's/^# *//' -e 's/^#//g' "$0"
414 }
415
416 _gsu_roffify_cmds()
417 {
418         local line cmd= desc= state='text' TAB='        '
419
420         while _gsu_read_line line; do
421                 if [[ "${line:0:1}" != '#' ]]; then # com_foo()
422                         line="${line#com_}"
423                         cmd="${line%()}"
424                         continue
425                 fi
426                 line="${line####}"
427                 if [[ "$line" =~ ^[[:space:]]*$ ]]; then
428                         printf '.PP\n'
429                         _gsu_change_roffify_state 'state' 'text'
430                         continue
431                 fi
432                 if [[ -n "$cmd" ]]; then # desc or usage
433                         if [[ -z "$desc" ]]; then # desc
434                                 desc="$line"
435                                 continue
436                         fi
437                         # usage
438                         _gsu_change_roffify_state 'state' 'text'
439                         printf '\n.SS %s \\- %s\n' "$cmd" "$desc"
440                         printf '\n.I %s\n'  "$line"
441                         cmd=
442                         desc=
443                         continue
444                 fi
445                 line="${line# }"
446                 if [[ "${line:0:1}" == "$TAB" ]]; then
447                         _gsu_change_roffify_state 'state' 'example'
448                         _gsu_print_protected_roff_line "$line"
449                         continue
450                 fi
451                 if [[ "$line" == -*:* ]]; then
452                         _gsu_change_roffify_state 'state' 'enum'
453                         printf '.PP\n.B %s:\n' "${line%%:*}"
454                         _gsu_print_protected_roff_line "${line#*:}"
455                         continue
456                 fi
457                 _gsu_print_protected_roff_line "$line"
458         done
459 }
460
461 _gsu_roffify_autocmds()
462 {
463         local cmd help_txt
464
465         for cmd in "${!_gsu_help_text[@]}"; do
466                 help_txt="${_gsu_help_text["$cmd"]}"
467                 {
468                         printf 'com_%s()\n' "$cmd"
469                         sed -e 's/^/## /g' <<< "$help_txt"
470                 } | _gsu_roffify_cmds
471         done
472 }
473
474 _gsu_roff_man()
475 {
476         local name="$1" sect_num="$2" month_year="$3" pkg="$4" sect_name="$5"
477         local purpose="$6"
478         local ere
479
480         cat << EOF
481 .TH "${name^^}" "$sect_num" "$month_year" "$pkg" "$sect_name"
482 .SH NAME
483 $name \- $purpose
484 .SH SYNOPSIS
485 .B $name
486 \fI\,<subcommand>\/\fR [\fI\,<options>\/\fR] [\fI\,<arguments>\/\fR]
487 .SH DESCRIPTION
488 EOF
489         _gsu_extract_maindoc | _gsu_roffify_maindoc
490
491         printf '\n.SH "GENERIC SUBCOMMANDS"\n'
492         printf 'The following commands are automatically created by gsu\n'
493         _gsu_roffify_autocmds
494
495         printf '\n.SH "LIST OF SUBCOMMANDS"\n'
496         printf 'Each command has its own set of options as described below.\n'
497
498         _gsu_get_command_regex
499         ere="$result"
500         # only consider lines in the comment of the function
501         sed -nEe '/'"$ere"'/,/^[^#]/p' "$0" | _gsu_roffify_cmds
502 }
503
504 _gsu_file_mtime()
505 {
506         local file="$1"
507         result="$(find "$file" -printf '%TB %TY' 2>/dev/null)" # GNU
508         (($? == 0)) && [[ -n "$result" ]] && return
509         result="$(stat -f %Sm -t '%B %Y' "$file" 2>/dev/null)" # BSD
510         (($? == 0)) && [[ -n "$result" ]] && return
511         result="$(date '+%B %Y' 2>/dev/null)" # POSIX
512         (($? == 0)) && [[ -n "$result" ]] && return
513         result='[unknown date]'
514 }
515
516 com_man()
517 {
518         local equal_signs="=================================================="
519         local minus_signs="--------------------------------------------------"
520         local filter='cat' pager='cat' browser=${BROWSER:-elinks} tmpfile=
521         local com num isatty pipeline
522
523         gsu_getopts "$_gsu_man_options"
524         eval "$result"
525         ((ret < 0)) && return
526         if [[ -n "$o_b" ]]; then
527                 o_m='html'
528                 browser="$o_b"
529         elif [[ -z "$o_m" ]]; then
530                 o_m='roff'
531         fi
532
533         _gsu_isatty && isatty='true' || isatty='false'
534         if [[ "$o_m" == 'roff' ]]; then
535                 if [[ "$isatty" == 'true' ]]; then
536                         filter='nroff -Tutf8 -mandoc'
537                         pager="${PAGER:-less}"
538                 fi
539         elif [[ "$o_m" == 'text' ]]; then
540                 if [[ "$isatty" == 'true' ]]; then
541                         pager="${PAGER:-less}"
542                 fi
543         elif [[ "$o_m" == 'html' ]]; then
544                 filter='groff -T html -m man'
545                 if [[ "$isatty" == 'true' ]]; then
546                         gsu_make_tempfile "gsu_html_man.XXXXXX.html"
547                         ((ret < 0)) && return || tmpfile="$result"
548                         trap "rm -f $tmpfile" RETURN EXIT
549                 fi
550         fi
551         [[ "$pager" == 'less' ]] && export LESS=${LESS-RI}
552         case "$o_m" in
553         roff|html)
554                 _gsu_file_mtime "$0"
555                 _gsu_roff_man "$gsu_name" '1' "$result" \
556                         "${gsu_package-${gsu_name^^}(1)}" \
557                         "User Commands" "${gsu_banner_txt}" \
558                 | $filter | {
559                         if [[ -n "$tmpfile" ]]; then
560                                 cat > "$tmpfile"
561                         else
562                                 $pager
563                         fi
564                 }
565                 if (($? != 0)); then
566                         ret=-$E_GSU_XCMD
567                         result="filter: $filter"
568                         return
569                 fi
570                 if [[ -n "$tmpfile" ]]; then
571                         ret=-$E_GSU_XCMD
572                         result="$browser"
573                         "$browser" "$tmpfile" || return
574                 fi
575                 ret=$GSU_SUCCESS
576                 return
577                 ;;
578         text) ;;
579         "") ;;
580         *)
581                 ret=-$E_GSU_INVAL
582                 result="$o_m"
583                 return
584         esac
585         {
586         echo "$gsu_name (_${gsu_banner_txt}_) manual"
587         echo "${equal_signs:0:${#gsu_name} + ${#gsu_banner_txt} + 16}"
588         echo
589         _gsu_extract_maindoc
590         echo "----"
591         echo
592         echo "$gsu_name usage"
593         echo "${minus_signs:0:${#gsu_name} + 6}"
594         printf "\t"
595         _gsu_usage 2>&1
596         echo "Each command has its own set of options as described below."
597         echo
598         echo "----"
599         echo
600         echo "Available commands:"
601
602         _gsu_available_commands
603         for com in $result; do
604                 num=${#com}
605                 ((num < 4)) && num=4
606                 echo "${minus_signs:0:$num}"
607                 echo "$com"
608                 echo "${minus_signs:0:$num}"
609                 "$0" help "$com"
610                 echo
611         done
612         } | $pager
613         ret=$GSU_SUCCESS
614 }
615
616 _gsu_help_text['help']='
617 Print online help.
618
619 Usage: help [-a] [command]
620
621 Without arguments, print the list of available commands. Otherwise,
622 print the help text for the given command.
623
624 -a: Also show the help of automatic commands. Ignored if a command
625 is given.'
626
627 _gsu_help_text['complete']='
628 Command line completion.
629
630 Usage: complete [<cword> <word>...]
631
632 When executed without argument the command writes bash code to
633 stdout. This code is suitable to be evaled from .bashrc to enable
634 completion.
635
636 If at least one argument is given, all possible completions are
637 written to stdout. This can be used from the completion function of
638 the subcommand.
639 '
640
641 com_help_options='a'
642 com_help()
643 {
644         local ere tab=' ' txt
645
646         gsu_getopts "$com_help_options"
647         eval "$result"
648         ((ret < 0)) && return
649
650         _gsu_get_command_regex
651         ere="$result"
652
653         if (($# == 0)); then
654                 gsu_short_msg "### $gsu_name -- $gsu_banner_txt ###"
655                 _gsu_usage 2>&1
656                 {
657                         if [[ "$o_a" == 'true' ]]; then
658                                 _gsu_mfcb() { printf '%s\n' "$2"; }
659                                 for cmd in "${!_gsu_help_text[@]}"; do
660                                         printf "com_%s()" "$cmd"
661                                         txt="${_gsu_help_text["$cmd"]}"
662                                         mapfile -n 3 -c 1 -C _gsu_mfcb <<< "$txt"
663                                 printf -- '--\n'
664                                 done
665                         fi
666                         grep -EA 2 "$ere" "$0"
667                 } | grep -v -- '--' \
668                         | sed -En "/$ere/"'!d
669                                 # remove everything but the command name
670                                 s/^com_(.*)\(\).*/\1/
671
672                                 # append tab after short commands (less than 8 chars)
673                                 s/^(.{1,7})$/\1'"$tab"'/g
674
675                                 # remove next line (should contain only ## anyway)
676                                 N
677                                 s/#.*//
678
679                                 # append next line, removing leading ##
680                                 N
681                                 s/#+ *//g
682
683                                 # replace newline by tab
684                                 y/\n/'"$tab"'/
685
686                                 # and print the sucker
687                                 p'
688                 printf "\n# Try %s help <command> for info on <command>, or %s help -a to see\n" \
689                         "$gsu_name" "$gsu_name"
690                 printf '# also the subcommands which are automatically generated by gsu.\n'
691                 ret=$GSU_SUCCESS
692                 return
693         fi
694         for cmd in "${!_gsu_help_text[@]}"; do
695                 [[ "$1" != "$cmd" ]] && continue
696                 printf '%s\n' "${_gsu_help_text["$cmd"]}"
697                 ret=$GSU_SUCCESS
698                 return
699         done
700         _gsu_get_command_regex "$1"
701         ere="$result"
702         if ! grep -Eq "$ere" "$0"; then
703                 _gsu_print_available_commands
704                 result="$1"
705                 ret=-$E_GSU_BAD_COMMAND
706                 return
707         fi
708         sed -nEe '
709                 # only consider lines in the comment of the function
710                 /'"$ere"'/,/^[^#]/ {
711
712                         # remove leading ##
713                         s/^## *//
714
715                         # if it did start with ##, jump to label p and print it
716                         tp
717
718                         # otherwise, move on to next line
719                         d
720
721                         # print it
722                         :p
723                         p
724                 }
725         ' "$0"
726         ret=$GSU_SUCCESS
727 }
728
729 complete_help()
730 {
731         _gsu_available_commands
732         echo "$result"
733 }
734
735 com_complete()
736 {
737         local cmd n cword
738         local -a words
739
740         if (($# == 0)); then
741                 cat <<EOF
742                 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
743                 local -a candidates;
744
745                 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
746                 if ((\$? == 0)); then
747                         COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
748                 else
749                         compopt -o filenames;
750                         COMPREPLY=(\$(compgen -fd -- "\$cur"));
751                 fi
752 EOF
753                 ret=$GSU_SUCCESS
754                 return
755         fi
756
757         cword="$1"
758         gsu_is_a_number "$cword"
759         ((ret < 0)) && return
760         if ((cword <= 1)); then
761                 _gsu_available_commands
762                 echo "${result}"
763                 ret=$GSU_SUCCESS
764                 return
765         fi
766         shift
767         words=("$@")
768         cmd="${words[1]}"
769         # if no completer is defined for this subcommand we exit unsuccessfully
770         # to let the generic completer above fall back to file name completion.
771         [[ "$(type -t "complete_$cmd")" != "function" ]] && exit 1
772         "complete_$cmd" "$cword" "${words[@]}"
773         # ignore errors, they would only clutter the completion output
774         ret=$GSU_SUCCESS
775 }
776
777 # Find out if the current word is a parameter for an option.
778 #
779 # $1:   usual getopts option string.
780 # $2:   The current word number.
781 # $3..: All words of the current command line.
782 #
783 # return: If yes, $result contains the letter of the option for which the
784 # current word is a parameter. Otherwise, $result is empty.
785 #
786 gsu_cword_is_option_parameter()
787 {
788         local opts="$1" cword="$2"
789         local opt prev i n
790         local -a words
791
792         result=
793         ((cword == 0)) && return
794         ((${#opts} < 2)) && return
795
796         shift 2
797         words=("$@")
798         prev="${words[$((cword - 1))]}"
799         [[ ! "$prev" == -* ]] && return
800
801         n=$((${#opts} - 1))
802         for ((i=0; i <= $n; i++)); do
803                 opt="${opts:$i:1}"
804                 [[ "${opts:$((i + 1)):1}" != ":" ]] && continue
805                 let i++
806                 [[ ! "$prev" =~ ^-.*$opt$ ]] && continue
807                 result="$opt"
808                 return
809         done
810         ret=0
811 }
812
813 # Get the word number on which the cursor is, not counting options.
814 #
815 # This is useful for completing commands whose possible completions depend
816 # on the word number, for example mount.
817 #
818 # $1:   Getopt option string.
819 # $2:   The current word number.
820 # $3..: All words of the current command line.
821 #
822 # return: If the current word is an option, or a parameter to an option,
823 # this function sets $result to -1. Otherwise, the number of the non-option
824 # is returned in $result.
825 #
826 gsu_get_unnamed_arg_num()
827 {
828         local opts="$1" cword="$2" prev cur
829         local -i i n=0
830         local -a words
831
832         shift 2
833         words=("$@")
834         cur="${words[$cword]}"
835         prev="${words[$((cword - 1))]}"
836         result=-1
837         [[ "$cur" == -* ]] && return
838         [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
839
840         for ((i=1; i <= $cword; i++)); do
841                 prev="${words[$((i - 1))]}"
842                 cur="${words[$i]}"
843                 [[ "$cur" == -* ]] && continue
844                 if [[ "$prev" == -* ]]; then
845                         opt=${prev#-}
846                         [[ "$opts" != *$opt:* ]] && let n++
847                         continue
848                 fi
849                 let n++
850         done
851         result="$((n - 1))"
852 }
853
854 # Entry point for all gsu-based scripts.
855 #
856 # The startup part of the application script should source this file to load
857 # the functions defined here, and then call gsu(). Functions starting with com_
858 # are automatically recognized as subcommands.
859 #
860 # Minimal example:
861 #
862 #       com_hello()
863 #       {
864 #               echo 'hello world'
865 #       }
866 #       gsu_dir=${gsu_dir:-/system/location/where/gsu/is/installed}
867 #       . $gsu_dir/subcommand || exit 1
868 #       gsu "$@"
869 gsu()
870 {
871         local i
872
873         if (($# == 0)); then
874                 _gsu_usage
875                 _gsu_print_available_commands
876                 exit 1
877         fi
878         arg="$1"
879         shift
880         if [[ "$(type -t "com_$arg")" == 'function' ]]; then
881                 "com_$arg" "$@"
882                 if ((ret < 0)); then
883                         gsu_err_msg
884                         exit 1
885                 fi
886                 exit 0
887         fi
888         ret=-$E_GSU_BAD_COMMAND
889         result="$arg"
890         gsu_err_msg
891         _gsu_print_available_commands 1>&2
892         exit 1
893 }