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