]> git.tuebingen.mpg.de Git - gsu.git/blob - subcommand
6011ee68aad6b88103455167ea129008da1ec792
[gsu.git] / subcommand
1 #!/bin/bash
2 # Copyright (C) 2006 Andre Noll
3 # Licensed under the LGPL, version 3. See COPYING and COPYING.LESSER.
4
5 if [[ "$(type -t _gsu_setup)" != "function" ]]; then
6         gsu_dir=${gsu_dir:-${BASH_SOURCE[0]%/*}}
7         . $gsu_dir/common || exit 1
8         _gsu_setup
9 fi
10
11 _gsu_usage()
12 {
13         gsu_short_msg "# Usage: $gsu_name command [options]"
14 }
15
16 # Return an extended regular expression to match against $0.
17 #
18 # When called without argument, the expression matches all lines which define a
19 # subcommand.
20 #
21 # If an argument is given, the returned expression matches only the subcommand
22 # passed as $1. This is useful to tell if a string is a valid subcommand.
23 #
24 # Regardless of whether an argument is given, the returned expression contains
25 # exactly one parenthesized subexpression for matching the command name.
26 _gsu_get_command_regex()
27 {
28         local cmd="${1:-[-a-zA-Z_0-9]{1,\}}"
29         result="^com_($cmd)\(\)"
30 }
31
32 _gsu_available_commands()
33 {
34         local ere
35
36         _gsu_get_command_regex
37         ere="$result"
38         result="$({
39                 printf "help\nman\nprefs\ncomplete\n"
40                 sed -Ee '
41                         # if line matches, isolate command name
42                         s/'"$ere"'/\1/g
43
44                         # if there is a match, (print it and) start next cycle
45                         t
46
47                         # otherwise delete it
48                         d
49                 ' $0
50         } | sort | tr '\n' ' ')"
51 }
52
53 _gsu_print_available_commands()
54 {
55         local cmd
56         local -i count=0
57
58         printf 'Available commands:\n'
59         for cmd in $gsu_cmds; do
60                 printf '%s' "$cmd"
61                 let ++count
62                 if (($count % 4)); then
63                         printf '\t'
64                         ((${#cmd} < 8)) && printf '\t'
65                 else
66                         printf '\n'
67                 fi
68         done
69         printf '\n'
70 }
71
72 # Print all options of the given optstring to stdout if the word in the current
73 # command line begins with a hyphen character.
74 gsu_complete_options()
75 {
76         local opts="$1" cword="$2" cur opt
77         local -a words
78
79         shift 2
80         words=("$@")
81         cur="${words[$cword]}"
82         ret=0
83         [[ ! "$cur" == -* ]] && return
84
85         ret=0
86         for ((i=0; i < ${#opts}; i++)); do
87                 opt="${opts:$i:1}"
88                 [[ "$opt" == ":" ]] && continue
89                 printf "%s" "-$opt "
90                 let ret++
91         done
92 }
93
94 export gsu_prefs_txt="
95 Print the current preferences.
96
97 Usage: prefs [-e]
98
99 If -e is given, the config file is opened with the default editor.
100 Without options, the command prints out a list of all config variables,
101 together with their current value and the default value.
102 "
103
104 com_prefs()
105 {
106         local i conf="${gsu_config_file:=${HOME:-}/.$gsu_name.rc}"
107
108         gsu_getopts "e"
109         eval "$result"
110         (($ret < 0)) && return
111         gsu_check_arg_count $# 0 0
112         (($ret < 0)) && return
113
114         if [[ "$o_e" == "true" ]]; then
115                 ret=-$E_GSU_MKDIR
116                 result="${conf%/*}"
117                 mkdir -p "$result"
118                 (($? != 0)) && return
119                 ret=-$E_GSU_EDITOR
120                 result="${EDITOR:-vi}"
121                 "$result" "$conf"
122                 (($? != 0)) && return
123                 ret=$GSU_SUCCESS
124                 return
125         fi
126
127         for ((i=0; i < ${#gsu_options[@]}; i++)); do
128                 local name= option_type= default_value= required=
129                 local description= help_text=
130                 eval "${gsu_options[$i]}"
131                 eval val='"${'${gsu_config_var_prefix}_$name:-'}"'
132                 case "$required" in
133                 true|yes)
134                         printf "# required"
135                         ;;
136                 *)
137                         printf "# optional"
138                         ;;
139                 esac
140                 printf " $option_type: $description"
141                 if [[ "$required" != "yes" && "$required" != "true" ]]; then
142                         printf " [$default_value]"
143                 fi
144                 echo
145                 [[ -n "$help_text" ]] && sed -e '/^[    ]*$/d; s/^[     ]*/#    /g' <<< "$help_text"
146                 printf "$name=$val"
147                 [[ "$val" == "$default_value" ]] && printf " # default"
148                 echo
149         done
150 }
151
152 complete_prefs()
153 {
154         gsu_complete_options "e" "$@"
155 }
156
157 export gsu_man_txt="
158 Print the manual.
159
160 Usage: man"
161
162
163 com_man()
164 {
165         local equal_signs="=================================================="
166         local minus_signs="--------------------------------------------------"
167         local com num
168
169         echo "$gsu_name (_${gsu_banner_txt}_) manual"
170         echo "${equal_signs:0:${#gsu_name} + ${#gsu_banner_txt} + 16}"
171         echo
172
173         sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
174         echo "----"
175         echo
176         echo "$gsu_name usage"
177         echo "${minus_signs:0:${#gsu_name} + 6}"
178         printf "\t"
179         _gsu_usage 2>&1
180         echo "Each command has its own set of options as described below."
181         echo
182         echo "----"
183         echo
184         echo "Available commands:"
185
186         _gsu_available_commands
187         for com in $result; do
188                 num=${#com}
189                 (($num < 4)) && num=4
190                 echo "${minus_signs:0:$num}"
191                 echo "$com"
192                 echo "${minus_signs:0:$num}"
193                 $0 help $com
194                 echo
195         done
196         ret=$GSU_SUCCESS
197 }
198
199 export gsu_help_txt="
200 Print online help.
201
202 Usage: help [command]
203
204 Without arguments, print the list of available commands. Otherwise,
205 print the help text for the given command."
206
207 export gsu_complete_txt="
208 Command line completion.
209
210 Usage: complete [<cword> <word>...]
211
212 When executed without argument the command writes bash code to
213 stdout. This code is suitable to be evaled from .bashrc to enable
214 completion.
215
216 If at least one argument is given, all possible completions are
217 written to stdout. This can be used from the completion function of
218 the subcommand.
219 "
220
221 com_help()
222 {
223         local a b ere tab='     '
224
225         _gsu_get_command_regex
226         ere="$result"
227
228         if (($# == 0)); then
229                 gsu_short_msg "### $gsu_name -- $gsu_banner_txt ###"
230                 _gsu_usage 2>&1
231                 {
232                         printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
233                         printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
234                         printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
235                         printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
236                         grep -EA 2 "$ere" $0
237                 } | grep -v -- '--' \
238                         | sed -En "/$ere/"'!d
239                                 # remove everything but the command name
240                                 s/^com_(.*)\(\).*/\1/
241
242                                 # append tab after short commands (less than 8 chars)
243                                 s/^(.{1,7})$/\1'"$tab"'/g
244
245                                 # remove next line (should contain only ## anyway)
246                                 N
247                                 s/#.*//
248
249                                 # append next line, removing leading ##
250                                 N
251                                 s/#+ *//g
252
253                                 # replace newline by tab
254                                 y/\n/'"$tab"'/
255
256                                 # and print the sucker
257                                 p'
258                 echo
259                 echo "# Try $gsu_name help <command> for info on <command>."
260                 ret=$GSU_SUCCESS
261                 return
262         fi
263         if test "$1" = "help"; then
264                 echo "$gsu_help_txt"
265                 ret=$GSU_SUCCESS
266                 return
267         fi
268         if test "$1" = "man"; then
269                 echo "$gsu_man_txt"
270                 ret=$GSU_SUCCESS
271                 return
272         fi
273         if test "$1" = "prefs"; then
274                 echo "$gsu_prefs_txt"
275                 ret=$GSU_SUCCESS
276                 return
277         fi
278         if test "$1" = "complete"; then
279                 echo "$gsu_complete_txt"
280                 ret=$GSU_SUCCESS
281                 return
282         fi
283         ret=$GSU_SUCCESS
284         _gsu_get_command_regex "$1"
285         ere="$result"
286         if ! grep -Eq "$ere" $0; then
287                 _gsu_print_available_commands
288                 result="$1"
289                 ret=-$E_GSU_BAD_COMMAND
290                 return
291         fi
292         sed -nEe '
293                 # only consider lines in the comment of the function
294                 /'"$ere"'/,/^[^#]/ {
295
296                         # remove leading ##
297                         s/^## *//
298
299                         # if it did start with ##, jump to label p and print it
300                         tp
301
302                         # otherwise, move on to next line
303                         d
304
305                         # print it
306                         :p
307                         p
308                 }
309         ' $0
310 }
311
312 complete_help()
313 {
314         _gsu_available_commands
315         echo "$result"
316 }
317
318 # Wrapper for the bash getopts builtin.
319 #
320 # Aborts on programming errors such as missing or invalid option string.  On
321 # success $result contains shell code that can be eval'ed. For each defined
322 # option x, the local variable o_x will be created when calling eval "$result".
323 # o_x contains true/false for options without argument and either the empty
324 # string or the given argument for options that take an argument.
325 #
326 # Example:
327 #       gsu_getopts abc:x:y
328 #       eval "$result"
329 #       (($ret < 0)) && return
330 #
331 #       [[ "$o_a" = 'true' ]] && echo 'The -a flag was given'
332 #       [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
333 gsu_getopts()
334 {
335         local i c tab=' ' cr='
336 '
337
338         gsu_check_arg_count $# 1 1
339         if (($ret < 0)); then
340                 gsu_err_msg
341                 exit 1
342         fi
343
344         ret=-$E_GSU_GETOPTS
345         result="invalid optstring $1"
346         if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
347                 gsu_err_msg
348                 exit 1
349         fi
350
351         for ((i=0; i < ${#1}; i++)); do
352                 c=${1:$i:1}
353                 case "$c" in
354                 [a-zA-Z:]);;
355                 *)
356                         ret=-$E_GSU_GETOPTS
357                         result="invalid character $c in optstring"
358                         gsu_err_msg
359                         exit 1
360                 esac
361         done
362         result="local _gsu_getopts_opt"
363         for ((i=0; i < ${#1}; i++)); do
364                 c1=${1:$i:1}
365                 c2=${1:$(($i + 1)):1}
366                 result+=" o_$c1="
367                 if [[ "$c2" = ":" ]]; then
368                         let i++
369                 else
370                         result+="false"
371                 fi
372         done
373         result+="
374         OPTIND=1
375         while getopts $1 _gsu_getopts_opt \"\$@\"; do
376                 case \"\$_gsu_getopts_opt\" in
377 "
378         for ((i=0; i < ${#1}; i++)); do
379                 c1=${1:$i:1}
380                 c2=${1:$(($i + 1)):1}
381                 result+="$tab$tab$c1) o_$c1="
382                 if [[ "$c2" = ":" ]]; then
383                         result+="\"\$OPTARG\""
384                         let i++
385                 else
386                         result+="true"
387                 fi
388                 result+=";;$cr"
389         done
390         result+="
391                 *)
392                         ret=-\$E_GSU_GETOPTS
393                         result=\"invalid option given\"
394                         return
395                         ;;
396                 esac
397         done
398         shift \$((\$OPTIND - 1))
399 "
400         ret=$GSU_SUCCESS
401 }
402
403 com_complete()
404 {
405         local cmd n cword
406         local -a words
407
408         if (($# == 0)); then
409                 cat <<EOF
410                 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
411                 local -a candidates;
412
413                 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
414                 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
415 EOF
416                 ret=$GSU_SUCCESS
417                 return
418         fi
419
420         cword="$1"
421         gsu_is_a_number "$cword"
422         (($ret < 0)) && return
423         if (($cword <= 1)); then
424                 _gsu_available_commands
425                 echo "${result}"
426                 ret=$GSU_SUCCESS
427                 return
428         fi
429         shift
430         words=("$@")
431         cmd="${words[1]}"
432         ret=$GSU_SUCCESS # It's not an error if no completer was defined
433         [[ "$(type -t complete_$cmd)" != "function" ]] && return
434         complete_$cmd "$cword" "${words[@]}"
435         # ignore errors, they would only clutter the completion output
436         ret=$GSU_SUCCESS
437 }
438
439 # Find out if the current word is a parameter for an option.
440 #
441 # $1:   usual getopts option string.
442 # $2:   The current word number.
443 # $3..: All words of the current command line.
444 #
445 # return: If yes, $result contains the letter of the option for which the
446 # current word is a parameter. Otherwise, $result is empty.
447 #
448 gsu_cword_is_option_parameter()
449 {
450         local opts="$1" cword="$2" prev i n
451         local -a words
452
453         result=
454         (($cword == 0)) && return
455         ((${#opts} < 2)) && return
456
457         shift 2
458         words=("$@")
459         prev="${words[$(($cword - 1))]}"
460         [[ ! "$prev" == -* ]] && return
461
462         n=$((${#opts} - 1))
463         for ((i=0; i <= $n; i++)); do
464                 opt="${opts:$i:1}"
465                 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
466                 let i++
467                 [[ "$prev" != "-$opt" ]] && continue
468                 result="$opt"
469                 return
470         done
471         ret=0
472 }
473
474 # Get the word number on which the cursor is, not counting options.
475 #
476 # This is useful for completing commands whose possible completions depend
477 # on the word number, for example mount.
478 #
479 # $1:   Getopt option string.
480 # $2:   The current word number.
481 # $3..: All words of the current command line.
482 #
483 # return: If the current word is an option, or a parameter to an option,
484 # this function sets $result to -1. Otherwise, the number of the non-option
485 # is returned in $result.
486 #
487 gsu_get_unnamed_arg_num()
488 {
489         local opts="$1" cword="$2" prev cur
490         local -i i n=0
491         local -a words
492
493         shift 2
494         words=("$@")
495         cur="${words[$cword]}"
496         prev="${words[$(($cword - 1))]}"
497         result=-1
498         [[ "$cur" == -* ]] && return
499         [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
500
501         for ((i=1; i <= $cword; i++)); do
502                 prev="${words[$(($i - 1))]}"
503                 cur="${words[$i]}"
504                 [[ "$cur" == -* ]] && continue
505                 if [[ "$prev" == -* ]]; then
506                         opt=${prev#-}
507                         [[ "$opts" != *$opt:* ]] && let n++
508                         continue
509                 fi
510                 let n++
511         done
512         result="$(($n - 1))"
513 }
514
515 # Entry point for all gsu-based scripts.
516 #
517 # The startup part of the application script should source this file to load
518 # the functions defined here, and then call gsu(). Functions starting with com_
519 # are automatically recognized as subcommands.
520 #
521 # Minimal example:
522 #
523 #       com_hello()
524 #       {
525 #               echo 'hello world'
526 #       }
527 #       gsu_dir=${gsu_dir:-/system/location/where/gsu/is/installed}
528 #       . $gsu_dir/subcommand || exit 1
529 #       gsu "$@"
530 gsu()
531 {
532         local i
533         _gsu_available_commands
534         gsu_cmds="$result"
535         if (($# == 0)); then
536                 _gsu_usage
537                 _gsu_print_available_commands
538                 exit 1
539         fi
540         arg="$1"
541         shift
542         if [[ "$(type -t com_$arg)" == 'function' ]]; then
543                 com_$arg "$@"
544                 if (("$ret" < 0)); then
545                         gsu_err_msg
546                         exit 1
547                 fi
548                 exit 0
549         fi
550         ret=-$E_GSU_BAD_COMMAND
551         result="$arg"
552         gsu_err_msg
553         _gsu_print_available_commands
554         exit 1
555 }
556
557 # Check number of arguments.
558 #
559 # Usage: gsu_check_arg_count <num_given> <num1> [<num2>]
560 #
561 # Check that <num_given> is between <num1> and <num2> inclusively.
562 # If only <num1> ist given, num2 is assumed to be infinity.
563 #
564 # Examples:
565 #       0 0 no argument allowed
566 #       1 1 exactly one argument required
567 #       0 2 at most two arguments admissible
568 #       2   at least two arguments required
569 gsu_check_arg_count()
570 {
571         ret=-$E_GSU_BAD_ARG_COUNT
572         if (($# == 2)); then # only num1 is given
573                 result="at least $2 args required, $1 given"
574                 (($1 < $2)) && return
575                 ret=$GSU_SUCCESS
576                 return
577         fi
578         # num1 and num2 given
579         result="need at least $2 args, $1 given"
580         (($1 < $2)) && return
581         result="need at most $3 args, $1 given"
582         (($1 > $3)) && return
583         ret=$GSU_SUCCESS
584 }