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