b409bc50bc75d50969d60a800c0710358c34c2d9
[gsu.git] / funcs / gsu
1 #!/bin/bash
2 # gsu -- the global subcommand utility
3 # (C) 2006-2009 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_SOURCE                    error in config file
12 E_GSU_CONFIG                    bad/missing config file option
13 E_GSU_BAD_CONFIG_VAR            invalid config variable
14 E_GSU_NEED_VALUE                value required but not given
15 E_GSU_BAD_BOOL                  bad value for boolian option
16 E_GSU_BAD_OPTION_TYPE           invalid option type
17 E_GSU_BAD_ARG_COUNT             invalid number of arguments
18 E_NO_DEFAULT                    missing default value
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 _gsu_available_commands()
141 {
142         result="$( (printf "help\nman\nprefs\n"; grep "^com_[a-z_]\+()" $0) \
143                 | sed -e 's/^com_//' -e 's/()//' \
144                 | sort \
145                 | tr '\n' ' ')"
146         ret=$GSU_SUCCESS
147 }
148 export -f _gsu_available_commands
149
150 _gsu_print_available_commands()
151 {(
152         local i count
153         gsu_short_msg "Available commands:"
154         for i in $gsu_cmds; do
155                 printf "$i"
156                 count=$(($count + 1))
157                 if test $(($count % 4)) -eq 0; then
158                         echo
159                 else
160                         printf "\t"
161                         if test ${#i} -lt 8; then
162                                 printf "\t"
163                         fi
164                 fi
165         done
166         echo
167 ) 2>&1
168 }
169 export -f _gsu_print_available_commands
170
171 export gsu_prefs_txt="
172 Print the current preferences.
173
174 Usage: prefs
175
176 Print out a list of all cmt config variables, together with their current value
177 and the default value."
178 com_prefs()
179 {
180         local i
181
182         for ((i=0; i < ${#gsu_options[@]}; i++)); do
183                 local name= option_type= default_value= required=
184                 local description= help_text=
185                 eval "${gsu_options[$i]}"
186                 eval val='"$'${gsu_config_var_prefix}_$name'"'
187                 case "$required" in
188                 true|yes)
189                         printf "# required"
190                         ;;
191                 *)
192                         printf "# optional"
193                         ;;
194                 esac
195                 printf " $option_type: $description"
196                 if [[ "$required" != "yes" && "$required" != "true" ]]; then
197                         printf " [$default_value]"
198                 fi
199                 echo
200                 [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
201                 printf "$name=$val"
202                 [[ "$val" == "$default_value" ]] && printf " # default"
203                 echo
204         done
205 }
206 export -f com_prefs
207
208 export gsu_man_txt="
209 Print the manual.
210
211 Usage: man"
212
213 com_man()
214 {
215         local equal_signs="=================================================="
216         local minus_signs="--------------------------------------------------"
217         local com num
218
219         echo "$_gsu_self (_${gsu_banner_txt}_) manual"
220         echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
221         echo
222
223         sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
224         echo "----"
225         echo
226         echo "$_gsu_self usage"
227         echo "${minus_signs:0:${#_gsu_self} + 6}"
228         printf "\t"
229         _gsu_usage 2>&1
230         echo "Each command has its own set of options as described below."
231         echo
232         echo "----"
233         echo
234         echo "Available commands:"
235
236         _gsu_available_commands
237         for com in $result; do
238                 num=${#com}
239                 if test $num -lt 4; then
240                         num=4
241                 fi
242                 echo "${minus_signs:0:$num}"
243                 echo "$com"
244                 echo "${minus_signs:0:$num}"
245                 $0 help $com
246                 echo
247         done
248         ret=$GSU_SUCCESS
249 }
250 export -f com_man
251
252 export gsu_help_txt="
253 Print online help.
254
255 Usage: help [command]
256
257 Without arguments, print the list of available commands. Otherwise,
258 print the help text for the given command."
259
260 com_help()
261 {
262         local a b
263         if test -z "$1"; then
264                 _gsu_banner_msg 2>&1
265                 _gsu_usage 2>&1
266                 # sed is magic, baby
267                 (printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
268                 printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
269                 printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
270
271                 grep -A 2 "^com_\([a-zA-Z_0-9]\+\)()" $0) \
272                         | grep -v -- '--' \
273                         | sed -e '/^com_\([a-zA-Z_0-9]\+\)()/bs' \
274                                 -e 'H;$!d;x;s/\n//g;b' \
275                                 -e :s \
276                                 -e 'x;s/\n//g;${p;x;}' \
277                         | sed -e 's/^com_\([a-zA-Z_0-9]\+\)()#*/\1\t/' \
278                         | sort \
279                         | while read a b; do
280                                 printf "$a\t"
281                                 if test ${#a} -lt 8; then
282                                         printf "\t"
283                                 fi
284                                 echo "$b"
285                         done
286                 echo
287                 echo "Try $_gsu_self help <command> for info on <command>."
288                 ret=$GSU_SUCCESS
289                 return
290         fi
291         if test "$1" = "help"; then
292                 echo "$gsu_help_txt"
293                 ret=$GSU_SUCCESS
294                 return
295         fi
296         if test "$1" = "man"; then
297                 echo "$gsu_man_txt"
298                 ret=$GSU_SUCCESS
299                 return
300         fi
301         if test "$1" = "prefs"; then
302                 echo "$gsu_prefs_txt"
303                 ret=$GSU_SUCCESS
304                 return
305         fi
306         ret=$GSU_SUCCESS
307         if grep -q "^com_$1()" $0; then
308                 sed -e "1,/com_$1()/d" -e '/^{/,$d' -e 's/^## *//' $0
309                 return
310         fi
311         _gsu_print_available_commands
312         result="$1"
313         ret=-$E_GSU_BAD_COMMAND
314 }
315 export -f com_help
316
317 # internal gsu function that syntactically checks the gsu_options array
318 # for errors and parses the config file.
319 _gsu_check_options()
320 {
321         local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}" val
322
323         for ((i=0; i < ${#gsu_options[@]}; i++)); do
324                 eval "${gsu_options[$i]}"
325                 eval val='"'\$$name'"'
326                 eval orig_${gsu_config_var_prefix}_$name='"'${val}'"'
327         done
328
329         [[ -r "$conf" ]] && source "$conf"
330
331         for ((i=0; i < ${#gsu_options[@]}; i++)); do
332                 local name= option_type= default_value= required=
333                 local description= help_text=
334                 local val orig_val
335
336                 eval "${gsu_options[$i]}"
337
338
339                 # Check name. It must be non-empty and consist of [a-zA-Z_0-9]
340                 # only.  Moreover it must not start with [a-zA-Z].
341
342                 ret=-$E_GSU_BAD_CONFIG_VAR
343                 result="name: '$name'"
344                 # bash's =~ works only for 3.2 and newer, so use grep
345                 echo "$name" | grep '^[a-zA-Z][a-zA-Z_0123456789]*$' &> /dev/null;
346                 [[ $? -ne 0 ]] && return
347
348                 eval orig_val='"'\$orig_${gsu_config_var_prefix}_$name'"'
349                 if [[ -z "$orig_val" ]]; then
350                         eval val='"'\$$name'"'
351                 else
352                         val="$orig_val"
353                 fi
354                 case "$required" in
355                 true|yes)
356                         ret=-$E_GSU_NEED_VALUE
357                         result="$name"
358                         [[ -z "$val" ]] && return
359                         ;;
360                 false|no)
361                         ;;
362                 *)
363                         ret=-$E_GSU_BAD_BOOL
364                         result="required: $required, name: $name, val: $val"
365                         return
366                 esac
367
368                 eval ${gsu_config_var_prefix}_$name='"'${val:=$default_value}'"'
369                 # Check option type. ATM, only num and string are supported
370                 # Other types may be added without breaking compatibility
371                 case "$option_type" in
372                 string)
373                         ;;
374                 num)
375                         gsu_is_a_number "$val"
376                         [[ $ret -lt 0 ]] && return
377                         ;;
378                 *)
379                         ret=-$E_GSU_BAD_OPTION_TYPE
380                         result="$name/$option_type"
381                         return
382                 esac
383         done
384         ret=$GSU_SUCCESS
385 }
386 export -f _gsu_check_options
387
388 gsu()
389 {
390         local i
391
392         _gsu_self="$(basename $0)"
393         gsu_name="${gsu_name:=$_gsu_self}"
394         gsu_config_var_prefix="${gsu_config_var_prefix:=$gsu_name}"
395         _gsu_init_errors
396         _gsu_check_options
397         if [[ "$ret" -lt 0 ]]; then
398                 if [[ "$1" != "help" && "$1" != "man" ]]; then
399                         gsu_err_msg
400                         exit 1
401                 fi
402         fi
403         _gsu_available_commands
404         gsu_cmds="$result"
405         if test $# -eq 0; then
406                 _gsu_usage
407                 _gsu_print_available_commands
408                 exit 1
409         fi
410         arg="$1"
411         shift
412         for i in $gsu_cmds; do
413                 if test "$arg" = "$i"; then
414                         com_$arg "$@"
415                         if test $ret -lt 0; then
416                                 gsu_err_msg
417                                 exit 1
418                         fi
419                         exit 0
420                 fi
421         done
422         ret=-$E_GSU_BAD_COMMAND
423         result="$arg"
424         gsu_err_msg
425         _gsu_print_available_commands
426         exit 1
427 }
428 export -f gsu
429
430 # TODO: gsu_strerror: get error string