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