]> git.tuebingen.mpg.de Git - gsu.git/blob - subcommand
bab8ded4ea1d9f0b99562eca7dc8d257244062a0
[gsu.git] / subcommand
1 #!/bin/bash
2 # (C) 2006-2011 Andre Noll
3
4 if [[ "$(type -t _gsu_setup)" != "function" ]]; then
5         gsu_dir=${gsu_dir:-${BASH_SOURCE[0]%/*}}
6         . $gsu_dir/common || exit 1
7         _gsu_setup
8 fi
9
10 _gsu_usage()
11 {
12         gsu_short_msg "# Usage: $_gsu_self command [options]"
13 }
14
15 # Return an extended regular expression to match against $0.
16 #
17 # When called without argument, the expression matches all lines which define a
18 # subcommand.
19 #
20 # If an argument is given, the returned expression matches only the subcommand
21 # passed as $1. This is useful to tell if a string is a valid subcommand.
22 #
23 # Regardless of whether an argument is given, the returned expression contains
24 # exactly one parenthesized subexpression for matching the command name.
25 _gsu_get_command_regex()
26 {
27         local cmd="${1:-[-a-zA-Z_0-9]+}"
28         result="^com_($cmd)\(\)"
29 }
30
31 _gsu_available_commands()
32 {
33         local ere
34
35         _gsu_get_command_regex
36         ere="$result"
37         result="$({
38                 printf "help\nman\nprefs\ncomplete\n"
39                 sed -Ee '
40                         # if line matches, isolate command name
41                         s/'"$ere"'/\1/g
42
43                         # if there is a match, (print it and) start next cycle
44                         t
45
46                         # otherwise delete it
47                         d
48                 ' $0
49         } | sort | tr '\n' ' ')"
50 }
51
52 _gsu_print_available_commands()
53 {(
54         local i count=0
55         gsu_short_msg "Available commands:"
56         for i in $gsu_cmds; do
57                 printf "$i"
58                 count=$(($count + 1))
59                 if test $(($count % 4)) -eq 0; then
60                         echo
61                 else
62                         printf "\t"
63                         if test ${#i} -lt 8; then
64                                 printf "\t"
65                         fi
66                 fi
67         done
68         echo
69 ) 2>&1
70 }
71
72 gsu_complete_options()
73 {
74         local opts="$1" cword="$2" cur opt
75         local -a words
76
77         shift 2
78         words=("$@")
79         cur="${words[$cword]}"
80         ret=0
81         [[ ! "$cur" == -* ]] && return
82
83         ret=0
84         for ((i=0; i < ${#opts}; i++)); do
85                 opt="${opts:$i:1}"
86                 [[ "$opt" == ":" ]] && continue
87                 printf "%s" "-$opt "
88                 let ret++
89         done
90 }
91
92 export gsu_prefs_txt="
93 Print the current preferences.
94
95 Usage: prefs [-e]
96
97 If -e is given, the config file is opened with the default editor.  Without
98 options, the command prints out a list of all cmt config variables, together
99 with their current value and the default value."
100 _com_prefs()
101 {
102         local i conf="${gsu_config_file:=${HOME:-}/.$gsu_name.rc}"
103
104         gsu_getopts "e"
105         eval "$result"
106         (($ret < 0)) && return
107         gsu_check_arg_count $# 0 0
108         (($ret < 0)) && return
109
110         if [[ "$o_e" == "true" ]]; then
111                 ret=-$E_GSU_MKDIR
112                 result="${conf%/*}"
113                 mkdir -p "$result"
114                 [[ $? -ne 0 ]] && return
115                 ret=-$E_GSU_EDITOR
116                 result="${EDITOR:-vi}"
117                 "$result" "$conf"
118                 [[ $? -ne 0 ]] && return
119                 ret=$GSU_SUCCESS
120                 return
121         fi
122
123         for ((i=0; i < ${#gsu_options[@]}; i++)); do
124                 local name= option_type= default_value= required=
125                 local description= help_text=
126                 eval "${gsu_options[$i]}"
127                 eval val='"${'${gsu_config_var_prefix}_$name:-'}"'
128                 case "$required" in
129                 true|yes)
130                         printf "# required"
131                         ;;
132                 *)
133                         printf "# optional"
134                         ;;
135                 esac
136                 printf " $option_type: $description"
137                 if [[ "$required" != "yes" && "$required" != "true" ]]; then
138                         printf " [$default_value]"
139                 fi
140                 echo
141                 [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
142                 printf "$name=$val"
143                 [[ "$val" == "$default_value" ]] && printf " # default"
144                 echo
145         done
146 }
147
148 complete_prefs()
149 {
150         gsu_complete_options "e" "$@"
151 }
152
153 export gsu_man_txt="
154 Print the manual.
155
156 Usage: man"
157
158 _com_man()
159 {
160         local equal_signs="=================================================="
161         local minus_signs="--------------------------------------------------"
162         local com num
163
164         echo "$_gsu_self (_${gsu_banner_txt}_) manual"
165         echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
166         echo
167
168         sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
169         echo "----"
170         echo
171         echo "$_gsu_self usage"
172         echo "${minus_signs:0:${#_gsu_self} + 6}"
173         printf "\t"
174         _gsu_usage 2>&1
175         echo "Each command has its own set of options as described below."
176         echo
177         echo "----"
178         echo
179         echo "Available commands:"
180
181         _gsu_available_commands
182         for com in $result; do
183                 num=${#com}
184                 if test $num -lt 4; then
185                         num=4
186                 fi
187                 echo "${minus_signs:0:$num}"
188                 echo "$com"
189                 echo "${minus_signs:0:$num}"
190                 $0 help $com
191                 echo
192         done
193         ret=$GSU_SUCCESS
194 }
195
196 _gsu_banner_msg()
197 {
198         gsu_short_msg "### $_gsu_self -- ###"
199 }
200
201 export gsu_help_txt="
202 Print online help.
203
204 Usage: help [command]
205
206 Without arguments, print the list of available commands. Otherwise,
207 print the help text for the given command."
208
209 export gsu_complete_txt="
210 Command line completion.
211
212 Usage: complete [<cword> <word>...]
213
214 When executed without argument the command writes bash code to
215 stdout. This code is suitable to be evaled from .bashrc to enable
216 completion.
217
218 If at least one argument is given, all possible completions are
219 written to stdout. This can be used from the completion function of
220 the subcommand.
221 "
222
223 _com_help()
224 {
225         local a b ere tab='     '
226
227         _gsu_get_command_regex
228         ere="$result"
229
230         if (($# == 0)); then
231                 _gsu_banner_msg 2>&1
232                 _gsu_usage 2>&1
233                 {
234                         printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
235                         printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
236                         printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
237                         printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
238                         grep -EA 2 "$ere" $0
239                 } | grep -v -- '--' \
240                         | sed -En "/$ere/"'!d
241                                 # remove everything but the command name
242                                 s/^com_(.*)\(\).*/\1/
243
244                                 # append tab after short commands (less than 8 chars)
245                                 s/^(.{1,7})$/\1'"$tab"'/g
246
247                                 # remove next line (should contain only ## anyway)
248                                 N
249                                 s/#.*//
250
251                                 # append next line, removing leading ##
252                                 N
253                                 s/#+ *//g
254
255                                 # replace newline by tab
256                                 y/\n/'"$tab"'/
257
258                                 # and print the sucker
259                                 p'
260                 echo
261                 echo "# Try $_gsu_self help <command> for info on <command>."
262                 ret=$GSU_SUCCESS
263                 return
264         fi
265         if test "$1" = "help"; then
266                 echo "$gsu_help_txt"
267                 ret=$GSU_SUCCESS
268                 return
269         fi
270         if test "$1" = "man"; then
271                 echo "$gsu_man_txt"
272                 ret=$GSU_SUCCESS
273                 return
274         fi
275         if test "$1" = "prefs"; then
276                 echo "$gsu_prefs_txt"
277                 ret=$GSU_SUCCESS
278                 return
279         fi
280         if test "$1" = "complete"; then
281                 echo "$gsu_complete_txt"
282                 ret=$GSU_SUCCESS
283                 return
284         fi
285         ret=$GSU_SUCCESS
286         _gsu_get_command_regex "$1"
287         ere="$result"
288         if ! grep -Eq "$ere" $0; then
289                 _gsu_print_available_commands
290                 result="$1"
291                 ret=-$E_GSU_BAD_COMMAND
292                 return
293         fi
294         sed -nEe '
295                 # only consider lines in the comment of the function
296                 /'"$ere"'/,/^[^#]/ {
297
298                         # remove leading ##
299                         s/^## *//
300
301                         # if it did start with ##, jump to label p and print it
302                         tp
303
304                         # otherwise, move on to next line
305                         d
306
307                         # print it
308                         :p
309                         p
310                 }
311         ' $0
312 }
313
314 complete_help()
315 {
316         _gsu_available_commands
317         echo "$result"
318 }
319
320 # Wrapper for the bash getopts builtin.
321 #
322 # Aborts on programming errors such as missing or invalid option string.  On
323 # success $result contains shell code that can be eval'ed. For each defined
324 # option x, the local variable o_x will be created when calling eval "$result".
325 # o_x contains true/false for options without argument and either the emtpy
326 # string or the given argument for options that take an argument.
327 #
328 # Example:
329 #       gsu_getopts abc:x:y
330 #       eval "$result"
331 #       (($ret < 0)) && return
332 #
333 #       [[ "$o_a" = 'true' ]] && echo 'The -a flag was given'
334 #       [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
335 gsu_getopts()
336 {
337         local i c tab=' ' cr='
338 '
339
340         gsu_check_arg_count $# 1 1
341         if [[ $ret -lt 0 ]]; then
342                 gsu_err_msg
343                 exit 1
344         fi
345
346         ret=-$E_GSU_GETOPTS
347         result="invalid optstring $1"
348         if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
349                 gsu_err_msg
350                 exit 1
351         fi
352
353         for ((i=0; i < ${#1}; i++)); do
354                 c=${1:$i:1}
355                 case "$c" in
356                 [a-zA-Z:]);;
357                 *)
358                         ret=-$E_GSU_GETOPTS
359                         result="invalid character $c in optstring"
360                         gsu_err_msg
361                         exit 1
362                 esac
363         done
364         result="local _gsu_getopts_opt"
365         for ((i=0; i < ${#1}; i++)); do
366                 c1=${1:$i:1}
367                 c2=${1:$(($i + 1)):1}
368                 result+=" o_$c1="
369                 if [[ "$c2" = ":" ]]; then
370                         let i++
371                 else
372                         result+="false"
373                 fi
374         done
375         result+="
376         OPTIND=1
377         while getopts $1 _gsu_getopts_opt \"\$@\"; do
378                 case \"\$_gsu_getopts_opt\" in
379 "
380         for ((i=0; i < ${#1}; i++)); do
381                 c1=${1:$i:1}
382                 c2=${1:$(($i + 1)):1}
383                 result+="$tab$tab$c1) o_$c1="
384                 if [[ "$c2" = ":" ]]; then
385                         result+="\"\$OPTARG\""
386                         let i++
387                 else
388                         result+="true"
389                 fi
390                 result+=";;$cr"
391         done
392         result+="
393                 *)
394                         ret=-\$E_GSU_GETOPTS
395                         result=\"invalid option given\"
396                         return
397                         ;;
398                 esac
399         done
400         shift \$((\$OPTIND - 1))
401 "
402         ret=$GSU_SUCCESS
403 }
404
405 _com_complete()
406 {
407         local cmd n cword
408         local -a words
409
410         if (($# == 0)); then
411                 cat <<EOF
412                 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
413                 local -a candidates;
414
415                 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
416                 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
417 EOF
418                 ret=$GSU_SUCCESS
419                 return
420         fi
421
422         cword="$1"
423         gsu_is_a_number "$cword"
424         (($ret < 0)) && return
425         if (($cword <= 1)); then
426                 _gsu_available_commands
427                 echo "${result}"
428                 ret=$GSU_SUCCESS
429                 return
430         fi
431         shift
432         words=("$@")
433         cmd="${words[1]}"
434         ret=$GSU_SUCCESS # It's not an error if no completer was defined
435         [[ "$(type -t complete_$cmd)" != "function" ]] && return
436         complete_$cmd "$cword" "${words[@]}"
437         # ignore errors, they would only clutter the completion output
438         ret=$GSU_SUCCESS
439 }
440
441 # Find out if the current word is a parameter for an option.
442 #
443 # $1:   usual getopts option string.
444 # $2:   The current word number.
445 # $3..: All words of the current command line.
446 #
447 # return: If yes, $result contains the letter of the option for which the
448 # current word is a parameter. Otherwise, $result is empty.
449 #
450 gsu_cword_is_option_parameter()
451 {
452         local opts="$1" cword="$2" prev i n
453         local -a words
454
455         result=
456         (($cword == 0)) && return
457         ((${#opts} < 2)) && return
458
459         shift 2
460         words=("$@")
461         prev="${words[$(($cword - 1))]}"
462         [[ ! "$prev" == -* ]] && return
463
464         n=$((${#opts} - 1))
465         for ((i=0; i <= $n; i++)); do
466                 opt="${opts:$i:1}"
467                 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
468                 let i++
469                 [[ "$prev" != "-$opt" ]] && continue
470                 result="$opt"
471                 return
472         done
473         ret=0
474 }
475
476 # Get the word number on which the cursor is, not counting options.
477 #
478 # This is useful for completing commands whose possible completions depend
479 # on the word number, for example mount.
480 #
481 # $1:   Getopt option string.
482 # $2:   The current word number.
483 # $3..: All words of the current command line.
484 #
485 # return: If the current word is an option, or a parameter to an option,
486 # this function sets $result to -1. Otherwise, the number of the non-option
487 # is returned in $result.
488 #
489 gsu_get_unnamed_arg_num()
490 {
491         local opts="$1" cword="$2" prev cur
492         local -i i n=0
493         local -a words
494
495         shift 2
496         words=("$@")
497         cur="${words[$cword]}"
498         prev="${words[$(($cword - 1))]}"
499         result=-1
500         [[ "$cur" == -* ]] && return
501         [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
502
503         for ((i=1; i <= $cword; i++)); do
504                 prev="${words[$(($i - 1))]}"
505                 cur="${words[$i]}"
506                 [[ "$cur" == -* ]] && continue
507                 if [[ "$prev" == -* ]]; then
508                         opt=${prev#-}
509                         [[ "$opts" != *$opt:* ]] && let n++
510                         continue
511                 fi
512                 let n++
513         done
514         result="$(($n - 1))"
515 }
516
517 # Entry point for all gsu-based scripts.
518 #
519 # The startup part of the application script should source this file to load
520 # the functions defined here, and then call gsu(). Functions starting with com_
521 # are automatically recognized as subcommands.
522 #
523 # Minimal example:
524 #
525 #       com_hello()
526 #       {
527 #               echo 'hello world'
528 #       }
529 #       gsu_dir=${gsu_dir:-/system/location/where/gsu/is/installed}
530 #       . $gsu_dir/subcommand || exit 1
531 #       gsu "$@"
532 gsu()
533 {
534         local i
535         _gsu_available_commands
536         gsu_cmds="$result"
537         if test $# -eq 0; then
538                 _gsu_usage
539                 _gsu_print_available_commands
540                 exit 1
541         fi
542         arg="$1"
543         shift
544         # check internal commands
545         if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then
546                 _com_$arg "$@"
547                 if [[ "$ret" -lt 0 ]]; then
548                         gsu_err_msg
549                         exit 1
550                 fi
551                 exit 0
552         fi
553
554         # external commands
555         for i in $gsu_cmds; do
556                 if test "$arg" = "$i"; then
557                         com_$arg "$@"
558                         if [[ "$ret" -lt 0 ]]; then
559                                 gsu_err_msg
560                                 exit 1
561                         fi
562                         exit 0
563                 fi
564         done
565
566         ret=-$E_GSU_BAD_COMMAND
567         result="$arg"
568         gsu_err_msg
569         _gsu_print_available_commands
570         exit 1
571 }
572
573 # Check number of arguments.
574 #
575 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
576 #
577 # Check that <num_given> is between <num1> and <num2> inclusively.
578 # If only <num1> ist given, num2 is assumed to be infinity.
579 #
580 # Examples:
581 #       0 0 no argument allowed
582 #       1 1 exactly one argument required
583 #       0 2 at most two arguments admissible
584 #       2   at least two arguments reqired
585 #
586 gsu_check_arg_count()
587 {
588         ret=-$E_GSU_BAD_ARG_COUNT
589         if [[ $# -eq 2 ]]; then # only num1 is given
590                 result="at least $2 args required, $1 given"
591                 [[ $1 -lt $2 ]] && return
592                 ret=$GSU_SUCCESS
593                 return
594         fi
595         # num1 and num2 given
596         result="need at least $2 args, $1 given"
597         [[ $1 -lt $2 ]] && return
598         result="need at most $3 args, $1 given"
599         [[ $1 -gt $3 ]] && return
600         ret=$GSU_SUCCESS
601 }