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