]> git.tuebingen.mpg.de Git - gsu.git/blob - misc/gsu/subcommand
5ccb3fc54fd309065482522b67a22c6e292934f2
[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 # Find out if the current word is a parameter for an option.
438 #
439 # $1:   usual getopts option string.
440 # $2:   The current word number.
441 # $3..: All words of the current command line.
442 #
443 # return: If yes, $result contains the letter of the option for which the
444 # current word is a parameter. Otherwise, $result is empty.
445 #
446 gsu_cword_is_option_parameter()
447 {
448         local opts="$1" cword="$2" prev i n
449         local -a words
450
451         result=
452         (($cword == 0)) && return
453         ((${#opts} < 2)) && return
454
455         shift 2
456         words=("$@")
457         prev="${words[$(($cword - 1))]}"
458         [[ ! "$prev" == -* ]] && return
459
460         n=$((${#opts} - 1))
461         for ((i=0; i <= $n; i++)); do
462                 opt="${opts:$i:1}"
463                 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
464                 let i++
465                 [[ "$prev" != "-$opt" ]] && continue
466                 result="$opt"
467                 return
468         done
469         ret=0
470 }
471
472 # Get the word number on which the cursor is, not counting options.
473 #
474 # This is useful for completing commands whose possible completions depend
475 # on the word number, for example mount.
476 #
477 # $1:   Getopt option string.
478 # $2:   The current word number.
479 # $3..: All words of the current command line.
480 #
481 # return: If the current word is an option, or a parameter to an option,
482 # this function sets $result to -1. Otherwise, the number of the non-option
483 # is returned in $result.
484 #
485 gsu_get_unnamed_arg_num()
486 {
487         local opts="$1" cword="$2" prev cur
488         local -i i n=0
489         local -a words
490
491         shift 2
492         words=("$@")
493         cur="${words[$cword]}"
494         prev="${words[$(($cword - 1))]}"
495         result=-1
496         [[ "$cur" == -* ]] && return
497         [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
498
499         for ((i=1; i <= $cword; i++)); do
500                 prev="${words[$(($i - 1))]}"
501                 cur="${words[$i]}"
502                 [[ "$cur" == -* ]] && continue
503                 if [[ "$prev" == -* ]]; then
504                         opt=${prev#-}
505                         [[ "$opts" != *$opt:* ]] && let n++
506                         continue
507                 fi
508                 let n++
509         done
510         result="$(($n - 1))"
511 }
512
513 gsu()
514 {
515         local i
516         _gsu_setup
517         _gsu_available_commands
518         gsu_cmds="$result"
519         if test $# -eq 0; then
520                 _gsu_usage
521                 _gsu_print_available_commands
522                 exit 1
523         fi
524         arg="$1"
525         shift
526         # check internal commands
527         if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
528                 _com_$arg "$@"
529                 if [[ "$ret" -lt 0 ]]; then
530                         gsu_err_msg
531                         exit 1
532                 fi
533                 exit 0
534         fi
535
536         # external commands
537         for i in $gsu_cmds; do
538                 if test "$arg" = "$i"; then
539                         com_$arg "$@"
540                         if [[ "$ret" -lt 0 ]]; then
541                                 gsu_err_msg
542                                 exit 1
543                         fi
544                         exit 0
545                 fi
546         done
547
548         ret=-$E_GSU_BAD_COMMAND
549         result="$arg"
550         gsu_err_msg
551         _gsu_print_available_commands
552         exit 1
553 }
554
555 # Check number of arguments.
556 #
557 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
558 #
559 # Check that <num_given> is between <num1> and <num2> inclusively.
560 # If only <num1> ist given, num2 is assumed to be infinity.
561 #
562 # Examples:
563 #       0 0 no argument allowed
564 #       1 1 exactly one argument required
565 #       0 2 at most two arguments admissible
566 #       2   at least two arguments reqired
567 #
568 gsu_check_arg_count()
569 {
570         ret=-$E_GSU_BAD_ARG_COUNT
571         if [[ $# -eq 2 ]]; then # only num1 is given
572                 result="at least $2 args required, $1 given"
573                 [[ $1 -lt $2 ]] && return
574                 ret=$GSU_SUCCESS
575                 return
576         fi
577         # num1 and num2 given
578         result="need at least $2 args, $1 given"
579         [[ $1 -lt $2 ]] && return
580         result="need at most $3 args, $1 given"
581         [[ $1 -gt $3 ]] && return
582         ret=$GSU_SUCCESS
583 }