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