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