]> git.tuebingen.mpg.de Git - gsu.git/blob - subcommand
d37481cdbdccb5b62268d3b788de84b61a2ab347
[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                         ' | {
689                                 local -a cmds=() descs=()
690                                 local -i i maxlen=1
691                                 local cmd desc
692                                 while read cmd desc; do
693                                         ((maxlen < ${#cmd})) && maxlen=${#cmd}
694                                         cmds[${#cmds[@]}]=$cmd
695                                         descs[${#descs[@]}]=$desc
696                                 done
697                                 for ((i = 0; i < ${#cmds[@]}; i++)); do
698                                         printf '%-*s %s\n' $maxlen ${cmds[$i]} \
699                                                 "${descs[$i]}"
700                                 done
701                         }
702                 printf "\n# Try %s help <command> for info on <command>, or %s help -a to see\n" \
703                         "$gsu_name" "$gsu_name"
704                 printf '# also the subcommands which are automatically generated by gsu.\n'
705                 ret=$GSU_SUCCESS
706                 return
707         fi
708         for cmd in "${!_gsu_help_text[@]}"; do
709                 [[ "$1" != "$cmd" ]] && continue
710                 printf '%s\n' "${_gsu_help_text["$cmd"]}"
711                 ret=$GSU_SUCCESS
712                 return
713         done
714         _gsu_get_command_regex "$1"
715         ere="$result"
716         if ! grep -Eq "$ere" "$0"; then
717                 _gsu_print_available_commands
718                 result="$1"
719                 ret=-$E_GSU_BAD_COMMAND
720                 return
721         fi
722         sed -nEe '
723                 # only consider lines in the comment of the function
724                 /'"$ere"'/,/^[^#]/ {
725
726                         # remove leading ##
727                         s/^## *//
728
729                         # if it did start with ##, jump to label p and print it
730                         tp
731
732                         # otherwise, move on to next line
733                         d
734
735                         # print it
736                         :p
737                         p
738                 }
739         ' "$0"
740         ret=$GSU_SUCCESS
741 }
742
743 complete_help()
744 {
745         _gsu_available_commands
746         echo "$result"
747 }
748
749 com_complete()
750 {
751         local cmd n cword
752         local -a words
753
754         if (($# == 0)); then
755                 cat <<EOF
756                 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
757                 local -a candidates;
758
759                 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
760                 if ((\$? == 0)); then
761                         COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
762                 else
763                         compopt -o filenames;
764                         COMPREPLY=(\$(compgen -fd -- "\$cur"));
765                 fi
766 EOF
767                 ret=$GSU_SUCCESS
768                 return
769         fi
770
771         cword="$1"
772         gsu_is_a_number "$cword"
773         ((ret < 0)) && return
774         if ((cword <= 1)); then
775                 _gsu_available_commands
776                 echo "${result}"
777                 ret=$GSU_SUCCESS
778                 return
779         fi
780         shift
781         words=("$@")
782         cmd="${words[1]}"
783         # if no completer is defined for this subcommand we exit unsuccessfully
784         # to let the generic completer above fall back to file name completion.
785         [[ "$(type -t "complete_$cmd")" != "function" ]] && exit 1
786         "complete_$cmd" "$cword" "${words[@]}"
787         # ignore errors, they would only clutter the completion output
788         ret=$GSU_SUCCESS
789 }
790
791 # Find out if the current word is a parameter for an option.
792 #
793 # $1:   usual getopts option string.
794 # $2:   The current word number.
795 # $3..: All words of the current command line.
796 #
797 # return: If yes, $result contains the letter of the option for which the
798 # current word is a parameter. Otherwise, $result is empty.
799 #
800 gsu_cword_is_option_parameter()
801 {
802         local opts="$1" cword="$2"
803         local opt prev i n
804         local -a words
805
806         result=
807         ((cword == 0)) && return
808         ((${#opts} < 2)) && return
809
810         shift 2
811         words=("$@")
812         prev="${words[$((cword - 1))]}"
813         [[ ! "$prev" == -* ]] && return
814
815         n=$((${#opts} - 1))
816         for ((i=0; i <= $n; i++)); do
817                 opt="${opts:$i:1}"
818                 [[ "${opts:$((i + 1)):1}" != ":" ]] && continue
819                 let i++
820                 [[ ! "$prev" =~ ^-.*$opt$ ]] && continue
821                 result="$opt"
822                 return
823         done
824         ret=0
825 }
826
827 # Get the word number on which the cursor is, not counting options.
828 #
829 # This is useful for completing commands whose possible completions depend
830 # on the word number, for example mount.
831 #
832 # $1:   Getopt option string.
833 # $2:   The current word number.
834 # $3..: All words of the current command line.
835 #
836 # return: If the current word is an option, or a parameter to an option,
837 # this function sets $result to -1. Otherwise, the number of the non-option
838 # is returned in $result.
839 #
840 gsu_get_unnamed_arg_num()
841 {
842         local opts="$1" cword="$2" prev cur
843         local -i i n=0
844         local -a words
845
846         shift 2
847         words=("$@")
848         cur="${words[$cword]}"
849         prev="${words[$((cword - 1))]}"
850         result=-1
851         [[ "$cur" == -* ]] && return
852         [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
853
854         for ((i=1; i <= $cword; i++)); do
855                 prev="${words[$((i - 1))]}"
856                 cur="${words[$i]}"
857                 [[ "$cur" == -* ]] && continue
858                 if [[ "$prev" == -* ]]; then
859                         opt=${prev#-}
860                         [[ "$opts" != *$opt:* ]] && let n++
861                         continue
862                 fi
863                 let n++
864         done
865         result="$((n - 1))"
866 }
867
868 # Entry point for all gsu-based scripts.
869 #
870 # The startup part of the application script should source this file to load
871 # the functions defined here, and then call gsu(). Functions starting with com_
872 # are automatically recognized as subcommands.
873 #
874 # Minimal example:
875 #
876 #       com_hello()
877 #       {
878 #               echo 'hello world'
879 #       }
880 #       gsu_dir=${gsu_dir:-/system/location/where/gsu/is/installed}
881 #       . $gsu_dir/subcommand || exit 1
882 #       gsu "$@"
883 gsu()
884 {
885         local i
886
887         if (($# == 0)); then
888                 _gsu_usage
889                 _gsu_print_available_commands
890                 exit 1
891         fi
892         arg="$1"
893         shift
894         if [[ "$(type -t "com_$arg")" == 'function' ]]; then
895                 "com_$arg" "$@"
896                 if ((ret < 0)); then
897                         gsu_err_msg
898                         exit 1
899                 fi
900                 exit 0
901         fi
902         ret=-$E_GSU_BAD_COMMAND
903         result="$arg"
904         gsu_err_msg
905         _gsu_print_available_commands 1>&2
906         exit 1
907 }