]> git.tuebingen.mpg.de Git - gsu.git/blob - misc/gsu/subcommand
gsu: Overhaul of the rexexp matching for help commands.
[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
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
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         if [[ "$1" = "-e" ]]; then
96                 ret=-$E_GSU_MKDIR
97                 result="${conf%/*}"
98                 mkdir -p "$result"
99                 [[ $? -ne 0 ]] && return
100                 ret=-$E_GSU_EDITOR
101                 result="${EDITOR:-vi}"
102                 "$result" "$conf"
103                 [[ $? -ne 0 ]] && return
104                 ret=$GSU_SUCCESS
105                 return
106         fi
107
108         for ((i=0; i < ${#gsu_options[@]}; i++)); do
109                 local name= option_type= default_value= required=
110                 local description= help_text=
111                 eval "${gsu_options[$i]}"
112                 eval val='"$'${gsu_config_var_prefix}_$name'"'
113                 case "$required" in
114                 true|yes)
115                         printf "# required"
116                         ;;
117                 *)
118                         printf "# optional"
119                         ;;
120                 esac
121                 printf " $option_type: $description"
122                 if [[ "$required" != "yes" && "$required" != "true" ]]; then
123                         printf " [$default_value]"
124                 fi
125                 echo
126                 [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
127                 printf "$name=$val"
128                 [[ "$val" == "$default_value" ]] && printf " # default"
129                 echo
130         done
131 }
132
133 complete_prefs()
134 {
135         gsu_complete_options "e" "$@"
136 }
137
138 export gsu_man_txt="
139 Print the manual.
140
141 Usage: man"
142
143 _com_man()
144 {
145         local equal_signs="=================================================="
146         local minus_signs="--------------------------------------------------"
147         local com num
148
149         echo "$_gsu_self (_${gsu_banner_txt}_) manual"
150         echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
151         echo
152
153         sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
154         echo "----"
155         echo
156         echo "$_gsu_self usage"
157         echo "${minus_signs:0:${#_gsu_self} + 6}"
158         printf "\t"
159         _gsu_usage 2>&1
160         echo "Each command has its own set of options as described below."
161         echo
162         echo "----"
163         echo
164         echo "Available commands:"
165
166         _gsu_available_commands
167         for com in $result; do
168                 num=${#com}
169                 if test $num -lt 4; then
170                         num=4
171                 fi
172                 echo "${minus_signs:0:$num}"
173                 echo "$com"
174                 echo "${minus_signs:0:$num}"
175                 $0 help $com
176                 echo
177         done
178         ret=$GSU_SUCCESS
179 }
180
181 _gsu_banner_msg()
182 {
183         local txt="### $_gsu_self --"
184         if test -z "$gsu_banner_txt"; then
185                 txt="$txt set \$gsu_banner_txt to customize this message"
186         else
187                 txt="$txt $gsu_banner_txt"
188         fi
189         gsu_short_msg "$txt ###"
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 In the first form, the command prints all possible completions to stdout.
206 This can be used from the completion function of the shell.
207
208 Completion code suitable to be evaled is written to stdout if no argument
209 was given.
210 "
211
212 _com_help()
213 {
214         local a b ere tab='     '
215
216         _gsu_get_command_regex
217         ere="$result"
218
219         if test -z "$1"; then
220                 _gsu_banner_msg 2>&1
221                 _gsu_usage 2>&1
222                 {
223                         printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
224                         printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
225                         printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
226                         printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
227                         grep -EA 2 "$ere" $0
228                 } | grep -v -- '--' \
229                         | sed -En "/$ere/"'!d
230                                 # remove everything but the command name
231                                 s/^com_(.*)\(\).*/\1/
232
233                                 # append tab after short commands (less than 8 chars)
234                                 s/^(.{1,7})$/\1'"$tab"'/g
235
236                                 # remove next line (should contain only ## anyway)
237                                 N
238                                 s/#.*//
239
240                                 # append next line, removing leading ##
241                                 N
242                                 s/#+ *//g
243
244                                 # replace newline by tab
245                                 y/\n/'"$tab"'/
246
247                                 # and print the sucker
248                                 p'
249                 echo
250                 echo "# Try $_gsu_self help <command> for info on <command>."
251                 ret=$GSU_SUCCESS
252                 return
253         fi
254         if test "$1" = "help"; then
255                 echo "$gsu_help_txt"
256                 ret=$GSU_SUCCESS
257                 return
258         fi
259         if test "$1" = "man"; then
260                 echo "$gsu_man_txt"
261                 ret=$GSU_SUCCESS
262                 return
263         fi
264         if test "$1" = "prefs"; then
265                 echo "$gsu_prefs_txt"
266                 ret=$GSU_SUCCESS
267                 return
268         fi
269         if test "$1" = "complete"; then
270                 echo "$gsu_complete_txt"
271                 ret=$GSU_SUCCESS
272                 return
273         fi
274         ret=$GSU_SUCCESS
275         _gsu_get_command_regex "$1"
276         ere="$result"
277         if ! grep -Eq "$ere" $0; then
278                 _gsu_print_available_commands
279                 result="$1"
280                 ret=-$E_GSU_BAD_COMMAND
281                 return
282         fi
283         sed -nEe '
284                 # only consider lines in the comment of the function
285                 /'"$ere"'/,/^[^#]/ {
286
287                         # remove leading ##
288                         s/^## *//
289
290                         # if it did start with ##, jump to label p and print it
291                         tp
292
293                         # otherwise, move on to next line
294                         d
295
296                         # print it
297                         :p
298                         p
299                 }
300         ' $0
301 }
302
303 complete_help()
304 {
305         _gsu_available_commands
306         echo "$result"
307 }
308
309 # Wrapper for bash's getopts.
310 #
311 # Aborts on programming errors such as missing or invalid option string.  On
312 # success $result contains shell code that can be eval'ed. For each defined
313 # option x, the local variable o_x will be created when calling eval "$result".
314 # o_x contains true/false for options without an argument or the emtpy string/the
315 # given argument, depending on whether this option was contained in the "$@"
316 # array.
317 #
318 # Example:
319 #       gsu_getopts abc:x:y
320 #       eval "$result"
321 #       [[ $ret -lt 0 ]] && return
322 #
323 #       [[ "$o_a" = "true ]] && echo "The -a flag was given"
324 #       [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
325 gsu_getopts()
326 {
327         local i c tab=' ' cr='
328 '
329
330         gsu_check_arg_count $# 1 1
331         if [[ $ret -lt 0 ]]; then
332                 gsu_err_msg
333                 exit 1
334         fi
335
336         ret=-$E_GSU_GETOPTS
337         result="invalid optstring $1"
338         if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
339                 gsu_err_msg
340                 exit 1
341         fi
342
343         for ((i=0; i < ${#1}; i++)); do
344                 c=${1:$i:1}
345                 case "$c" in
346                 [a-zA-Z:]);;
347                 *)
348                         ret=-$E_GSU_GETOPTS
349                         result="invalid character $c in optstring"
350                         gsu_err_msg
351                         exit 1
352                 esac
353         done
354         result="local opt"
355         for ((i=0; i < ${#1}; i++)); do
356                 c1=${1:$i:1}
357                 c2=${1:$(($i + 1)):1}
358                 result+=" o_$c1"
359                 if [[ "$c2" = ":" ]]; then
360                         let i++
361                 else
362                         result+="=false"
363                 fi
364         done
365         result+="
366         OPTIND=1
367         while getopts $1 opt \"\$@\"; do
368                 case \"\$opt\" in
369 "
370         for ((i=0; i < ${#1}; i++)); do
371                 c1=${1:$i:1}
372                 c2=${1:$(($i + 1)):1}
373                 result+="$tab$tab$c1) o_$c1="
374                 if [[ "$c2" = ":" ]]; then
375                         result+="\"\$OPTARG\""
376                         let i++
377                 else
378                         result+="true"
379                 fi
380                 result+=";;$cr"
381         done
382         result+="
383                 *)
384                         ret=-\$E_GSU_GETOPTS
385                         result=\"invalid option given\"
386                         return
387                         ;;
388                 esac
389         done
390         shift \$((\$OPTIND - 1))
391 "
392         ret=$GSU_SUCCESS
393 }
394
395 _com_complete()
396 {
397         local cmd n cword="$1"
398         local -a words
399
400         if (($# == 0)); then
401                 cat <<EOF
402                 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
403                 local -a candidates;
404
405                 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
406                 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
407 EOF
408         fi
409
410         [[ -z "$cword" ]] && return
411         if (($cword <= 1)); then
412                 _gsu_available_commands
413                 echo "${result}"
414                 ret=$GSU_SUCCESS
415                 return
416         fi
417         shift
418         words=("$@")
419         cmd="${words[1]}"
420         ret=$GSU_SUCCESS # It's not an error if no completer was defined
421         [[ "$(type -t complete_$cmd)" != "function" ]] && return
422         complete_$cmd "$cword" "${words[@]}"
423         # ignore errors, they would only clutter the completion output
424         ret=$GSU_SUCCESS
425 }
426
427 gsu_cword_is_option_parameter()
428 {
429         local opts="$1" cword="$2" prev i n
430         local -a words
431
432         result=
433         (($cword == 0)) && return
434         ((${#opts} < 2)) && return
435
436         shift 2
437         words=("$@")
438         prev="${words[$(($cword - 1))]}"
439         [[ ! "$prev" == -* ]] && return
440
441         n=$((${#opts} - 1))
442         for ((i=0; i < $n; i++)); do
443                 opt="${opts:$i:1}"
444                 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
445                 let i++
446                 [[ "$prev" != "-$opt" ]] && continue
447                 result="$opt"
448                 return
449         done
450         ret=0
451 }
452
453 gsu()
454 {
455         local i
456         _gsu_setup
457         _gsu_available_commands
458         gsu_cmds="$result"
459         if test $# -eq 0; then
460                 _gsu_usage
461                 _gsu_print_available_commands
462                 exit 1
463         fi
464         arg="$1"
465         shift
466         # check internal commands
467         if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
468                 _com_$arg "$@"
469                 if [[ "$ret" -lt 0 ]]; then
470                         gsu_err_msg
471                         exit 1
472                 fi
473                 exit 0
474         fi
475
476         # external commands
477         for i in $gsu_cmds; do
478                 if test "$arg" = "$i"; then
479                         com_$arg "$@"
480                         if [[ "$ret" -lt 0 ]]; then
481                                 gsu_err_msg
482                                 exit 1
483                         fi
484                         exit 0
485                 fi
486         done
487
488         ret=-$E_GSU_BAD_COMMAND
489         result="$arg"
490         gsu_err_msg
491         _gsu_print_available_commands
492         exit 1
493 }
494
495 # Check number of arguments.
496 #
497 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
498 #
499 # Check that <num_given> is between <num1> and <num2> inclusively.
500 # If only <num1> ist given, num2 is assumed to be infinity.
501 #
502 # Examples:
503 #       0 0 no argument allowed
504 #       1 1 exactly one argument required
505 #       0 2 at most two arguments admissible
506 #       2   at least two arguments reqired
507 #
508 gsu_check_arg_count()
509 {
510         ret=-$E_GSU_BAD_ARG_COUNT
511         if [[ $# -eq 2 ]]; then # only num1 is given
512                 result="at least $2 args required, $1 given"
513                 [[ $1 -lt $2 ]] && return
514                 ret=$GSU_SUCCESS
515                 return
516         fi
517         # num1 and num2 given
518         result="need at least $2 args, $1 given"
519         [[ $1 -lt $2 ]] && return
520         result="need at most $3 args, $1 given"
521         [[ $1 -gt $3 ]] && return
522         ret=$GSU_SUCCESS
523 }