]> git.tuebingen.mpg.de Git - gsu.git/blob - subcommand
71f06141abbdd7f0f0b81068abb2037190abec99
[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 _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 _gsu_man_txt="
276 Print the manual.
277
278 Usage: man"
279
280 com_man()
281 {
282         local equal_signs="=================================================="
283         local minus_signs="--------------------------------------------------"
284         local com num
285
286         echo "$gsu_name (_${gsu_banner_txt}_) manual"
287         echo "${equal_signs:0:${#gsu_name} + ${#gsu_banner_txt} + 16}"
288         echo
289
290         sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' $0 -e 's/^# *//'
291         echo "----"
292         echo
293         echo "$gsu_name usage"
294         echo "${minus_signs:0:${#gsu_name} + 6}"
295         printf "\t"
296         _gsu_usage 2>&1
297         echo "Each command has its own set of options as described below."
298         echo
299         echo "----"
300         echo
301         echo "Available commands:"
302
303         _gsu_available_commands
304         for com in $result; do
305                 num=${#com}
306                 (($num < 4)) && num=4
307                 echo "${minus_signs:0:$num}"
308                 echo "$com"
309                 echo "${minus_signs:0:$num}"
310                 $0 help $com
311                 echo
312         done
313         ret=$GSU_SUCCESS
314 }
315
316 _gsu_help_txt="
317 Print online help.
318
319 Usage: help [command]
320
321 Without arguments, print the list of available commands. Otherwise,
322 print the help text for the given command."
323
324 _gsu_complete_txt="
325 Command line completion.
326
327 Usage: complete [<cword> <word>...]
328
329 When executed without argument the command writes bash code to
330 stdout. This code is suitable to be evaled from .bashrc to enable
331 completion.
332
333 If at least one argument is given, all possible completions are
334 written to stdout. This can be used from the completion function of
335 the subcommand.
336 "
337
338 com_help()
339 {
340         local a b ere tab='     '
341
342         _gsu_get_command_regex
343         ere="$result"
344
345         if (($# == 0)); then
346                 gsu_short_msg "### $gsu_name -- $gsu_banner_txt ###"
347                 _gsu_usage 2>&1
348                 {
349                         printf "com_help()\n$_gsu_help_txt" | head -n 4; echo "--"
350                         printf "com_man()\n$_gsu_man_txt" | head -n 4; echo "--"
351                         printf "com_prefs()\n$_gsu_prefs_txt" | head -n 4; echo "--"
352                         printf "com_complete()\n$_gsu_complete_txt" | head -n 4; echo "--"
353                         grep -EA 2 "$ere" $0
354                 } | grep -v -- '--' \
355                         | sed -En "/$ere/"'!d
356                                 # remove everything but the command name
357                                 s/^com_(.*)\(\).*/\1/
358
359                                 # append tab after short commands (less than 8 chars)
360                                 s/^(.{1,7})$/\1'"$tab"'/g
361
362                                 # remove next line (should contain only ## anyway)
363                                 N
364                                 s/#.*//
365
366                                 # append next line, removing leading ##
367                                 N
368                                 s/#+ *//g
369
370                                 # replace newline by tab
371                                 y/\n/'"$tab"'/
372
373                                 # and print the sucker
374                                 p'
375                 echo
376                 echo "# Try $gsu_name help <command> for info on <command>."
377                 ret=$GSU_SUCCESS
378                 return
379         fi
380         if test "$1" = "help"; then
381                 echo "$_gsu_help_txt"
382                 ret=$GSU_SUCCESS
383                 return
384         fi
385         if test "$1" = "man"; then
386                 echo "$_gsu_man_txt"
387                 ret=$GSU_SUCCESS
388                 return
389         fi
390         if test "$1" = "prefs"; then
391                 echo "$_gsu_prefs_txt"
392                 ret=$GSU_SUCCESS
393                 return
394         fi
395         if test "$1" = "complete"; then
396                 echo "$_gsu_complete_txt"
397                 ret=$GSU_SUCCESS
398                 return
399         fi
400         ret=$GSU_SUCCESS
401         _gsu_get_command_regex "$1"
402         ere="$result"
403         if ! grep -Eq "$ere" $0; then
404                 _gsu_print_available_commands
405                 result="$1"
406                 ret=-$E_GSU_BAD_COMMAND
407                 return
408         fi
409         sed -nEe '
410                 # only consider lines in the comment of the function
411                 /'"$ere"'/,/^[^#]/ {
412
413                         # remove leading ##
414                         s/^## *//
415
416                         # if it did start with ##, jump to label p and print it
417                         tp
418
419                         # otherwise, move on to next line
420                         d
421
422                         # print it
423                         :p
424                         p
425                 }
426         ' $0
427 }
428
429 complete_help()
430 {
431         _gsu_available_commands
432         echo "$result"
433 }
434
435 com_complete()
436 {
437         local cmd n cword
438         local -a words
439
440         if (($# == 0)); then
441                 cat <<EOF
442                 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
443                 local -a candidates;
444
445                 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
446                 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
447 EOF
448                 ret=$GSU_SUCCESS
449                 return
450         fi
451
452         cword="$1"
453         gsu_is_a_number "$cword"
454         (($ret < 0)) && return
455         if (($cword <= 1)); then
456                 _gsu_available_commands
457                 echo "${result}"
458                 ret=$GSU_SUCCESS
459                 return
460         fi
461         shift
462         words=("$@")
463         cmd="${words[1]}"
464         ret=$GSU_SUCCESS # It's not an error if no completer was defined
465         [[ "$(type -t complete_$cmd)" != "function" ]] && return
466         complete_$cmd "$cword" "${words[@]}"
467         # ignore errors, they would only clutter the completion output
468         ret=$GSU_SUCCESS
469 }
470
471 # Find out if the current word is a parameter for an option.
472 #
473 # $1:   usual getopts option string.
474 # $2:   The current word number.
475 # $3..: All words of the current command line.
476 #
477 # return: If yes, $result contains the letter of the option for which the
478 # current word is a parameter. Otherwise, $result is empty.
479 #
480 gsu_cword_is_option_parameter()
481 {
482         local opts="$1" cword="$2" prev i n
483         local -a words
484
485         result=
486         (($cword == 0)) && return
487         ((${#opts} < 2)) && return
488
489         shift 2
490         words=("$@")
491         prev="${words[$(($cword - 1))]}"
492         [[ ! "$prev" == -* ]] && return
493
494         n=$((${#opts} - 1))
495         for ((i=0; i <= $n; i++)); do
496                 opt="${opts:$i:1}"
497                 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
498                 let i++
499                 [[ "$prev" != "-$opt" ]] && continue
500                 result="$opt"
501                 return
502         done
503         ret=0
504 }
505
506 # Get the word number on which the cursor is, not counting options.
507 #
508 # This is useful for completing commands whose possible completions depend
509 # on the word number, for example mount.
510 #
511 # $1:   Getopt option string.
512 # $2:   The current word number.
513 # $3..: All words of the current command line.
514 #
515 # return: If the current word is an option, or a parameter to an option,
516 # this function sets $result to -1. Otherwise, the number of the non-option
517 # is returned in $result.
518 #
519 gsu_get_unnamed_arg_num()
520 {
521         local opts="$1" cword="$2" prev cur
522         local -i i n=0
523         local -a words
524
525         shift 2
526         words=("$@")
527         cur="${words[$cword]}"
528         prev="${words[$(($cword - 1))]}"
529         result=-1
530         [[ "$cur" == -* ]] && return
531         [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
532
533         for ((i=1; i <= $cword; i++)); do
534                 prev="${words[$(($i - 1))]}"
535                 cur="${words[$i]}"
536                 [[ "$cur" == -* ]] && continue
537                 if [[ "$prev" == -* ]]; then
538                         opt=${prev#-}
539                         [[ "$opts" != *$opt:* ]] && let n++
540                         continue
541                 fi
542                 let n++
543         done
544         result="$(($n - 1))"
545 }
546
547 # Entry point for all gsu-based scripts.
548 #
549 # The startup part of the application script should source this file to load
550 # the functions defined here, and then call gsu(). Functions starting with com_
551 # are automatically recognized as subcommands.
552 #
553 # Minimal example:
554 #
555 #       com_hello()
556 #       {
557 #               echo 'hello world'
558 #       }
559 #       gsu_dir=${gsu_dir:-/system/location/where/gsu/is/installed}
560 #       . $gsu_dir/subcommand || exit 1
561 #       gsu "$@"
562 gsu()
563 {
564         local i
565
566         if (($# == 0)); then
567                 _gsu_usage
568                 _gsu_print_available_commands
569                 exit 1
570         fi
571         arg="$1"
572         shift
573         if [[ "$(type -t com_$arg)" == 'function' ]]; then
574                 com_$arg "$@"
575                 if (("$ret" < 0)); then
576                         gsu_err_msg
577                         exit 1
578                 fi
579                 exit 0
580         fi
581         ret=-$E_GSU_BAD_COMMAND
582         result="$arg"
583         gsu_err_msg
584         _gsu_print_available_commands
585         exit 1
586 }