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