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