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