]> git.tuebingen.mpg.de Git - gsu.git/blob - subcommand
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 }