Merge branch 'refs/heads/t/help-a' into pu
[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 declare -A _gsu_help_text=() # indexed by autocmd
212 com_prefs_options='e'
213
214 _gsu_help_text['prefs']='
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_help_text['man']='
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_autocmds()
456 {
457 local cmd help_txt
458
459 for cmd in "${!_gsu_help_text[@]}"; do
460 help_txt="${_gsu_help_text["$cmd"]}"
461 {
462 printf 'com_%s()\n' "$cmd"
463 sed -e 's/^/## /g' <<< "$help_txt"
464 } | _gsu_roffify_cmds
465 done
466 }
467
468 _gsu_roff_man()
469 {
470 local name="$1" sect_num="$2" month_year="$3" pkg="$4" sect_name="$5"
471 local purpose="$6"
472 local ere
473
474 cat << EOF
475 .TH "${name^^}" "$sect_num" "$month_year" "$pkg" "$sect_name"
476 .SH NAME
477 $name \- $purpose
478 .SH SYNOPSIS
479 .B $name
480 \fI\,<subcommand>\/\fR [\fI\,<options>\/\fR] [\fI\,<arguments>\/\fR]
481 .SH DESCRIPTION
482 EOF
483 _gsu_extract_maindoc | _gsu_roffify_maindoc
484
485 printf '\n.SH "GENERIC SUBCOMMANDS"\n'
486 printf 'The following commands are automatically created by gsu\n'
487 _gsu_roffify_autocmds
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_text['help']='
611 Print online help.
612
613 Usage: help [-a] [command]
614
615 Without arguments, print the list of available commands. Otherwise,
616 print the help text for the given command.
617
618 -a: Also show the help of automatic commands. Ignored if a command
619 is given.'
620
621 _gsu_help_text['complete']='
622 Command line completion.
623
624 Usage: complete [<cword> <word>...]
625
626 When executed without argument the command writes bash code to
627 stdout. This code is suitable to be evaled from .bashrc to enable
628 completion.
629
630 If at least one argument is given, all possible completions are
631 written to stdout. This can be used from the completion function of
632 the subcommand.
633 '
634
635 com_help_options='a'
636 com_help()
637 {
638 local ere tab=' ' txt
639
640 gsu_getopts "$com_help_options"
641 eval "$result"
642 ((ret < 0)) && return
643
644 _gsu_get_command_regex
645 ere="$result"
646
647 if (($# == 0)); then
648 gsu_short_msg "### $gsu_name -- $gsu_banner_txt ###"
649 _gsu_usage 2>&1
650 {
651 if [[ "$o_a" == 'true' ]]; then
652 _gsu_mfcb() { printf '%s\n' "$2"; }
653 for cmd in "${!_gsu_help_text[@]}"; do
654 printf "com_%s()" "$cmd"
655 txt="${_gsu_help_text["$cmd"]}"
656 mapfile -n 3 -c 1 -C _gsu_mfcb <<< "$txt"
657 printf -- '--\n'
658 done
659 fi
660 grep -EA 2 "$ere" "$0"
661 } | grep -v -- '--' \
662 | sed -En "/$ere/"'!d
663 # remove everything but the command name
664 s/^com_(.*)\(\).*/\1/
665
666 # append tab after short commands (less than 8 chars)
667 s/^(.{1,7})$/\1'"$tab"'/g
668
669 # remove next line (should contain only ## anyway)
670 N
671 s/#.*//
672
673 # append next line, removing leading ##
674 N
675 s/#+ *//g
676
677 # replace newline by tab
678 y/\n/'"$tab"'/
679
680 # and print the sucker
681 p'
682 echo
683 echo "# Try $gsu_name help <command> for info on <command>."
684 ret=$GSU_SUCCESS
685 return
686 fi
687 for cmd in "${!_gsu_help_text[@]}"; do
688 [[ "$1" != "$cmd" ]] && continue
689 printf '%s\n' "${_gsu_help_text["$cmd"]}"
690 ret=$GSU_SUCCESS
691 return
692 done
693 _gsu_get_command_regex "$1"
694 ere="$result"
695 if ! grep -Eq "$ere" "$0"; then
696 _gsu_print_available_commands
697 result="$1"
698 ret=-$E_GSU_BAD_COMMAND
699 return
700 fi
701 sed -nEe '
702 # only consider lines in the comment of the function
703 /'"$ere"'/,/^[^#]/ {
704
705 # remove leading ##
706 s/^## *//
707
708 # if it did start with ##, jump to label p and print it
709 tp
710
711 # otherwise, move on to next line
712 d
713
714 # print it
715 :p
716 p
717 }
718 ' "$0"
719 ret=$GSU_SUCCESS
720 }
721
722 complete_help()
723 {
724 _gsu_available_commands
725 echo "$result"
726 }
727
728 com_complete()
729 {
730 local cmd n cword
731 local -a words
732
733 if (($# == 0)); then
734 cat <<EOF
735 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
736 local -a candidates;
737
738 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
739 if ((\$? == 0)); then
740 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
741 else
742 compopt -o filenames;
743 COMPREPLY=(\$(compgen -fd -- "\$cur"));
744 fi
745 EOF
746 ret=$GSU_SUCCESS
747 return
748 fi
749
750 cword="$1"
751 gsu_is_a_number "$cword"
752 ((ret < 0)) && return
753 if ((cword <= 1)); then
754 _gsu_available_commands
755 echo "${result}"
756 ret=$GSU_SUCCESS
757 return
758 fi
759 shift
760 words=("$@")
761 cmd="${words[1]}"
762 # if no completer is defined for this subcommand we exit unsuccessfully
763 # to let the generic completer above fall back to file name completion.
764 [[ "$(type -t "complete_$cmd")" != "function" ]] && exit 1
765 "complete_$cmd" "$cword" "${words[@]}"
766 # ignore errors, they would only clutter the completion output
767 ret=$GSU_SUCCESS
768 }
769
770 # Find out if the current word is a parameter for an option.
771 #
772 # $1: usual getopts option string.
773 # $2: The current word number.
774 # $3..: All words of the current command line.
775 #
776 # return: If yes, $result contains the letter of the option for which the
777 # current word is a parameter. Otherwise, $result is empty.
778 #
779 gsu_cword_is_option_parameter()
780 {
781 local opts="$1" cword="$2"
782 local opt prev i n
783 local -a words
784
785 result=
786 ((cword == 0)) && return
787 ((${#opts} < 2)) && return
788
789 shift 2
790 words=("$@")
791 prev="${words[$((cword - 1))]}"
792 [[ ! "$prev" == -* ]] && return
793
794 n=$((${#opts} - 1))
795 for ((i=0; i <= $n; i++)); do
796 opt="${opts:$i:1}"
797 [[ "${opts:$((i + 1)):1}" != ":" ]] && continue
798 let i++
799 [[ ! "$prev" =~ ^-.*$opt$ ]] && continue
800 result="$opt"
801 return
802 done
803 ret=0
804 }
805
806 # Get the word number on which the cursor is, not counting options.
807 #
808 # This is useful for completing commands whose possible completions depend
809 # on the word number, for example mount.
810 #
811 # $1: Getopt option string.
812 # $2: The current word number.
813 # $3..: All words of the current command line.
814 #
815 # return: If the current word is an option, or a parameter to an option,
816 # this function sets $result to -1. Otherwise, the number of the non-option
817 # is returned in $result.
818 #
819 gsu_get_unnamed_arg_num()
820 {
821 local opts="$1" cword="$2" prev cur
822 local -i i n=0
823 local -a words
824
825 shift 2
826 words=("$@")
827 cur="${words[$cword]}"
828 prev="${words[$((cword - 1))]}"
829 result=-1
830 [[ "$cur" == -* ]] && return
831 [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
832
833 for ((i=1; i <= $cword; i++)); do
834 prev="${words[$((i - 1))]}"
835 cur="${words[$i]}"
836 [[ "$cur" == -* ]] && continue
837 if [[ "$prev" == -* ]]; then
838 opt=${prev#-}
839 [[ "$opts" != *$opt:* ]] && let n++
840 continue
841 fi
842 let n++
843 done
844 result="$((n - 1))"
845 }
846
847 # Entry point for all gsu-based scripts.
848 #
849 # The startup part of the application script should source this file to load
850 # the functions defined here, and then call gsu(). Functions starting with com_
851 # are automatically recognized as subcommands.
852 #
853 # Minimal example:
854 #
855 # com_hello()
856 # {
857 # echo 'hello world'
858 # }
859 # gsu_dir=${gsu_dir:-/system/location/where/gsu/is/installed}
860 # . $gsu_dir/subcommand || exit 1
861 # gsu "$@"
862 gsu()
863 {
864 local i
865
866 if (($# == 0)); then
867 _gsu_usage
868 _gsu_print_available_commands
869 exit 1
870 fi
871 arg="$1"
872 shift
873 if [[ "$(type -t "com_$arg")" == 'function' ]]; then
874 "com_$arg" "$@"
875 if ((ret < 0)); then
876 gsu_err_msg
877 exit 1
878 fi
879 exit 0
880 fi
881 ret=-$E_GSU_BAD_COMMAND
882 result="$arg"
883 gsu_err_msg
884 _gsu_print_available_commands 1>&2
885 exit 1
886 }