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