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