]> git.tuebingen.mpg.de Git - gsu.git/blob - misc/gsu/subcommand
Fix gsu com_complete().
[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
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         local txt="### $_gsu_self --"
190         if test -z "$gsu_banner_txt"; then
191                 txt="$txt set \$gsu_banner_txt to customize this message"
192         else
193                 txt="$txt $gsu_banner_txt"
194         fi
195         gsu_short_msg "$txt ###"
196 }
197
198 export gsu_help_txt="
199 Print online help.
200
201 Usage: help [command]
202
203 Without arguments, print the list of available commands. Otherwise,
204 print the help text for the given command."
205
206 export gsu_complete_txt="
207 Command line completion.
208
209 Usage: complete [<cword> <word>...]
210
211 In the first form, the command prints all possible completions to stdout.
212 This can be used from the completion function of the shell.
213
214 Completion code suitable to be evaled is written to stdout if no argument
215 was given.
216 "
217
218 _com_help()
219 {
220         local a b ere tab='     '
221
222         _gsu_get_command_regex
223         ere="$result"
224
225         if (($# == 0)); then
226                 _gsu_banner_msg 2>&1
227                 _gsu_usage 2>&1
228                 {
229                         printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
230                         printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
231                         printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
232                         printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
233                         grep -EA 2 "$ere" $0
234                 } | grep -v -- '--' \
235                         | sed -En "/$ere/"'!d
236                                 # remove everything but the command name
237                                 s/^com_(.*)\(\).*/\1/
238
239                                 # append tab after short commands (less than 8 chars)
240                                 s/^(.{1,7})$/\1'"$tab"'/g
241
242                                 # remove next line (should contain only ## anyway)
243                                 N
244                                 s/#.*//
245
246                                 # append next line, removing leading ##
247                                 N
248                                 s/#+ *//g
249
250                                 # replace newline by tab
251                                 y/\n/'"$tab"'/
252
253                                 # and print the sucker
254                                 p'
255                 echo
256                 echo "# Try $_gsu_self help <command> for info on <command>."
257                 ret=$GSU_SUCCESS
258                 return
259         fi
260         if test "$1" = "help"; then
261                 echo "$gsu_help_txt"
262                 ret=$GSU_SUCCESS
263                 return
264         fi
265         if test "$1" = "man"; then
266                 echo "$gsu_man_txt"
267                 ret=$GSU_SUCCESS
268                 return
269         fi
270         if test "$1" = "prefs"; then
271                 echo "$gsu_prefs_txt"
272                 ret=$GSU_SUCCESS
273                 return
274         fi
275         if test "$1" = "complete"; then
276                 echo "$gsu_complete_txt"
277                 ret=$GSU_SUCCESS
278                 return
279         fi
280         ret=$GSU_SUCCESS
281         _gsu_get_command_regex "$1"
282         ere="$result"
283         if ! grep -Eq "$ere" $0; then
284                 _gsu_print_available_commands
285                 result="$1"
286                 ret=-$E_GSU_BAD_COMMAND
287                 return
288         fi
289         sed -nEe '
290                 # only consider lines in the comment of the function
291                 /'"$ere"'/,/^[^#]/ {
292
293                         # remove leading ##
294                         s/^## *//
295
296                         # if it did start with ##, jump to label p and print it
297                         tp
298
299                         # otherwise, move on to next line
300                         d
301
302                         # print it
303                         :p
304                         p
305                 }
306         ' $0
307 }
308
309 complete_help()
310 {
311         _gsu_available_commands
312         echo "$result"
313 }
314
315 # Wrapper for bash's getopts.
316 #
317 # Aborts on programming errors such as missing or invalid option string.  On
318 # success $result contains shell code that can be eval'ed. For each defined
319 # option x, the local variable o_x will be created when calling eval "$result".
320 # o_x contains true/false for options without an argument or the emtpy string/the
321 # given argument, depending on whether this option was contained in the "$@"
322 # array.
323 #
324 # Example:
325 #       gsu_getopts abc:x:y
326 #       eval "$result"
327 #       [[ $ret -lt 0 ]] && return
328 #
329 #       [[ "$o_a" = "true ]] && echo "The -a flag was given"
330 #       [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
331 gsu_getopts()
332 {
333         local i c tab=' ' cr='
334 '
335
336         gsu_check_arg_count $# 1 1
337         if [[ $ret -lt 0 ]]; then
338                 gsu_err_msg
339                 exit 1
340         fi
341
342         ret=-$E_GSU_GETOPTS
343         result="invalid optstring $1"
344         if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
345                 gsu_err_msg
346                 exit 1
347         fi
348
349         for ((i=0; i < ${#1}; i++)); do
350                 c=${1:$i:1}
351                 case "$c" in
352                 [a-zA-Z:]);;
353                 *)
354                         ret=-$E_GSU_GETOPTS
355                         result="invalid character $c in optstring"
356                         gsu_err_msg
357                         exit 1
358                 esac
359         done
360         result="local _gsu_getopts_opt"
361         for ((i=0; i < ${#1}; i++)); do
362                 c1=${1:$i:1}
363                 c2=${1:$(($i + 1)):1}
364                 result+=" o_$c1="
365                 if [[ "$c2" = ":" ]]; then
366                         let i++
367                 else
368                         result+="false"
369                 fi
370         done
371         result+="
372         OPTIND=1
373         while getopts $1 _gsu_getopts_opt \"\$@\"; do
374                 case \"\$_gsu_getopts_opt\" in
375 "
376         for ((i=0; i < ${#1}; i++)); do
377                 c1=${1:$i:1}
378                 c2=${1:$(($i + 1)):1}
379                 result+="$tab$tab$c1) o_$c1="
380                 if [[ "$c2" = ":" ]]; then
381                         result+="\"\$OPTARG\""
382                         let i++
383                 else
384                         result+="true"
385                 fi
386                 result+=";;$cr"
387         done
388         result+="
389                 *)
390                         ret=-\$E_GSU_GETOPTS
391                         result=\"invalid option given\"
392                         return
393                         ;;
394                 esac
395         done
396         shift \$((\$OPTIND - 1))
397 "
398         ret=$GSU_SUCCESS
399 }
400
401 _com_complete()
402 {
403         local cmd n cword
404         local -a words
405
406         if (($# == 0)); then
407                 cat <<EOF
408                 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
409                 local -a candidates;
410
411                 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
412                 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
413 EOF
414                 ret=$GSU_SUCCESS
415                 return
416         fi
417
418         cword="$1"
419         gsu_is_a_number "$cword"
420         (($ret < 0)) && return
421         if (($cword <= 1)); then
422                 _gsu_available_commands
423                 echo "${result}"
424                 ret=$GSU_SUCCESS
425                 return
426         fi
427         shift
428         words=("$@")
429         cmd="${words[1]}"
430         ret=$GSU_SUCCESS # It's not an error if no completer was defined
431         [[ "$(type -t complete_$cmd)" != "function" ]] && return
432         complete_$cmd "$cword" "${words[@]}"
433         # ignore errors, they would only clutter the completion output
434         ret=$GSU_SUCCESS
435 }
436
437 gsu_cword_is_option_parameter()
438 {
439         local opts="$1" cword="$2" prev i n
440         local -a words
441
442         result=
443         (($cword == 0)) && return
444         ((${#opts} < 2)) && return
445
446         shift 2
447         words=("$@")
448         prev="${words[$(($cword - 1))]}"
449         [[ ! "$prev" == -* ]] && return
450
451         n=$((${#opts} - 1))
452         for ((i=0; i < $n; i++)); do
453                 opt="${opts:$i:1}"
454                 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
455                 let i++
456                 [[ "$prev" != "-$opt" ]] && continue
457                 result="$opt"
458                 return
459         done
460         ret=0
461 }
462
463 gsu()
464 {
465         local i
466         _gsu_setup
467         _gsu_available_commands
468         gsu_cmds="$result"
469         if test $# -eq 0; then
470                 _gsu_usage
471                 _gsu_print_available_commands
472                 exit 1
473         fi
474         arg="$1"
475         shift
476         # check internal commands
477         if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
478                 _com_$arg "$@"
479                 if [[ "$ret" -lt 0 ]]; then
480                         gsu_err_msg
481                         exit 1
482                 fi
483                 exit 0
484         fi
485
486         # external commands
487         for i in $gsu_cmds; do
488                 if test "$arg" = "$i"; then
489                         com_$arg "$@"
490                         if [[ "$ret" -lt 0 ]]; then
491                                 gsu_err_msg
492                                 exit 1
493                         fi
494                         exit 0
495                 fi
496         done
497
498         ret=-$E_GSU_BAD_COMMAND
499         result="$arg"
500         gsu_err_msg
501         _gsu_print_available_commands
502         exit 1
503 }
504
505 # Check number of arguments.
506 #
507 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
508 #
509 # Check that <num_given> is between <num1> and <num2> inclusively.
510 # If only <num1> ist given, num2 is assumed to be infinity.
511 #
512 # Examples:
513 #       0 0 no argument allowed
514 #       1 1 exactly one argument required
515 #       0 2 at most two arguments admissible
516 #       2   at least two arguments reqired
517 #
518 gsu_check_arg_count()
519 {
520         ret=-$E_GSU_BAD_ARG_COUNT
521         if [[ $# -eq 2 ]]; then # only num1 is given
522                 result="at least $2 args required, $1 given"
523                 [[ $1 -lt $2 ]] && return
524                 ret=$GSU_SUCCESS
525                 return
526         fi
527         # num1 and num2 given
528         result="need at least $2 args, $1 given"
529         [[ $1 -lt $2 ]] && return
530         result="need at most $3 args, $1 given"
531         [[ $1 -gt $3 ]] && return
532         ret=$GSU_SUCCESS
533 }