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