06f677b020a66547921215ff91ae47e5a4d8d8bb
[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 export gsu_help_txt="
196 Print online help.
197
198 Usage: help [command]
199
200 Without arguments, print the list of available commands. Otherwise,
201 print the help text for the given command."
202
203 export gsu_complete_txt="
204 Command line completion.
205
206 Usage: complete [<cword> <word>...]
207
208 When executed without argument the command writes bash code to
209 stdout. This code is suitable to be evaled from .bashrc to enable
210 completion.
211
212 If at least one argument is given, all possible completions are
213 written to stdout. This can be used from the completion function of
214 the subcommand.
215 "
216
217 _com_help()
218 {
219         local a b ere tab='     '
220
221         _gsu_get_command_regex
222         ere="$result"
223
224         if (($# == 0)); then
225                 gsu_short_msg "### $_gsu_self -- $gsu_banner_txt ###"
226                 _gsu_usage 2>&1
227                 {
228                         printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--"
229                         printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--"
230                         printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--"
231                         printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--"
232                         grep -EA 2 "$ere" $0
233                 } | grep -v -- '--' \
234                         | sed -En "/$ere/"'!d
235                                 # remove everything but the command name
236                                 s/^com_(.*)\(\).*/\1/
237
238                                 # append tab after short commands (less than 8 chars)
239                                 s/^(.{1,7})$/\1'"$tab"'/g
240
241                                 # remove next line (should contain only ## anyway)
242                                 N
243                                 s/#.*//
244
245                                 # append next line, removing leading ##
246                                 N
247                                 s/#+ *//g
248
249                                 # replace newline by tab
250                                 y/\n/'"$tab"'/
251
252                                 # and print the sucker
253                                 p'
254                 echo
255                 echo "# Try $_gsu_self help <command> for info on <command>."
256                 ret=$GSU_SUCCESS
257                 return
258         fi
259         if test "$1" = "help"; then
260                 echo "$gsu_help_txt"
261                 ret=$GSU_SUCCESS
262                 return
263         fi
264         if test "$1" = "man"; then
265                 echo "$gsu_man_txt"
266                 ret=$GSU_SUCCESS
267                 return
268         fi
269         if test "$1" = "prefs"; then
270                 echo "$gsu_prefs_txt"
271                 ret=$GSU_SUCCESS
272                 return
273         fi
274         if test "$1" = "complete"; then
275                 echo "$gsu_complete_txt"
276                 ret=$GSU_SUCCESS
277                 return
278         fi
279         ret=$GSU_SUCCESS
280         _gsu_get_command_regex "$1"
281         ere="$result"
282         if ! grep -Eq "$ere" $0; then
283                 _gsu_print_available_commands
284                 result="$1"
285                 ret=-$E_GSU_BAD_COMMAND
286                 return
287         fi
288         sed -nEe '
289                 # only consider lines in the comment of the function
290                 /'"$ere"'/,/^[^#]/ {
291
292                         # remove leading ##
293                         s/^## *//
294
295                         # if it did start with ##, jump to label p and print it
296                         tp
297
298                         # otherwise, move on to next line
299                         d
300
301                         # print it
302                         :p
303                         p
304                 }
305         ' $0
306 }
307
308 complete_help()
309 {
310         _gsu_available_commands
311         echo "$result"
312 }
313
314 # Wrapper for the bash getopts builtin.
315 #
316 # Aborts on programming errors such as missing or invalid option string.  On
317 # success $result contains shell code that can be eval'ed. For each defined
318 # option x, the local variable o_x will be created when calling eval "$result".
319 # o_x contains true/false for options without argument and either the emtpy
320 # string or the given argument for options that take an argument.
321 #
322 # Example:
323 #       gsu_getopts abc:x:y
324 #       eval "$result"
325 #       (($ret < 0)) && return
326 #
327 #       [[ "$o_a" = 'true' ]] && echo 'The -a flag was given'
328 #       [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c"
329 gsu_getopts()
330 {
331         local i c tab=' ' cr='
332 '
333
334         gsu_check_arg_count $# 1 1
335         if [[ $ret -lt 0 ]]; then
336                 gsu_err_msg
337                 exit 1
338         fi
339
340         ret=-$E_GSU_GETOPTS
341         result="invalid optstring $1"
342         if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then
343                 gsu_err_msg
344                 exit 1
345         fi
346
347         for ((i=0; i < ${#1}; i++)); do
348                 c=${1:$i:1}
349                 case "$c" in
350                 [a-zA-Z:]);;
351                 *)
352                         ret=-$E_GSU_GETOPTS
353                         result="invalid character $c in optstring"
354                         gsu_err_msg
355                         exit 1
356                 esac
357         done
358         result="local _gsu_getopts_opt"
359         for ((i=0; i < ${#1}; i++)); do
360                 c1=${1:$i:1}
361                 c2=${1:$(($i + 1)):1}
362                 result+=" o_$c1="
363                 if [[ "$c2" = ":" ]]; then
364                         let i++
365                 else
366                         result+="false"
367                 fi
368         done
369         result+="
370         OPTIND=1
371         while getopts $1 _gsu_getopts_opt \"\$@\"; do
372                 case \"\$_gsu_getopts_opt\" in
373 "
374         for ((i=0; i < ${#1}; i++)); do
375                 c1=${1:$i:1}
376                 c2=${1:$(($i + 1)):1}
377                 result+="$tab$tab$c1) o_$c1="
378                 if [[ "$c2" = ":" ]]; then
379                         result+="\"\$OPTARG\""
380                         let i++
381                 else
382                         result+="true"
383                 fi
384                 result+=";;$cr"
385         done
386         result+="
387                 *)
388                         ret=-\$E_GSU_GETOPTS
389                         result=\"invalid option given\"
390                         return
391                         ;;
392                 esac
393         done
394         shift \$((\$OPTIND - 1))
395 "
396         ret=$GSU_SUCCESS
397 }
398
399 _com_complete()
400 {
401         local cmd n cword
402         local -a words
403
404         if (($# == 0)); then
405                 cat <<EOF
406                 local cur="\${COMP_WORDS[\$COMP_CWORD]}";
407                 local -a candidates;
408
409                 candidates=(\$($0 complete "\$COMP_CWORD" "\${COMP_WORDS[@]}"));
410                 COMPREPLY=(\$(compgen -W "\${candidates[*]}" -- "\$cur"));
411 EOF
412                 ret=$GSU_SUCCESS
413                 return
414         fi
415
416         cword="$1"
417         gsu_is_a_number "$cword"
418         (($ret < 0)) && return
419         if (($cword <= 1)); then
420                 _gsu_available_commands
421                 echo "${result}"
422                 ret=$GSU_SUCCESS
423                 return
424         fi
425         shift
426         words=("$@")
427         cmd="${words[1]}"
428         ret=$GSU_SUCCESS # It's not an error if no completer was defined
429         [[ "$(type -t complete_$cmd)" != "function" ]] && return
430         complete_$cmd "$cword" "${words[@]}"
431         # ignore errors, they would only clutter the completion output
432         ret=$GSU_SUCCESS
433 }
434
435 # Find out if the current word is a parameter for an option.
436 #
437 # $1:   usual getopts option string.
438 # $2:   The current word number.
439 # $3..: All words of the current command line.
440 #
441 # return: If yes, $result contains the letter of the option for which the
442 # current word is a parameter. Otherwise, $result is empty.
443 #
444 gsu_cword_is_option_parameter()
445 {
446         local opts="$1" cword="$2" prev i n
447         local -a words
448
449         result=
450         (($cword == 0)) && return
451         ((${#opts} < 2)) && return
452
453         shift 2
454         words=("$@")
455         prev="${words[$(($cword - 1))]}"
456         [[ ! "$prev" == -* ]] && return
457
458         n=$((${#opts} - 1))
459         for ((i=0; i <= $n; i++)); do
460                 opt="${opts:$i:1}"
461                 [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue
462                 let i++
463                 [[ "$prev" != "-$opt" ]] && continue
464                 result="$opt"
465                 return
466         done
467         ret=0
468 }
469
470 # Get the word number on which the cursor is, not counting options.
471 #
472 # This is useful for completing commands whose possible completions depend
473 # on the word number, for example mount.
474 #
475 # $1:   Getopt option string.
476 # $2:   The current word number.
477 # $3..: All words of the current command line.
478 #
479 # return: If the current word is an option, or a parameter to an option,
480 # this function sets $result to -1. Otherwise, the number of the non-option
481 # is returned in $result.
482 #
483 gsu_get_unnamed_arg_num()
484 {
485         local opts="$1" cword="$2" prev cur
486         local -i i n=0
487         local -a words
488
489         shift 2
490         words=("$@")
491         cur="${words[$cword]}"
492         prev="${words[$(($cword - 1))]}"
493         result=-1
494         [[ "$cur" == -* ]] && return
495         [[ "$prev" == -* ]] && [[ "$opts" == *${prev#-}:* ]] && return
496
497         for ((i=1; i <= $cword; i++)); do
498                 prev="${words[$(($i - 1))]}"
499                 cur="${words[$i]}"
500                 [[ "$cur" == -* ]] && continue
501                 if [[ "$prev" == -* ]]; then
502                         opt=${prev#-}
503                         [[ "$opts" != *$opt:* ]] && let n++
504                         continue
505                 fi
506                 let n++
507         done
508         result="$(($n - 1))"
509 }
510
511 # Entry point for all gsu-based scripts.
512 #
513 # The startup part of the application script should source this file to load
514 # the functions defined here, and then call gsu(). Functions starting with com_
515 # are automatically recognized as subcommands.
516 #
517 # Minimal example:
518 #
519 #       com_hello()
520 #       {
521 #               echo 'hello world'
522 #       }
523 #       gsu_dir=${gsu_dir:-/system/location/where/gsu/is/installed}
524 #       . $gsu_dir/subcommand || exit 1
525 #       gsu "$@"
526 gsu()
527 {
528         local i
529         _gsu_setup
530         _gsu_available_commands
531         gsu_cmds="$result"
532         if test $# -eq 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" -lt 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" -lt 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 reqired
580 #
581 gsu_check_arg_count()
582 {
583         ret=-$E_GSU_BAD_ARG_COUNT
584         if [[ $# -eq 2 ]]; then # only num1 is given
585                 result="at least $2 args required, $1 given"
586                 [[ $1 -lt $2 ]] && return
587                 ret=$GSU_SUCCESS
588                 return
589         fi
590         # num1 and num2 given
591         result="need at least $2 args, $1 given"
592         [[ $1 -lt $2 ]] && return
593         result="need at most $3 args, $1 given"
594         [[ $1 -gt $3 ]] && return
595         ret=$GSU_SUCCESS
596 }