subcommand: Declare local variable i of gsu_getopts() as integer.
[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 }