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