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