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