gsu: Improve documentation of gsu_getopts().
[gsu.git] / misc / gsu / subcommand
1 #!/bin/bash
2 # (C) 2006-2011 Andre Noll
3
4 if [[ $(type -t gsu_is_a_number) != "function" ]]; then
5 GSU_DIR=${GSU_DIR:=$HOME/.gsu}
6 . $GSU_DIR/common || exit 1
7 fi
8
9 _gsu_usage()
10 {
11 gsu_short_msg "# Usage: $_gsu_self command [options]"
12 }
13
14 # Each line matching this is recognized as a subcommand. The name of the may be
15 # given as $1. In any case the subcommand is the first subexpression.
16 _gsu_get_command_regex()
17 {
18 local cmd="${1:-[-a-zA-Z_0-9]+}"
19 result="^com_($cmd)\(\)"
20 }
21
22 _gsu_available_commands()
23 {
24 local ere
25
26 _gsu_get_command_regex
27 ere="$result"
28 result="$({
29 printf "help\nman\nprefs\ncomplete\n"
30 sed -Ee '
31 # if line matches, isolate command name
32 s/'"$ere"'/\1/g
33
34 # if there is a match, (print it and) start next cycle
35 t
36
37 # otherwise delete it
38 d
39 ' $0
40 } | sort | tr '\n' ' ')"
41 }
42
43 _gsu_print_available_commands()
44 {(
45 local i count=0
46 gsu_short_msg "Available commands:"
47 for i in $gsu_cmds; do
48 printf "$i"
49 count=$(($count + 1))
50 if test $(($count % 4)) -eq 0; then
51 echo
52 else
53 printf "\t"
54 if test ${#i} -lt 8; then
55 printf "\t"
56 fi
57 fi
58 done
59 echo
60 ) 2>&1
61 }
62
63 gsu_complete_options()
64 {
65 local opts="$1" cword="$2" cur opt
66 local -a words
67
68 shift 2
69 words=("$@")
70 cur="${words[$cword]}"
71 ret=0
72 [[ ! "$cur" == -* ]] && return
73
74 ret=0
75 for ((i=0; i < ${#opts}; i++)); do
76 opt="${opts:$i:1}"
77 [[ "$opt" == ":" ]] && continue
78 printf "%s" "-$opt "
79 let ret++
80 done
81 }
82
83 export gsu_prefs_txt="
84 Print the current preferences.
85
86 Usage: prefs [-e]
87
88 If -e is given, the config file is opened with the default editor. Without
89 options, the command prints out a list of all cmt config variables, together
90 with their current value and the default value."
91 _com_prefs()
92 {
93 local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}"
94
95 gsu_getopts "e"
96 eval "$result"
97 (($ret < 0)) && return
98 gsu_check_arg_count $# 0 0
99 (($ret < 0)) && return
100
101 if [[ "$o_e" == "true" ]]; then
102 ret=-$E_GSU_MKDIR
103 result="${conf%/*}"
104 mkdir -p "$result"
105 [[ $? -ne 0 ]] && return
106 ret=-$E_GSU_EDITOR
107 result="${EDITOR:-vi}"
108 "$result" "$conf"
109 [[ $? -ne 0 ]] && return
110 ret=$GSU_SUCCESS
111 return
112 fi
113
114 for ((i=0; i < ${#gsu_options[@]}; i++)); do
115 local name= option_type= default_value= required=
116 local description= help_text=
117 eval "${gsu_options[$i]}"
118 eval val='"${'${gsu_config_var_prefix}_$name:-'}"'
119 case "$required" in
120 true|yes)
121 printf "# required"
122 ;;
123 *)
124 printf "# optional"
125 ;;
126 esac
127 printf " $option_type: $description"
128 if [[ "$required" != "yes" && "$required" != "true" ]]; then
129 printf " [$default_value]"
130 fi
131 echo
132 [[ -n "$help_text" ]] && sed -e '/^[ ]*$/d; s/^[ ]*/# /g' <<< "$help_text"
133 printf "$name=$val"
134 [[ "$val" == "$default_value" ]] && printf " # default"
135 echo
136 done
137 }
138
139 complete_prefs()
140 {
141 gsu_complete_options "e" "$@"
142 }
143
144 export gsu_man_txt="
145 Print the manual.
146
147 Usage: man"
148
149 _com_man()
150 {
151 local equal_signs="=================================================="
152 local minus_signs="--------------------------------------------------"
153 local com num
154
155 echo "$_gsu_self (_${gsu_banner_txt}_) manual"
156 echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
157 echo
158
159 sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
160 echo "----"
161 echo
162 echo "$_gsu_self usage"
163 echo "${minus_signs:0:${#_gsu_self} + 6}"
164 printf "\t"
165 _gsu_usage 2>&1
166 echo "Each command has its own set of options as described below."
167 echo
168 echo "----"
169 echo
170 echo "Available commands:"
171
172 _gsu_available_commands
173 for com in $result; do
174 num=${#com}
175 if test $num -lt 4; then
176 num=4
177 fi
178 echo "${minus_signs:0:$num}"
179 echo "$com"
180 echo "${minus_signs:0:$num}"
181 $0 help $com
182 echo
183 done
184 ret=$GSU_SUCCESS
185 }
186
187 _gsu_banner_msg()
188 {
189 gsu_short_msg "### $_gsu_self -- ###"
190 }
191
192 export gsu_help_txt="
193 Print online help.
194
195 Usage: help [command]
196
197 Without arguments, print the list of available commands. Otherwise,
198 print the help text for the given command."
199
200 export gsu_complete_txt="
201 Command line completion.
202
203 Usage: complete [<cword> <word>...]
204
205 When executed without argument the command writes bash code to
206 stdout. This code is suitable to be evaled from .bashrc to enable
207 completion.
208
209 If at least one argument is given, all possible completions are
210 written to stdout. This can be used from the completion function of
211 the subcommand.
212 "
213
214 _com_help()
215 {
216 local a b ere tab=' '
217
218 _gsu_get_command_regex
219 ere="$result"
220
221 if (($# == 0)); then
222 _gsu_banner_msg 2>&1
223 _gsu_usage 2>&1
224 {
225 printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
226 printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
227 printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
228 printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
229 grep -EA 2 "$ere" $0
230 } | grep -v -- '--' \
231 | sed -En "/$ere/"'!d
232 # remove everything but the command name
233 s/^com_(.*)\(\).*/\1/
234
235 # append tab after short commands (less than 8 chars)
236 s/^(.{1,7})$/\1'"$tab"'/g
237
238 # remove next line (should contain only ## anyway)
239 N
240 s/#.*//
241
242 # append next line, removing leading ##
243 N
244 s/#+ *//g
245
246 # replace newline by tab
247 y/\n/'"$tab"'/
248
249 # and print the sucker
250 p'
251 echo
252 echo "# Try $_gsu_self help <command> for info on <command>."
253 ret=$GSU_SUCCESS
254 return
255 fi
256 if test "$1" = "help"; then
257 echo "$gsu_help_txt"
258 ret=$GSU_SUCCESS
259 return
260 fi
261 if test "$1" = "man"; then
262 echo "$gsu_man_txt"
263 ret=$GSU_SUCCESS
264 return
265 fi
266 if test "$1" = "prefs"; then
267 echo "$gsu_prefs_txt"
268 ret=$GSU_SUCCESS
269 return
270 fi
271 if test "$1" = "complete"; then
272 echo "$gsu_complete_txt"
273 ret=$GSU_SUCCESS
274 return
275 fi
276 ret=$GSU_SUCCESS
277 _gsu_get_command_regex "$1"
278 ere="$result"
279 if ! grep -Eq "$ere" $0; then
280 _gsu_print_available_commands
281 result="$1"
282 ret=-$E_GSU_BAD_COMMAND
283 return
284 fi
285 sed -nEe '
286 # only consider lines in the comment of the function
287 /'"$ere"'/,/^[^#]/ {
288
289 # remove leading ##
290 s/^## *//
291
292 # if it did start with ##, jump to label p and print it
293 tp
294
295 # otherwise, move on to next line
296 d
297
298 # print it
299 :p
300 p
301 }
302 ' $0
303 }
304
305 complete_help()
306 {
307 _gsu_available_commands
308 echo "$result"
309 }
310
311 # Wrapper for the bash getopts builtin.
312 #
313 # Aborts on programming errors such as missing or invalid option string. On
314 # success $result contains shell code that can be eval'ed. For each defined
315 # option x, the local variable o_x will be created when calling eval "$result".
316 # o_x contains true/false for options without argument and either the emtpy
317 # string or the given argument for options that take an argument.
318 #
319 # Example:
320 # gsu_getopts abc:x:y
321 # eval "$result"
322 # (($ret < 0)) && return
323 #
324 # [[ "$o_a" = 'true' ]] && echo 'The -a flag was given'
325 # [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
326 gsu_getopts()
327 {
328 local i c tab=' ' cr='
329 '
330
331 gsu_check_arg_count $# 1 1
332 if [[ $ret -lt 0 ]]; then
333 gsu_err_msg
334 exit 1
335 fi
336
337 ret=-$E_GSU_GETOPTS
338 result="invalid optstring $1"
339 if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
340 gsu_err_msg
341 exit 1
342 fi
343
344 for ((i=0; i < ${#1}; i++)); do
345 c=${1:$i:1}
346 case "$c" in
347 [a-zA-Z:]);;
348 *)
349 ret=-$E_GSU_GETOPTS
350 result="invalid character $c in optstring"
351 gsu_err_msg
352 exit 1
353 esac
354 done
355 result="local _gsu_getopts_opt"
356 for ((i=0; i < ${#1}; i++)); do
357 c1=${1:$i:1}
358 c2=${1:$(($i + 1)):1}
359 result+=" o_$c1="
360 if [[ "$c2" = ":" ]]; then
361 let i++
362 else
363 result+="false"
364 fi
365 done
366 result+="
367 OPTIND=1
368 while getopts $1 _gsu_getopts_opt \"\$@\"; do
369 case \"\$_gsu_getopts_opt\" in
370 "
371 for ((i=0; i < ${#1}; i++)); do
372 c1=${1:$i:1}
373 c2=${1:$(($i + 1)):1}
374 result+="$tab$tab$c1) o_$c1="
375 if [[ "$c2" = ":" ]]; then
376 result+="\"\$OPTARG\""
377 let i++
378 else
379 result+="true"
380 fi
381 result+=";;$cr"
382 done
383 result+="
384 *)
385 ret=-\$E_GSU_GETOPTS
386 result=\"invalid option given\"
387 return
388 ;;
389 esac
390 done
391 shift \$((\$OPTIND - 1))
392 "
393 ret=$GSU_SUCCESS
394 }
395
396 _com_complete()
397 {
398 local cmd n cword
399 local -a words
400
401 if (($# == 0)); then
402 cat <<EOF
403 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
404 local -a candidates;
405
406 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
407 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
408 EOF
409 ret=$GSU_SUCCESS
410 return
411 fi
412
413 cword="$1"
414 gsu_is_a_number "$cword"
415 (($ret < 0)) && return
416 if (($cword <= 1)); then
417 _gsu_available_commands
418 echo "${result}"
419 ret=$GSU_SUCCESS
420 return
421 fi
422 shift
423 words=("$@")
424 cmd="${words[1]}"
425 ret=$GSU_SUCCESS # It's not an error if no completer was defined
426 [[ "$(type -t complete_$cmd)" != "function" ]] && return
427 complete_$cmd "$cword" "${words[@]}"
428 # ignore errors, they would only clutter the completion output
429 ret=$GSU_SUCCESS
430 }
431
432 # Find out if the current word is a parameter for an option.
433 #
434 # $1: usual getopts option string.
435 # $2: The current word number.
436 # $3..: All words of the current command line.
437 #
438 # return: If yes, $result contains the letter of the option for which the
439 # current word is a parameter. Otherwise, $result is empty.
440 #
441 gsu_cword_is_option_parameter()
442 {
443 local opts="$1" cword="$2" prev i n
444 local -a words
445
446 result=
447 (($cword == 0)) && return
448 ((${#opts} < 2)) && return
449
450 shift 2
451 words=("$@")
452 prev="${words[$(($cword - 1))]}"
453 [[ ! "$prev" == -* ]] && return
454
455 n=$((${#opts} - 1))
456 for ((i=0; i <= $n; i++)); do
457 opt="${opts:$i:1}"
458 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
459 let i++
460 [[ "$prev" != "-$opt" ]] && continue
461 result="$opt"
462 return
463 done
464 ret=0
465 }
466
467 # Get the word number on which the cursor is, not counting options.
468 #
469 # This is useful for completing commands whose possible completions depend
470 # on the word number, for example mount.
471 #
472 # $1: Getopt option string.
473 # $2: The current word number.
474 # $3..: All words of the current command line.
475 #
476 # return: If the current word is an option, or a parameter to an option,
477 # this function sets $result to -1. Otherwise, the number of the non-option
478 # is returned in $result.
479 #
480 gsu_get_unnamed_arg_num()
481 {
482 local opts="$1" cword="$2" prev cur
483 local -i i n=0
484 local -a words
485
486 shift 2
487 words=("$@")
488 cur="${words[$cword]}"
489 prev="${words[$(($cword - 1))]}"
490 result=-1
491 [[ "$cur" == -* ]] && return
492 [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
493
494 for ((i=1; i <= $cword; i++)); do
495 prev="${words[$(($i - 1))]}"
496 cur="${words[$i]}"
497 [[ "$cur" == -* ]] && continue
498 if [[ "$prev" == -* ]]; then
499 opt=${prev#-}
500 [[ "$opts" != *$opt:* ]] && let n++
501 continue
502 fi
503 let n++
504 done
505 result="$(($n - 1))"
506 }
507
508 gsu()
509 {
510 local i
511 _gsu_setup
512 _gsu_available_commands
513 gsu_cmds="$result"
514 if test $# -eq 0; then
515 _gsu_usage
516 _gsu_print_available_commands
517 exit 1
518 fi
519 arg="$1"
520 shift
521 # check internal commands
522 if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
523 _com_$arg "$@"
524 if [[ "$ret" -lt 0 ]]; then
525 gsu_err_msg
526 exit 1
527 fi
528 exit 0
529 fi
530
531 # external commands
532 for i in $gsu_cmds; do
533 if test "$arg" = "$i"; then
534 com_$arg "$@"
535 if [[ "$ret" -lt 0 ]]; then
536 gsu_err_msg
537 exit 1
538 fi
539 exit 0
540 fi
541 done
542
543 ret=-$E_GSU_BAD_COMMAND
544 result="$arg"
545 gsu_err_msg
546 _gsu_print_available_commands
547 exit 1
548 }
549
550 # Check number of arguments.
551 #
552 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
553 #
554 # Check that <num_given> is between <num1> and <num2> inclusively.
555 # If only <num1> ist given, num2 is assumed to be infinity.
556 #
557 # Examples:
558 # 0 0 no argument allowed
559 # 1 1 exactly one argument required
560 # 0 2 at most two arguments admissible
561 # 2 at least two arguments reqired
562 #
563 gsu_check_arg_count()
564 {
565 ret=-$E_GSU_BAD_ARG_COUNT
566 if [[ $# -eq 2 ]]; then # only num1 is given
567 result="at least $2 args required, $1 given"
568 [[ $1 -lt $2 ]] && return
569 ret=$GSU_SUCCESS
570 return
571 fi
572 # num1 and num2 given
573 result="need at least $2 args, $1 given"
574 [[ $1 -lt $2 ]] && return
575 result="need at most $3 args, $1 given"
576 [[ $1 -gt $3 ]] && return
577 ret=$GSU_SUCCESS
578 }