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