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