]> git.tuebingen.mpg.de Git - gsu.git/blob - funcs/gsu
63e0acf7dfb705047efb1f4809c9af3282fbe9a3
[gsu.git] / funcs / gsu
1 #!/bin/bash
2 # gsu -- the global subcommand utility
3 # (C) 2006-2011 Andre Noll
4
5 _gsu_init_errors()
6 {
7         gsu_errors="
8 GSU_SUCCESS                     success
9 E_GSU_BAD_COMMAND               invalid command
10 E_GSU_NOT_A_NUMBER              not a number
11 E_GSU_BAD_CONFIG_VAR            invalid config variable
12 E_GSU_NEED_VALUE                value required but not given
13 E_GSU_BAD_BOOL                  bad value for boolian option
14 E_GSU_BAD_OPTION_TYPE           invalid option type
15 E_GSU_BAD_ARG_COUNT             invalid number of arguments
16 E_GSU_EDITOR                    failed to execute editor
17 E_GSU_MKDIR                     failed to create directory
18 E_GSU_GETOPTS                   getopts error
19 $gsu_errors
20 "
21         local a b i=0
22         while read a b; do
23                 if test -z "$a"; then
24                         continue
25                 fi
26                 #echo "a:$a,  b: $b"
27                 gsu_error_txt[i]="$b"
28                 eval $a=$i
29                 i=$(($i + 1))
30         done << EOF
31         $gsu_errors
32 EOF
33 }
34 export -f _gsu_init_errors
35
36 # check if $1 is a number
37 gsu_is_a_number()
38 {
39         result="$1"
40         if test "$1" -eq "$1" &> /dev/null; then
41                 ret=$GSU_SUCCESS
42         else
43                 ret=-$E_GSU_NOT_A_NUMBER
44         fi
45 }
46 export -f gsu_is_a_number
47
48 # Check number of arguments.
49 #
50 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
51 #
52 # Check that <num_given> is between <num1> and <num2> inclusively.
53 # If only <num1> ist given, num2 is assumed to be infinity.
54 #
55 # Examples:
56 #       0 0 no argument allowed
57 #       1 1 exactly one argument required
58 #       0 2 at most two arguments admissible
59 #       2   at least two arguments reqired
60 #
61 gsu_check_arg_count()
62 {
63         ret=-$E_GSU_BAD_ARG_COUNT
64         if [[ $# -eq 2 ]]; then # only num1 is given
65                 result="at least $2 args required, $1 given"
66                 [[ $1 -lt $2 ]] && return
67                 ret=$GSU_SUCCESS
68                 return
69         fi
70         # num1 and num2 given
71         result="need at least $2 args, $1 given"
72         [[ $1 -lt $2 ]] && return
73         result="need at most $3 args, $1 given"
74         [[ $1 -gt $3 ]] && return
75         ret=$GSU_SUCCESS
76 }
77 export -f gsu_check_arg_count
78
79 gsu_short_msg()
80 {
81         echo "$1" 1>&2
82 }
83 export -f gsu_short_msg
84
85 gsu_msg()
86 {
87         gsu_short_msg "$_gsu_self: $1"
88 }
89 export -f gsu_msg
90
91 gsu_date_msg()
92 {
93         gsu_short_msg "$_gsu_self $(date): $1"
94 }
95 export -f gsu_date_msg
96
97
98
99 _gsu_banner_msg()
100 {
101         local txt="### $_gsu_self --"
102         if test -z "$gsu_banner_txt"; then
103                 txt="$txt set \$gsu_banner_txt to customize this message"
104         else
105                 txt="$txt $gsu_banner_txt"
106         fi
107         gsu_short_msg "$txt ###"
108 }
109 export -f _gsu_banner_msg
110
111 gsu_err_msg()
112 {
113         local txt="$result" err
114
115         gsu_is_a_number "$ret"
116         if test $ret -lt 0; then
117                 gsu_msg "unknown error ($ret:$txt)"
118                 exit 1
119         fi
120         if test $result -ge 0; then
121                 gsu_msg "unknown error ($result:$txt)"
122                 exit 1
123         fi
124         err=$((0 - $result))
125         if test -n "$txt"; then
126                 txt="$txt: ${gsu_error_txt[$err]}"
127         else
128                 txt="${gsu_error_txt[$err]}"
129         fi
130         gsu_msg "$txt"
131 }
132 export -f gsu_err_msg
133
134 _gsu_usage()
135 {
136         gsu_short_msg "# Usage: $_gsu_self command [options]"
137 }
138 export -f _gsu_usage
139
140 # Each line matching this is recognized as a subcommand. The name
141 # of the subcommand is the first subexpression.
142 export gsu_command_regex='^com_\([a-zA-Z_0-9]\+\)()'
143
144 _gsu_available_commands()
145 {
146         result="$({
147                 printf "help\nman\nprefs\n"
148                 sed -ne "s/$gsu_command_regex/\1/g;T;p" $0
149                 } | sort | tr '\n' ' ')"
150 }
151 export -f _gsu_available_commands
152
153 _gsu_print_available_commands()
154 {(
155         local i count
156         gsu_short_msg "Available commands:"
157         for i in $gsu_cmds; do
158                 printf "$i"
159                 count=$(($count + 1))
160                 if test $(($count % 4)) -eq 0; then
161                         echo
162                 else
163                         printf "\t"
164                         if test ${#i} -lt 8; then
165                                 printf "\t"
166                         fi
167                 fi
168         done
169         echo
170 ) 2>&1
171 }
172 export -f _gsu_print_available_commands
173
174 export gsu_prefs_txt="
175 Print the current preferences.
176
177 Usage: prefs [-e]
178
179 If -e is given, the config file is opened with the default editor.  Without
180 options, the command prints out a list of all cmt config variables, together
181 with their current value and the default value."
182 _com_prefs()
183 {
184         local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}"
185
186         if [[ "$1" = "-e" ]]; then
187                 ret=-$E_GSU_MKDIR
188                 result="${conf%/*}"
189                 mkdir -p "$result"
190                 [[ $? -ne 0 ]] && return
191                 ret=-$E_GSU_EDITOR
192                 result="${EDITOR:-vi}"
193                 "$result" "$conf"
194                 [[ $? -ne 0 ]] && return
195                 ret=$GSU_SUCCESS
196                 return
197         fi
198
199         for ((i=0; i < ${#gsu_options[@]}; i++)); do
200                 local name= option_type= default_value= required=
201                 local description= help_text=
202                 eval "${gsu_options[$i]}"
203                 eval val='"$'${gsu_config_var_prefix}_$name'"'
204                 case "$required" in
205                 true|yes)
206                         printf "# required"
207                         ;;
208                 *)
209                         printf "# optional"
210                         ;;
211                 esac
212                 printf " $option_type: $description"
213                 if [[ "$required" != "yes" && "$required" != "true" ]]; then
214                         printf " [$default_value]"
215                 fi
216                 echo
217                 [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
218                 printf "$name=$val"
219                 [[ "$val" == "$default_value" ]] && printf " # default"
220                 echo
221         done
222 }
223 export -f _com_prefs
224
225 export gsu_man_txt="
226 Print the manual.
227
228 Usage: man"
229
230 _com_man()
231 {
232         local equal_signs="=================================================="
233         local minus_signs="--------------------------------------------------"
234         local com num
235
236         echo "$_gsu_self (_${gsu_banner_txt}_) manual"
237         echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
238         echo
239
240         sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
241         echo "----"
242         echo
243         echo "$_gsu_self usage"
244         echo "${minus_signs:0:${#_gsu_self} + 6}"
245         printf "\t"
246         _gsu_usage 2>&1
247         echo "Each command has its own set of options as described below."
248         echo
249         echo "----"
250         echo
251         echo "Available commands:"
252
253         _gsu_available_commands
254         for com in $result; do
255                 num=${#com}
256                 if test $num -lt 4; then
257                         num=4
258                 fi
259                 echo "${minus_signs:0:$num}"
260                 echo "$com"
261                 echo "${minus_signs:0:$num}"
262                 $0 help $com
263                 echo
264         done
265         ret=$GSU_SUCCESS
266 }
267 export -f _com_man
268
269 export gsu_help_txt="
270 Print online help.
271
272 Usage: help [command]
273
274 Without arguments, print the list of available commands. Otherwise,
275 print the help text for the given command."
276
277 _com_help()
278 {
279         local a b
280         if test -z "$1"; then
281                 _gsu_banner_msg 2>&1
282                 _gsu_usage 2>&1
283                 {
284                         printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
285                         printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
286                         printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
287                         grep -A 2 "$gsu_command_regex" $0
288                 } | grep -v -- '--' \
289                         | sed -e "/$gsu_command_regex/bs" \
290                                 -e 'H;$!d;x;s/\n//g;b' \
291                                 -e :s \
292                                 -e 'x;s/\n//g;${p;x;}' \
293                         | sed -e "s/${gsu_command_regex}#*/\1\t/" \
294                         | sort \
295                         | while read a b; do
296                                 printf "$a\t"
297                                 if test ${#a} -lt 8; then
298                                         printf "\t"
299                                 fi
300                                 echo "$b"
301                         done
302                 echo
303                 echo "# Try $_gsu_self help <command> for info on <command>."
304                 ret=$GSU_SUCCESS
305                 return
306         fi
307         if test "$1" = "help"; then
308                 echo "$gsu_help_txt"
309                 ret=$GSU_SUCCESS
310                 return
311         fi
312         if test "$1" = "man"; then
313                 echo "$gsu_man_txt"
314                 ret=$GSU_SUCCESS
315                 return
316         fi
317         if test "$1" = "prefs"; then
318                 echo "$gsu_prefs_txt"
319                 ret=$GSU_SUCCESS
320                 return
321         fi
322         ret=$GSU_SUCCESS
323         if grep -q "^com_$1()" $0; then
324                 sed -e "1,/^com_$1()$/d" -e '/^{/,$d' -e 's/^## *//' $0
325                 return
326         fi
327         _gsu_print_available_commands
328         result="$1"
329         ret=-$E_GSU_BAD_COMMAND
330 }
331 export -f _com_help
332
333 # internal gsu function that syntactically checks the gsu_options array
334 # for errors and parses the config file.
335 _gsu_check_options()
336 {
337         local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}" val
338
339         for ((i=0; i < ${#gsu_options[@]}; i++)); do
340                 eval "${gsu_options[$i]}"
341                 eval val='"'\$$name'"'
342                 eval orig_${gsu_config_var_prefix}_$name='"'${val}'"'
343         done
344
345         [[ -r "$conf" ]] && source "$conf"
346
347         for ((i=0; i < ${#gsu_options[@]}; i++)); do
348                 local name= option_type= default_value= required=
349                 local description= help_text=
350                 local val orig_val
351
352                 eval "${gsu_options[$i]}"
353
354
355                 # Check name. It must be non-empty and consist of [a-zA-Z_0-9]
356                 # only.  Moreover it must not start with [a-zA-Z].
357
358                 ret=-$E_GSU_BAD_CONFIG_VAR
359                 result="name: '$name'"
360                 # bash's =~ works only for 3.2 and newer, so use grep
361                 echo "$name" | grep '^[a-zA-Z][a-zA-Z_0123456789]*$' &> /dev/null;
362                 [[ $? -ne 0 ]] && return
363
364                 eval orig_val='"'\$orig_${gsu_config_var_prefix}_$name'"'
365                 if [[ -z "$orig_val" ]]; then
366                         eval val='"'\$$name'"'
367                 else
368                         val="$orig_val"
369                 fi
370                 case "$required" in
371                 true|yes)
372                         ret=-$E_GSU_NEED_VALUE
373                         result="$name"
374                         [[ -z "$val" ]] && return
375                         ;;
376                 false|no)
377                         ;;
378                 *)
379                         ret=-$E_GSU_BAD_BOOL
380                         result="required: $required, name: $name, val: $val"
381                         return
382                 esac
383
384                 eval ${gsu_config_var_prefix}_$name='"'\${val:=$default_value}'"'
385                 # Check option type. ATM, only num and string are supported
386                 # Other types may be added without breaking compatibility
387                 case "$option_type" in
388                 string)
389                         ;;
390                 num)
391                         gsu_is_a_number "$val"
392                         [[ $ret -lt 0 ]] && return
393                         ;;
394                 *)
395                         ret=-$E_GSU_BAD_OPTION_TYPE
396                         result="$name/$option_type"
397                         return
398                 esac
399         done
400         ret=$GSU_SUCCESS
401 }
402 export -f _gsu_check_options
403
404 # Wrapper for bash's getopts.
405 #
406 # Aborts on programming errors such as missing or invalid option string.  On
407 # success $result contains shell code that can be eval'ed. For each defined
408 # option x, the local variable o_x will be created when calling eval "$result".
409 # o_x contains true/false for options without an argument or the emtpy string/the
410 # given argument, depending on whether this option was contained in the "$@"
411 # array.
412 #
413 # Example:
414 #       gsu_getopts abc:x:y
415 #       eval "$result"
416 #       [[ $ret -lt 0 ]] && return
417 #
418 #       [[ "$o_a" = "true ]] && echo "The -a flag was given"
419 #       [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
420 gsu_getopts()
421 {
422         local i c tab=' ' cr='
423 '
424
425         gsu_check_arg_count $# 1 1
426         if [[ $ret -lt 0 ]]; then
427                 gsu_err_msg
428                 exit 1
429         fi
430
431         ret=-$E_GSU_GETOPTS
432         result="invalid optstring $1"
433         if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
434                 gsu_err_msg
435                 exit 1
436         fi
437
438         for ((i=0; i < ${#1}; i++)); do
439                 c=${1:$i:1}
440                 case "$c" in
441                 [a-zA-Z:]);;
442                 *)
443                         ret=-$E_GSU_GETOPTS
444                         result="invalid character $c in optstring"
445                         gsu_err_msg
446                         exit 1
447                 esac
448         done
449         result="local opt"
450         for ((i=0; i < ${#1}; i++)); do
451                 c1=${1:$i:1}
452                 c2=${1:$(($i + 1)):1}
453                 result+=" o_$c1"
454                 if [[ "$c2" = ":" ]]; then
455                         let i++
456                 else
457                         result+="=false"
458                 fi
459         done
460         result+="
461         OPTIND=1
462         while getopts $1 opt \"\$@\"; do
463                 case \"\$opt\" in
464 "
465         for ((i=0; i < ${#1}; i++)); do
466                 c1=${1:$i:1}
467                 c2=${1:$(($i + 1)):1}
468                 result+="$tab$tab$c1) o_$c1="
469                 if [[ "$c2" = ":" ]]; then
470                         result+="\"\$OPTARG\""
471                         let i++
472                 else
473                         result+="true"
474                 fi
475                 result+=";;$cr"
476         done
477         result+="
478                 *)
479                         ret=-\$E_GSU_GETOPTS
480                         result=\"invalid option given\"
481                         return
482                         ;;
483                 esac
484         done
485         shift \$((\$OPTIND - 1))
486 "
487         ret=$GSU_SUCCESS
488 }
489 export -f gsu_getopts
490
491 gsu()
492 {
493         local i
494         _gsu_self="$(basename $0)"
495         gsu_name="${gsu_name:=$_gsu_self}"
496         gsu_config_var_prefix="${gsu_config_var_prefix:=$gsu_name}"
497         _gsu_init_errors
498         _gsu_check_options
499         if [[ "$ret" -lt 0 ]]; then
500                 if [[ "$1" != "help" && "$1" != "man" ]]; then
501                         gsu_err_msg
502                         exit 1
503                 fi
504         fi
505         _gsu_available_commands
506         gsu_cmds="$result"
507         if test $# -eq 0; then
508                 _gsu_usage
509                 _gsu_print_available_commands
510                 exit 1
511         fi
512         arg="$1"
513         shift
514         # check internal commands
515         if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" ]]; then
516                 _com_$arg "$@"
517                 if [[ "$ret" -lt 0 ]]; then
518                         gsu_err_msg
519                         exit 1
520                 fi
521                 exit 0
522         fi
523
524         # external commands
525         for i in $gsu_cmds; do
526                 if test "$arg" = "$i"; then
527                         com_$arg "$@"
528                         if [[ "$ret" -lt 0 ]]; then
529                                 gsu_err_msg
530                                 exit 1
531                         fi
532                         exit 0
533                 fi
534         done
535
536         ret=-$E_GSU_BAD_COMMAND
537         result="$arg"
538         gsu_err_msg
539         _gsu_print_available_commands
540         exit 1
541 }
542 export -f gsu
543
544 # TODO: gsu_strerror: get error string