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