gsu: Set copyright year to 2011.
[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 _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 [-e]
175
176 If -e is given, the config file is opened with the default editor. Without
177 options, the command prints out a list of all cmt config variables, together
178 with their current value and the default value."
179 _com_prefs()
180 {
181 local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}"
182
183 if [[ "$1" = "-e" ]]; then
184 ret=-$E_GSU_MKDIR
185 result="${conf%/*}"
186 mkdir -p "$result"
187 [[ $? -ne 0 ]] && return
188 ret=-$E_GSU_EDITOR
189 result="${EDITOR:-vi}"
190 "$result" "$conf"
191 [[ $? -ne 0 ]] && return
192 ret=$GSU_SUCCESS
193 return
194 fi
195
196 for ((i=0; i < ${#gsu_options[@]}; i++)); do
197 local name= option_type= default_value= required=
198 local description= help_text=
199 eval "${gsu_options[$i]}"
200 eval val='"$'${gsu_config_var_prefix}_$name'"'
201 case "$required" in
202 true|yes)
203 printf "# required"
204 ;;
205 *)
206 printf "# optional"
207 ;;
208 esac
209 printf " $option_type: $description"
210 if [[ "$required" != "yes" && "$required" != "true" ]]; then
211 printf " [$default_value]"
212 fi
213 echo
214 [[ -n "$help_text" ]] && sed -e '/^[ ]*$/d; s/^[ ]*/# /g' <<< "$help_text"
215 printf "$name=$val"
216 [[ "$val" == "$default_value" ]] && printf " # default"
217 echo
218 done
219 }
220 export -f _com_prefs
221
222 export gsu_man_txt="
223 Print the manual.
224
225 Usage: man"
226
227 _com_man()
228 {
229 local equal_signs="=================================================="
230 local minus_signs="--------------------------------------------------"
231 local com num
232
233 echo "$_gsu_self (_${gsu_banner_txt}_) manual"
234 echo "${equal_signs:0:${#_gsu_self} + ${#gsu_banner_txt} + 16}"
235 echo
236
237 sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
238 echo "----"
239 echo
240 echo "$_gsu_self usage"
241 echo "${minus_signs:0:${#_gsu_self} + 6}"
242 printf "\t"
243 _gsu_usage 2>&1
244 echo "Each command has its own set of options as described below."
245 echo
246 echo "----"
247 echo
248 echo "Available commands:"
249
250 _gsu_available_commands
251 for com in $result; do
252 num=${#com}
253 if test $num -lt 4; then
254 num=4
255 fi
256 echo "${minus_signs:0:$num}"
257 echo "$com"
258 echo "${minus_signs:0:$num}"
259 $0 help $com
260 echo
261 done
262 ret=$GSU_SUCCESS
263 }
264 export -f _com_man
265
266 export gsu_help_txt="
267 Print online help.
268
269 Usage: help [command]
270
271 Without arguments, print the list of available commands. Otherwise,
272 print the help text for the given command."
273
274 _com_help()
275 {
276 local a b
277 if test -z "$1"; then
278 _gsu_banner_msg 2>&1
279 _gsu_usage 2>&1
280 {
281 printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
282 printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
283 printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
284 grep -A 2 "^com_\([a-zA-Z_0-9]\+\)()" $0
285 } | grep -v -- '--' \
286 | sed -e '/^com_\([a-zA-Z_0-9]\+\)()/bs' \
287 -e 'H;$!d;x;s/\n//g;b' \
288 -e :s \
289 -e 'x;s/\n//g;${p;x;}' \
290 | sed -e 's/^com_\([a-zA-Z_0-9]\+\)()#*/\1\t/' \
291 | sort \
292 | while read a b; do
293 printf "$a\t"
294 if test ${#a} -lt 8; then
295 printf "\t"
296 fi
297 echo "$b"
298 done
299 echo
300 echo "# Try $_gsu_self help <command> for info on <command>."
301 ret=$GSU_SUCCESS
302 return
303 fi
304 if test "$1" = "help"; then
305 echo "$gsu_help_txt"
306 ret=$GSU_SUCCESS
307 return
308 fi
309 if test "$1" = "man"; then
310 echo "$gsu_man_txt"
311 ret=$GSU_SUCCESS
312 return
313 fi
314 if test "$1" = "prefs"; then
315 echo "$gsu_prefs_txt"
316 ret=$GSU_SUCCESS
317 return
318 fi
319 ret=$GSU_SUCCESS
320 if grep -q "^com_$1()" $0; then
321 sed -e "1,/^com_$1()$/d" -e '/^{/,$d' -e 's/^## *//' $0
322 return
323 fi
324 _gsu_print_available_commands
325 result="$1"
326 ret=-$E_GSU_BAD_COMMAND
327 }
328 export -f _com_help
329
330 # internal gsu function that syntactically checks the gsu_options array
331 # for errors and parses the config file.
332 _gsu_check_options()
333 {
334 local i conf="${gsu_config_file:=$HOME/.$gsu_name.rc}" val
335
336 for ((i=0; i < ${#gsu_options[@]}; i++)); do
337 eval "${gsu_options[$i]}"
338 eval val='"'\$$name'"'
339 eval orig_${gsu_config_var_prefix}_$name='"'${val}'"'
340 done
341
342 [[ -r "$conf" ]] && source "$conf"
343
344 for ((i=0; i < ${#gsu_options[@]}; i++)); do
345 local name= option_type= default_value= required=
346 local description= help_text=
347 local val orig_val
348
349 eval "${gsu_options[$i]}"
350
351
352 # Check name. It must be non-empty and consist of [a-zA-Z_0-9]
353 # only. Moreover it must not start with [a-zA-Z].
354
355 ret=-$E_GSU_BAD_CONFIG_VAR
356 result="name: '$name'"
357 # bash's =~ works only for 3.2 and newer, so use grep
358 echo "$name" | grep '^[a-zA-Z][a-zA-Z_0123456789]*$' &> /dev/null;
359 [[ $? -ne 0 ]] && return
360
361 eval orig_val='"'\$orig_${gsu_config_var_prefix}_$name'"'
362 if [[ -z "$orig_val" ]]; then
363 eval val='"'\$$name'"'
364 else
365 val="$orig_val"
366 fi
367 case "$required" in
368 true|yes)
369 ret=-$E_GSU_NEED_VALUE
370 result="$name"
371 [[ -z "$val" ]] && return
372 ;;
373 false|no)
374 ;;
375 *)
376 ret=-$E_GSU_BAD_BOOL
377 result="required: $required, name: $name, val: $val"
378 return
379 esac
380
381 eval ${gsu_config_var_prefix}_$name='"'\${val:=$default_value}'"'
382 # Check option type. ATM, only num and string are supported
383 # Other types may be added without breaking compatibility
384 case "$option_type" in
385 string)
386 ;;
387 num)
388 gsu_is_a_number "$val"
389 [[ $ret -lt 0 ]] && return
390 ;;
391 *)
392 ret=-$E_GSU_BAD_OPTION_TYPE
393 result="$name/$option_type"
394 return
395 esac
396 done
397 ret=$GSU_SUCCESS
398 }
399 export -f _gsu_check_options
400
401 # Wrapper for bash's getopts.
402 #
403 # Aborts on programming errors such as missing or invalid option string. On
404 # success $result contains shell code that can be eval'ed. For each defined
405 # option x, the local variable o_x will be created when calling eval "$result".
406 # o_x contains true/false for options without an argument or the emtpy string/the
407 # given argument, depending on whether this option was contained in the "$@"
408 # array.
409 #
410 # Example:
411 # gsu_getopts abc:x:y
412 # eval "$result"
413 # [[ $ret -lt 0 ]] && return
414 #
415 # [[ "$o_a" = "true ]] && echo "The -a flag was given"
416 # [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
417 gsu_getopts()
418 {
419 local i c tab=' ' cr='
420 '
421
422 gsu_check_arg_count $# 1 1
423 if [[ $ret -lt 0 ]]; then
424 gsu_err_msg
425 exit 1
426 fi
427
428 ret=-$E_GSU_GETOPTS
429 result="invalid optstring $1"
430 if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
431 gsu_err_msg
432 exit 1
433 fi
434
435 for ((i=0; i < ${#1}; i++)); do
436 c=${1:$i:1}
437 case "$c" in
438 [a-zA-Z:]);;
439 *)
440 ret=-$E_GSU_GETOPTS
441 result="invalid character $c in optstring"
442 gsu_err_msg
443 exit 1
444 esac
445 done
446 result="local opt"
447 for ((i=0; i < ${#1}; i++)); do
448 c1=${1:$i:1}
449 c2=${1:$(($i + 1)):1}
450 result+=" o_$c1"
451 if [[ "$c2" = ":" ]]; then
452 let i++
453 else
454 result+="=false"
455 fi
456 done
457 result+="
458 OPTIND=1
459 while getopts $1 opt \"\$@\"; do
460 case \"\$opt\" in
461 "
462 for ((i=0; i < ${#1}; i++)); do
463 c1=${1:$i:1}
464 c2=${1:$(($i + 1)):1}
465 result+="$tab$tab$c1) o_$c1="
466 if [[ "$c2" = ":" ]]; then
467 result+="\"\$OPTARG\""
468 let i++
469 else
470 result+="true"
471 fi
472 result+=";;$cr"
473 done
474 result+="
475 *)
476 ret=-\$E_GSU_GETOPTS
477 result=\"invalid option given\"
478 return
479 ;;
480 esac
481 done
482 shift \$((\$OPTIND - 1))
483 "
484 ret=$GSU_SUCCESS
485 }
486 export -f gsu_getopts
487
488 gsu()
489 {
490 local i
491 _gsu_self="$(basename $0)"
492 gsu_name="${gsu_name:=$_gsu_self}"
493 gsu_config_var_prefix="${gsu_config_var_prefix:=$gsu_name}"
494 _gsu_init_errors
495 _gsu_check_options
496 if [[ "$ret" -lt 0 ]]; then
497 if [[ "$1" != "help" && "$1" != "man" ]]; then
498 gsu_err_msg
499 exit 1
500 fi
501 fi
502 _gsu_available_commands
503 gsu_cmds="$result"
504 if test $# -eq 0; then
505 _gsu_usage
506 _gsu_print_available_commands
507 exit 1
508 fi
509 arg="$1"
510 shift
511 # check internal commands
512 if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" ]]; then
513 _com_$arg "$@"
514 if [[ "$ret" -lt 0 ]]; then
515 gsu_err_msg
516 exit 1
517 fi
518 exit 0
519 fi
520
521 # external commands
522 for i in $gsu_cmds; do
523 if test "$arg" = "$i"; then
524 com_$arg "$@"
525 if [[ "$ret" -lt 0 ]]; then
526 gsu_err_msg
527 exit 1
528 fi
529 exit 0
530 fi
531 done
532
533 ret=-$E_GSU_BAD_COMMAND
534 result="$arg"
535 gsu_err_msg
536 _gsu_print_available_commands
537 exit 1
538 }
539 export -f gsu
540
541 # TODO: gsu_strerror: get error string