]> git.tuebingen.mpg.de Git - gsu.git/blob - subcommand
Implement help -a.
[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 count=0
172
173         _gsu_available_commands
174         cmds="$result"
175         printf 'Available commands:\n'
176         for cmd in $cmds; do
177                 printf '%s' "$cmd"
178                 let ++count
179                 if ((count % 4)); then
180                         printf '\t'
181                         ((${#cmd} < 8)) && printf '\t'
182                 else
183                         printf '\n'
184                 fi
185         done
186         printf '\n'
187 }
188
189 # Print all options of the given optstring to stdout if the word in the current
190 # command line begins with a hyphen character.
191 #
192 # Returns 0 if the current word does not start with a hyphen, one otherwise.
193 gsu_complete_options()
194 {
195         local opts="$1" cword="$2" cur opt
196         local -a words
197
198         shift 2
199         words=("$@")
200         cur="${words[$cword]}"
201         ret=0
202         [[ ! "$cur" == -* ]] && return
203
204         for ((i=0; i < ${#opts}; i++)); do
205                 opt="${opts:$i:1}"
206                 [[ "$opt" == ":" ]] && continue
207                 printf "%s" "-$opt "
208         done
209         ret=1
210 }
211
212 declare -A _gsu_help_text=() # indexed by autocmd
213 com_prefs_options='e'
214
215 _gsu_help_text['prefs']='
216 Print the current preferences.
217
218 Usage: prefs [-e]
219
220 If -e is given, the config file is opened with the default editor.
221 Without options, the command prints out a list of all config variables,
222 together with their current value and the default value.
223 '
224
225 com_prefs()
226 {
227         local i conf="${gsu_config_file:=${HOME:-}/.$gsu_name.rc}"
228
229         gsu_getopts "$com_prefs_options"
230         eval "$result"
231         ((ret < 0)) && return
232         gsu_check_arg_count $# 0 0
233         ((ret < 0)) && return
234
235         if [[ "$o_e" == "true" ]]; then
236                 ret=-$E_GSU_MKDIR
237                 result="${conf%/*}"
238                 mkdir -p "$result"
239                 (($? != 0)) && return
240                 ret=-$E_GSU_EDITOR
241                 result="${EDITOR:-vi}"
242                 "$result" $conf
243                 (($? != 0)) && return
244                 ret=$GSU_SUCCESS
245                 return
246         fi
247
248         for ((i=0; i < ${#gsu_options[@]}; i++)); do
249                 local name= option_type= default_value= required=
250                 local description= help_text=
251                 eval "${gsu_options[$i]}"
252                 eval val='"${'${gsu_config_var_prefix}_$name:-'}"'
253                 case "$required" in
254                 true|yes)
255                         printf "# required"
256                         ;;
257                 *)
258                         printf "# optional"
259                         ;;
260                 esac
261                 printf " %s: %s" "$option_type" "$description"
262                 if [[ "$required" != "yes" && "$required" != "true" ]]; then
263                         printf " [%s]" "$default_value"
264                 fi
265                 echo
266                 [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
267                 printf "%s=%s" "$name" "$val"
268                 [[ "$val" == "$default_value" ]] && printf " # default"
269                 echo
270         done
271 }
272
273 _gsu_isatty()
274 {(
275         exec 3<&1
276         stty 0<&3 &> /dev/null
277 )}
278
279 complete_prefs()
280 {
281         gsu_complete_options "$com_prefs_options" "$@"
282 }
283
284 _gsu_man_options='m:b:'
285
286 complete_man()
287 {
288         gsu_complete_options "$_gsu_man_options" "$@"
289         ((ret > 0)) && return
290         gsu_cword_is_option_parameter "$_gsu_man_options" "$@"
291         [[ "$result" == 'm' ]] && printf 'roff\ntext\nhtml\n'
292 }
293
294 _gsu_help_text['man']='
295 Print the manual.
296
297 Usage: man [-m <mode>] [-b <browser>]
298
299 -m: Set output format (text, roff or html). Default: roff.
300 -b: Use the specified browser. Implies html mode.
301
302 If stdout is not associated with a terminal device, the command
303 dumps the man page to stdout and exits.
304
305 Otherwise, it tries to display the manual page as follows. In text
306 mode, plain text is piped to $PAGER. In roff mode, the roff output
307 is filtered through nroff, then piped to $PAGER. For both formats,
308 if $PAGER is unset, less(1) is assumed.
309
310 In html mode, html output is written to a temporary file, and this
311 file is displayed as a page in the web browser. If -b is not given,
312 the command stored in the $BROWSER environment variable is executed
313 with the path to the temporary file as an argument. If $BROWSER is
314 unset, elinks(1) is assumed.
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                         _gsu_print_protected_roff_line "$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_autocmds()
457 {
458         local cmd help_txt
459
460         for cmd in "${!_gsu_help_text[@]}"; do
461                 help_txt="${_gsu_help_text["$cmd"]}"
462                 {
463                         printf 'com_%s()\n' "$cmd"
464                         sed -e 's/^/## /g' <<< "$help_txt"
465                 } | _gsu_roffify_cmds
466         done
467 }
468
469 _gsu_roff_man()
470 {
471         local name="$1" sect_num="$2" month_year="$3" pkg="$4" sect_name="$5"
472         local purpose="$6"
473         local ere
474
475         cat << EOF
476 .TH "${name^^}" "$sect_num" "$month_year" "$pkg" "$sect_name"
477 .SH NAME
478 $name \- $purpose
479 .SH SYNOPSIS
480 .B $name
481 \fI\,<subcommand>\/\fR [\fI\,<options>\/\fR] [\fI\,<arguments>\/\fR]
482 .SH DESCRIPTION
483 EOF
484         _gsu_extract_maindoc | _gsu_roffify_maindoc
485
486         printf '\n.SH "GENERIC SUBCOMMANDS"\n'
487         printf 'The following commands are automatically created by gsu\n'
488         _gsu_roffify_autocmds
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='roff'
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_text['help']='
612 Print online help.
613
614 Usage: help [-a] [command]
615
616 Without arguments, print the list of available commands. Otherwise,
617 print the help text for the given command.
618
619 -a: Also show the help of automatic commands. Ignored if a command
620 is given.'
621
622 _gsu_help_text['complete']='
623 Command line completion.
624
625 Usage: complete [<cword> <word>...]
626
627 When executed without argument the command writes bash code to
628 stdout. This code is suitable to be evaled from .bashrc to enable
629 completion.
630
631 If at least one argument is given, all possible completions are
632 written to stdout. This can be used from the completion function of
633 the subcommand.
634 '
635
636 com_help_options='a'
637 com_help()
638 {
639         local ere tab=' ' txt
640
641         gsu_getopts "$com_help_options"
642         eval "$result"
643         ((ret < 0)) && return
644
645         _gsu_get_command_regex
646         ere="$result"
647
648         if (($# == 0)); then
649                 gsu_short_msg "### $gsu_name -- $gsu_banner_txt ###"
650                 _gsu_usage 2>&1
651                 {
652                         if [[ "$o_a" == 'true' ]]; then
653                                 _gsu_mfcb() { printf '%s\n' "$2"; }
654                                 for cmd in "${!_gsu_help_text[@]}"; do
655                                         printf "com_%s()" "$cmd"
656                                         txt="${_gsu_help_text["$cmd"]}"
657                                         mapfile -n 3 -c 1 -C _gsu_mfcb <<< "$txt"
658                                 printf -- '--\n'
659                                 done
660                         fi
661                         grep -EA 2 "$ere" "$0"
662                 } | grep -v -- '--' \
663                         | sed -En "/$ere/"'!d
664                                 # remove everything but the command name
665                                 s/^com_(.*)\(\).*/\1/
666
667                                 # append tab after short commands (less than 8 chars)
668                                 s/^(.{1,7})$/\1'"$tab"'/g
669
670                                 # remove next line (should contain only ## anyway)
671                                 N
672                                 s/#.*//
673
674                                 # append next line, removing leading ##
675                                 N
676                                 s/#+ *//g
677
678                                 # replace newline by tab
679                                 y/\n/'"$tab"'/
680
681                                 # and print the sucker
682                                 p'
683                 printf "\n# Try %s help <command> for info on <command>, or %s help -a to see\n" \
684                         "$gsu_name" "$gsu_name"
685                 printf '# also the subcommands which are automatically generated by gsu.\n'
686                 ret=$GSU_SUCCESS
687                 return
688         fi
689         for cmd in "${!_gsu_help_text[@]}"; do
690                 [[ "$1" != "$cmd" ]] && continue
691                 printf '%s\n' "${_gsu_help_text["$cmd"]}"
692                 ret=$GSU_SUCCESS
693                 return
694         done
695         _gsu_get_command_regex "$1"
696         ere="$result"
697         if ! grep -Eq "$ere" "$0"; then
698                 _gsu_print_available_commands
699                 result="$1"
700                 ret=-$E_GSU_BAD_COMMAND
701                 return
702         fi
703         sed -nEe '
704                 # only consider lines in the comment of the function
705                 /'"$ere"'/,/^[^#]/ {
706
707                         # remove leading ##
708                         s/^## *//
709
710                         # if it did start with ##, jump to label p and print it
711                         tp
712
713                         # otherwise, move on to next line
714                         d
715
716                         # print it
717                         :p
718                         p
719                 }
720         ' "$0"
721         ret=$GSU_SUCCESS
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                 if ((\$? == 0)); then
742                         COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
743                 else
744                         compopt -o filenames;
745                         COMPREPLY=(\$(compgen -fd -- "\$cur"));
746                 fi
747 EOF
748                 ret=$GSU_SUCCESS
749                 return
750         fi
751
752         cword="$1"
753         gsu_is_a_number "$cword"
754         ((ret < 0)) && return
755         if ((cword <= 1)); then
756                 _gsu_available_commands
757                 echo "${result}"
758                 ret=$GSU_SUCCESS
759                 return
760         fi
761         shift
762         words=("$@")
763         cmd="${words[1]}"
764         # if no completer is defined for this subcommand we exit unsuccessfully
765         # to let the generic completer above fall back to file name completion.
766         [[ "$(type -t "complete_$cmd")" != "function" ]] && exit 1
767         "complete_$cmd" "$cword" "${words[@]}"
768         # ignore errors, they would only clutter the completion output
769         ret=$GSU_SUCCESS
770 }
771
772 # Find out if the current word is a parameter for an option.
773 #
774 # $1:   usual getopts option string.
775 # $2:   The current word number.
776 # $3..: All words of the current command line.
777 #
778 # return: If yes, $result contains the letter of the option for which the
779 # current word is a parameter. Otherwise, $result is empty.
780 #
781 gsu_cword_is_option_parameter()
782 {
783         local opts="$1" cword="$2"
784         local opt prev i n
785         local -a words
786
787         result=
788         ((cword == 0)) && return
789         ((${#opts} < 2)) && return
790
791         shift 2
792         words=("$@")
793         prev="${words[$((cword - 1))]}"
794         [[ ! "$prev" == -* ]] && return
795
796         n=$((${#opts} - 1))
797         for ((i=0; i <= $n; i++)); do
798                 opt="${opts:$i:1}"
799                 [[ "${opts:$((i + 1)):1}" != ":" ]] && continue
800                 let i++
801                 [[ ! "$prev" =~ ^-.*$opt$ ]] && continue
802                 result="$opt"
803                 return
804         done
805         ret=0
806 }
807
808 # Get the word number on which the cursor is, not counting options.
809 #
810 # This is useful for completing commands whose possible completions depend
811 # on the word number, for example mount.
812 #
813 # $1:   Getopt option string.
814 # $2:   The current word number.
815 # $3..: All words of the current command line.
816 #
817 # return: If the current word is an option, or a parameter to an option,
818 # this function sets $result to -1. Otherwise, the number of the non-option
819 # is returned in $result.
820 #
821 gsu_get_unnamed_arg_num()
822 {
823         local opts="$1" cword="$2" prev cur
824         local -i i n=0
825         local -a words
826
827         shift 2
828         words=("$@")
829         cur="${words[$cword]}"
830         prev="${words[$((cword - 1))]}"
831         result=-1
832         [[ "$cur" == -* ]] && return
833         [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
834
835         for ((i=1; i <= $cword; i++)); do
836                 prev="${words[$((i - 1))]}"
837                 cur="${words[$i]}"
838                 [[ "$cur" == -* ]] && continue
839                 if [[ "$prev" == -* ]]; then
840                         opt=${prev#-}
841                         [[ "$opts" != *$opt:* ]] && let n++
842                         continue
843                 fi
844                 let n++
845         done
846         result="$((n - 1))"
847 }
848
849 # Entry point for all gsu-based scripts.
850 #
851 # The startup part of the application script should source this file to load
852 # the functions defined here, and then call gsu(). Functions starting with com_
853 # are automatically recognized as subcommands.
854 #
855 # Minimal example:
856 #
857 #       com_hello()
858 #       {
859 #               echo 'hello world'
860 #       }
861 #       gsu_dir=${gsu_dir:-/system/location/where/gsu/is/installed}
862 #       . $gsu_dir/subcommand || exit 1
863 #       gsu "$@"
864 gsu()
865 {
866         local i
867
868         if (($# == 0)); then
869                 _gsu_usage
870                 _gsu_print_available_commands
871                 exit 1
872         fi
873         arg="$1"
874         shift
875         if [[ "$(type -t "com_$arg")" == 'function' ]]; then
876                 "com_$arg" "$@"
877                 if ((ret < 0)); then
878                         gsu_err_msg
879                         exit 1
880                 fi
881                 exit 0
882         fi
883         ret=-$E_GSU_BAD_COMMAND
884         result="$arg"
885         gsu_err_msg
886         _gsu_print_available_commands 1>&2
887         exit 1
888 }