8cfa4de0f89b2fc1574c7cb00cf58a9fbd59ce7d
[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 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
49 #
50 # If only <num1> ist given, then <num_given> must equal <num1>.
51 # Otherwise, <num1>..<num2> is treated as a range and it is checked
52 # that <num_given> lies between <num1> and <num2> inclusively.
53 gsu_check_arg_count()
54 {
55         ret=-$E_GSU_BAD_ARG_COUNT
56         if [[ $# -eq 2 ]]; then # only num1 is given
57                 result="exactly $2 args needed, $1 given"
58                 [[ $1 -ne $2 ]] && return
59                 ret=$GSU_SUCCESS
60                 return
61         fi
62         # num1 and num2 given
63         result="need at least $2 args, $1 given"
64         [[ $1 -lt $2 ]] && return
65         result="need at most $3 args, $1 given"
66         [[ $1 -gt $3 ]] && return
67         ret=$GSU_SUCCESS
68 }
69 export -f gsu_check_arg_count
70
71 gsu_short_msg()
72 {
73         echo "$1" 1>&2
74 }
75 export -f gsu_short_msg
76
77 gsu_msg()
78 {
79         gsu_short_msg "$_gsu_self: $1"
80 }
81 export -f gsu_msg
82
83 gsu_date_msg()
84 {
85         gsu_short_msg "$_gsu_self $(date): $1"
86 }
87 export -f gsu_date_msg
88
89
90
91 _gsu_banner_msg()
92 {
93         local txt="*** $_gsu_self --"
94         if test -z "$gsu_banner_txt"; then
95                 txt="$txt set \$gsu_banner_txt to customize this message"
96         else
97                 txt="$txt $gsu_banner_txt"
98         fi
99         gsu_short_msg "$txt ***"
100 }
101 export -f _gsu_banner_msg
102
103 gsu_err_msg()
104 {
105         local txt="$result" err
106
107         gsu_is_a_number "$ret"
108         if test $ret -lt 0; then
109                 gsu_msg "unknown error ($ret:$txt)"
110                 exit 1
111         fi
112         if test $result -ge 0; then
113                 gsu_msg "unknown error ($result:$txt)"
114                 exit 1
115         fi
116         err=$((0 - $result))
117         if test -n "$txt"; then
118                 txt="$txt: ${gsu_error_txt[$err]}"
119         else
120                 txt="${gsu_error_txt[$err]}"
121         fi
122         gsu_msg "$txt"
123 }
124 export -f gsu_err_msg
125
126 _gsu_usage()
127 {
128         gsu_short_msg "Usage: $_gsu_self command [options]"
129 }
130 export -f _gsu_usage
131
132 _gsu_available_commands()
133 {
134         result="$( (printf "help\nman\nprefs\n"; grep "^com_[a-z_]\+()" $0) \
135                 | sed -e 's/^com_//' -e 's/()//' \
136                 | sort \
137                 | tr '\n' ' ')"
138         ret=$GSU_SUCCESS
139 }
140 export -f _gsu_available_commands
141
142 _gsu_print_available_commands()
143 {(
144         local i count
145         gsu_short_msg "Available commands:"
146         for i in $gsu_cmds; do
147                 printf "$i"
148                 count=$(($count + 1))
149                 if test $(($count % 4)) -eq 0; then
150                         echo
151                 else
152                         printf "\t"
153                         if test ${#i} -lt 8; then
154                                 printf "\t"
155                         fi
156                 fi
157         done
158         echo
159 ) 2>&1
160 }
161 export -f _gsu_print_available_commands
162
163 export gsu_prefs_txt="
164 Print the current preferences.
165
166 Usage: prefs
167
168 Print out a list of all cmt config variables, together with their current value
169 and the default value."
170 com_prefs()
171 {
172         local i
173
174         for ((i=0; i < ${#gsu_options[@]}; i++)); do
175                 local name= option_type= default_value= required=
176                 local description= help_text=
177                 eval "${gsu_options[$i]}"
178                 eval val='"$'${gsu_config_var_prefix}_$name'"'
179                 case "$required" in
180                 true|yes)
181                         printf "# required"
182                         ;;
183                 *)
184                         printf "# optional"
185                         ;;
186                 esac
187                 printf " $option_type: $description"
188                 if [[ "$required" != "yes" && "$required" != "true" ]]; then
189                         printf " [$default_value]"
190                 fi
191                 echo
192                 [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
193                 printf "$name=$val"
194                 [[ "$val" == "$default_value" ]] && printf " # default"
195                 echo
196         done
197 }
198 export -f com_prefs
199
200 export gsu_man_txt="
201 Print the manual.
202
203 Usage: man"
204
205 com_man()
206 {
207         local equal_signs="=================================================="
208         local minus_signs="--------------------------------------------------"
209         local com num
210
211         echo "$_gsu_self (_${gsu_banner_txt}_) manual"
212         echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
213         echo
214
215         sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
216         echo "----"
217         echo
218         echo "$_gsu_self usage"
219         echo "${minus_signs:0:${#_gsu_self} + 6}"
220         printf "\t"
221         _gsu_usage 2>&1
222         echo "Each command has its own set of options as described below."
223         echo
224         echo "----"
225         echo
226         echo "Available commands:"
227
228         _gsu_available_commands
229         for com in $result; do
230                 num=${#com}
231                 if test $num -lt 4; then
232                         num=4
233                 fi
234                 echo "${minus_signs:0:$num}"
235                 echo "$com"
236                 echo "${minus_signs:0:$num}"
237                 $0 help $com
238                 echo
239         done
240         ret=$GSU_SUCCESS
241 }
242 export -f com_man
243
244 export gsu_help_txt="
245 Print online help.
246
247 Usage: help [command]
248
249 Without arguments, print the list of available commands. Otherwise,
250 print the help text for the given command."
251
252 com_help()
253 {
254         local a b
255         if test -z "$1"; then
256                 _gsu_banner_msg 2>&1
257                 _gsu_usage 2>&1
258                 # sed is magic, baby
259                 (printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
260                 printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
261                 printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
262
263                 grep -A 2 "^com_\([a-zA-Z_0-9]\+\)()" $0) \
264                         | grep -v -- '--' \
265                         | sed -e '/^com_\([a-zA-Z_0-9]\+\)()/bs' \
266                                 -e 'H;$!d;x;s/\n//g;b' \
267                                 -e :s \
268                                 -e 'x;s/\n//g;${p;x;}' \
269                         | sed -e 's/^com_\([a-zA-Z_0-9]\+\)()#*/\1\t/' \
270                         | sort \
271                         | while read a b; do
272                                 printf "$a\t"
273                                 if test ${#a} -lt 8; then
274                                         printf "\t"
275                                 fi
276                                 echo "$b"
277                         done
278                 echo
279                 echo "Try $_gsu_self help <command> for info on <command>."
280                 ret=$GSU_SUCCESS
281                 return
282         fi
283         if test "$1" = "help"; then
284                 echo "$gsu_help_txt"
285                 ret=$GSU_SUCCESS
286                 return
287         fi
288         if test "$1" = "man"; then
289                 echo "$gsu_man_txt"
290                 ret=$GSU_SUCCESS
291                 return
292         fi
293         if test "$1" = "prefs"; then
294                 echo "$gsu_prefs_txt"
295                 ret=$GSU_SUCCESS
296                 return
297         fi
298         ret=$GSU_SUCCESS
299         if grep -q "^com_$1()" $0; then
300                 sed -e "1,/com_$1()/d" -e '/^{/,$d' -e 's/^## *//' $0
301                 return
302         fi
303         _gsu_print_available_commands
304         result="$1"
305         ret=-$E_GSU_BAD_COMMAND
306 }
307 export -f com_help
308
309 # internal gsu function that syntactically checks the gsu_options array
310 # for errors and parses the config file.
311 _gsu_check_options()
312 {
313         local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}" val
314
315         for ((i=0; i < ${#gsu_options[@]}; i++)); do
316                 eval "${gsu_options[$i]}"
317                 eval val='"'\$$name'"'
318                 eval orig_${gsu_config_var_prefix}_$name='"'${val}'"'
319         done
320
321         [[ -r "$conf" ]] && source "$conf"
322
323         for ((i=0; i < ${#gsu_options[@]}; i++)); do
324                 local name= option_type= default_value= required=
325                 local description= help_text=
326                 local val orig_val
327
328                 eval "${gsu_options[$i]}"
329
330
331                 # Check name. It must be non-empty and consist of [a-zA-Z_0-9]
332                 # only.  Moreover it must not start with [a-zA-Z].
333
334                 ret=-$E_GSU_BAD_CONFIG_VAR
335                 result="name: '$name'"
336                 # bash's =~ works only for 3.2 and newer, so use grep
337                 echo "$name" | grep '^[a-zA-Z][a-zA-Z_0123456789]*$' &> /dev/null;
338                 [[ $? -ne 0 ]] && return
339
340                 eval orig_val='"'\$orig_${gsu_config_var_prefix}_$name'"'
341                 if [[ -z "$orig_val" ]]; then
342                         eval val='"'\$$name'"'
343                 else
344                         val="$orig_val"
345                 fi
346                 case "$required" in
347                 true|yes)
348                         ret=-$E_GSU_NEED_VALUE
349                         result="$name"
350                         [[ -z "$val" ]] && return
351                         ;;
352                 false|no)
353                         ;;
354                 *)
355                         ret=-$E_GSU_BAD_BOOL
356                         result="required: $required, name: $name, val: $val"
357                         return
358                 esac
359
360                 eval ${gsu_config_var_prefix}_$name='"'${val:=$default_value}'"'
361                 # Check option type. ATM, only num and string are supported
362                 # Other types may be added without breaking compatibility
363                 case "$option_type" in
364                 string)
365                         ;;
366                 num)
367                         gsu_is_a_number "$val"
368                         [[ $ret -lt 0 ]] && return
369                         ;;
370                 *)
371                         ret=-$E_GSU_BAD_OPTION_TYPE
372                         result="$name/$option_type"
373                         return
374                 esac
375         done
376         ret=$GSU_SUCCESS
377 }
378 export -f _gsu_check_options
379
380 gsu()
381 {
382         local i
383
384         _gsu_self="$(basename $0)"
385         gsu_name="${gsu_name:=$_gsu_self}"
386         gsu_config_var_prefix="${gsu_config_var_prefix:=$gsu_name}"
387         _gsu_init_errors
388         _gsu_check_options
389         if [[ "$ret" -lt 0 ]]; then
390                 if [[ "$1" != "help" && "$1" != "man" ]]; then
391                         gsu_err_msg
392                         exit 1
393                 fi
394         fi
395         _gsu_available_commands
396         gsu_cmds="$result"
397         if test $# -eq 0; then
398                 _gsu_usage
399                 _gsu_print_available_commands
400                 exit 1
401         fi
402         arg="$1"
403         shift
404         for i in $gsu_cmds; do
405                 if test "$arg" = "$i"; then
406                         com_$arg "$@"
407                         if test $ret -lt 0; then
408                                 gsu_err_msg
409                                 exit 1
410                         fi
411                         exit 0
412                 fi
413         done
414         ret=-$E_GSU_BAD_COMMAND
415         result="$arg"
416         gsu_err_msg
417         _gsu_print_available_commands
418         exit 1
419 }
420 export -f gsu
421
422 # TODO: gsu_strerror: get error string